@rubytech/create-realagent 1.0.817 → 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/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/memory-guide.md +1 -1
- 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-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-2w0XSMC6.js → admin-CdVYoqKD.js} +1 -1
- 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 +305 -1890
package/payload/server/server.js
CHANGED
|
@@ -51,7 +51,7 @@ import {
|
|
|
51
51
|
vncLog,
|
|
52
52
|
waitForExit,
|
|
53
53
|
writeChromiumWrapper
|
|
54
|
-
} from "./chunk-
|
|
54
|
+
} from "./chunk-AJLGI7Y3.js";
|
|
55
55
|
import {
|
|
56
56
|
agentLogStream,
|
|
57
57
|
clearSessionHistory,
|
|
@@ -79,7 +79,7 @@ import {
|
|
|
79
79
|
sigtermFlushStreamLogs,
|
|
80
80
|
unregisterSession,
|
|
81
81
|
validateSession
|
|
82
|
-
} from "./chunk-
|
|
82
|
+
} from "./chunk-ON3LBL2Y.js";
|
|
83
83
|
import {
|
|
84
84
|
ACCOUNTS_DIR,
|
|
85
85
|
GREETING_DIRECTIVE,
|
|
@@ -119,7 +119,7 @@ import {
|
|
|
119
119
|
verifyAndGetConversationUpdatedAt,
|
|
120
120
|
verifyConversationOwnership,
|
|
121
121
|
writeAdminUserAndPerson
|
|
122
|
-
} from "./chunk-
|
|
122
|
+
} from "./chunk-PXQA2MA3.js";
|
|
123
123
|
|
|
124
124
|
// ../lib/graph-trash/dist/index.js
|
|
125
125
|
var require_dist = __commonJS({
|
|
@@ -618,15 +618,15 @@ var serveStatic = (options = { root: "" }) => {
|
|
|
618
618
|
};
|
|
619
619
|
|
|
620
620
|
// server/index.ts
|
|
621
|
-
import { readFileSync as
|
|
622
|
-
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";
|
|
623
623
|
import { homedir as homedir2 } from "os";
|
|
624
624
|
|
|
625
625
|
// app/lib/agent-slug-pattern.ts
|
|
626
626
|
var AGENT_SLUG_PATTERN = /^\/([a-z][a-z0-9-]{2,49})$/;
|
|
627
627
|
|
|
628
628
|
// server/routes/health.ts
|
|
629
|
-
import { existsSync as
|
|
629
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
|
|
630
630
|
import { createConnection } from "net";
|
|
631
631
|
|
|
632
632
|
// app/lib/network.ts
|
|
@@ -647,1562 +647,6 @@ function getLanIp() {
|
|
|
647
647
|
return fallback;
|
|
648
648
|
}
|
|
649
649
|
|
|
650
|
-
// app/lib/review-detector/boot.ts
|
|
651
|
-
import { basename as basename2 } from "path";
|
|
652
|
-
|
|
653
|
-
// app/lib/review-detector/rules.ts
|
|
654
|
-
import { readFileSync, writeFileSync, existsSync as existsSync2, statSync as statSync2, mkdirSync, renameSync } from "fs";
|
|
655
|
-
import { resolve, dirname } from "path";
|
|
656
|
-
var DEFAULT_SCAN_INTERVAL_MS = 5e3;
|
|
657
|
-
var RATE_LIMIT_PATTERN = "rate[- ]?limit(?:ed| reached| hit)|(?:HTTP|status)[^a-z]{0,3}429|too many requests";
|
|
658
|
-
var RATE_LIMIT_PATTERN_V1 = "\\b429\\b|rate.?limit|too.?many.?requests";
|
|
659
|
-
var VALID_TYPES = /* @__PURE__ */ new Set([
|
|
660
|
-
"reconnect-loop",
|
|
661
|
-
"repeated-error",
|
|
662
|
-
"silent-catch",
|
|
663
|
-
"file-write-storm",
|
|
664
|
-
"stale-log",
|
|
665
|
-
"rate-limit",
|
|
666
|
-
"absent-followup"
|
|
667
|
-
]);
|
|
668
|
-
var MAX_FOLLOWUP_WINDOW_MS = 6e5;
|
|
669
|
-
var VALID_SOURCES = /* @__PURE__ */ new Set([
|
|
670
|
-
"any",
|
|
671
|
-
"server",
|
|
672
|
-
"vnc",
|
|
673
|
-
"system",
|
|
674
|
-
"error",
|
|
675
|
-
"session",
|
|
676
|
-
"public",
|
|
677
|
-
"mcp",
|
|
678
|
-
"cloudflared",
|
|
679
|
-
"config-dir"
|
|
680
|
-
]);
|
|
681
|
-
var VALID_SCOPES = /* @__PURE__ */ new Set(["global", "session"]);
|
|
682
|
-
function defaultRules() {
|
|
683
|
-
return [
|
|
684
|
-
{
|
|
685
|
-
id: "whatsapp-reconnect-loop",
|
|
686
|
-
name: "WhatsApp Baileys reconnect loop",
|
|
687
|
-
type: "reconnect-loop",
|
|
688
|
-
logSource: "server",
|
|
689
|
-
pattern: "\\[whatsapp:baileys\\] ERROR",
|
|
690
|
-
thresholdCount: 5,
|
|
691
|
-
thresholdWindowMinutes: 10,
|
|
692
|
-
suggestedAction: "Check Baileys init-queries error. Run `/investigate` on the WhatsApp subsystem, or pause WhatsApp and re-pair the account from chat."
|
|
693
|
-
},
|
|
694
|
-
{
|
|
695
|
-
id: "repeated-error-generic",
|
|
696
|
-
name: "Repeated error signature",
|
|
697
|
-
type: "repeated-error",
|
|
698
|
-
// Match generic ERROR lines with a bracketed prefix. Fingerprinting by
|
|
699
|
-
// first 80 chars happens in the evaluator.
|
|
700
|
-
logSource: "server",
|
|
701
|
-
pattern: "\\] ERROR ",
|
|
702
|
-
thresholdCount: 20,
|
|
703
|
-
thresholdWindowMinutes: 60,
|
|
704
|
-
suggestedAction: "Repeated error burst. Tail the relevant log via `logs-read` and identify the upstream cause."
|
|
705
|
-
},
|
|
706
|
-
{
|
|
707
|
-
id: "silent-catch-fingerprint",
|
|
708
|
-
name: "Silent catch-block fingerprint",
|
|
709
|
-
type: "silent-catch",
|
|
710
|
-
// Patterns that historically indicated a silent catch. Every match is
|
|
711
|
-
// worth an alert — never suppressed by frequency.
|
|
712
|
-
logSource: "any",
|
|
713
|
-
pattern: "(UnhandledPromiseRejection|catch.*ignored|swallowed error|silent failure)",
|
|
714
|
-
thresholdCount: 0,
|
|
715
|
-
thresholdWindowMinutes: 0,
|
|
716
|
-
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'."
|
|
717
|
-
},
|
|
718
|
-
{
|
|
719
|
-
id: "credentials-write-storm",
|
|
720
|
-
name: "Credentials directory write storm",
|
|
721
|
-
type: "file-write-storm",
|
|
722
|
-
logSource: "config-dir",
|
|
723
|
-
pattern: "",
|
|
724
|
-
watchPath: "credentials",
|
|
725
|
-
thresholdCount: 5,
|
|
726
|
-
thresholdWindowMinutes: 5,
|
|
727
|
-
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."
|
|
728
|
-
},
|
|
729
|
-
// The stale-log rule type is fully supported by the evaluator but the
|
|
730
|
-
// default seed does not ship an instance of it. Choosing the right file
|
|
731
|
-
// to watch is subsystem-specific (e.g. a plugin-specific log that stops
|
|
732
|
-
// when the plugin dies); server.log is the wrong target because it is
|
|
733
|
-
// written continuously by the detector's own cycle events, so it never
|
|
734
|
-
// goes stale. Users can add a targeted stale-log rule via
|
|
735
|
-
// `review-rules-add` when they know which file matters for their
|
|
736
|
-
// subsystem.
|
|
737
|
-
{
|
|
738
|
-
id: "http-rate-limit-429",
|
|
739
|
-
name: "HTTP 429 rate-limit hit",
|
|
740
|
-
type: "rate-limit",
|
|
741
|
-
logSource: "any",
|
|
742
|
-
pattern: RATE_LIMIT_PATTERN,
|
|
743
|
-
thresholdCount: 0,
|
|
744
|
-
thresholdWindowMinutes: 0,
|
|
745
|
-
suggestedAction: "An external API returned 429 (rate-limited). Identify the caller and add backoff, or check the quota on the relevant API key."
|
|
746
|
-
},
|
|
747
|
-
{
|
|
748
|
-
id: "approval-bypass-detected",
|
|
749
|
-
name: "Approval gating bypass",
|
|
750
|
-
type: "repeated-error",
|
|
751
|
-
logSource: "error",
|
|
752
|
-
pattern: "\\[persist\\].*(?:email-send|email-reply|whatsapp-send|whatsapp-send-document|message|contact-erase).*approval=auto-executed",
|
|
753
|
-
thresholdCount: 0,
|
|
754
|
-
thresholdWindowMinutes: 0,
|
|
755
|
-
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."
|
|
756
|
-
},
|
|
757
|
-
{
|
|
758
|
-
// Task 530: catches the bridgeai-style class where a single conversation
|
|
759
|
-
// sees the same tool error repeatedly and the agent silently falls back.
|
|
760
|
-
// Session-scoped so cross-conversation coincidence doesn't trigger it.
|
|
761
|
-
id: "tool-result-recurring-errors",
|
|
762
|
-
name: "Tool result errors recurring in a conversation",
|
|
763
|
-
type: "repeated-error",
|
|
764
|
-
logSource: "system",
|
|
765
|
-
pattern: "\\[tool-result\\].*error=true",
|
|
766
|
-
thresholdCount: 2,
|
|
767
|
-
thresholdWindowMinutes: 5,
|
|
768
|
-
scope: "session",
|
|
769
|
-
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.'
|
|
770
|
-
},
|
|
771
|
-
{
|
|
772
|
-
// Task 532: closes the "60-second black-box tool wait" class. Fires when
|
|
773
|
-
// a tool hits the 30-second mark of the mid-flight heartbeat. The
|
|
774
|
-
// pattern anchors on elapsed=30s specifically (the tool-wait tick emits
|
|
775
|
-
// one line per 5s per tool, so matching every tick would be noisy) and
|
|
776
|
-
// excludes tools whose long runtime is expected: `Task`/`Agent` subagent
|
|
777
|
-
// dispatch, `Bash` with explicit long timeouts.
|
|
778
|
-
id: "tool-wait-long-stall",
|
|
779
|
-
name: "Tool wait exceeds 30 seconds (possible stall)",
|
|
780
|
-
type: "repeated-error",
|
|
781
|
-
logSource: "system",
|
|
782
|
-
pattern: "\\[tool-wait\\][^\\n]*name=(?!Task\\b|Agent\\b|Bash\\b)[A-Za-z0-9_]+[^\\n]*elapsed=30s",
|
|
783
|
-
thresholdCount: 0,
|
|
784
|
-
thresholdWindowMinutes: 0,
|
|
785
|
-
scope: "session",
|
|
786
|
-
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."
|
|
787
|
-
},
|
|
788
|
-
{
|
|
789
|
-
// Task 536: detect agents ignoring the WEBFETCH_CANNOT_READ_JS_SPA
|
|
790
|
-
// structured failure. A single SPA short-circuit per conversation is
|
|
791
|
-
// expected — the hook is doing its job. Two or more in the same
|
|
792
|
-
// conversation within 5 minutes means either (a) the agent retried
|
|
793
|
-
// WebFetch on the same SPA URL despite the directive, or (b) the
|
|
794
|
-
// owner is asking about multiple SPA URLs in one session and the
|
|
795
|
-
// pattern needs surfacing as a recurring class. Both signal that the
|
|
796
|
-
// IDENTITY.md "Tool Failure Discipline" guidance is not landing in the
|
|
797
|
-
// prompt — revise the copy rather than add mechanical enforcement.
|
|
798
|
-
id: "webfetch-spa-short-circuit-recurring",
|
|
799
|
-
name: "WebFetch JS-SPA short-circuit fired repeatedly in conversation",
|
|
800
|
-
type: "repeated-error",
|
|
801
|
-
logSource: "system",
|
|
802
|
-
pattern: "WEBFETCH_CANNOT_READ_JS_SPA",
|
|
803
|
-
thresholdCount: 2,
|
|
804
|
-
thresholdWindowMinutes: 5,
|
|
805
|
-
scope: "session",
|
|
806
|
-
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."
|
|
807
|
-
},
|
|
808
|
-
{
|
|
809
|
-
// Task 538: fires when a [spawn] line appears in a conversation's stream
|
|
810
|
-
// log but no subprocess-lifecycle marker follows within 10s. The three
|
|
811
|
-
// acceptable followups are Task 535's contract — at least one must be
|
|
812
|
-
// emitted immediately at every spawn site. Their absence means
|
|
813
|
-
// `teeProcStderrToStreamLog` regressed, the markers drifted, or the
|
|
814
|
-
// spawn site was added without wiring them up. Session scope so a single
|
|
815
|
-
// broken conversation fires exactly once, not N times for every spawn.
|
|
816
|
-
id: "subproc-tee-silent-spawn",
|
|
817
|
-
name: "Subprocess spawn without a stderr-tee lifecycle marker",
|
|
818
|
-
type: "absent-followup",
|
|
819
|
-
logSource: "system",
|
|
820
|
-
pattern: "\\[spawn\\] pid=\\d+",
|
|
821
|
-
followupPattern: "\\[subproc-stderr-tee-attached\\]|\\[subproc-debug-unavailable\\]|\\[subproc-stderr-skip\\]",
|
|
822
|
-
followupWindowMs: 1e4,
|
|
823
|
-
thresholdCount: 0,
|
|
824
|
-
thresholdWindowMinutes: 0,
|
|
825
|
-
scope: "session",
|
|
826
|
-
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)."
|
|
827
|
-
},
|
|
828
|
-
{
|
|
829
|
-
// Task 533: surface every Cloudflare-plugin refusal. The plugin emits
|
|
830
|
-
// exactly one [cloudflare:refuse] line per refusal with a structured
|
|
831
|
-
// reason field; any single occurrence on a previously-clean device
|
|
832
|
-
// means the bound Cloudflare account does not match the operator's
|
|
833
|
-
// intent (or the post-flight FQDN drifted) and the operator needs to
|
|
834
|
-
// act in the dashboard.
|
|
835
|
-
id: "cloudflare-refuse",
|
|
836
|
-
name: "Cloudflare plugin refusal",
|
|
837
|
-
type: "silent-catch",
|
|
838
|
-
logSource: "any",
|
|
839
|
-
pattern: "\\[cloudflare:refuse\\]|\\[cloudflare:post-flight-mismatch\\]",
|
|
840
|
-
thresholdCount: 0,
|
|
841
|
-
thresholdWindowMinutes: 0,
|
|
842
|
-
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`."
|
|
843
|
-
},
|
|
844
|
-
{
|
|
845
|
-
// Task 540: the single highest-priority refusal — surface it immediately
|
|
846
|
-
// and independently of the generic cloudflare-refuse rule so the admin
|
|
847
|
-
// agent sees it on the very next turn. This is the exact class that
|
|
848
|
-
// burned the operator for 8 days across 9+ sessions (Apr 11–18, 2026):
|
|
849
|
-
// tunnel running locally, dashboard serving the wrong account, nothing
|
|
850
|
-
// from the internet reaches the laptop, and no prior telemetry surfaced
|
|
851
|
-
// it in time for the agent to self-correct.
|
|
852
|
-
id: "cloudflare-bound-account-mismatch",
|
|
853
|
-
name: "Cloudflare bound account does not own the configured hostnames",
|
|
854
|
-
type: "silent-catch",
|
|
855
|
-
logSource: "any",
|
|
856
|
-
pattern: '"reason":"bound-account-does-not-own-hostname"',
|
|
857
|
-
thresholdCount: 0,
|
|
858
|
-
thresholdWindowMinutes: 0,
|
|
859
|
-
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`."
|
|
860
|
-
},
|
|
861
|
-
{
|
|
862
|
-
// Task 545: tunnel-login's terminal-failure class — cloudflared's
|
|
863
|
-
// login process died without writing cert.pem. Covers every reason
|
|
864
|
-
// the handler emits on the `failed` branch: either an unknown exit
|
|
865
|
-
// (`-without-cert`), an exit preceded by the courtesy browser-launch
|
|
866
|
-
// marker (`-with-marker`), auth URL never produced (`-timeout`), or
|
|
867
|
-
// crashed before producing it at all. Task 541's original pattern
|
|
868
|
-
// matched `reason=browser-launch-fetch-error` — Task 545 retired
|
|
869
|
-
// that reason because the marker alone is no longer terminal
|
|
870
|
-
// (cloudflared keeps its OAuth-callback loop alive after emitting
|
|
871
|
-
// it). Use this pattern for any new terminal reason the handler
|
|
872
|
-
// gains: extend the alternation rather than adding parallel rules.
|
|
873
|
-
id: "cloudflare-tunnel-login-failed",
|
|
874
|
-
name: "Cloudflare tunnel-login process terminated without writing cert",
|
|
875
|
-
type: "silent-catch",
|
|
876
|
-
logSource: "any",
|
|
877
|
-
pattern: "\\[cloudflare:tunnel-login:failed\\] reason=(login-process-exited-without-cert|login-process-exited-with-marker|auth-url-timeout|process-exited-before-auth-url)",
|
|
878
|
-
thresholdCount: 0,
|
|
879
|
-
thresholdWindowMinutes: 0,
|
|
880
|
-
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."
|
|
881
|
-
},
|
|
882
|
-
{
|
|
883
|
-
// Task 545: non-terminal advisory. cloudflared's browser-launch
|
|
884
|
-
// subcommand failed (DISPLAY unreachable, xdg-open absent, etc.) but
|
|
885
|
-
// its OAuth-callback listener is still running — the login can still
|
|
886
|
-
// complete if a human opens the URL. This rule fires so the admin
|
|
887
|
-
// agent can relay "open the URL yourself" to the operator the moment
|
|
888
|
-
// the condition appears, rather than waiting for the operator to
|
|
889
|
-
// notice nothing is happening in their browser. Task 546 will
|
|
890
|
-
// obsolete the advisory by rendering auth URLs that auto-open in
|
|
891
|
-
// the VNC browser.
|
|
892
|
-
id: "cloudflare-tunnel-login-browser-launch-failed",
|
|
893
|
-
name: "cloudflared couldn't open the sign-in URL (login still live)",
|
|
894
|
-
type: "silent-catch",
|
|
895
|
-
logSource: "any",
|
|
896
|
-
pattern: "\\[cloudflare:tunnel-login:browser-launch-failed\\]",
|
|
897
|
-
thresholdCount: 0,
|
|
898
|
-
thresholdWindowMinutes: 0,
|
|
899
|
-
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)."
|
|
900
|
-
},
|
|
901
|
-
{
|
|
902
|
-
// Task 545: raw-log surface. cloudflared-login.log is now read by
|
|
903
|
-
// the review-detector's `cloudflared` source (see sources.ts). The
|
|
904
|
-
// literal "Failed to fetch resource" is emitted by cloudflared
|
|
905
|
-
// itself when its browser-launch subcommand can't reach a display.
|
|
906
|
-
// This rule catches the cloudflared-side event even if the MCP
|
|
907
|
-
// handler's classification drifts or the platform is restarting
|
|
908
|
-
// mid-login and the advisory log line is not yet written. Keeping
|
|
909
|
-
// this rule on a different source (log-line, not MCP stderr) makes
|
|
910
|
-
// the detection redundant in the "defence in depth" sense — if the
|
|
911
|
-
// MCP classification ever regresses, this still fires.
|
|
912
|
-
id: "cloudflared-login-browser-launch-failed-raw",
|
|
913
|
-
name: "cloudflared login \u2014 browser-launch fetch error in log",
|
|
914
|
-
type: "silent-catch",
|
|
915
|
-
logSource: "cloudflared",
|
|
916
|
-
pattern: "Failed to fetch resource",
|
|
917
|
-
thresholdCount: 0,
|
|
918
|
-
thresholdWindowMinutes: 0,
|
|
919
|
-
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."
|
|
920
|
-
},
|
|
921
|
-
{
|
|
922
|
-
// Task 540: cloudflared.log is the one file most likely to carry the
|
|
923
|
-
// "tunnel is having real-world connectivity problems" signal — QUIC
|
|
924
|
-
// connection failures, connector drops, edge unreachability. Prior to
|
|
925
|
-
// this rule it was written but never read (the review-detector had no
|
|
926
|
-
// rule coverage for it). A single ERR line is worth surfacing; the
|
|
927
|
-
// tee'd output is typically noise-free.
|
|
928
|
-
// Task 862: setup-tunnel.sh emits `[script:setup-tunnel]
|
|
929
|
-
// step=onboarding-persist result=skipped reason=no-account-dir` via
|
|
930
|
-
// phase_line when ACCOUNT_DIR is unset. Pre-Task-862, the form-driven
|
|
931
|
-
// action runner threaded STREAM_LOG_PATH but not ACCOUNT_DIR, so the
|
|
932
|
-
// line landed in the agent's stream log (system source) and the user
|
|
933
|
-
// looped on currentStep=6 indefinitely.
|
|
934
|
-
// The agent-via-Bash path also threads STREAM_LOG_PATH; operator-SSH
|
|
935
|
-
// does not — that's the disambiguator. If this pattern reappears in
|
|
936
|
-
// a system log, a future invocation surface forgot to declare
|
|
937
|
-
// ACCOUNT_DIR. Fix at action-runner.ts WHITELIST['cloudflare-setup'],
|
|
938
|
-
// not in the script.
|
|
939
|
-
id: "cloudflare-setup-account-dir-missing",
|
|
940
|
-
name: "cloudflare-setup ran without ACCOUNT_DIR \u2014 onboarding step-7 will not persist",
|
|
941
|
-
type: "silent-catch",
|
|
942
|
-
logSource: "system",
|
|
943
|
-
pattern: "\\[script:setup-tunnel\\] step=onboarding-persist result=skipped reason=no-account-dir",
|
|
944
|
-
thresholdCount: 0,
|
|
945
|
-
thresholdWindowMinutes: 0,
|
|
946
|
-
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."
|
|
947
|
-
},
|
|
948
|
-
{
|
|
949
|
-
id: "cloudflared-edge-errors",
|
|
950
|
-
name: "cloudflared edge connectivity errors",
|
|
951
|
-
type: "silent-catch",
|
|
952
|
-
logSource: "cloudflared",
|
|
953
|
-
pattern: "^\\S+ ERR (Failed to refresh protocol|no more connections active|Failed to dial a quic connection)",
|
|
954
|
-
thresholdCount: 0,
|
|
955
|
-
thresholdWindowMinutes: 0,
|
|
956
|
-
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."
|
|
957
|
-
},
|
|
958
|
-
{
|
|
959
|
-
// Task 543: fires when an agent turn contains an opposing-axis choice-fork
|
|
960
|
-
// question ("Want me to X, or Y?"). Every occurrence is a violation of the
|
|
961
|
-
// IDENTITY § Questions rule — Rule A (one-sided questions) catches them
|
|
962
|
-
// all, and the sharper sub-class (Rule B — no menu when a tool returned a
|
|
963
|
-
// deterministic signal) is isolated by Task 544's `preceded-by` tightening.
|
|
964
|
-
// logSource is "any" so the rule catches violations on both the admin
|
|
965
|
-
// stream (claude-agent-stream-*) and the public agent stream
|
|
966
|
-
// (public-agent-stream-*); the regex is agent-phrasing-specific and has
|
|
967
|
-
// not been observed in other log contexts. Session scope groups matches
|
|
968
|
-
// by conversationId so a single offending turn fires exactly once.
|
|
969
|
-
id: "agent-choice-fork",
|
|
970
|
-
name: "Agent emitted a choice-fork question",
|
|
971
|
-
type: "repeated-error",
|
|
972
|
-
logSource: "any",
|
|
973
|
-
pattern: "Want me to [^\\n]+, or [^\\n]+\\?",
|
|
974
|
-
thresholdCount: 1,
|
|
975
|
-
thresholdWindowMinutes: 60,
|
|
976
|
-
scope: "session",
|
|
977
|
-
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.'
|
|
978
|
-
},
|
|
979
|
-
{
|
|
980
|
-
// Task 546: fires when the operator clicks a device-bound URL affordance
|
|
981
|
-
// and the chat UI cannot drive the device browser — either CDP is
|
|
982
|
-
// unreachable, the navigation timed out, or CDP returned an error. The
|
|
983
|
-
// log line carries intent, hostname, and navigateResult so the admin
|
|
984
|
-
// agent can name the affected flow and hostname verbatim on its next
|
|
985
|
-
// turn. Every occurrence is worth surfacing (thresholdCount: 0) because
|
|
986
|
-
// this is the exact class of silent failure Task 546 exists to close:
|
|
987
|
-
// the operator clicked, nothing happened on the device, and if we don't
|
|
988
|
-
// review the click telemetry the agent has no way to know the flow is
|
|
989
|
-
// stuck.
|
|
990
|
-
id: "device-url-click-failed",
|
|
991
|
-
name: "Device-bound URL click failed to drive the VNC browser",
|
|
992
|
-
type: "silent-catch",
|
|
993
|
-
logSource: "server",
|
|
994
|
-
// Enumerate the NavigateResult union explicitly rather than relying
|
|
995
|
-
// on a (?!ok) negative lookahead anchored to a specific token order.
|
|
996
|
-
// If a new member is added to the union in cdp-client.ts, this rule
|
|
997
|
-
// must be updated in the same commit — the pattern is order-agnostic
|
|
998
|
-
// (browser= and navigateResult= can appear in either order) and the
|
|
999
|
-
// enumerated list compile-fails the source if it ever drifts from
|
|
1000
|
-
// the shared type in device-url-schema.ts.
|
|
1001
|
-
pattern: "\\[device-url:click\\][^\\n]*(?:browser=fallback|navigateResult=(?:timeout|cdp-unreachable|error))",
|
|
1002
|
-
thresholdCount: 0,
|
|
1003
|
-
thresholdWindowMinutes: 0,
|
|
1004
|
-
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."
|
|
1005
|
-
},
|
|
1006
|
-
{
|
|
1007
|
-
// Task 554: fires when an agent turn emits a synthesized localhost/127.0.0.1
|
|
1008
|
-
// `__remote-auth/setup` URL. The only legitimate source for that URL is
|
|
1009
|
-
// `remote-auth-status`, which emits it inside a `maxy-device-url` affordance
|
|
1010
|
-
// block with the device's real hostname. A synthesized localhost variant is
|
|
1011
|
-
// the exact failure mode Task 554 closed: the removed "direct the user to
|
|
1012
|
-
// /__remote-auth/setup" clause in the password-setter's tool description
|
|
1013
|
-
// invited URL synthesis from a partial path. Session scope groups matches
|
|
1014
|
-
// by conversationId so a single offending turn fires exactly once, even if
|
|
1015
|
-
// the URL appears multiple times in the streamed response.
|
|
1016
|
-
id: "invented-remote-auth-url",
|
|
1017
|
-
name: "Invented localhost/127.0.0.1 remote-auth setup URL",
|
|
1018
|
-
type: "silent-catch",
|
|
1019
|
-
logSource: "any",
|
|
1020
|
-
pattern: "http(s)?://(localhost|127\\.0\\.0\\.1)[:\\w/.-]*__remote-auth/setup",
|
|
1021
|
-
thresholdCount: 0,
|
|
1022
|
-
thresholdWindowMinutes: 0,
|
|
1023
|
-
scope: "session",
|
|
1024
|
-
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."
|
|
1025
|
-
},
|
|
1026
|
-
{
|
|
1027
|
-
// Task 553: fires when `anthropic-setup` auto-resets a revoked API key.
|
|
1028
|
-
// The auto-reset is silent to the agent (the tool falls through to
|
|
1029
|
-
// awaiting_signin in the same call), but it is operator-visible — the
|
|
1030
|
-
// user's previously-stored key was just deleted because Anthropic
|
|
1031
|
-
// rejected it. Surfacing the event lets the admin agent explain on the
|
|
1032
|
-
// next turn why the user is being asked to sign in again instead of
|
|
1033
|
-
// continuing normally. The matching string is the exact stable prefix
|
|
1034
|
-
// emitted by the state machine immediately before `deleteKey()` is
|
|
1035
|
-
// invoked.
|
|
1036
|
-
id: "anthropic-setup-auth-error-auto-reset",
|
|
1037
|
-
name: "Anthropic API key auto-reset on auth_error",
|
|
1038
|
-
type: "silent-catch",
|
|
1039
|
-
logSource: "any",
|
|
1040
|
-
pattern: "\\[anthropic-setup\\] auth_error",
|
|
1041
|
-
thresholdCount: 0,
|
|
1042
|
-
thresholdWindowMinutes: 0,
|
|
1043
|
-
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."
|
|
1044
|
-
},
|
|
1045
|
-
{
|
|
1046
|
-
// Task 562: setup-tunnel.sh persists step-7 completion to a filesystem
|
|
1047
|
-
// flag before arming the service restart. The flag is consumed by the
|
|
1048
|
-
// next session's `loadOnboardingStep` and `getOnboardingState` calls,
|
|
1049
|
-
// so any runtime failure to write it means the next admin session
|
|
1050
|
-
// will re-ask the Cloudflare question the user just answered. Every
|
|
1051
|
-
// occurrence is loud-surface-worthy: the cause is either a permission
|
|
1052
|
-
// issue on the onboarding directory or a filesystem problem on the
|
|
1053
|
-
// device, and the recovery is deterministic (the operator can re-run
|
|
1054
|
-
// setup-tunnel.sh or call `onboarding-complete-step` explicitly).
|
|
1055
|
-
id: "setup-tunnel-onboarding-persist-failed",
|
|
1056
|
-
name: "setup-tunnel.sh failed to persist step-7 completion",
|
|
1057
|
-
type: "silent-catch",
|
|
1058
|
-
logSource: "any",
|
|
1059
|
-
pattern: "\\[script:setup-tunnel\\] step=onboarding-persist result=error",
|
|
1060
|
-
thresholdCount: 0,
|
|
1061
|
-
thresholdWindowMinutes: 0,
|
|
1062
|
-
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."
|
|
1063
|
-
},
|
|
1064
|
-
{
|
|
1065
|
-
// Task 570: every `[llm-call] adminModel resolution FAILED:` line
|
|
1066
|
-
// is a workflow LLM step that could not resolve a model because
|
|
1067
|
-
// readAdminModel returned null. The stderr line carries `reason=`
|
|
1068
|
-
// (enoent/eacces/fs_error/parse_error/field_missing/
|
|
1069
|
-
// field_wrong_type/field_empty) so the admin agent can act on the
|
|
1070
|
-
// specific failure mode rather than reading the generic persisted
|
|
1071
|
-
// error and misdiagnosing. Every occurrence is worth surfacing —
|
|
1072
|
-
// the workflow already failed when this line was emitted.
|
|
1073
|
-
id: "workflow-admin-model-resolution-failed",
|
|
1074
|
-
name: "Workflow readAdminModel returned null (reason code in log)",
|
|
1075
|
-
type: "silent-catch",
|
|
1076
|
-
logSource: "any",
|
|
1077
|
-
pattern: "\\[llm-call\\] adminModel resolution FAILED:",
|
|
1078
|
-
thresholdCount: 0,
|
|
1079
|
-
thresholdWindowMinutes: 0,
|
|
1080
|
-
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."
|
|
1081
|
-
},
|
|
1082
|
-
{
|
|
1083
|
-
// Task 561: fires when an admin-agent Bash tool call installs
|
|
1084
|
-
// `bind9-dnsutils` at runtime. Post-Task-561 the Maxy installer
|
|
1085
|
-
// (platform/scripts/setup.sh) provisions the package on every fresh
|
|
1086
|
-
// and upgrade install, so `dig` is always in PATH before
|
|
1087
|
-
// setup-tunnel.sh runs. An admin apt-install of bind9-dnsutils is
|
|
1088
|
-
// therefore evidence the installer regressed (or the host is on a
|
|
1089
|
-
// pre-Task-561 image); in either case the fix is to re-run the
|
|
1090
|
-
// installer, not to patch apt-state from chat. Session scope so a
|
|
1091
|
-
// single offending turn fires exactly once.
|
|
1092
|
-
id: "admin-agent-apt-bind9-install",
|
|
1093
|
-
name: "Admin agent installed bind9-dnsutils at runtime (installer regression)",
|
|
1094
|
-
type: "repeated-error",
|
|
1095
|
-
logSource: "any",
|
|
1096
|
-
pattern: "\\[tool-use\\][^\\n]*name=Bash[^\\n]*apt-get install[^\\n]*bind9-dnsutils",
|
|
1097
|
-
thresholdCount: 1,
|
|
1098
|
-
thresholdWindowMinutes: 60,
|
|
1099
|
-
scope: "session",
|
|
1100
|
-
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."
|
|
1101
|
-
}
|
|
1102
|
-
];
|
|
1103
|
-
}
|
|
1104
|
-
function rulesFilePath(configDir2) {
|
|
1105
|
-
return resolve(configDir2, "review-rules.json");
|
|
1106
|
-
}
|
|
1107
|
-
function ensureRulesFile(configDir2) {
|
|
1108
|
-
const path2 = rulesFilePath(configDir2);
|
|
1109
|
-
if (existsSync2(path2)) return { created: false, path: path2 };
|
|
1110
|
-
mkdirSync(dirname(path2), { recursive: true });
|
|
1111
|
-
const body = {
|
|
1112
|
-
scanIntervalMs: DEFAULT_SCAN_INTERVAL_MS,
|
|
1113
|
-
rules: defaultRules()
|
|
1114
|
-
};
|
|
1115
|
-
atomicWriteJson(path2, body);
|
|
1116
|
-
return { created: true, path: path2 };
|
|
1117
|
-
}
|
|
1118
|
-
function loadRules(configDir2) {
|
|
1119
|
-
const path2 = rulesFilePath(configDir2);
|
|
1120
|
-
if (!existsSync2(path2)) {
|
|
1121
|
-
throw new Error(`rules file missing at ${path2}`);
|
|
1122
|
-
}
|
|
1123
|
-
const raw = readFileSync(path2, "utf-8");
|
|
1124
|
-
let parsed;
|
|
1125
|
-
try {
|
|
1126
|
-
parsed = JSON.parse(raw);
|
|
1127
|
-
} catch (err) {
|
|
1128
|
-
throw new Error(`rules file ${path2} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`);
|
|
1129
|
-
}
|
|
1130
|
-
return validateRulesFile(parsed, path2);
|
|
1131
|
-
}
|
|
1132
|
-
function rulesFileMtime(configDir2) {
|
|
1133
|
-
const path2 = rulesFilePath(configDir2);
|
|
1134
|
-
try {
|
|
1135
|
-
return statSync2(path2).mtimeMs;
|
|
1136
|
-
} catch {
|
|
1137
|
-
return null;
|
|
1138
|
-
}
|
|
1139
|
-
}
|
|
1140
|
-
function saveRules(configDir2, file) {
|
|
1141
|
-
validateRulesFile(file, rulesFilePath(configDir2));
|
|
1142
|
-
atomicWriteJson(rulesFilePath(configDir2), file);
|
|
1143
|
-
}
|
|
1144
|
-
function atomicWriteJson(path2, body) {
|
|
1145
|
-
const tmp = `${path2}.tmp.${process.pid}.${Date.now()}`;
|
|
1146
|
-
writeFileSync(tmp, JSON.stringify(body, null, 2) + "\n", "utf-8");
|
|
1147
|
-
renameSync(tmp, path2);
|
|
1148
|
-
}
|
|
1149
|
-
function validateRulesFile(input, sourceLabel) {
|
|
1150
|
-
if (!input || typeof input !== "object") {
|
|
1151
|
-
throw new Error(`${sourceLabel}: top-level must be an object`);
|
|
1152
|
-
}
|
|
1153
|
-
const obj = input;
|
|
1154
|
-
const scanIntervalMs = obj.scanIntervalMs;
|
|
1155
|
-
if (typeof scanIntervalMs !== "number" || scanIntervalMs < 500 || scanIntervalMs > 3e5) {
|
|
1156
|
-
throw new Error(`${sourceLabel}: scanIntervalMs must be a number between 500 and 300000`);
|
|
1157
|
-
}
|
|
1158
|
-
const rulesRaw = obj.rules;
|
|
1159
|
-
if (!Array.isArray(rulesRaw)) {
|
|
1160
|
-
throw new Error(`${sourceLabel}: rules must be an array`);
|
|
1161
|
-
}
|
|
1162
|
-
const ids = /* @__PURE__ */ new Set();
|
|
1163
|
-
const rules = rulesRaw.map((r, i) => validateRule(r, `${sourceLabel}#rules[${i}]`, ids));
|
|
1164
|
-
return { scanIntervalMs, rules };
|
|
1165
|
-
}
|
|
1166
|
-
function addMissingDefaultRules(rulesFile) {
|
|
1167
|
-
const existingIds = new Set(rulesFile.rules.map((r) => r.id));
|
|
1168
|
-
const defaults = defaultRules();
|
|
1169
|
-
let mutated = false;
|
|
1170
|
-
for (const rule of defaults) {
|
|
1171
|
-
if (!existingIds.has(rule.id)) {
|
|
1172
|
-
rulesFile.rules.push(rule);
|
|
1173
|
-
mutated = true;
|
|
1174
|
-
}
|
|
1175
|
-
}
|
|
1176
|
-
return mutated;
|
|
1177
|
-
}
|
|
1178
|
-
function migrateRateLimitPattern(rulesFile) {
|
|
1179
|
-
const rule = rulesFile.rules.find((r) => r.id === "http-rate-limit-429");
|
|
1180
|
-
if (!rule) return false;
|
|
1181
|
-
if (rule.pattern !== RATE_LIMIT_PATTERN_V1) return false;
|
|
1182
|
-
rule.pattern = RATE_LIMIT_PATTERN;
|
|
1183
|
-
return true;
|
|
1184
|
-
}
|
|
1185
|
-
function validateRule(input, label, seenIds) {
|
|
1186
|
-
if (!input || typeof input !== "object") {
|
|
1187
|
-
throw new Error(`${label}: rule must be an object`);
|
|
1188
|
-
}
|
|
1189
|
-
const r = input;
|
|
1190
|
-
const id = r.id;
|
|
1191
|
-
if (typeof id !== "string" || id.length === 0) {
|
|
1192
|
-
throw new Error(`${label}: id must be a non-empty string`);
|
|
1193
|
-
}
|
|
1194
|
-
if (seenIds.has(id)) {
|
|
1195
|
-
throw new Error(`${label}: duplicate rule id "${id}"`);
|
|
1196
|
-
}
|
|
1197
|
-
seenIds.add(id);
|
|
1198
|
-
const name = r.name;
|
|
1199
|
-
if (typeof name !== "string" || name.length === 0) {
|
|
1200
|
-
throw new Error(`${label}: name must be a non-empty string`);
|
|
1201
|
-
}
|
|
1202
|
-
const type = r.type;
|
|
1203
|
-
if (typeof type !== "string" || !VALID_TYPES.has(type)) {
|
|
1204
|
-
throw new Error(`${label}: type must be one of ${[...VALID_TYPES].join(", ")}`);
|
|
1205
|
-
}
|
|
1206
|
-
const logSource = r.logSource;
|
|
1207
|
-
if (typeof logSource !== "string" || !VALID_SOURCES.has(logSource)) {
|
|
1208
|
-
throw new Error(`${label}: logSource must be one of ${[...VALID_SOURCES].join(", ")}`);
|
|
1209
|
-
}
|
|
1210
|
-
const pattern = r.pattern;
|
|
1211
|
-
if (typeof pattern !== "string") {
|
|
1212
|
-
throw new Error(`${label}: pattern must be a string (may be empty for stale-log/file-write-storm)`);
|
|
1213
|
-
}
|
|
1214
|
-
if (pattern.length > 0) {
|
|
1215
|
-
try {
|
|
1216
|
-
new RegExp(pattern);
|
|
1217
|
-
} catch (err) {
|
|
1218
|
-
throw new Error(`${label}: pattern is not a valid regex: ${err instanceof Error ? err.message : String(err)}`);
|
|
1219
|
-
}
|
|
1220
|
-
}
|
|
1221
|
-
const thresholdCount = r.thresholdCount;
|
|
1222
|
-
if (typeof thresholdCount !== "number" || thresholdCount < 0) {
|
|
1223
|
-
throw new Error(`${label}: thresholdCount must be a non-negative number`);
|
|
1224
|
-
}
|
|
1225
|
-
const thresholdWindowMinutes = r.thresholdWindowMinutes;
|
|
1226
|
-
if (typeof thresholdWindowMinutes !== "number" || thresholdWindowMinutes < 0) {
|
|
1227
|
-
throw new Error(`${label}: thresholdWindowMinutes must be a non-negative number`);
|
|
1228
|
-
}
|
|
1229
|
-
const suggestedAction = r.suggestedAction;
|
|
1230
|
-
if (typeof suggestedAction !== "string" || suggestedAction.length === 0) {
|
|
1231
|
-
throw new Error(`${label}: suggestedAction must be a non-empty string`);
|
|
1232
|
-
}
|
|
1233
|
-
const rule = {
|
|
1234
|
-
id,
|
|
1235
|
-
name,
|
|
1236
|
-
type,
|
|
1237
|
-
logSource,
|
|
1238
|
-
pattern,
|
|
1239
|
-
thresholdCount,
|
|
1240
|
-
thresholdWindowMinutes,
|
|
1241
|
-
suggestedAction
|
|
1242
|
-
};
|
|
1243
|
-
if (typeof r.watchPath === "string") rule.watchPath = r.watchPath;
|
|
1244
|
-
if (typeof r.staleHours === "number") rule.staleHours = r.staleHours;
|
|
1245
|
-
if (typeof r.followupPattern === "string") {
|
|
1246
|
-
if (r.followupPattern.length > 0) {
|
|
1247
|
-
try {
|
|
1248
|
-
new RegExp(r.followupPattern);
|
|
1249
|
-
} catch (err) {
|
|
1250
|
-
throw new Error(`${label}: followupPattern is not a valid regex: ${err instanceof Error ? err.message : String(err)}`);
|
|
1251
|
-
}
|
|
1252
|
-
}
|
|
1253
|
-
rule.followupPattern = r.followupPattern;
|
|
1254
|
-
}
|
|
1255
|
-
if (typeof r.followupWindowMs === "number") rule.followupWindowMs = r.followupWindowMs;
|
|
1256
|
-
if (typeof r.suppressedUntil === "string") rule.suppressedUntil = r.suppressedUntil;
|
|
1257
|
-
if (r.scope !== void 0) {
|
|
1258
|
-
if (typeof r.scope !== "string" || !VALID_SCOPES.has(r.scope)) {
|
|
1259
|
-
throw new Error(`${label}: scope must be one of ${[...VALID_SCOPES].join(", ")}`);
|
|
1260
|
-
}
|
|
1261
|
-
rule.scope = r.scope;
|
|
1262
|
-
}
|
|
1263
|
-
if (rule.type === "file-write-storm" || rule.type === "stale-log") {
|
|
1264
|
-
if (!rule.watchPath) {
|
|
1265
|
-
throw new Error(`${label}: ${rule.type} rules require watchPath`);
|
|
1266
|
-
}
|
|
1267
|
-
}
|
|
1268
|
-
if (rule.type === "stale-log") {
|
|
1269
|
-
if (typeof rule.staleHours !== "number" || rule.staleHours <= 0) {
|
|
1270
|
-
throw new Error(`${label}: stale-log rules require a positive staleHours`);
|
|
1271
|
-
}
|
|
1272
|
-
}
|
|
1273
|
-
if (rule.type === "absent-followup") {
|
|
1274
|
-
if (rule.pattern.length === 0) {
|
|
1275
|
-
throw new Error(`${label}: absent-followup rules require a non-empty pattern`);
|
|
1276
|
-
}
|
|
1277
|
-
if (typeof rule.followupPattern !== "string" || rule.followupPattern.length === 0) {
|
|
1278
|
-
throw new Error(`${label}: absent-followup rules require a non-empty followupPattern`);
|
|
1279
|
-
}
|
|
1280
|
-
if (typeof rule.followupWindowMs !== "number" || rule.followupWindowMs <= 0 || rule.followupWindowMs > MAX_FOLLOWUP_WINDOW_MS) {
|
|
1281
|
-
throw new Error(`${label}: absent-followup rules require followupWindowMs in (0, ${MAX_FOLLOWUP_WINDOW_MS}]`);
|
|
1282
|
-
}
|
|
1283
|
-
}
|
|
1284
|
-
return rule;
|
|
1285
|
-
}
|
|
1286
|
-
|
|
1287
|
-
// app/lib/review-detector/sources.ts
|
|
1288
|
-
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";
|
|
1289
|
-
import { resolve as resolve2, join as join2, basename, dirname as dirname2 } from "path";
|
|
1290
|
-
function tailStatePath(configDir2) {
|
|
1291
|
-
return resolve2(configDir2, "review-state.json");
|
|
1292
|
-
}
|
|
1293
|
-
function loadTailState(configDir2) {
|
|
1294
|
-
const path2 = tailStatePath(configDir2);
|
|
1295
|
-
if (!existsSync3(path2)) return {};
|
|
1296
|
-
try {
|
|
1297
|
-
const raw = readFileSync2(path2, "utf-8");
|
|
1298
|
-
const parsed = JSON.parse(raw);
|
|
1299
|
-
if (!parsed || typeof parsed !== "object") return {};
|
|
1300
|
-
const clean = {};
|
|
1301
|
-
for (const [key, value] of Object.entries(parsed)) {
|
|
1302
|
-
const entry = value;
|
|
1303
|
-
if (entry && typeof entry.offset === "number" && typeof entry.size === "number" && typeof entry.inode === "number") {
|
|
1304
|
-
clean[key] = entry;
|
|
1305
|
-
}
|
|
1306
|
-
}
|
|
1307
|
-
return clean;
|
|
1308
|
-
} catch (err) {
|
|
1309
|
-
console.error(`[review] tail state corrupt at ${path2}, starting fresh: ${err instanceof Error ? err.message : String(err)}`);
|
|
1310
|
-
return {};
|
|
1311
|
-
}
|
|
1312
|
-
}
|
|
1313
|
-
function saveTailState(configDir2, state) {
|
|
1314
|
-
const path2 = tailStatePath(configDir2);
|
|
1315
|
-
mkdirSync2(dirname2(path2), { recursive: true });
|
|
1316
|
-
const tmp = `${path2}.tmp.${process.pid}.${Date.now()}`;
|
|
1317
|
-
writeFileSync2(tmp, JSON.stringify(state, null, 2) + "\n", "utf-8");
|
|
1318
|
-
renameSync2(tmp, path2);
|
|
1319
|
-
}
|
|
1320
|
-
function discoverSourceFiles(configDir2, accountLogDir2, logicalSource) {
|
|
1321
|
-
if (logicalSource === "server") {
|
|
1322
|
-
const p = resolve2(configDir2, "logs", "server.log");
|
|
1323
|
-
return existsSync3(p) ? [{ logicalSource: "server", filepath: p }] : [];
|
|
1324
|
-
}
|
|
1325
|
-
if (logicalSource === "vnc") {
|
|
1326
|
-
const p = resolve2(configDir2, "logs", "vnc-boot.log");
|
|
1327
|
-
return existsSync3(p) ? [{ logicalSource: "vnc", filepath: p }] : [];
|
|
1328
|
-
}
|
|
1329
|
-
if (logicalSource === "cloudflared") {
|
|
1330
|
-
const files2 = [];
|
|
1331
|
-
const daemon = resolve2(configDir2, "logs", "cloudflared.log");
|
|
1332
|
-
if (existsSync3(daemon)) files2.push({ logicalSource: "cloudflared", filepath: daemon });
|
|
1333
|
-
const login = resolve2(configDir2, "logs", "cloudflared-login.log");
|
|
1334
|
-
if (existsSync3(login)) files2.push({ logicalSource: "cloudflared", filepath: login });
|
|
1335
|
-
return files2;
|
|
1336
|
-
}
|
|
1337
|
-
const prefix = {
|
|
1338
|
-
system: "claude-agent-stream-",
|
|
1339
|
-
error: "claude-agent-stderr-",
|
|
1340
|
-
session: "sse-events-",
|
|
1341
|
-
public: "public-agent-stream-",
|
|
1342
|
-
mcp: "mcp-"
|
|
1343
|
-
}[logicalSource];
|
|
1344
|
-
if (!existsSync3(accountLogDir2)) return [];
|
|
1345
|
-
const files = [];
|
|
1346
|
-
let scanned = 0;
|
|
1347
|
-
let skippedPrefixMismatch = 0;
|
|
1348
|
-
let skippedNotLog = 0;
|
|
1349
|
-
for (const entry of readdirSync(accountLogDir2)) {
|
|
1350
|
-
scanned += 1;
|
|
1351
|
-
const matchesPrefix = entry.startsWith(prefix);
|
|
1352
|
-
const isLog = entry.endsWith(".log");
|
|
1353
|
-
if (matchesPrefix && isLog) {
|
|
1354
|
-
files.push({ logicalSource, filepath: join2(accountLogDir2, entry) });
|
|
1355
|
-
} else if (!matchesPrefix) {
|
|
1356
|
-
skippedPrefixMismatch += 1;
|
|
1357
|
-
} else {
|
|
1358
|
-
skippedNotLog += 1;
|
|
1359
|
-
}
|
|
1360
|
-
}
|
|
1361
|
-
files.sort((a, b) => {
|
|
1362
|
-
try {
|
|
1363
|
-
return statSync3(b.filepath).mtimeMs - statSync3(a.filepath).mtimeMs;
|
|
1364
|
-
} catch {
|
|
1365
|
-
return a.filepath.localeCompare(b.filepath);
|
|
1366
|
-
}
|
|
1367
|
-
});
|
|
1368
|
-
if (skippedPrefixMismatch > 0 || skippedNotLog > 0) {
|
|
1369
|
-
console.error(`[review-scan-skip] dir=${accountLogDir2} source=${logicalSource} prefix=${prefix} scanned=${scanned} matched=${files.length} skipped_prefix=${skippedPrefixMismatch} skipped_non_log=${skippedNotLog}`);
|
|
1370
|
-
}
|
|
1371
|
-
return files;
|
|
1372
|
-
}
|
|
1373
|
-
function discoverAllSources(configDir2, accountLogDir2) {
|
|
1374
|
-
return [
|
|
1375
|
-
...discoverSourceFiles(configDir2, accountLogDir2, "server"),
|
|
1376
|
-
...discoverSourceFiles(configDir2, accountLogDir2, "vnc"),
|
|
1377
|
-
...discoverSourceFiles(configDir2, accountLogDir2, "system"),
|
|
1378
|
-
...discoverSourceFiles(configDir2, accountLogDir2, "error"),
|
|
1379
|
-
...discoverSourceFiles(configDir2, accountLogDir2, "session"),
|
|
1380
|
-
...discoverSourceFiles(configDir2, accountLogDir2, "public"),
|
|
1381
|
-
...discoverSourceFiles(configDir2, accountLogDir2, "mcp"),
|
|
1382
|
-
...discoverSourceFiles(configDir2, accountLogDir2, "cloudflared")
|
|
1383
|
-
];
|
|
1384
|
-
}
|
|
1385
|
-
function readNewLines(filepath, prev) {
|
|
1386
|
-
if (!existsSync3(filepath)) return null;
|
|
1387
|
-
const stat7 = statSync3(filepath);
|
|
1388
|
-
const size = stat7.size;
|
|
1389
|
-
const inode = stat7.ino;
|
|
1390
|
-
let startOffset = 0;
|
|
1391
|
-
let rotated = false;
|
|
1392
|
-
let truncated = false;
|
|
1393
|
-
if (prev) {
|
|
1394
|
-
if (prev.inode !== inode) {
|
|
1395
|
-
rotated = true;
|
|
1396
|
-
startOffset = 0;
|
|
1397
|
-
} else if (size < prev.offset) {
|
|
1398
|
-
truncated = true;
|
|
1399
|
-
startOffset = 0;
|
|
1400
|
-
} else {
|
|
1401
|
-
startOffset = prev.offset;
|
|
1402
|
-
}
|
|
1403
|
-
}
|
|
1404
|
-
if (startOffset >= size) {
|
|
1405
|
-
return {
|
|
1406
|
-
lines: [],
|
|
1407
|
-
entry: { offset: size, size, inode },
|
|
1408
|
-
rotated,
|
|
1409
|
-
truncated
|
|
1410
|
-
};
|
|
1411
|
-
}
|
|
1412
|
-
const fd = openSync(filepath, "r");
|
|
1413
|
-
try {
|
|
1414
|
-
const bufSize = Math.max(0, size - startOffset);
|
|
1415
|
-
const buf = Buffer.alloc(bufSize);
|
|
1416
|
-
if (bufSize > 0) {
|
|
1417
|
-
readSync(fd, buf, 0, bufSize, startOffset);
|
|
1418
|
-
}
|
|
1419
|
-
const text = buf.toString("utf-8");
|
|
1420
|
-
const lines = text.length > 0 ? text.split("\n") : [];
|
|
1421
|
-
let newOffset = size;
|
|
1422
|
-
if (lines.length > 0 && !text.endsWith("\n")) {
|
|
1423
|
-
const partial = lines.pop();
|
|
1424
|
-
newOffset = size - Buffer.byteLength(partial, "utf-8");
|
|
1425
|
-
} else if (lines.length > 0 && text.endsWith("\n")) {
|
|
1426
|
-
if (lines[lines.length - 1] === "") lines.pop();
|
|
1427
|
-
}
|
|
1428
|
-
return {
|
|
1429
|
-
lines,
|
|
1430
|
-
entry: { offset: newOffset, size, inode },
|
|
1431
|
-
rotated,
|
|
1432
|
-
truncated
|
|
1433
|
-
};
|
|
1434
|
-
} finally {
|
|
1435
|
-
closeSync(fd);
|
|
1436
|
-
}
|
|
1437
|
-
}
|
|
1438
|
-
function countRecentWrites(dir, sinceMs) {
|
|
1439
|
-
if (!existsSync3(dir)) return 0;
|
|
1440
|
-
let count = 0;
|
|
1441
|
-
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
1442
|
-
if (!entry.isFile()) continue;
|
|
1443
|
-
try {
|
|
1444
|
-
const st = statSync3(join2(dir, entry.name));
|
|
1445
|
-
if (st.mtimeMs >= sinceMs) count += 1;
|
|
1446
|
-
} catch {
|
|
1447
|
-
}
|
|
1448
|
-
}
|
|
1449
|
-
return count;
|
|
1450
|
-
}
|
|
1451
|
-
function fileLastWriteMs(path2) {
|
|
1452
|
-
if (!existsSync3(path2)) return null;
|
|
1453
|
-
try {
|
|
1454
|
-
return statSync3(path2).mtimeMs;
|
|
1455
|
-
} catch {
|
|
1456
|
-
return null;
|
|
1457
|
-
}
|
|
1458
|
-
}
|
|
1459
|
-
function accountLogDir(accountDir) {
|
|
1460
|
-
return resolve2(accountDir, "logs");
|
|
1461
|
-
}
|
|
1462
|
-
function sourceKey(file) {
|
|
1463
|
-
return `${file.logicalSource}:${basename(file.filepath)}`;
|
|
1464
|
-
}
|
|
1465
|
-
|
|
1466
|
-
// app/lib/review-detector/writer.ts
|
|
1467
|
-
import { appendFileSync, existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3, renameSync as renameSync3, statSync as statSync4 } from "fs";
|
|
1468
|
-
import { resolve as resolve3, dirname as dirname3 } from "path";
|
|
1469
|
-
import { randomUUID } from "crypto";
|
|
1470
|
-
function reviewLogPath(configDir2) {
|
|
1471
|
-
return resolve3(configDir2, "logs", "review.log");
|
|
1472
|
-
}
|
|
1473
|
-
function pendingAlertsPath(configDir2) {
|
|
1474
|
-
return resolve3(configDir2, "review-pending-alerts.jsonl");
|
|
1475
|
-
}
|
|
1476
|
-
function reviewLog(configDir2, event) {
|
|
1477
|
-
const path2 = reviewLogPath(configDir2);
|
|
1478
|
-
try {
|
|
1479
|
-
mkdirSync3(dirname3(path2), { recursive: true });
|
|
1480
|
-
const line = `${new Date(
|
|
1481
|
-
typeof event.ts === "number" ? event.ts : Date.now()
|
|
1482
|
-
).toISOString()} [review] ${JSON.stringify(event)}
|
|
1483
|
-
`;
|
|
1484
|
-
appendFileSync(path2, line, "utf-8");
|
|
1485
|
-
} catch (err) {
|
|
1486
|
-
console.error(`[review] failed to write review log at ${path2}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1487
|
-
}
|
|
1488
|
-
}
|
|
1489
|
-
async function ensureReviewAlertIndex() {
|
|
1490
|
-
const session = getSession();
|
|
1491
|
-
try {
|
|
1492
|
-
await session.run(
|
|
1493
|
-
`CREATE INDEX review_alert_lookup IF NOT EXISTS
|
|
1494
|
-
FOR (a:ReviewAlert)
|
|
1495
|
-
ON (a.accountId, a.resolvedAt, a.lastMatchAt)`
|
|
1496
|
-
);
|
|
1497
|
-
} finally {
|
|
1498
|
-
await session.close();
|
|
1499
|
-
}
|
|
1500
|
-
}
|
|
1501
|
-
async function ensureReviewDigestSchedule(accountId) {
|
|
1502
|
-
const eventId = `review-digest-${accountId}`;
|
|
1503
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1504
|
-
const next = /* @__PURE__ */ new Date();
|
|
1505
|
-
if (next.getHours() >= 8) {
|
|
1506
|
-
next.setDate(next.getDate() + 1);
|
|
1507
|
-
}
|
|
1508
|
-
next.setHours(8, 0, 0, 0);
|
|
1509
|
-
const nextRun = next.toISOString();
|
|
1510
|
-
const session = getSession();
|
|
1511
|
-
try {
|
|
1512
|
-
await session.run(
|
|
1513
|
-
`MERGE (e:Event { eventId: $eventId })
|
|
1514
|
-
ON CREATE SET
|
|
1515
|
-
e.accountId = $accountId,
|
|
1516
|
-
e.name = 'Daily review digest',
|
|
1517
|
-
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.',
|
|
1518
|
-
e.startDate = $now,
|
|
1519
|
-
e.eventStatus = 'scheduled',
|
|
1520
|
-
e.recurrence = '0 8 * * *',
|
|
1521
|
-
e.nextRun = datetime($nextRun),
|
|
1522
|
-
e.sourcePlugin = 'admin',
|
|
1523
|
-
e.actionPlugin = 'admin',
|
|
1524
|
-
e.actionTool = 'review-digest-compose',
|
|
1525
|
-
e.actionArgs = '{}',
|
|
1526
|
-
e.createdAt = $now,
|
|
1527
|
-
e.updatedAt = $now
|
|
1528
|
-
ON MATCH SET
|
|
1529
|
-
e.actionPlugin = 'admin',
|
|
1530
|
-
e.actionTool = 'review-digest-compose',
|
|
1531
|
-
e.updatedAt = $now`,
|
|
1532
|
-
{ eventId, accountId, now, nextRun }
|
|
1533
|
-
);
|
|
1534
|
-
} finally {
|
|
1535
|
-
await session.close();
|
|
1536
|
-
}
|
|
1537
|
-
}
|
|
1538
|
-
async function countActiveReviewAlerts(accountId) {
|
|
1539
|
-
const session = getSession();
|
|
1540
|
-
try {
|
|
1541
|
-
const result = await session.run(
|
|
1542
|
-
`MATCH (a:ReviewAlert {accountId: $accountId})
|
|
1543
|
-
WHERE a.resolvedAt IS NULL
|
|
1544
|
-
AND (a.suppressedUntil IS NULL OR a.suppressedUntil < datetime())
|
|
1545
|
-
RETURN count(a) AS n`,
|
|
1546
|
-
{ accountId }
|
|
1547
|
-
);
|
|
1548
|
-
const n = result.records[0]?.get("n");
|
|
1549
|
-
if (typeof n === "number") return n;
|
|
1550
|
-
if (n && typeof n.toNumber === "function") {
|
|
1551
|
-
return n.toNumber();
|
|
1552
|
-
}
|
|
1553
|
-
return 0;
|
|
1554
|
-
} finally {
|
|
1555
|
-
await session.close();
|
|
1556
|
-
}
|
|
1557
|
-
}
|
|
1558
|
-
async function upsertReviewAlert(accountId, match) {
|
|
1559
|
-
const session = getSession();
|
|
1560
|
-
try {
|
|
1561
|
-
await session.run(
|
|
1562
|
-
`MERGE (a:ReviewAlert { ruleId: $ruleId, accountId: $accountId })
|
|
1563
|
-
ON CREATE SET
|
|
1564
|
-
a.alertId = $alertId,
|
|
1565
|
-
a.ruleName = $ruleName,
|
|
1566
|
-
a.firstMatchAt = datetime($matchedAt),
|
|
1567
|
-
a.lastMatchAt = datetime($matchedAt),
|
|
1568
|
-
a.cumulativeMatchCount = 1,
|
|
1569
|
-
a.sampleEvidence = $sampleEvidence,
|
|
1570
|
-
a.suggestedAction = $suggestedAction,
|
|
1571
|
-
a.suppressedUntil = null,
|
|
1572
|
-
a.resolvedAt = null
|
|
1573
|
-
ON MATCH SET
|
|
1574
|
-
a.lastMatchAt = datetime($matchedAt),
|
|
1575
|
-
a.cumulativeMatchCount = a.cumulativeMatchCount + 1,
|
|
1576
|
-
a.sampleEvidence = $sampleEvidence,
|
|
1577
|
-
a.suggestedAction = $suggestedAction,
|
|
1578
|
-
a.resolvedAt = null`,
|
|
1579
|
-
{
|
|
1580
|
-
ruleId: match.ruleId,
|
|
1581
|
-
accountId,
|
|
1582
|
-
alertId: randomUUID(),
|
|
1583
|
-
ruleName: match.ruleName,
|
|
1584
|
-
matchedAt: new Date(match.matchedAt).toISOString(),
|
|
1585
|
-
sampleEvidence: match.sampleEvidence,
|
|
1586
|
-
suggestedAction: match.suggestedAction
|
|
1587
|
-
}
|
|
1588
|
-
);
|
|
1589
|
-
} finally {
|
|
1590
|
-
await session.close();
|
|
1591
|
-
}
|
|
1592
|
-
}
|
|
1593
|
-
function queueAlert(configDir2, accountId, match) {
|
|
1594
|
-
const path2 = pendingAlertsPath(configDir2);
|
|
1595
|
-
try {
|
|
1596
|
-
mkdirSync3(dirname3(path2), { recursive: true });
|
|
1597
|
-
const line = JSON.stringify({ accountId, match }) + "\n";
|
|
1598
|
-
appendFileSync(path2, line, "utf-8");
|
|
1599
|
-
} catch (err) {
|
|
1600
|
-
console.error(`[review] failed to queue alert at ${path2}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1601
|
-
}
|
|
1602
|
-
}
|
|
1603
|
-
async function drainPendingAlerts(configDir2) {
|
|
1604
|
-
const path2 = pendingAlertsPath(configDir2);
|
|
1605
|
-
if (!existsSync4(path2)) return { drained: 0, remaining: 0 };
|
|
1606
|
-
const raw = readFileSync3(path2, "utf-8");
|
|
1607
|
-
const lines = raw.split("\n").filter((l) => l.trim().length > 0);
|
|
1608
|
-
if (lines.length === 0) return { drained: 0, remaining: 0 };
|
|
1609
|
-
const remaining = [];
|
|
1610
|
-
let drained = 0;
|
|
1611
|
-
for (const line of lines) {
|
|
1612
|
-
let entry = null;
|
|
1613
|
-
try {
|
|
1614
|
-
entry = JSON.parse(line);
|
|
1615
|
-
} catch {
|
|
1616
|
-
continue;
|
|
1617
|
-
}
|
|
1618
|
-
try {
|
|
1619
|
-
await upsertReviewAlert(entry.accountId, entry.match);
|
|
1620
|
-
drained += 1;
|
|
1621
|
-
} catch {
|
|
1622
|
-
remaining.push(line);
|
|
1623
|
-
}
|
|
1624
|
-
}
|
|
1625
|
-
const tmp = `${path2}.tmp.${process.pid}.${Date.now()}`;
|
|
1626
|
-
if (remaining.length > 0) {
|
|
1627
|
-
writeFileSync3(tmp, remaining.join("\n") + "\n", "utf-8");
|
|
1628
|
-
} else {
|
|
1629
|
-
writeFileSync3(tmp, "", "utf-8");
|
|
1630
|
-
}
|
|
1631
|
-
renameSync3(tmp, path2);
|
|
1632
|
-
return { drained, remaining: remaining.length };
|
|
1633
|
-
}
|
|
1634
|
-
|
|
1635
|
-
// app/lib/review-detector/boot.ts
|
|
1636
|
-
async function bootDetector() {
|
|
1637
|
-
const account = resolveAccount();
|
|
1638
|
-
if (!account) {
|
|
1639
|
-
console.error("[review] boot: no account resolved \u2014 detector not starting (Phase 0 expects exactly one account)");
|
|
1640
|
-
return null;
|
|
1641
|
-
}
|
|
1642
|
-
const configDir2 = MAXY_DIR;
|
|
1643
|
-
const accountId = account.accountId;
|
|
1644
|
-
const accountDir = account.accountDir;
|
|
1645
|
-
const ensured = ensureRulesFile(configDir2);
|
|
1646
|
-
if (ensured.created) {
|
|
1647
|
-
reviewLog(configDir2, { event: "rules-defaults-created", path: ensured.path });
|
|
1648
|
-
}
|
|
1649
|
-
let rulesFile;
|
|
1650
|
-
try {
|
|
1651
|
-
rulesFile = loadRules(configDir2);
|
|
1652
|
-
} catch (err) {
|
|
1653
|
-
reviewLog(configDir2, {
|
|
1654
|
-
event: "boot-failed",
|
|
1655
|
-
reason: "rules-invalid",
|
|
1656
|
-
error: err instanceof Error ? err.message : String(err)
|
|
1657
|
-
});
|
|
1658
|
-
console.error(`[review] boot: rules file invalid \u2014 ${err instanceof Error ? err.message : String(err)}`);
|
|
1659
|
-
return null;
|
|
1660
|
-
}
|
|
1661
|
-
if (addMissingDefaultRules(rulesFile)) {
|
|
1662
|
-
saveRules(configDir2, rulesFile);
|
|
1663
|
-
reviewLog(configDir2, { event: "rules-updated", update: "added-missing-defaults" });
|
|
1664
|
-
console.error("[review] boot: added missing default rules to existing rules file");
|
|
1665
|
-
}
|
|
1666
|
-
if (migrateRateLimitPattern(rulesFile)) {
|
|
1667
|
-
saveRules(configDir2, rulesFile);
|
|
1668
|
-
reviewLog(configDir2, { event: "rules-migrated", migration: "rate-limit-pattern-v2" });
|
|
1669
|
-
console.error("[review] boot: migrated http-rate-limit-429 pattern to v2 (Task 408)");
|
|
1670
|
-
}
|
|
1671
|
-
try {
|
|
1672
|
-
await ensureReviewAlertIndex();
|
|
1673
|
-
reviewLog(configDir2, { event: "neo4j-index-ensured" });
|
|
1674
|
-
} catch (err) {
|
|
1675
|
-
reviewLog(configDir2, {
|
|
1676
|
-
event: "neo4j-index-failed",
|
|
1677
|
-
error: err instanceof Error ? err.message : String(err)
|
|
1678
|
-
});
|
|
1679
|
-
console.error(`[review] boot: ensureReviewAlertIndex failed (continuing): ${err instanceof Error ? err.message : String(err)}`);
|
|
1680
|
-
}
|
|
1681
|
-
try {
|
|
1682
|
-
await ensureReviewDigestSchedule(accountId);
|
|
1683
|
-
reviewLog(configDir2, { event: "digest-schedule-ensured" });
|
|
1684
|
-
} catch (err) {
|
|
1685
|
-
reviewLog(configDir2, {
|
|
1686
|
-
event: "digest-schedule-failed",
|
|
1687
|
-
error: err instanceof Error ? err.message : String(err)
|
|
1688
|
-
});
|
|
1689
|
-
console.error(`[review] boot: ensureReviewDigestSchedule failed (continuing): ${err instanceof Error ? err.message : String(err)}`);
|
|
1690
|
-
}
|
|
1691
|
-
const tailState = loadTailState(configDir2);
|
|
1692
|
-
const logDir = accountLogDir(accountDir);
|
|
1693
|
-
const initialSources = discoverAllSources(configDir2, logDir);
|
|
1694
|
-
const snapshot = {
|
|
1695
|
-
state: "running",
|
|
1696
|
-
startedAt: Date.now(),
|
|
1697
|
-
lastScanAt: null,
|
|
1698
|
-
lastScanDurationMs: null,
|
|
1699
|
-
scanCycles: 0,
|
|
1700
|
-
rulesLoaded: rulesFile.rules.length,
|
|
1701
|
-
sourcesTracked: initialSources.length,
|
|
1702
|
-
activeAlerts: 0,
|
|
1703
|
-
lastError: null
|
|
1704
|
-
};
|
|
1705
|
-
reviewLog(configDir2, {
|
|
1706
|
-
event: "detector-started",
|
|
1707
|
-
configDir: basename2(configDir2),
|
|
1708
|
-
accountId: accountId.slice(0, 8),
|
|
1709
|
-
rulesLoaded: rulesFile.rules.length,
|
|
1710
|
-
sourcesDiscovered: initialSources.length,
|
|
1711
|
-
tailStateEntries: Object.keys(tailState).length,
|
|
1712
|
-
scanIntervalMs: rulesFile.scanIntervalMs
|
|
1713
|
-
});
|
|
1714
|
-
const runtime = {
|
|
1715
|
-
configDir: configDir2,
|
|
1716
|
-
accountId,
|
|
1717
|
-
accountDir,
|
|
1718
|
-
rulesFile,
|
|
1719
|
-
rulesFileMtimeMs: rulesFileMtime(configDir2) ?? Date.now(),
|
|
1720
|
-
ruleState: /* @__PURE__ */ new Map(),
|
|
1721
|
-
tailState,
|
|
1722
|
-
snapshot,
|
|
1723
|
-
stopped: false
|
|
1724
|
-
};
|
|
1725
|
-
return runtime;
|
|
1726
|
-
}
|
|
1727
|
-
|
|
1728
|
-
// app/lib/review-detector/scan-loop.ts
|
|
1729
|
-
import { resolve as resolve4 } from "path";
|
|
1730
|
-
|
|
1731
|
-
// app/lib/review-detector/evaluator.ts
|
|
1732
|
-
var SAMPLE_MAX_CHARS = 500;
|
|
1733
|
-
var CONV_ID_REGEX = /conversationId=([a-f0-9]{8})/;
|
|
1734
|
-
function scopeKeyFor(rule, line) {
|
|
1735
|
-
if (rule.scope !== "session") return "";
|
|
1736
|
-
const m = CONV_ID_REGEX.exec(line);
|
|
1737
|
-
return m ? m[1] : "";
|
|
1738
|
-
}
|
|
1739
|
-
var compiledRegexCache = /* @__PURE__ */ new Map();
|
|
1740
|
-
function compileRegex(pattern) {
|
|
1741
|
-
let re = compiledRegexCache.get(pattern);
|
|
1742
|
-
if (re === void 0) {
|
|
1743
|
-
re = new RegExp(pattern, "i");
|
|
1744
|
-
compiledRegexCache.set(pattern, re);
|
|
1745
|
-
}
|
|
1746
|
-
return re;
|
|
1747
|
-
}
|
|
1748
|
-
function isSuppressed(rule, nowMs) {
|
|
1749
|
-
if (!rule.suppressedUntil) return false;
|
|
1750
|
-
const until = Date.parse(rule.suppressedUntil);
|
|
1751
|
-
if (Number.isNaN(until)) return false;
|
|
1752
|
-
return nowMs < until;
|
|
1753
|
-
}
|
|
1754
|
-
function toSample(line) {
|
|
1755
|
-
if (line.length <= SAMPLE_MAX_CHARS) return line;
|
|
1756
|
-
return line.slice(0, SAMPLE_MAX_CHARS) + "\u2026";
|
|
1757
|
-
}
|
|
1758
|
-
function newRuleState() {
|
|
1759
|
-
return {
|
|
1760
|
-
matchTimestamps: [],
|
|
1761
|
-
matchTimestampsByScope: /* @__PURE__ */ new Map(),
|
|
1762
|
-
lastAlertAt: null,
|
|
1763
|
-
cumulativeSinceLastAlert: 0,
|
|
1764
|
-
lastSeenAt: null,
|
|
1765
|
-
pendingFollowups: []
|
|
1766
|
-
};
|
|
1767
|
-
}
|
|
1768
|
-
function evaluateTextRule(rule, lines, state, nowMs) {
|
|
1769
|
-
if (isSuppressed(rule, nowMs)) return { match: null, state };
|
|
1770
|
-
if (rule.pattern.length === 0) return { match: null, state };
|
|
1771
|
-
const regex = compileRegex(rule.pattern);
|
|
1772
|
-
const matchesByScope = /* @__PURE__ */ new Map();
|
|
1773
|
-
let firstSample = null;
|
|
1774
|
-
for (const line of lines) {
|
|
1775
|
-
if (!regex.test(line)) continue;
|
|
1776
|
-
if (!firstSample) firstSample = line;
|
|
1777
|
-
const key = scopeKeyFor(rule, line);
|
|
1778
|
-
const existing = matchesByScope.get(key) ?? [];
|
|
1779
|
-
existing.push(line);
|
|
1780
|
-
matchesByScope.set(key, existing);
|
|
1781
|
-
}
|
|
1782
|
-
if (matchesByScope.size === 0) return { match: null, state };
|
|
1783
|
-
const windowStart = rule.thresholdWindowMinutes > 0 ? nowMs - rule.thresholdWindowMinutes * 6e4 : -Infinity;
|
|
1784
|
-
const updated = {
|
|
1785
|
-
...state,
|
|
1786
|
-
matchTimestamps: [...state.matchTimestamps],
|
|
1787
|
-
matchTimestampsByScope: new Map(state.matchTimestampsByScope ?? [])
|
|
1788
|
-
};
|
|
1789
|
-
let firingSample = null;
|
|
1790
|
-
let fires = false;
|
|
1791
|
-
const isCountZero = rule.thresholdCount === 0;
|
|
1792
|
-
if (rule.scope === "session") {
|
|
1793
|
-
for (const [key, hits] of matchesByScope) {
|
|
1794
|
-
const prior = updated.matchTimestampsByScope.get(key) ?? [];
|
|
1795
|
-
const merged = [...prior, ...hits.map(() => nowMs)].filter((t) => t >= windowStart);
|
|
1796
|
-
if (merged.length === 0) {
|
|
1797
|
-
updated.matchTimestampsByScope.delete(key);
|
|
1798
|
-
} else {
|
|
1799
|
-
updated.matchTimestampsByScope.set(key, merged);
|
|
1800
|
-
}
|
|
1801
|
-
if (!fires && (isCountZero || merged.length >= rule.thresholdCount)) {
|
|
1802
|
-
fires = true;
|
|
1803
|
-
firingSample = hits[0];
|
|
1804
|
-
}
|
|
1805
|
-
}
|
|
1806
|
-
} else {
|
|
1807
|
-
for (const hits of matchesByScope.values()) {
|
|
1808
|
-
for (const _ of hits) updated.matchTimestamps.push(nowMs);
|
|
1809
|
-
}
|
|
1810
|
-
updated.matchTimestamps = updated.matchTimestamps.filter((t) => t >= windowStart);
|
|
1811
|
-
fires = isCountZero || updated.matchTimestamps.length >= rule.thresholdCount;
|
|
1812
|
-
if (fires) firingSample = firstSample;
|
|
1813
|
-
}
|
|
1814
|
-
if (!fires) {
|
|
1815
|
-
return { match: null, state: updated };
|
|
1816
|
-
}
|
|
1817
|
-
const match = {
|
|
1818
|
-
ruleId: rule.id,
|
|
1819
|
-
ruleName: rule.name,
|
|
1820
|
-
matchedAt: nowMs,
|
|
1821
|
-
sampleEvidence: toSample(firingSample ?? firstSample ?? lines[0] ?? ""),
|
|
1822
|
-
suggestedAction: rule.suggestedAction
|
|
1823
|
-
};
|
|
1824
|
-
return { match, state: updated };
|
|
1825
|
-
}
|
|
1826
|
-
function evaluateFileWriteStormRule(rule, recentWriteCount, state, nowMs) {
|
|
1827
|
-
if (isSuppressed(rule, nowMs)) return { match: null, state };
|
|
1828
|
-
if (recentWriteCount < rule.thresholdCount) {
|
|
1829
|
-
return { match: null, state };
|
|
1830
|
-
}
|
|
1831
|
-
const match = {
|
|
1832
|
-
ruleId: rule.id,
|
|
1833
|
-
ruleName: rule.name,
|
|
1834
|
-
matchedAt: nowMs,
|
|
1835
|
-
sampleEvidence: `${recentWriteCount} file writes in ${rule.thresholdWindowMinutes}m at ${rule.watchPath}`,
|
|
1836
|
-
suggestedAction: rule.suggestedAction
|
|
1837
|
-
};
|
|
1838
|
-
return { match, state };
|
|
1839
|
-
}
|
|
1840
|
-
function evaluateStaleLogRule(rule, lastMtimeMs, state, nowMs) {
|
|
1841
|
-
if (isSuppressed(rule, nowMs)) return { match: null, state };
|
|
1842
|
-
let lastSeenAt = state.lastSeenAt;
|
|
1843
|
-
if (lastMtimeMs !== null) {
|
|
1844
|
-
lastSeenAt = Math.max(lastSeenAt ?? 0, lastMtimeMs);
|
|
1845
|
-
}
|
|
1846
|
-
const updated = { ...state, lastSeenAt };
|
|
1847
|
-
if (lastMtimeMs === null || lastSeenAt === null) {
|
|
1848
|
-
return { match: null, state: updated };
|
|
1849
|
-
}
|
|
1850
|
-
const staleMs = (rule.staleHours ?? 24) * 60 * 60 * 1e3;
|
|
1851
|
-
if (nowMs - lastSeenAt < staleMs) {
|
|
1852
|
-
return { match: null, state: updated };
|
|
1853
|
-
}
|
|
1854
|
-
const match = {
|
|
1855
|
-
ruleId: rule.id,
|
|
1856
|
-
ruleName: rule.name,
|
|
1857
|
-
matchedAt: nowMs,
|
|
1858
|
-
sampleEvidence: `last write at ${new Date(lastSeenAt).toISOString()} (${rule.watchPath})`,
|
|
1859
|
-
suggestedAction: rule.suggestedAction
|
|
1860
|
-
};
|
|
1861
|
-
return { match, state: updated };
|
|
1862
|
-
}
|
|
1863
|
-
function evaluateAbsentFollowupRule(rule, lines, state, nowMs) {
|
|
1864
|
-
if (isSuppressed(rule, nowMs)) return { matches: [], state };
|
|
1865
|
-
if (!rule.pattern || !rule.followupPattern || !rule.followupWindowMs) {
|
|
1866
|
-
return { matches: [], state };
|
|
1867
|
-
}
|
|
1868
|
-
const triggerRegex = compileRegex(rule.pattern);
|
|
1869
|
-
const followupRegex = compileRegex(rule.followupPattern);
|
|
1870
|
-
const pending = [...state.pendingFollowups ?? []];
|
|
1871
|
-
for (const line of lines) {
|
|
1872
|
-
if (triggerRegex.test(line)) {
|
|
1873
|
-
pending.push({
|
|
1874
|
-
scope: scopeKeyFor(rule, line),
|
|
1875
|
-
timestamp: nowMs,
|
|
1876
|
-
line,
|
|
1877
|
-
fulfilled: false
|
|
1878
|
-
});
|
|
1879
|
-
continue;
|
|
1880
|
-
}
|
|
1881
|
-
if (followupRegex.test(line)) {
|
|
1882
|
-
const scope = scopeKeyFor(rule, line);
|
|
1883
|
-
for (const entry of pending) {
|
|
1884
|
-
if (!entry.fulfilled && entry.scope === scope) {
|
|
1885
|
-
entry.fulfilled = true;
|
|
1886
|
-
break;
|
|
1887
|
-
}
|
|
1888
|
-
}
|
|
1889
|
-
}
|
|
1890
|
-
}
|
|
1891
|
-
const matches = [];
|
|
1892
|
-
const kept = [];
|
|
1893
|
-
for (const entry of pending) {
|
|
1894
|
-
const age = nowMs - entry.timestamp;
|
|
1895
|
-
if (age >= rule.followupWindowMs) {
|
|
1896
|
-
if (!entry.fulfilled) {
|
|
1897
|
-
matches.push({
|
|
1898
|
-
ruleId: rule.id,
|
|
1899
|
-
ruleName: rule.name,
|
|
1900
|
-
matchedAt: nowMs,
|
|
1901
|
-
sampleEvidence: toSample(entry.line),
|
|
1902
|
-
suggestedAction: rule.suggestedAction,
|
|
1903
|
-
missedForMs: age
|
|
1904
|
-
});
|
|
1905
|
-
}
|
|
1906
|
-
continue;
|
|
1907
|
-
}
|
|
1908
|
-
kept.push(entry);
|
|
1909
|
-
}
|
|
1910
|
-
return {
|
|
1911
|
-
matches,
|
|
1912
|
-
state: { ...state, pendingFollowups: kept }
|
|
1913
|
-
};
|
|
1914
|
-
}
|
|
1915
|
-
var ALERT_WINDOW_MS = 60 * 60 * 1e3;
|
|
1916
|
-
function rateLimitDecision(state, nowMs) {
|
|
1917
|
-
const since = state.lastAlertAt === null ? Infinity : nowMs - state.lastAlertAt;
|
|
1918
|
-
if (since >= ALERT_WINDOW_MS) {
|
|
1919
|
-
return {
|
|
1920
|
-
surface: true,
|
|
1921
|
-
state: { ...state, lastAlertAt: nowMs, cumulativeSinceLastAlert: 0 }
|
|
1922
|
-
};
|
|
1923
|
-
}
|
|
1924
|
-
return {
|
|
1925
|
-
surface: false,
|
|
1926
|
-
state: { ...state, cumulativeSinceLastAlert: state.cumulativeSinceLastAlert + 1 }
|
|
1927
|
-
};
|
|
1928
|
-
}
|
|
1929
|
-
|
|
1930
|
-
// app/lib/review-detector/scan-loop.ts
|
|
1931
|
-
async function runScanCycle(runtime) {
|
|
1932
|
-
const cycleStart = Date.now();
|
|
1933
|
-
runtime.snapshot.scanCycles += 1;
|
|
1934
|
-
try {
|
|
1935
|
-
const currentMtime = rulesFileMtime(runtime.configDir);
|
|
1936
|
-
if (currentMtime !== null && currentMtime !== runtime.rulesFileMtimeMs) {
|
|
1937
|
-
try {
|
|
1938
|
-
runtime.rulesFile = loadRules(runtime.configDir);
|
|
1939
|
-
runtime.rulesFileMtimeMs = currentMtime;
|
|
1940
|
-
runtime.snapshot.rulesLoaded = runtime.rulesFile.rules.length;
|
|
1941
|
-
reviewLog(runtime.configDir, {
|
|
1942
|
-
event: "rules-reloaded",
|
|
1943
|
-
rulesLoaded: runtime.rulesFile.rules.length,
|
|
1944
|
-
mtime: new Date(currentMtime).toISOString()
|
|
1945
|
-
});
|
|
1946
|
-
} catch (err) {
|
|
1947
|
-
reviewLog(runtime.configDir, {
|
|
1948
|
-
event: "rules-reload-failed",
|
|
1949
|
-
error: err instanceof Error ? err.message : String(err)
|
|
1950
|
-
});
|
|
1951
|
-
runtime.snapshot.state = "degraded";
|
|
1952
|
-
runtime.snapshot.lastError = err instanceof Error ? err.message : String(err);
|
|
1953
|
-
}
|
|
1954
|
-
}
|
|
1955
|
-
const logDir = accountLogDir(runtime.accountDir);
|
|
1956
|
-
const files = discoverAllSources(runtime.configDir, logDir);
|
|
1957
|
-
runtime.snapshot.sourcesTracked = files.length;
|
|
1958
|
-
const linesBySource = /* @__PURE__ */ new Map();
|
|
1959
|
-
for (const file of files) {
|
|
1960
|
-
const key = sourceKey(file);
|
|
1961
|
-
const prev = runtime.tailState[key];
|
|
1962
|
-
const result = readNewLines(file.filepath, prev);
|
|
1963
|
-
if (result === null) {
|
|
1964
|
-
if (prev) {
|
|
1965
|
-
reviewLog(runtime.configDir, {
|
|
1966
|
-
event: "source-vanished",
|
|
1967
|
-
source: file.logicalSource,
|
|
1968
|
-
filepath: file.filepath
|
|
1969
|
-
});
|
|
1970
|
-
delete runtime.tailState[key];
|
|
1971
|
-
}
|
|
1972
|
-
continue;
|
|
1973
|
-
}
|
|
1974
|
-
if (result.rotated) {
|
|
1975
|
-
reviewLog(runtime.configDir, {
|
|
1976
|
-
event: "source-rotated",
|
|
1977
|
-
source: file.logicalSource,
|
|
1978
|
-
filepath: file.filepath
|
|
1979
|
-
});
|
|
1980
|
-
}
|
|
1981
|
-
if (result.truncated) {
|
|
1982
|
-
reviewLog(runtime.configDir, {
|
|
1983
|
-
event: "source-truncated",
|
|
1984
|
-
source: file.logicalSource,
|
|
1985
|
-
filepath: file.filepath
|
|
1986
|
-
});
|
|
1987
|
-
}
|
|
1988
|
-
runtime.tailState[key] = result.entry;
|
|
1989
|
-
if (result.lines.length > 0) {
|
|
1990
|
-
const bucket = linesBySource.get(file.logicalSource) ?? [];
|
|
1991
|
-
bucket.push(...result.lines);
|
|
1992
|
-
linesBySource.set(file.logicalSource, bucket);
|
|
1993
|
-
}
|
|
1994
|
-
}
|
|
1995
|
-
const matches = [];
|
|
1996
|
-
for (const rule of runtime.rulesFile.rules) {
|
|
1997
|
-
let state = runtime.ruleState.get(rule.id) ?? newRuleState();
|
|
1998
|
-
if (isSuppressed(rule, cycleStart)) {
|
|
1999
|
-
reviewLog(runtime.configDir, {
|
|
2000
|
-
event: "rule-suppressed",
|
|
2001
|
-
ruleId: rule.id,
|
|
2002
|
-
suppressedUntil: rule.suppressedUntil
|
|
2003
|
-
});
|
|
2004
|
-
runtime.ruleState.set(rule.id, state);
|
|
2005
|
-
continue;
|
|
2006
|
-
}
|
|
2007
|
-
let match = null;
|
|
2008
|
-
if (rule.type === "reconnect-loop" || rule.type === "repeated-error" || rule.type === "silent-catch" || rule.type === "rate-limit") {
|
|
2009
|
-
let inputLines = [];
|
|
2010
|
-
if (rule.logSource === "any") {
|
|
2011
|
-
for (const [src, lines] of linesBySource.entries()) {
|
|
2012
|
-
if (src !== "config-dir") inputLines.push(...lines);
|
|
2013
|
-
}
|
|
2014
|
-
} else {
|
|
2015
|
-
inputLines = linesBySource.get(rule.logSource) ?? [];
|
|
2016
|
-
}
|
|
2017
|
-
if (inputLines.length > 0) {
|
|
2018
|
-
const result = evaluateTextRule(rule, inputLines, state, cycleStart);
|
|
2019
|
-
state = result.state;
|
|
2020
|
-
match = result.match;
|
|
2021
|
-
}
|
|
2022
|
-
} else if (rule.type === "file-write-storm") {
|
|
2023
|
-
const dir = resolve4(runtime.configDir, rule.watchPath ?? "");
|
|
2024
|
-
const sinceMs = cycleStart - rule.thresholdWindowMinutes * 6e4;
|
|
2025
|
-
const count = countRecentWrites(dir, sinceMs);
|
|
2026
|
-
const result = evaluateFileWriteStormRule(rule, count, state, cycleStart);
|
|
2027
|
-
state = result.state;
|
|
2028
|
-
match = result.match;
|
|
2029
|
-
} else if (rule.type === "stale-log") {
|
|
2030
|
-
const trackedPath = resolve4(runtime.configDir, rule.watchPath ?? "");
|
|
2031
|
-
const lastMs = fileLastWriteMs(trackedPath);
|
|
2032
|
-
const result = evaluateStaleLogRule(rule, lastMs, state, cycleStart);
|
|
2033
|
-
state = result.state;
|
|
2034
|
-
match = result.match;
|
|
2035
|
-
} else if (rule.type === "absent-followup") {
|
|
2036
|
-
let inputLines = [];
|
|
2037
|
-
if (rule.logSource === "any") {
|
|
2038
|
-
for (const [src, lines] of linesBySource.entries()) {
|
|
2039
|
-
if (src !== "config-dir") inputLines.push(...lines);
|
|
2040
|
-
}
|
|
2041
|
-
} else {
|
|
2042
|
-
inputLines = linesBySource.get(rule.logSource) ?? [];
|
|
2043
|
-
}
|
|
2044
|
-
const result = evaluateAbsentFollowupRule(rule, inputLines, state, cycleStart);
|
|
2045
|
-
state = result.state;
|
|
2046
|
-
for (const m of result.matches) {
|
|
2047
|
-
const safeTrigger = m.sampleEvidence.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
2048
|
-
console.error(
|
|
2049
|
-
`[review-detector] absent-followup rule=${rule.id} trigger="${safeTrigger}" missed_for_ms=${m.missedForMs ?? ""}`
|
|
2050
|
-
);
|
|
2051
|
-
matches.push(m);
|
|
2052
|
-
}
|
|
2053
|
-
}
|
|
2054
|
-
runtime.ruleState.set(rule.id, state);
|
|
2055
|
-
if (match) matches.push(match);
|
|
2056
|
-
}
|
|
2057
|
-
for (const match of matches) {
|
|
2058
|
-
const state = runtime.ruleState.get(match.ruleId) ?? newRuleState();
|
|
2059
|
-
const decision = rateLimitDecision(state, cycleStart);
|
|
2060
|
-
runtime.ruleState.set(match.ruleId, decision.state);
|
|
2061
|
-
if (!decision.surface) {
|
|
2062
|
-
reviewLog(runtime.configDir, {
|
|
2063
|
-
event: "rate-limit-deferred",
|
|
2064
|
-
ruleId: match.ruleId,
|
|
2065
|
-
cumulativeSinceLastAlert: decision.state.cumulativeSinceLastAlert
|
|
2066
|
-
});
|
|
2067
|
-
continue;
|
|
2068
|
-
}
|
|
2069
|
-
reviewLog(runtime.configDir, {
|
|
2070
|
-
event: "match",
|
|
2071
|
-
ruleId: match.ruleId,
|
|
2072
|
-
ruleName: match.ruleName,
|
|
2073
|
-
sampleEvidence: match.sampleEvidence,
|
|
2074
|
-
suggestedAction: match.suggestedAction
|
|
2075
|
-
});
|
|
2076
|
-
try {
|
|
2077
|
-
await upsertReviewAlert(runtime.accountId, match);
|
|
2078
|
-
reviewLog(runtime.configDir, { event: "alert-persisted", ruleId: match.ruleId });
|
|
2079
|
-
} catch (err) {
|
|
2080
|
-
queueAlert(runtime.configDir, runtime.accountId, match);
|
|
2081
|
-
reviewLog(runtime.configDir, {
|
|
2082
|
-
event: "alert-queued",
|
|
2083
|
-
ruleId: match.ruleId,
|
|
2084
|
-
error: err instanceof Error ? err.message : String(err)
|
|
2085
|
-
});
|
|
2086
|
-
}
|
|
2087
|
-
}
|
|
2088
|
-
try {
|
|
2089
|
-
const drain = await drainPendingAlerts(runtime.configDir);
|
|
2090
|
-
if (drain.drained > 0 || drain.remaining > 0) {
|
|
2091
|
-
reviewLog(runtime.configDir, {
|
|
2092
|
-
event: "queue-drain",
|
|
2093
|
-
drained: drain.drained,
|
|
2094
|
-
remaining: drain.remaining
|
|
2095
|
-
});
|
|
2096
|
-
}
|
|
2097
|
-
} catch (err) {
|
|
2098
|
-
reviewLog(runtime.configDir, {
|
|
2099
|
-
event: "queue-drain-failed",
|
|
2100
|
-
error: err instanceof Error ? err.message : String(err)
|
|
2101
|
-
});
|
|
2102
|
-
}
|
|
2103
|
-
try {
|
|
2104
|
-
runtime.snapshot.activeAlerts = await countActiveReviewAlerts(runtime.accountId);
|
|
2105
|
-
} catch (err) {
|
|
2106
|
-
reviewLog(runtime.configDir, {
|
|
2107
|
-
event: "active-alerts-count-failed",
|
|
2108
|
-
error: err instanceof Error ? err.message : String(err)
|
|
2109
|
-
});
|
|
2110
|
-
}
|
|
2111
|
-
saveTailState(runtime.configDir, runtime.tailState);
|
|
2112
|
-
const cycleDuration = Date.now() - cycleStart;
|
|
2113
|
-
runtime.snapshot.lastScanAt = cycleStart;
|
|
2114
|
-
runtime.snapshot.lastScanDurationMs = cycleDuration;
|
|
2115
|
-
if (runtime.snapshot.state !== "degraded" && runtime.snapshot.state !== "failed") {
|
|
2116
|
-
runtime.snapshot.state = "running";
|
|
2117
|
-
}
|
|
2118
|
-
runtime.snapshot.lastError = null;
|
|
2119
|
-
reviewLog(runtime.configDir, {
|
|
2120
|
-
event: "cycle",
|
|
2121
|
-
cycles: runtime.snapshot.scanCycles,
|
|
2122
|
-
sources: files.length,
|
|
2123
|
-
rules: runtime.rulesFile.rules.length,
|
|
2124
|
-
matches: matches.length,
|
|
2125
|
-
durationMs: cycleDuration
|
|
2126
|
-
});
|
|
2127
|
-
} catch (err) {
|
|
2128
|
-
runtime.snapshot.state = "degraded";
|
|
2129
|
-
runtime.snapshot.lastError = err instanceof Error ? err.message : String(err);
|
|
2130
|
-
reviewLog(runtime.configDir, {
|
|
2131
|
-
event: "cycle-failed",
|
|
2132
|
-
error: err instanceof Error ? err.message : String(err)
|
|
2133
|
-
});
|
|
2134
|
-
}
|
|
2135
|
-
}
|
|
2136
|
-
function startScanLoop(runtime) {
|
|
2137
|
-
let inFlight = null;
|
|
2138
|
-
const interval = setInterval(async () => {
|
|
2139
|
-
if (runtime.stopped) return;
|
|
2140
|
-
if (inFlight) return;
|
|
2141
|
-
inFlight = runScanCycle(runtime);
|
|
2142
|
-
try {
|
|
2143
|
-
await inFlight;
|
|
2144
|
-
} finally {
|
|
2145
|
-
inFlight = null;
|
|
2146
|
-
}
|
|
2147
|
-
}, runtime.rulesFile.scanIntervalMs);
|
|
2148
|
-
inFlight = runScanCycle(runtime);
|
|
2149
|
-
return async () => {
|
|
2150
|
-
runtime.stopped = true;
|
|
2151
|
-
clearInterval(interval);
|
|
2152
|
-
if (inFlight) {
|
|
2153
|
-
try {
|
|
2154
|
-
await inFlight;
|
|
2155
|
-
} catch {
|
|
2156
|
-
}
|
|
2157
|
-
}
|
|
2158
|
-
runtime.snapshot.state = "stopped";
|
|
2159
|
-
reviewLog(runtime.configDir, { event: "detector-stopped", cycles: runtime.snapshot.scanCycles });
|
|
2160
|
-
};
|
|
2161
|
-
}
|
|
2162
|
-
|
|
2163
|
-
// app/lib/review-detector/index.ts
|
|
2164
|
-
var activeRuntime = null;
|
|
2165
|
-
var stopFn = null;
|
|
2166
|
-
async function startReviewDetector() {
|
|
2167
|
-
if (stopFn) {
|
|
2168
|
-
console.error("[review] startReviewDetector called twice \u2014 ignoring second call");
|
|
2169
|
-
return;
|
|
2170
|
-
}
|
|
2171
|
-
try {
|
|
2172
|
-
activeRuntime = await bootDetector();
|
|
2173
|
-
if (!activeRuntime) return;
|
|
2174
|
-
stopFn = startScanLoop(activeRuntime);
|
|
2175
|
-
} catch (err) {
|
|
2176
|
-
console.error(`[review] detector start failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
2177
|
-
}
|
|
2178
|
-
}
|
|
2179
|
-
async function shutdownReviewDetector() {
|
|
2180
|
-
if (!stopFn) return;
|
|
2181
|
-
try {
|
|
2182
|
-
await stopFn();
|
|
2183
|
-
} catch (err) {
|
|
2184
|
-
console.error(`[review] detector shutdown error: ${err instanceof Error ? err.message : String(err)}`);
|
|
2185
|
-
}
|
|
2186
|
-
stopFn = null;
|
|
2187
|
-
activeRuntime = null;
|
|
2188
|
-
}
|
|
2189
|
-
function getReviewDetectorSnapshot() {
|
|
2190
|
-
if (!activeRuntime) {
|
|
2191
|
-
return {
|
|
2192
|
-
state: "stopped",
|
|
2193
|
-
startedAt: null,
|
|
2194
|
-
lastScanAt: null,
|
|
2195
|
-
lastScanDurationMs: null,
|
|
2196
|
-
scanCycles: 0,
|
|
2197
|
-
rulesLoaded: 0,
|
|
2198
|
-
sourcesTracked: 0,
|
|
2199
|
-
activeAlerts: 0,
|
|
2200
|
-
lastError: null
|
|
2201
|
-
};
|
|
2202
|
-
}
|
|
2203
|
-
return activeRuntime.snapshot;
|
|
2204
|
-
}
|
|
2205
|
-
|
|
2206
650
|
// app/lib/whatsapp/schema.ts
|
|
2207
651
|
import { z } from "zod";
|
|
2208
652
|
var DmPolicySchema = z.enum(["open", "allowlist", "disabled"]);
|
|
@@ -2267,20 +711,20 @@ var WhatsAppConfigSchema = z.object({
|
|
|
2267
711
|
});
|
|
2268
712
|
|
|
2269
713
|
// app/lib/whatsapp/config-persist.ts
|
|
2270
|
-
import { readFileSync
|
|
2271
|
-
import { resolve
|
|
714
|
+
import { readFileSync, writeFileSync, existsSync as existsSync2 } from "fs";
|
|
715
|
+
import { resolve, join as join2 } from "path";
|
|
2272
716
|
var TAG = "[whatsapp:config]";
|
|
2273
717
|
function configPath(accountDir) {
|
|
2274
|
-
return
|
|
718
|
+
return resolve(accountDir, "account.json");
|
|
2275
719
|
}
|
|
2276
720
|
function readConfig(accountDir) {
|
|
2277
721
|
const path2 = configPath(accountDir);
|
|
2278
|
-
if (!
|
|
2279
|
-
return JSON.parse(
|
|
722
|
+
if (!existsSync2(path2)) throw new Error(`account.json not found at ${path2}`);
|
|
723
|
+
return JSON.parse(readFileSync(path2, "utf-8"));
|
|
2280
724
|
}
|
|
2281
725
|
function writeConfig(accountDir, config) {
|
|
2282
726
|
const path2 = configPath(accountDir);
|
|
2283
|
-
|
|
727
|
+
writeFileSync(path2, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
2284
728
|
}
|
|
2285
729
|
function reloadManagerConfig(accountDir) {
|
|
2286
730
|
try {
|
|
@@ -2450,8 +894,8 @@ function setPublicAgent(accountDir, slug) {
|
|
|
2450
894
|
if (!trimmed) {
|
|
2451
895
|
return { ok: false, error: "Agent slug cannot be empty." };
|
|
2452
896
|
}
|
|
2453
|
-
const agentConfigPath =
|
|
2454
|
-
if (!
|
|
897
|
+
const agentConfigPath = join2(accountDir, "agents", trimmed, "config.json");
|
|
898
|
+
if (!existsSync2(agentConfigPath)) {
|
|
2455
899
|
return { ok: false, error: `Agent "${trimmed}" not found \u2014 no config.json at ${agentConfigPath}. Check the agent slug and try again.` };
|
|
2456
900
|
}
|
|
2457
901
|
try {
|
|
@@ -2514,8 +958,8 @@ function setGroupPublicAgent(accountDir, accountId, groupJid, slug) {
|
|
|
2514
958
|
if (!trimmedSlug) return { ok: false, error: "Agent slug cannot be empty." };
|
|
2515
959
|
if (!trimmedGroup) return { ok: false, error: "Group JID cannot be empty." };
|
|
2516
960
|
if (!trimmedAccount) return { ok: false, error: "Account ID cannot be empty." };
|
|
2517
|
-
const agentConfigPath =
|
|
2518
|
-
if (!
|
|
961
|
+
const agentConfigPath = join2(accountDir, "agents", trimmedSlug, "config.json");
|
|
962
|
+
if (!existsSync2(agentConfigPath)) {
|
|
2519
963
|
return { ok: false, error: `Agent "${trimmedSlug}" not found \u2014 no config.json at ${agentConfigPath}. Check the agent slug and try again.` };
|
|
2520
964
|
}
|
|
2521
965
|
try {
|
|
@@ -2744,7 +1188,7 @@ function listCredentialAccountIds(configDir2) {
|
|
|
2744
1188
|
}
|
|
2745
1189
|
|
|
2746
1190
|
// app/lib/whatsapp/session.ts
|
|
2747
|
-
import { randomUUID
|
|
1191
|
+
import { randomUUID } from "crypto";
|
|
2748
1192
|
import fsSync2 from "fs";
|
|
2749
1193
|
import fs2 from "fs/promises";
|
|
2750
1194
|
import { inspect } from "util";
|
|
@@ -2819,7 +1263,7 @@ var credsSaveQueue = Promise.resolve();
|
|
|
2819
1263
|
async function drainCredsSaveQueue(timeoutMs = 5e3) {
|
|
2820
1264
|
console.error(`${TAG3} draining credential save queue\u2026`);
|
|
2821
1265
|
const timer2 = new Promise(
|
|
2822
|
-
(
|
|
1266
|
+
(resolve22) => setTimeout(() => resolve22("timeout"), timeoutMs)
|
|
2823
1267
|
);
|
|
2824
1268
|
const result = await Promise.race([
|
|
2825
1269
|
credsSaveQueue.then(() => "drained"),
|
|
@@ -2947,11 +1391,11 @@ async function createWaSocket(opts) {
|
|
|
2947
1391
|
return sock;
|
|
2948
1392
|
}
|
|
2949
1393
|
async function waitForConnection(sock) {
|
|
2950
|
-
return new Promise((
|
|
1394
|
+
return new Promise((resolve22, reject) => {
|
|
2951
1395
|
const handler = (update) => {
|
|
2952
1396
|
if (update.connection === "open") {
|
|
2953
1397
|
sock.ev.off("connection.update", handler);
|
|
2954
|
-
|
|
1398
|
+
resolve22();
|
|
2955
1399
|
}
|
|
2956
1400
|
if (update.connection === "close") {
|
|
2957
1401
|
sock.ev.off("connection.update", handler);
|
|
@@ -3065,14 +1509,14 @@ ${inspected}`;
|
|
|
3065
1509
|
return inspect2(err, INSPECT_OPTS2);
|
|
3066
1510
|
}
|
|
3067
1511
|
function withTimeout(label, promise, timeoutMs) {
|
|
3068
|
-
return new Promise((
|
|
1512
|
+
return new Promise((resolve22, reject) => {
|
|
3069
1513
|
const timer2 = setTimeout(() => {
|
|
3070
1514
|
reject(new Error(`${label} timed out after ${timeoutMs}ms`));
|
|
3071
1515
|
}, timeoutMs);
|
|
3072
1516
|
promise.then(
|
|
3073
1517
|
(value) => {
|
|
3074
1518
|
clearTimeout(timer2);
|
|
3075
|
-
|
|
1519
|
+
resolve22(value);
|
|
3076
1520
|
},
|
|
3077
1521
|
(err) => {
|
|
3078
1522
|
clearTimeout(timer2);
|
|
@@ -3607,8 +2051,8 @@ async function persistWhatsAppMessage(input) {
|
|
|
3607
2051
|
const { givenName, familyName } = splitName(input.pushName);
|
|
3608
2052
|
const prev = sessionWriteLocks.get(input.sessionKey);
|
|
3609
2053
|
let release;
|
|
3610
|
-
const mine = new Promise((
|
|
3611
|
-
release =
|
|
2054
|
+
const mine = new Promise((resolve22) => {
|
|
2055
|
+
release = resolve22;
|
|
3612
2056
|
});
|
|
3613
2057
|
const chained = (prev ?? Promise.resolve()).then(() => mine);
|
|
3614
2058
|
sessionWriteLocks.set(input.sessionKey, chained);
|
|
@@ -3793,8 +2237,8 @@ async function ensureWhatsAppConversation(input) {
|
|
|
3793
2237
|
}
|
|
3794
2238
|
|
|
3795
2239
|
// app/lib/whatsapp/platform-account-id.ts
|
|
3796
|
-
import { readdirSync
|
|
3797
|
-
import { resolve as
|
|
2240
|
+
import { readdirSync, readFileSync as readFileSync2 } from "fs";
|
|
2241
|
+
import { resolve as resolve2 } from "path";
|
|
3798
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;
|
|
3799
2243
|
var cached = null;
|
|
3800
2244
|
var cachedAccountsDir = null;
|
|
@@ -3818,7 +2262,7 @@ function resolvePlatformAccountId(accountsDir = ACCOUNTS_DIR) {
|
|
|
3818
2262
|
function enumerateValidAccountIds(accountsDir) {
|
|
3819
2263
|
let names;
|
|
3820
2264
|
try {
|
|
3821
|
-
names =
|
|
2265
|
+
names = readdirSync(accountsDir);
|
|
3822
2266
|
} catch (err) {
|
|
3823
2267
|
if (err.code === "ENOENT") return [];
|
|
3824
2268
|
throw err;
|
|
@@ -3826,9 +2270,9 @@ function enumerateValidAccountIds(accountsDir) {
|
|
|
3826
2270
|
const valid = [];
|
|
3827
2271
|
for (const name of names) {
|
|
3828
2272
|
if (!UUID_RE.test(name)) continue;
|
|
3829
|
-
const configPath2 =
|
|
2273
|
+
const configPath2 = resolve2(accountsDir, name, "account.json");
|
|
3830
2274
|
try {
|
|
3831
|
-
JSON.parse(
|
|
2275
|
+
JSON.parse(readFileSync2(configPath2, "utf-8"));
|
|
3832
2276
|
valid.push(name);
|
|
3833
2277
|
} catch (err) {
|
|
3834
2278
|
const code = err.code;
|
|
@@ -3839,9 +2283,9 @@ function enumerateValidAccountIds(accountsDir) {
|
|
|
3839
2283
|
}
|
|
3840
2284
|
|
|
3841
2285
|
// app/lib/whatsapp/inbound/media.ts
|
|
3842
|
-
import { randomUUID as
|
|
2286
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
3843
2287
|
import { writeFile, mkdir } from "fs/promises";
|
|
3844
|
-
import { join as
|
|
2288
|
+
import { join as join3 } from "path";
|
|
3845
2289
|
import {
|
|
3846
2290
|
downloadMediaMessage,
|
|
3847
2291
|
downloadContentFromMessage,
|
|
@@ -3926,8 +2370,8 @@ async function downloadInboundMedia(msg, sock, opts) {
|
|
|
3926
2370
|
}
|
|
3927
2371
|
await mkdir(MEDIA_DIR, { recursive: true });
|
|
3928
2372
|
const ext = mimeToExt(mimetype ?? "application/octet-stream");
|
|
3929
|
-
const filename = `${
|
|
3930
|
-
const filePath =
|
|
2373
|
+
const filename = `${randomUUID2()}.${ext}`;
|
|
2374
|
+
const filePath = join3(MEDIA_DIR, filename);
|
|
3931
2375
|
await writeFile(filePath, buffer);
|
|
3932
2376
|
const sizeKB = (buffer.length / 1024).toFixed(0);
|
|
3933
2377
|
console.error(`${TAG9} media downloaded type=${mimetype ?? "unknown"} size=${sizeKB}KB path=${filePath}`);
|
|
@@ -4615,11 +3059,11 @@ async function connectWithReconnect(conn) {
|
|
|
4615
3059
|
console.error(
|
|
4616
3060
|
`${TAG13} reconnecting account=${conn.accountId} in ${delay}ms (attempt ${decision.nextAttempts}/${maxAttempts})`
|
|
4617
3061
|
);
|
|
4618
|
-
await new Promise((
|
|
4619
|
-
const timer2 = setTimeout(
|
|
3062
|
+
await new Promise((resolve22) => {
|
|
3063
|
+
const timer2 = setTimeout(resolve22, delay);
|
|
4620
3064
|
conn.abortController.signal.addEventListener("abort", () => {
|
|
4621
3065
|
clearTimeout(timer2);
|
|
4622
|
-
|
|
3066
|
+
resolve22();
|
|
4623
3067
|
}, { once: true });
|
|
4624
3068
|
});
|
|
4625
3069
|
}
|
|
@@ -4627,16 +3071,16 @@ async function connectWithReconnect(conn) {
|
|
|
4627
3071
|
}
|
|
4628
3072
|
}
|
|
4629
3073
|
function waitForDisconnectEvent(conn) {
|
|
4630
|
-
return new Promise((
|
|
3074
|
+
return new Promise((resolve22) => {
|
|
4631
3075
|
if (!conn.sock) {
|
|
4632
|
-
|
|
3076
|
+
resolve22();
|
|
4633
3077
|
return;
|
|
4634
3078
|
}
|
|
4635
3079
|
const sock = conn.sock;
|
|
4636
3080
|
const handler = (update) => {
|
|
4637
3081
|
if (update.connection === "close") {
|
|
4638
3082
|
sock.ev.off("connection.update", handler);
|
|
4639
|
-
|
|
3083
|
+
resolve22();
|
|
4640
3084
|
}
|
|
4641
3085
|
};
|
|
4642
3086
|
sock.ev.on("connection.update", handler);
|
|
@@ -4897,8 +3341,8 @@ async function handleInboundMessage(conn, msg) {
|
|
|
4897
3341
|
const conversationKey = isGroup ? remoteJid : senderPhone;
|
|
4898
3342
|
const debounceKey = `${conn.accountId}:${conversationKey}:${senderPhone}`;
|
|
4899
3343
|
let resolvePending;
|
|
4900
|
-
const sttPending = new Promise((
|
|
4901
|
-
resolvePending =
|
|
3344
|
+
const sttPending = new Promise((resolve22) => {
|
|
3345
|
+
resolvePending = resolve22;
|
|
4902
3346
|
});
|
|
4903
3347
|
if (conn.debouncer) conn.debouncer.registerPending(debounceKey, sttPending);
|
|
4904
3348
|
try {
|
|
@@ -5019,20 +3463,20 @@ async function probeApiKey() {
|
|
|
5019
3463
|
return result.status;
|
|
5020
3464
|
}
|
|
5021
3465
|
function checkPort(port2, timeoutMs = 500) {
|
|
5022
|
-
return new Promise((
|
|
3466
|
+
return new Promise((resolve22) => {
|
|
5023
3467
|
const socket = createConnection(port2, "127.0.0.1");
|
|
5024
3468
|
socket.setTimeout(timeoutMs);
|
|
5025
3469
|
socket.once("connect", () => {
|
|
5026
3470
|
socket.destroy();
|
|
5027
|
-
|
|
3471
|
+
resolve22(true);
|
|
5028
3472
|
});
|
|
5029
3473
|
socket.once("error", () => {
|
|
5030
3474
|
socket.destroy();
|
|
5031
|
-
|
|
3475
|
+
resolve22(false);
|
|
5032
3476
|
});
|
|
5033
3477
|
socket.once("timeout", () => {
|
|
5034
3478
|
socket.destroy();
|
|
5035
|
-
|
|
3479
|
+
resolve22(false);
|
|
5036
3480
|
});
|
|
5037
3481
|
});
|
|
5038
3482
|
}
|
|
@@ -5041,8 +3485,8 @@ app.get("/", async (c) => {
|
|
|
5041
3485
|
const browserTransport = resolveBrowserTransport(c.req.raw, c.env?.incoming?.socket?.remoteAddress);
|
|
5042
3486
|
let pinConfigured = false;
|
|
5043
3487
|
try {
|
|
5044
|
-
if (
|
|
5045
|
-
const raw =
|
|
3488
|
+
if (existsSync3(USERS_FILE)) {
|
|
3489
|
+
const raw = readFileSync3(USERS_FILE, "utf-8").trim();
|
|
5046
3490
|
if (raw) {
|
|
5047
3491
|
const users = JSON.parse(raw);
|
|
5048
3492
|
pinConfigured = Array.isArray(users) && users.length > 0;
|
|
@@ -5061,7 +3505,7 @@ app.get("/", async (c) => {
|
|
|
5061
3505
|
const vncRunning = await checkPort(6080);
|
|
5062
3506
|
let apiKeyConfigured = false;
|
|
5063
3507
|
try {
|
|
5064
|
-
apiKeyConfigured =
|
|
3508
|
+
apiKeyConfigured = existsSync3(keyFilePath());
|
|
5065
3509
|
} catch {
|
|
5066
3510
|
}
|
|
5067
3511
|
let apiKeyStatus = "missing";
|
|
@@ -5094,7 +3538,6 @@ app.get("/", async (c) => {
|
|
|
5094
3538
|
const step = await loadOnboardingStep(account.accountId);
|
|
5095
3539
|
if (step !== null) onboardingComplete = step >= 6;
|
|
5096
3540
|
}
|
|
5097
|
-
const reviewDetector = getReviewDetectorSnapshot();
|
|
5098
3541
|
return c.json({
|
|
5099
3542
|
pin_configured: pinConfigured,
|
|
5100
3543
|
claude_authenticated: claudeAuthenticated,
|
|
@@ -5110,31 +3553,20 @@ app.get("/", async (c) => {
|
|
|
5110
3553
|
any_connected: whatsappAnyConnected,
|
|
5111
3554
|
any_stuck: whatsappAnyStuck,
|
|
5112
3555
|
accounts: whatsappAccounts
|
|
5113
|
-
},
|
|
5114
|
-
review_detector: {
|
|
5115
|
-
state: reviewDetector.state,
|
|
5116
|
-
started_at: reviewDetector.startedAt,
|
|
5117
|
-
last_scan_at: reviewDetector.lastScanAt,
|
|
5118
|
-
last_scan_duration_ms: reviewDetector.lastScanDurationMs,
|
|
5119
|
-
scan_cycles: reviewDetector.scanCycles,
|
|
5120
|
-
rules_loaded: reviewDetector.rulesLoaded,
|
|
5121
|
-
sources_tracked: reviewDetector.sourcesTracked,
|
|
5122
|
-
active_alerts: reviewDetector.activeAlerts,
|
|
5123
|
-
last_error: reviewDetector.lastError
|
|
5124
3556
|
}
|
|
5125
3557
|
});
|
|
5126
3558
|
});
|
|
5127
3559
|
var health_default = app;
|
|
5128
3560
|
|
|
5129
3561
|
// server/routes/session.ts
|
|
5130
|
-
import { resolve as
|
|
5131
|
-
import { existsSync as
|
|
3562
|
+
import { resolve as resolve3 } from "path";
|
|
3563
|
+
import { existsSync as existsSync4, writeFileSync as writeFileSync2, mkdirSync } from "fs";
|
|
5132
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;
|
|
5133
3565
|
function writeBrandingCache(accountId, agentSlug, branding) {
|
|
5134
3566
|
try {
|
|
5135
|
-
const cacheDir =
|
|
5136
|
-
|
|
5137
|
-
|
|
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");
|
|
5138
3570
|
} catch (err) {
|
|
5139
3571
|
console.error(`[branding] cache write failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
5140
3572
|
}
|
|
@@ -5204,9 +3636,9 @@ app2.post("/", async (c) => {
|
|
|
5204
3636
|
}
|
|
5205
3637
|
let agentConfig = null;
|
|
5206
3638
|
if (account) {
|
|
5207
|
-
const agentDir =
|
|
5208
|
-
const agentConfigPath =
|
|
5209
|
-
if (!
|
|
3639
|
+
const agentDir = resolve3(account.accountDir, "agents", agentSlug);
|
|
3640
|
+
const agentConfigPath = resolve3(agentDir, "config.json");
|
|
3641
|
+
if (!existsSync4(agentDir) || !existsSync4(agentConfigPath)) {
|
|
5210
3642
|
return c.json({ error: "Agent not found" }, 404);
|
|
5211
3643
|
}
|
|
5212
3644
|
agentConfig = resolveAgentConfig(account.accountDir, agentSlug);
|
|
@@ -5456,12 +3888,12 @@ ${raw}`;
|
|
|
5456
3888
|
}
|
|
5457
3889
|
|
|
5458
3890
|
// app/lib/attachments.ts
|
|
5459
|
-
import { randomUUID as
|
|
3891
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
5460
3892
|
import { mkdir as mkdir2, readFile, stat as stat2, writeFile as writeFile2 } from "fs/promises";
|
|
5461
3893
|
import { realpathSync } from "fs";
|
|
5462
|
-
import { resolve as
|
|
5463
|
-
var PLATFORM_ROOT2 = process.env.MAXY_PLATFORM_ROOT ??
|
|
5464
|
-
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");
|
|
5465
3897
|
var SUPPORTED_MIME_TYPES = /* @__PURE__ */ new Set([
|
|
5466
3898
|
"image/jpeg",
|
|
5467
3899
|
"image/png",
|
|
@@ -5487,12 +3919,12 @@ function assertSupportedMime(mimeType) {
|
|
|
5487
3919
|
}
|
|
5488
3920
|
}
|
|
5489
3921
|
async function writeAttachment(scope, filename, mimeType, sizeBytes, buffer) {
|
|
5490
|
-
const attachmentId =
|
|
5491
|
-
const dir =
|
|
3922
|
+
const attachmentId = randomUUID3();
|
|
3923
|
+
const dir = resolve4(ATTACHMENTS_ROOT, scope, attachmentId);
|
|
5492
3924
|
await mkdir2(dir, { recursive: true });
|
|
5493
3925
|
const ext = extname(filename) || "";
|
|
5494
|
-
const storagePath =
|
|
5495
|
-
const metaPath =
|
|
3926
|
+
const storagePath = resolve4(dir, `${attachmentId}${ext}`);
|
|
3927
|
+
const metaPath = resolve4(dir, `${attachmentId}.meta.json`);
|
|
5496
3928
|
const meta = {
|
|
5497
3929
|
attachmentId,
|
|
5498
3930
|
scope,
|
|
@@ -5566,7 +3998,7 @@ async function storeGeneratedFile(accountId, filePath) {
|
|
|
5566
3998
|
`File exceeds the 20 MB limit (${(fileStat.size / 1024 / 1024).toFixed(1)} MB).`
|
|
5567
3999
|
);
|
|
5568
4000
|
}
|
|
5569
|
-
const filename =
|
|
4001
|
+
const filename = basename(filePath);
|
|
5570
4002
|
const mimeType = detectMimeType(filePath);
|
|
5571
4003
|
const buffer = await readFile(filePath);
|
|
5572
4004
|
return writeAttachment(accountId, filename, mimeType, fileStat.size, buffer);
|
|
@@ -5575,7 +4007,7 @@ async function storeGeneratedFile(accountId, filePath) {
|
|
|
5575
4007
|
// app/lib/stt/voice-note.ts
|
|
5576
4008
|
import { writeFile as writeFile3, mkdtemp, rm } from "fs/promises";
|
|
5577
4009
|
import { tmpdir } from "os";
|
|
5578
|
-
import { join as
|
|
4010
|
+
import { join as join4 } from "path";
|
|
5579
4011
|
var TAG14 = "[voice]";
|
|
5580
4012
|
var AUDIO_MIME_TYPES = /* @__PURE__ */ new Set([
|
|
5581
4013
|
"audio/ogg",
|
|
@@ -5613,9 +4045,9 @@ async function transcribeVoiceNote(file, source) {
|
|
|
5613
4045
|
let tempDir;
|
|
5614
4046
|
let tempPath;
|
|
5615
4047
|
try {
|
|
5616
|
-
tempDir = await mkdtemp(
|
|
4048
|
+
tempDir = await mkdtemp(join4(tmpdir(), "voice-"));
|
|
5617
4049
|
const ext = audioExtension(mimeType);
|
|
5618
|
-
tempPath =
|
|
4050
|
+
tempPath = join4(tempDir, `recording${ext}`);
|
|
5619
4051
|
const buffer = Buffer.from(await file.arrayBuffer());
|
|
5620
4052
|
await writeFile3(tempPath, buffer);
|
|
5621
4053
|
} catch (err) {
|
|
@@ -6170,16 +4602,16 @@ var group_default = app4;
|
|
|
6170
4602
|
|
|
6171
4603
|
// app/lib/access-gate.ts
|
|
6172
4604
|
import neo4j from "neo4j-driver";
|
|
6173
|
-
import { readFileSync as
|
|
6174
|
-
import { resolve as
|
|
6175
|
-
import { randomUUID as
|
|
6176
|
-
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(), "..");
|
|
6177
4609
|
var driver = null;
|
|
6178
4610
|
function readPassword() {
|
|
6179
4611
|
if (process.env.NEO4J_PASSWORD) return process.env.NEO4J_PASSWORD;
|
|
6180
|
-
const passwordFile =
|
|
4612
|
+
const passwordFile = resolve5(PLATFORM_ROOT3, "config/.neo4j-password");
|
|
6181
4613
|
try {
|
|
6182
|
-
return
|
|
4614
|
+
return readFileSync4(passwordFile, "utf-8").trim();
|
|
6183
4615
|
} catch {
|
|
6184
4616
|
throw new Error(
|
|
6185
4617
|
`Neo4j password not found. Expected at ${passwordFile} or in NEO4J_PASSWORD env var.`
|
|
@@ -6428,7 +4860,7 @@ async function setGrantPassword(grantId, passwordHash) {
|
|
|
6428
4860
|
}
|
|
6429
4861
|
}
|
|
6430
4862
|
async function generateNewMagicToken(grantId) {
|
|
6431
|
-
const token =
|
|
4863
|
+
const token = randomUUID4();
|
|
6432
4864
|
const session = getSession2();
|
|
6433
4865
|
try {
|
|
6434
4866
|
await session.run(
|
|
@@ -6490,19 +4922,19 @@ async function findActiveGrantByContact(contactValue, agentSlug, accountId) {
|
|
|
6490
4922
|
}
|
|
6491
4923
|
|
|
6492
4924
|
// app/lib/brevo-sms.ts
|
|
6493
|
-
import { readFileSync as
|
|
6494
|
-
import { dirname
|
|
6495
|
-
import { resolve as
|
|
6496
|
-
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");
|
|
6497
4929
|
var BREVO_API_URL = "https://api.brevo.com/v3/transactionalSMS/sms";
|
|
6498
4930
|
var BREVO_TIMEOUT_MS = 1e4;
|
|
6499
4931
|
var BREVO_SENDER = "Maxy";
|
|
6500
4932
|
var platformRoot = process.env.MAXY_PLATFORM_ROOT;
|
|
6501
4933
|
if (platformRoot) {
|
|
6502
4934
|
try {
|
|
6503
|
-
const brandPath =
|
|
6504
|
-
if (
|
|
6505
|
-
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"));
|
|
6506
4938
|
if (brand.productName) BREVO_SENDER = brand.productName;
|
|
6507
4939
|
}
|
|
6508
4940
|
} catch {
|
|
@@ -6510,7 +4942,7 @@ if (platformRoot) {
|
|
|
6510
4942
|
}
|
|
6511
4943
|
function readBrevoApiKey() {
|
|
6512
4944
|
try {
|
|
6513
|
-
const key =
|
|
4945
|
+
const key = readFileSync5(BREVO_API_KEY_FILE, "utf-8").trim();
|
|
6514
4946
|
if (!key) {
|
|
6515
4947
|
throw new Error(`Brevo API key file is empty: ${BREVO_API_KEY_FILE}`);
|
|
6516
4948
|
}
|
|
@@ -6525,7 +4957,7 @@ function readBrevoApiKey() {
|
|
|
6525
4957
|
}
|
|
6526
4958
|
}
|
|
6527
4959
|
function hasBrevoApiKey() {
|
|
6528
|
-
return
|
|
4960
|
+
return existsSync5(BREVO_API_KEY_FILE);
|
|
6529
4961
|
}
|
|
6530
4962
|
async function sendSms(recipient, content, opts) {
|
|
6531
4963
|
let apiKey;
|
|
@@ -6941,7 +5373,7 @@ app5.post("/send-otp", async (c) => {
|
|
|
6941
5373
|
var access_default = app5;
|
|
6942
5374
|
|
|
6943
5375
|
// server/routes/telegram.ts
|
|
6944
|
-
import { existsSync as
|
|
5376
|
+
import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
|
|
6945
5377
|
import { timingSafeEqual } from "crypto";
|
|
6946
5378
|
|
|
6947
5379
|
// app/lib/telegram/access-control.ts
|
|
@@ -6978,8 +5410,8 @@ var TELEGRAM_API = "https://api.telegram.org";
|
|
|
6978
5410
|
function getWebhookSecret(botType) {
|
|
6979
5411
|
const filePath = botType === "admin" ? TELEGRAM_ADMIN_WEBHOOK_SECRET_FILE : TELEGRAM_WEBHOOK_SECRET_FILE;
|
|
6980
5412
|
try {
|
|
6981
|
-
if (!
|
|
6982
|
-
const secret =
|
|
5413
|
+
if (!existsSync6(filePath)) return null;
|
|
5414
|
+
const secret = readFileSync6(filePath, "utf-8").trim();
|
|
6983
5415
|
return secret || null;
|
|
6984
5416
|
} catch {
|
|
6985
5417
|
return null;
|
|
@@ -7137,12 +5569,12 @@ app6.post("/webhook", async (c) => {
|
|
|
7137
5569
|
var telegram_default = app6;
|
|
7138
5570
|
|
|
7139
5571
|
// server/routes/whatsapp.ts
|
|
7140
|
-
import { join as
|
|
5572
|
+
import { join as join5, resolve as resolve7, basename as basename2 } from "path";
|
|
7141
5573
|
import { readFile as readFile2, stat as stat3 } from "fs/promises";
|
|
7142
|
-
import { realpathSync as realpathSync2, readdirSync as
|
|
5574
|
+
import { realpathSync as realpathSync2, readdirSync as readdirSync2, readFileSync as readFileSync7, existsSync as existsSync7 } from "fs";
|
|
7143
5575
|
|
|
7144
5576
|
// app/lib/whatsapp/login.ts
|
|
7145
|
-
import { randomUUID as
|
|
5577
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
7146
5578
|
var TAG17 = "[whatsapp:login]";
|
|
7147
5579
|
var ACTIVE_LOGIN_TTL_MS = 3 * 6e4;
|
|
7148
5580
|
var activeLogins = /* @__PURE__ */ new Map();
|
|
@@ -7245,8 +5677,8 @@ async function startLogin(opts) {
|
|
|
7245
5677
|
resetActiveLogin(accountId);
|
|
7246
5678
|
let resolveQr = null;
|
|
7247
5679
|
let rejectQr = null;
|
|
7248
|
-
const qrPromise = new Promise((
|
|
7249
|
-
resolveQr =
|
|
5680
|
+
const qrPromise = new Promise((resolve22, reject) => {
|
|
5681
|
+
resolveQr = resolve22;
|
|
7250
5682
|
rejectQr = reject;
|
|
7251
5683
|
});
|
|
7252
5684
|
const qrTimer = setTimeout(
|
|
@@ -7281,7 +5713,7 @@ async function startLogin(opts) {
|
|
|
7281
5713
|
const login = {
|
|
7282
5714
|
accountId,
|
|
7283
5715
|
authDir,
|
|
7284
|
-
id:
|
|
5716
|
+
id: randomUUID5(),
|
|
7285
5717
|
sock,
|
|
7286
5718
|
startedAt: Date.now(),
|
|
7287
5719
|
connected: false
|
|
@@ -7480,7 +5912,7 @@ app7.post("/login/start", async (c) => {
|
|
|
7480
5912
|
const body = await c.req.json().catch(() => ({}));
|
|
7481
5913
|
const accountId = validateAccountId(body.accountId);
|
|
7482
5914
|
const force = body.force ?? false;
|
|
7483
|
-
const authDir =
|
|
5915
|
+
const authDir = join5(MAXY_DIR, "credentials", "whatsapp", accountId);
|
|
7484
5916
|
const result = await startLogin({ accountId, authDir, force });
|
|
7485
5917
|
console.error(`${TAG18} login/start result account=${accountId} hasQr=${!!result.qrRaw}${result.selfPhone ? ` phone=${result.selfPhone}` : ""}`);
|
|
7486
5918
|
return c.json(result);
|
|
@@ -7640,17 +6072,17 @@ app7.post("/config", async (c) => {
|
|
|
7640
6072
|
return c.json(result, result.ok ? 200 : 400);
|
|
7641
6073
|
}
|
|
7642
6074
|
case "list-public-agents": {
|
|
7643
|
-
const agentsDir =
|
|
6075
|
+
const agentsDir = resolve7(account.accountDir, "agents");
|
|
7644
6076
|
const agents = [];
|
|
7645
|
-
if (
|
|
6077
|
+
if (existsSync7(agentsDir)) {
|
|
7646
6078
|
try {
|
|
7647
|
-
const entries =
|
|
6079
|
+
const entries = readdirSync2(agentsDir, { withFileTypes: true });
|
|
7648
6080
|
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
7649
6081
|
if (!entry.isDirectory() || entry.name === "admin") continue;
|
|
7650
|
-
const configPath2 =
|
|
7651
|
-
if (!
|
|
6082
|
+
const configPath2 = resolve7(agentsDir, entry.name, "config.json");
|
|
6083
|
+
if (!existsSync7(configPath2)) continue;
|
|
7652
6084
|
try {
|
|
7653
|
-
const config = JSON.parse(
|
|
6085
|
+
const config = JSON.parse(readFileSync7(configPath2, "utf-8"));
|
|
7654
6086
|
agents.push({ slug: entry.name, displayName: config.displayName ?? entry.name });
|
|
7655
6087
|
} catch {
|
|
7656
6088
|
console.error(`${TAG18} config action=list-public-agents error="failed to parse config.json for agent ${entry.name}" \u2014 skipping`);
|
|
@@ -7725,7 +6157,7 @@ app7.post("/send-document", async (c) => {
|
|
|
7725
6157
|
if (!maxyAccountId || !PLATFORM_ROOT4) {
|
|
7726
6158
|
return c.json({ error: "Cannot validate file path: missing account or platform context" }, 400);
|
|
7727
6159
|
}
|
|
7728
|
-
const accountDir =
|
|
6160
|
+
const accountDir = resolve7(PLATFORM_ROOT4, "..", "data/accounts", maxyAccountId);
|
|
7729
6161
|
let resolvedPath;
|
|
7730
6162
|
try {
|
|
7731
6163
|
resolvedPath = realpathSync2(filePath);
|
|
@@ -7749,7 +6181,7 @@ app7.post("/send-document", async (c) => {
|
|
|
7749
6181
|
return c.json({ error: `File exceeds 20 MB limit (${(fileStat.size / 1024 / 1024).toFixed(1)} MB)` }, 400);
|
|
7750
6182
|
}
|
|
7751
6183
|
const buffer = Buffer.from(await readFile2(resolvedPath));
|
|
7752
|
-
const filename =
|
|
6184
|
+
const filename = basename2(resolvedPath);
|
|
7753
6185
|
const mimetype = detectMimeType(resolvedPath);
|
|
7754
6186
|
const sock = getSocket(accountId);
|
|
7755
6187
|
if (!sock) {
|
|
@@ -7949,16 +6381,16 @@ var whatsapp_default = app7;
|
|
|
7949
6381
|
|
|
7950
6382
|
// server/routes/onboarding.ts
|
|
7951
6383
|
import { spawn, execFileSync } from "child_process";
|
|
7952
|
-
import { openSync
|
|
7953
|
-
import { resolve as
|
|
7954
|
-
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";
|
|
7955
6387
|
var PLATFORM_ROOT5 = process.env.MAXY_PLATFORM_ROOT || "";
|
|
7956
6388
|
function hashPin(pin) {
|
|
7957
6389
|
return createHash("sha256").update(pin).digest("hex");
|
|
7958
6390
|
}
|
|
7959
6391
|
function readUsersFile() {
|
|
7960
|
-
if (!
|
|
7961
|
-
const raw =
|
|
6392
|
+
if (!existsSync8(USERS_FILE)) return null;
|
|
6393
|
+
const raw = readFileSync8(USERS_FILE, "utf-8").trim();
|
|
7962
6394
|
if (!raw) return [];
|
|
7963
6395
|
return JSON.parse(raw);
|
|
7964
6396
|
}
|
|
@@ -8024,11 +6456,11 @@ app8.post("/claude-auth", async (c) => {
|
|
|
8024
6456
|
if (!vncReady) return c.json({ error: "VNC display failed to start" }, 500);
|
|
8025
6457
|
}
|
|
8026
6458
|
await ensureCdp(transport);
|
|
8027
|
-
|
|
6459
|
+
writeFileSync4(logPath("claude-auth"), "");
|
|
8028
6460
|
const chromiumWrapper = writeChromiumWrapper();
|
|
8029
6461
|
const x11Env = buildX11Env(chromiumWrapper, transport);
|
|
8030
6462
|
vncLog("claude-auth", { action: "start", transport });
|
|
8031
|
-
const claudeAuthLogFd =
|
|
6463
|
+
const claudeAuthLogFd = openSync(logPath("claude-auth"), "a");
|
|
8032
6464
|
const claudeProc = spawn("claude", ["auth", "login"], {
|
|
8033
6465
|
env: x11Env,
|
|
8034
6466
|
stdio: ["ignore", "pipe", "pipe"]
|
|
@@ -8037,7 +6469,7 @@ app8.post("/claude-auth", async (c) => {
|
|
|
8037
6469
|
const onClaudeOutput = (chunk) => writeSync(claudeAuthLogFd, chunk);
|
|
8038
6470
|
claudeProc.stdout?.on("data", onClaudeOutput);
|
|
8039
6471
|
claudeProc.stderr?.on("data", onClaudeOutput);
|
|
8040
|
-
claudeProc.once("close", () =>
|
|
6472
|
+
claudeProc.once("close", () => closeSync(claudeAuthLogFd));
|
|
8041
6473
|
await waitForAuthPage(2e4);
|
|
8042
6474
|
return c.json({ started: true, transport });
|
|
8043
6475
|
});
|
|
@@ -8068,22 +6500,22 @@ app8.post("/set-pin", async (c) => {
|
|
|
8068
6500
|
const hash = hashPin(body.pin);
|
|
8069
6501
|
const account = resolveAccount();
|
|
8070
6502
|
const existingOwnerUserId = account?.config.admins?.find((a) => a.role === "owner")?.userId;
|
|
8071
|
-
const userId = existingOwnerUserId ??
|
|
6503
|
+
const userId = existingOwnerUserId ?? randomUUID6();
|
|
8072
6504
|
if (existingOwnerUserId) {
|
|
8073
6505
|
console.log(`[set-pin] reusing existing owner userId=${userId.slice(0, 8)}\u2026 (change-PIN preserves identity)`);
|
|
8074
6506
|
} else {
|
|
8075
6507
|
console.log(`[set-pin] minted new userId=${userId.slice(0, 8)}\u2026 (first-time install)`);
|
|
8076
6508
|
}
|
|
8077
|
-
|
|
8078
|
-
|
|
6509
|
+
mkdirSync3(dirname2(USERS_FILE), { recursive: true });
|
|
6510
|
+
writeFileSync4(USERS_FILE, JSON.stringify([{ userId, pin: hash }]), { mode: 384 });
|
|
8079
6511
|
console.log(`[set-pin] wrote users.json: userId=${userId.slice(0, 8)}\u2026 hash=${hash.slice(0, 8)}\u2026`);
|
|
8080
6512
|
if (account) {
|
|
8081
6513
|
try {
|
|
8082
|
-
const config = JSON.parse(
|
|
6514
|
+
const config = JSON.parse(readFileSync8(`${account.accountDir}/account.json`, "utf-8"));
|
|
8083
6515
|
if (!config.admins) config.admins = [];
|
|
8084
6516
|
if (!config.admins.some((a) => a.userId === userId)) {
|
|
8085
6517
|
config.admins.push({ userId, role: "owner" });
|
|
8086
|
-
|
|
6518
|
+
writeFileSync4(`${account.accountDir}/account.json`, JSON.stringify(config, null, 2) + "\n");
|
|
8087
6519
|
console.log(`[set-pin] added userId=${userId.slice(0, 8)}\u2026 to account.json admins`);
|
|
8088
6520
|
}
|
|
8089
6521
|
} catch (err) {
|
|
@@ -8136,7 +6568,7 @@ app8.delete("/set-pin", async (c) => {
|
|
|
8136
6568
|
unlinkSync(USERS_FILE);
|
|
8137
6569
|
console.log(`[set-pin] cleared users.json (last entry removed): userId=${matchedUser.userId.slice(0, 8)}\u2026`);
|
|
8138
6570
|
} else {
|
|
8139
|
-
|
|
6571
|
+
writeFileSync4(USERS_FILE, JSON.stringify(remaining), { mode: 384 });
|
|
8140
6572
|
console.log(`[set-pin] removed entry from users.json: userId=${matchedUser.userId.slice(0, 8)}\u2026 remaining=${remaining.length}`);
|
|
8141
6573
|
}
|
|
8142
6574
|
return c.json({ ok: true });
|
|
@@ -8155,19 +6587,19 @@ app8.post("/skip", async (c) => {
|
|
|
8155
6587
|
}
|
|
8156
6588
|
const { accountId, accountDir } = account;
|
|
8157
6589
|
let agentName = "Maxy";
|
|
8158
|
-
const brandPath = PLATFORM_ROOT5 ?
|
|
8159
|
-
if (brandPath &&
|
|
6590
|
+
const brandPath = PLATFORM_ROOT5 ? resolve8(PLATFORM_ROOT5, "config", "brand.json") : "";
|
|
6591
|
+
if (brandPath && existsSync8(brandPath)) {
|
|
8160
6592
|
try {
|
|
8161
|
-
const brand = JSON.parse(
|
|
6593
|
+
const brand = JSON.parse(readFileSync8(brandPath, "utf-8"));
|
|
8162
6594
|
if (brand.productName) agentName = brand.productName;
|
|
8163
6595
|
} catch (err) {
|
|
8164
6596
|
console.error(`[onboarding-skip] brand.json read failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
8165
6597
|
}
|
|
8166
6598
|
}
|
|
8167
|
-
const soulPath =
|
|
6599
|
+
const soulPath = resolve8(accountDir, "agents", "admin", "SOUL.md");
|
|
8168
6600
|
try {
|
|
8169
|
-
|
|
8170
|
-
|
|
6601
|
+
mkdirSync3(dirname2(soulPath), { recursive: true });
|
|
6602
|
+
writeFileSync4(soulPath, `You are ${agentName}, an AI operations manager.
|
|
8171
6603
|
`);
|
|
8172
6604
|
console.log(`[onboarding-skip] wrote SOUL.md: ${soulPath}`);
|
|
8173
6605
|
} catch (err) {
|
|
@@ -8211,9 +6643,9 @@ app8.post("/skip", async (c) => {
|
|
|
8211
6643
|
var onboarding_default = app8;
|
|
8212
6644
|
|
|
8213
6645
|
// server/routes/client-error.ts
|
|
8214
|
-
import { appendFileSync
|
|
8215
|
-
import { join as
|
|
8216
|
-
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");
|
|
8217
6649
|
var MAX_LOG_SIZE = 10 * 1024 * 1024;
|
|
8218
6650
|
var MAX_BODY_SIZE = 8 * 1024;
|
|
8219
6651
|
var MAX_STACK_LEN = 2e3;
|
|
@@ -8256,10 +6688,10 @@ function stackHead(stack) {
|
|
|
8256
6688
|
}
|
|
8257
6689
|
function rotateIfNeeded() {
|
|
8258
6690
|
try {
|
|
8259
|
-
if (!
|
|
8260
|
-
const stats =
|
|
6691
|
+
if (!existsSync9(CLIENT_ERRORS_LOG)) return;
|
|
6692
|
+
const stats = statSync2(CLIENT_ERRORS_LOG);
|
|
8261
6693
|
if (stats.size < MAX_LOG_SIZE) return;
|
|
8262
|
-
|
|
6694
|
+
renameSync(CLIENT_ERRORS_LOG, CLIENT_ERRORS_LOG + ".1");
|
|
8263
6695
|
} catch (err) {
|
|
8264
6696
|
console.error(`[client-error] log rotation failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
8265
6697
|
}
|
|
@@ -8360,7 +6792,7 @@ app9.post("/", async (c) => {
|
|
|
8360
6792
|
tag: typeof body.tag === "string" ? truncate(body.tag, 32) : void 0,
|
|
8361
6793
|
status: typeof body.status === "number" ? body.status : void 0
|
|
8362
6794
|
};
|
|
8363
|
-
|
|
6795
|
+
appendFileSync(CLIENT_ERRORS_LOG, JSON.stringify(payload) + "\n", "utf-8");
|
|
8364
6796
|
} catch (err) {
|
|
8365
6797
|
console.error(`[client-error] append failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
8366
6798
|
}
|
|
@@ -8370,15 +6802,15 @@ app9.post("/", async (c) => {
|
|
|
8370
6802
|
var client_error_default = app9;
|
|
8371
6803
|
|
|
8372
6804
|
// server/routes/admin/session.ts
|
|
8373
|
-
import { readFileSync as
|
|
6805
|
+
import { readFileSync as readFileSync9, writeFileSync as writeFileSync5, existsSync as existsSync10 } from "fs";
|
|
8374
6806
|
import { createHash as createHash2 } from "crypto";
|
|
8375
6807
|
var deprecationLogged = /* @__PURE__ */ new Set();
|
|
8376
6808
|
function hashPin2(pin) {
|
|
8377
6809
|
return createHash2("sha256").update(pin).digest("hex");
|
|
8378
6810
|
}
|
|
8379
6811
|
function readUsersFile2() {
|
|
8380
|
-
if (!
|
|
8381
|
-
const raw =
|
|
6812
|
+
if (!existsSync10(USERS_FILE)) return null;
|
|
6813
|
+
const raw = readFileSync9(USERS_FILE, "utf-8").trim();
|
|
8382
6814
|
if (!raw) return [];
|
|
8383
6815
|
return JSON.parse(raw);
|
|
8384
6816
|
}
|
|
@@ -8395,7 +6827,7 @@ function stripLegacyNameField(users) {
|
|
|
8395
6827
|
}
|
|
8396
6828
|
}
|
|
8397
6829
|
try {
|
|
8398
|
-
|
|
6830
|
+
writeFileSync5(USERS_FILE, JSON.stringify(users), { mode: 384 });
|
|
8399
6831
|
} catch (err) {
|
|
8400
6832
|
console.error(`[admin-identity] users-json strip failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
8401
6833
|
}
|
|
@@ -8552,13 +6984,13 @@ app10.post("/", async (c) => {
|
|
|
8552
6984
|
var session_default2 = app10;
|
|
8553
6985
|
|
|
8554
6986
|
// server/routes/admin/chat.ts
|
|
8555
|
-
import { resolve as
|
|
8556
|
-
import { appendFileSync as
|
|
6987
|
+
import { resolve as resolve9 } from "path";
|
|
6988
|
+
import { appendFileSync as appendFileSync3 } from "fs";
|
|
8557
6989
|
|
|
8558
6990
|
// app/lib/script-stream-tailer.ts
|
|
8559
6991
|
import * as childProcess from "child_process";
|
|
8560
|
-
import { appendFileSync as
|
|
8561
|
-
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";
|
|
8562
6994
|
import { StringDecoder } from "string_decoder";
|
|
8563
6995
|
var SCRIPT_STREAM_RE = /^\[([^\]]+)\] \[script:([a-z][a-z0-9-]*)((?::[a-z0-9:_-]+)?)\] (.*)$/;
|
|
8564
6996
|
function parseLine(line) {
|
|
@@ -8576,7 +7008,7 @@ function startScriptStreamTailer(opts) {
|
|
|
8576
7008
|
const { path: path2, onEvent, onError } = opts;
|
|
8577
7009
|
let offset;
|
|
8578
7010
|
try {
|
|
8579
|
-
offset =
|
|
7011
|
+
offset = statSync3(path2).size;
|
|
8580
7012
|
} catch {
|
|
8581
7013
|
offset = 0;
|
|
8582
7014
|
}
|
|
@@ -8595,7 +7027,7 @@ function startScriptStreamTailer(opts) {
|
|
|
8595
7027
|
try {
|
|
8596
7028
|
let size;
|
|
8597
7029
|
try {
|
|
8598
|
-
size =
|
|
7030
|
+
size = statSync3(path2).size;
|
|
8599
7031
|
} catch {
|
|
8600
7032
|
return;
|
|
8601
7033
|
}
|
|
@@ -8667,8 +7099,8 @@ function writeRouteMilestone(streamLogPath, scope, line) {
|
|
|
8667
7099
|
}
|
|
8668
7100
|
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
8669
7101
|
try {
|
|
8670
|
-
|
|
8671
|
-
|
|
7102
|
+
mkdirSync4(dirname3(streamLogPath), { recursive: true });
|
|
7103
|
+
appendFileSync2(streamLogPath, `[${ts}] [script:${scope}] ${line}
|
|
8672
7104
|
`);
|
|
8673
7105
|
} catch (err) {
|
|
8674
7106
|
console.error(
|
|
@@ -8843,7 +7275,7 @@ var app11 = new Hono();
|
|
|
8843
7275
|
app11.post("/cancel", requireAdminSession, async (c) => {
|
|
8844
7276
|
const session_key = c.var.sessionKey;
|
|
8845
7277
|
try {
|
|
8846
|
-
const { interruptClient: interruptClient2 } = await import("./client-pool-
|
|
7278
|
+
const { interruptClient: interruptClient2 } = await import("./client-pool-GBY5I2KQ.js");
|
|
8847
7279
|
await interruptClient2(session_key);
|
|
8848
7280
|
return c.json({ ok: true });
|
|
8849
7281
|
} catch (err) {
|
|
@@ -9007,10 +7439,10 @@ app11.post("/", requireAdminSession, async (c) => {
|
|
|
9007
7439
|
function resolveTeeStreamLogPath() {
|
|
9008
7440
|
const liveConvId = getConversationIdForSession(session_key);
|
|
9009
7441
|
const key = liveConvId ?? preflushStreamLogKey(session_key);
|
|
9010
|
-
return
|
|
7442
|
+
return resolve9(account.accountDir, "logs", `claude-agent-stream-${key}.log`);
|
|
9011
7443
|
}
|
|
9012
7444
|
try {
|
|
9013
|
-
|
|
7445
|
+
appendFileSync3(resolveTeeStreamLogPath(), `[${(/* @__PURE__ */ new Date()).toISOString()}] [chat-route-version=task606-tee-path-resolve] sessionKey=${session_key.slice(0, 12)}\u2026
|
|
9014
7446
|
`);
|
|
9015
7447
|
} catch {
|
|
9016
7448
|
}
|
|
@@ -9019,7 +7451,7 @@ app11.post("/", requireAdminSession, async (c) => {
|
|
|
9019
7451
|
incoming.on("close", () => {
|
|
9020
7452
|
const tsClose = (/* @__PURE__ */ new Date()).toISOString();
|
|
9021
7453
|
try {
|
|
9022
|
-
|
|
7454
|
+
appendFileSync3(resolveTeeStreamLogPath(), `[${tsClose}] [incoming-close] sessionKey=${session_key.slice(0, 12)}\u2026 complete=${incoming.complete}
|
|
9023
7455
|
`);
|
|
9024
7456
|
} catch {
|
|
9025
7457
|
}
|
|
@@ -9027,7 +7459,7 @@ app11.post("/", requireAdminSession, async (c) => {
|
|
|
9027
7459
|
console.error(`[${tsClose}] [incoming-close] DISCONNECT sessionKey=${session_key.slice(0, 12)}\u2026`);
|
|
9028
7460
|
interruptClient(session_key).catch((err) => {
|
|
9029
7461
|
try {
|
|
9030
|
-
|
|
7462
|
+
appendFileSync3(resolveTeeStreamLogPath(), `[${(/* @__PURE__ */ new Date()).toISOString()}] [incoming-close] interrupt-failed: ${err instanceof Error ? err.message : String(err)}
|
|
9031
7463
|
`);
|
|
9032
7464
|
} catch {
|
|
9033
7465
|
}
|
|
@@ -9036,7 +7468,7 @@ app11.post("/", requireAdminSession, async (c) => {
|
|
|
9036
7468
|
});
|
|
9037
7469
|
} else {
|
|
9038
7470
|
try {
|
|
9039
|
-
|
|
7471
|
+
appendFileSync3(resolveTeeStreamLogPath(), `[${(/* @__PURE__ */ new Date()).toISOString()}] [incoming-close] UNAVAILABLE \u2014 c.env.incoming is not an EventEmitter
|
|
9040
7472
|
`);
|
|
9041
7473
|
} catch {
|
|
9042
7474
|
}
|
|
@@ -9048,7 +7480,7 @@ app11.post("/", requireAdminSession, async (c) => {
|
|
|
9048
7480
|
} catch {
|
|
9049
7481
|
}
|
|
9050
7482
|
try {
|
|
9051
|
-
|
|
7483
|
+
appendFileSync3(resolveTeeStreamLogPath(), line);
|
|
9052
7484
|
} catch {
|
|
9053
7485
|
}
|
|
9054
7486
|
return true;
|
|
@@ -9109,7 +7541,7 @@ app11.post("/", requireAdminSession, async (c) => {
|
|
|
9109
7541
|
try {
|
|
9110
7542
|
registerAdminSSE(sseEntry);
|
|
9111
7543
|
if (sseConvId) {
|
|
9112
|
-
const streamLogPath =
|
|
7544
|
+
const streamLogPath = resolve9(account.accountDir, "logs", `claude-agent-stream-${sseConvId}.log`);
|
|
9113
7545
|
tailer = startScriptStreamTailer({
|
|
9114
7546
|
path: streamLogPath,
|
|
9115
7547
|
onEvent: (event) => {
|
|
@@ -9238,22 +7670,22 @@ app12.post("/", requireAdminSession, async (c) => {
|
|
|
9238
7670
|
var compact_default = app12;
|
|
9239
7671
|
|
|
9240
7672
|
// server/routes/admin/logs.ts
|
|
9241
|
-
import { existsSync as
|
|
9242
|
-
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";
|
|
9243
7675
|
|
|
9244
7676
|
// app/lib/logs-read-resolve.ts
|
|
9245
|
-
import { existsSync as
|
|
9246
|
-
import { join as
|
|
7677
|
+
import { existsSync as existsSync11 } from "fs";
|
|
7678
|
+
import { join as join7 } from "path";
|
|
9247
7679
|
function resolveConversationLogPaths(fullFilename, preflushFilename, logDirs) {
|
|
9248
7680
|
const tried = [fullFilename, preflushFilename];
|
|
9249
7681
|
const hits = [];
|
|
9250
7682
|
const stalePreflushPaths = [];
|
|
9251
7683
|
for (const dir of logDirs) {
|
|
9252
|
-
const fullPath =
|
|
9253
|
-
if (
|
|
7684
|
+
const fullPath = join7(dir, fullFilename);
|
|
7685
|
+
if (existsSync11(fullPath)) {
|
|
9254
7686
|
hits.push({ path: fullPath, shape: "full", dir });
|
|
9255
|
-
const preflushSibling =
|
|
9256
|
-
if (
|
|
7687
|
+
const preflushSibling = join7(dir, preflushFilename);
|
|
7688
|
+
if (existsSync11(preflushSibling)) {
|
|
9257
7689
|
stalePreflushPaths.push(preflushSibling);
|
|
9258
7690
|
}
|
|
9259
7691
|
}
|
|
@@ -9262,8 +7694,8 @@ function resolveConversationLogPaths(fullFilename, preflushFilename, logDirs) {
|
|
|
9262
7694
|
return { hits, stalePreflushPaths, tried };
|
|
9263
7695
|
}
|
|
9264
7696
|
for (const dir of logDirs) {
|
|
9265
|
-
const preflushPath =
|
|
9266
|
-
if (
|
|
7697
|
+
const preflushPath = join7(dir, preflushFilename);
|
|
7698
|
+
if (existsSync11(preflushPath)) {
|
|
9267
7699
|
hits.push({ path: preflushPath, shape: "preflush", dir });
|
|
9268
7700
|
}
|
|
9269
7701
|
}
|
|
@@ -9283,19 +7715,19 @@ app13.get("/", async (c) => {
|
|
|
9283
7715
|
const sessionKeyParam = c.req.query("sessionKey");
|
|
9284
7716
|
const download = c.req.query("download") === "1";
|
|
9285
7717
|
const account = resolveAccount();
|
|
9286
|
-
const
|
|
7718
|
+
const accountLogDir = account ? resolve10(account.accountDir, "logs") : null;
|
|
9287
7719
|
const logDirs = [];
|
|
9288
|
-
if (
|
|
7720
|
+
if (accountLogDir) logDirs.push(accountLogDir);
|
|
9289
7721
|
logDirs.push(LOG_DIR);
|
|
9290
7722
|
if (fileParam) {
|
|
9291
|
-
const safe =
|
|
7723
|
+
const safe = basename3(fileParam);
|
|
9292
7724
|
const searched = [];
|
|
9293
7725
|
for (const dir of logDirs) {
|
|
9294
|
-
const filePath =
|
|
7726
|
+
const filePath = resolve10(dir, safe);
|
|
9295
7727
|
searched.push(filePath);
|
|
9296
7728
|
try {
|
|
9297
|
-
const buffer =
|
|
9298
|
-
const onDiskBytes =
|
|
7729
|
+
const buffer = readFileSync10(filePath);
|
|
7730
|
+
const onDiskBytes = statSync4(filePath).size;
|
|
9299
7731
|
const headers = {
|
|
9300
7732
|
"Content-Type": "text/plain; charset=utf-8",
|
|
9301
7733
|
"Content-Length": String(buffer.byteLength)
|
|
@@ -9367,9 +7799,9 @@ app13.get("/", async (c) => {
|
|
|
9367
7799
|
const hit = hits[0];
|
|
9368
7800
|
console.info(`[admin/logs] resolved sessionKey=${sessionKeySlice} conversationId=${conversationIdSlice} shape=${hit.shape} stalePreflushCount=${stalePreflushCount}`);
|
|
9369
7801
|
try {
|
|
9370
|
-
const filename =
|
|
7802
|
+
const filename = basename3(hit.path);
|
|
9371
7803
|
if (stalePreflushCount > 0 && !download) {
|
|
9372
|
-
const content =
|
|
7804
|
+
const content = readFileSync10(hit.path, "utf-8");
|
|
9373
7805
|
return c.json({
|
|
9374
7806
|
log: content,
|
|
9375
7807
|
filename,
|
|
@@ -9377,8 +7809,8 @@ app13.get("/", async (c) => {
|
|
|
9377
7809
|
warnings: stalePreflushPaths.map((path2) => ({ kind: "stale-preflush", path: path2 }))
|
|
9378
7810
|
});
|
|
9379
7811
|
}
|
|
9380
|
-
const buffer =
|
|
9381
|
-
const onDiskBytes =
|
|
7812
|
+
const buffer = readFileSync10(hit.path);
|
|
7813
|
+
const onDiskBytes = statSync4(hit.path).size;
|
|
9382
7814
|
const headers = {
|
|
9383
7815
|
"Content-Type": "text/plain; charset=utf-8",
|
|
9384
7816
|
"Content-Length": String(buffer.byteLength)
|
|
@@ -9411,19 +7843,19 @@ app13.get("/", async (c) => {
|
|
|
9411
7843
|
const seen = /* @__PURE__ */ new Set();
|
|
9412
7844
|
const logs = {};
|
|
9413
7845
|
for (const dir of logDirs) {
|
|
9414
|
-
if (!
|
|
7846
|
+
if (!existsSync12(dir)) continue;
|
|
9415
7847
|
let files;
|
|
9416
7848
|
try {
|
|
9417
|
-
files =
|
|
7849
|
+
files = readdirSync3(dir).filter((f) => f.endsWith(".log"));
|
|
9418
7850
|
} catch (err) {
|
|
9419
7851
|
const reason = err instanceof Error ? err.message : String(err);
|
|
9420
7852
|
console.warn(`[admin/logs] readdir-fail dir=${dir} reason=${reason}`);
|
|
9421
7853
|
continue;
|
|
9422
7854
|
}
|
|
9423
|
-
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 }) => {
|
|
9424
7856
|
seen.add(name);
|
|
9425
7857
|
try {
|
|
9426
|
-
const content =
|
|
7858
|
+
const content = readFileSync10(resolve10(dir, name));
|
|
9427
7859
|
const tail = content.length > TAIL_BYTES ? content.subarray(content.length - TAIL_BYTES).toString("utf-8") : content.toString("utf-8");
|
|
9428
7860
|
logs[name] = tail.trim() || "(empty)";
|
|
9429
7861
|
} catch (err) {
|
|
@@ -9462,8 +7894,8 @@ var claude_info_default = app14;
|
|
|
9462
7894
|
|
|
9463
7895
|
// server/routes/admin/attachment.ts
|
|
9464
7896
|
import { readFile as readFile3, readdir } from "fs/promises";
|
|
9465
|
-
import { existsSync as
|
|
9466
|
-
import { resolve as
|
|
7897
|
+
import { existsSync as existsSync13 } from "fs";
|
|
7898
|
+
import { resolve as resolve11 } from "path";
|
|
9467
7899
|
var app15 = new Hono();
|
|
9468
7900
|
app15.get("/:attachmentId", requireAdminSession, async (c) => {
|
|
9469
7901
|
const attachmentId = c.req.param("attachmentId");
|
|
@@ -9475,12 +7907,12 @@ app15.get("/:attachmentId", requireAdminSession, async (c) => {
|
|
|
9475
7907
|
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(attachmentId)) {
|
|
9476
7908
|
return new Response("Not found", { status: 404 });
|
|
9477
7909
|
}
|
|
9478
|
-
const dir =
|
|
9479
|
-
if (!
|
|
7910
|
+
const dir = resolve11(ATTACHMENTS_ROOT, accountId, attachmentId);
|
|
7911
|
+
if (!existsSync13(dir)) {
|
|
9480
7912
|
return new Response("Not found", { status: 404 });
|
|
9481
7913
|
}
|
|
9482
|
-
const metaPath =
|
|
9483
|
-
if (!
|
|
7914
|
+
const metaPath = resolve11(dir, `${attachmentId}.meta.json`);
|
|
7915
|
+
if (!existsSync13(metaPath)) {
|
|
9484
7916
|
return new Response("Not found", { status: 404 });
|
|
9485
7917
|
}
|
|
9486
7918
|
let meta;
|
|
@@ -9494,7 +7926,7 @@ app15.get("/:attachmentId", requireAdminSession, async (c) => {
|
|
|
9494
7926
|
if (!dataFile) {
|
|
9495
7927
|
return new Response("Not found", { status: 404 });
|
|
9496
7928
|
}
|
|
9497
|
-
const filePath =
|
|
7929
|
+
const filePath = resolve11(dir, dataFile);
|
|
9498
7930
|
const buffer = await readFile3(filePath);
|
|
9499
7931
|
return new Response(new Uint8Array(buffer), {
|
|
9500
7932
|
headers: {
|
|
@@ -9507,24 +7939,24 @@ app15.get("/:attachmentId", requireAdminSession, async (c) => {
|
|
|
9507
7939
|
var attachment_default = app15;
|
|
9508
7940
|
|
|
9509
7941
|
// server/routes/admin/agents.ts
|
|
9510
|
-
import { resolve as
|
|
9511
|
-
import { readdirSync as
|
|
7942
|
+
import { resolve as resolve12 } from "path";
|
|
7943
|
+
import { readdirSync as readdirSync4, readFileSync as readFileSync11, existsSync as existsSync14, rmSync } from "fs";
|
|
9512
7944
|
var app16 = new Hono();
|
|
9513
7945
|
app16.get("/", (c) => {
|
|
9514
7946
|
const account = resolveAccount();
|
|
9515
7947
|
if (!account) return c.json({ agents: [] });
|
|
9516
|
-
const agentsDir =
|
|
9517
|
-
if (!
|
|
7948
|
+
const agentsDir = resolve12(account.accountDir, "agents");
|
|
7949
|
+
if (!existsSync14(agentsDir)) return c.json({ agents: [] });
|
|
9518
7950
|
const agents = [];
|
|
9519
7951
|
try {
|
|
9520
|
-
const entries =
|
|
7952
|
+
const entries = readdirSync4(agentsDir, { withFileTypes: true });
|
|
9521
7953
|
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
9522
7954
|
if (!entry.isDirectory()) continue;
|
|
9523
7955
|
if (entry.name === "admin") continue;
|
|
9524
|
-
const configPath2 =
|
|
9525
|
-
if (!
|
|
7956
|
+
const configPath2 = resolve12(agentsDir, entry.name, "config.json");
|
|
7957
|
+
if (!existsSync14(configPath2)) continue;
|
|
9526
7958
|
try {
|
|
9527
|
-
const config = JSON.parse(
|
|
7959
|
+
const config = JSON.parse(readFileSync11(configPath2, "utf-8"));
|
|
9528
7960
|
agents.push({
|
|
9529
7961
|
slug: entry.name,
|
|
9530
7962
|
displayName: config.displayName ?? entry.name,
|
|
@@ -9550,8 +7982,8 @@ app16.delete("/:slug", async (c) => {
|
|
|
9550
7982
|
if (slug.includes("/") || slug.includes("..") || slug.includes("\\")) {
|
|
9551
7983
|
return c.json({ error: "Invalid agent slug" }, 400);
|
|
9552
7984
|
}
|
|
9553
|
-
const agentDir =
|
|
9554
|
-
if (!
|
|
7985
|
+
const agentDir = resolve12(account.accountDir, "agents", slug);
|
|
7986
|
+
if (!existsSync14(agentDir)) {
|
|
9555
7987
|
return c.json({ error: "Agent not found" }, 404);
|
|
9556
7988
|
}
|
|
9557
7989
|
try {
|
|
@@ -9580,8 +8012,8 @@ app16.post("/:slug/project", async (c) => {
|
|
|
9580
8012
|
if (slug.includes("/") || slug.includes("..") || slug.includes("\\")) {
|
|
9581
8013
|
return c.json({ error: "Invalid agent slug" }, 400);
|
|
9582
8014
|
}
|
|
9583
|
-
const agentDir =
|
|
9584
|
-
if (!
|
|
8015
|
+
const agentDir = resolve12(account.accountDir, "agents", slug);
|
|
8016
|
+
if (!existsSync14(agentDir)) {
|
|
9585
8017
|
return c.json({ error: "Agent not found on disk" }, 404);
|
|
9586
8018
|
}
|
|
9587
8019
|
try {
|
|
@@ -9597,7 +8029,7 @@ var agents_default = app16;
|
|
|
9597
8029
|
// server/routes/admin/sessions.ts
|
|
9598
8030
|
import crypto2 from "crypto";
|
|
9599
8031
|
import { resolve as resolvePath } from "path";
|
|
9600
|
-
import { appendFileSync as
|
|
8032
|
+
import { appendFileSync as appendFileSync4, existsSync as existsSync15 } from "fs";
|
|
9601
8033
|
function validateAndShapeAttachments(raws, conversationAccountId, conversationId, messageId, streamLogPath) {
|
|
9602
8034
|
const chips = [];
|
|
9603
8035
|
let valid = 0;
|
|
@@ -9606,11 +8038,11 @@ function validateAndShapeAttachments(raws, conversationAccountId, conversationId
|
|
|
9606
8038
|
let reason = null;
|
|
9607
8039
|
if (!a.attachmentId || !a.filename || !a.mimeType || !a.storagePath) reason = "schema-fail";
|
|
9608
8040
|
else if (a.accountId !== conversationAccountId) reason = "account-mismatch";
|
|
9609
|
-
else if (!
|
|
8041
|
+
else if (!existsSync15(a.storagePath)) reason = "missing-file";
|
|
9610
8042
|
if (reason) {
|
|
9611
8043
|
invalid++;
|
|
9612
8044
|
try {
|
|
9613
|
-
|
|
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}
|
|
9614
8046
|
`);
|
|
9615
8047
|
} catch {
|
|
9616
8048
|
}
|
|
@@ -9673,7 +8105,7 @@ function reconstructAssistantEvents(content, components, conversationId, message
|
|
|
9673
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}
|
|
9674
8106
|
`;
|
|
9675
8107
|
try {
|
|
9676
|
-
|
|
8108
|
+
appendFileSync4(streamLogPath, line);
|
|
9677
8109
|
} catch {
|
|
9678
8110
|
}
|
|
9679
8111
|
continue;
|
|
@@ -9876,14 +8308,14 @@ app17.post("/:id/resume", async (c) => {
|
|
|
9876
8308
|
const userMessageCount = rehydrated.filter((m) => m.role !== "assistant").length;
|
|
9877
8309
|
const reason = bridged ? "post-restart" : "page-refresh";
|
|
9878
8310
|
try {
|
|
9879
|
-
|
|
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}
|
|
9880
8312
|
`);
|
|
9881
8313
|
if (totalComponents > 0) {
|
|
9882
|
-
|
|
8314
|
+
appendFileSync4(streamLogPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] [component-rehydrate] conversationId=${conversationId.slice(0, 8)} count=${totalComponents} valid=${totalValid} invalid=${totalInvalid} textRuns=${textRuns}
|
|
9883
8315
|
`);
|
|
9884
8316
|
}
|
|
9885
8317
|
if (totalAttachments > 0 || totalAttachmentInvalid > 0) {
|
|
9886
|
-
|
|
8318
|
+
appendFileSync4(streamLogPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] [attachment-rehydrate] conversationId=${conversationId.slice(0, 8)} userMessages=${userMessageCount} attachments=${totalAttachments} invalid=${totalAttachmentInvalid}
|
|
9887
8319
|
`);
|
|
9888
8320
|
}
|
|
9889
8321
|
} catch {
|
|
@@ -10147,8 +8579,8 @@ var events_default = app20;
|
|
|
10147
8579
|
|
|
10148
8580
|
// server/routes/admin/cloudflare.ts
|
|
10149
8581
|
import { homedir } from "os";
|
|
10150
|
-
import { resolve as
|
|
10151
|
-
import { readFileSync as
|
|
8582
|
+
import { resolve as resolve14 } from "path";
|
|
8583
|
+
import { readFileSync as readFileSync13 } from "fs";
|
|
10152
8584
|
|
|
10153
8585
|
// app/lib/dns-label.ts
|
|
10154
8586
|
var VALID_LABEL = /^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
@@ -10164,14 +8596,14 @@ function isValidDomain(value) {
|
|
|
10164
8596
|
}
|
|
10165
8597
|
|
|
10166
8598
|
// app/lib/alias-domains.ts
|
|
10167
|
-
import { existsSync as
|
|
10168
|
-
import { dirname as
|
|
10169
|
-
import { resolve as
|
|
10170
|
-
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");
|
|
10171
8603
|
function readExisting() {
|
|
10172
|
-
if (!
|
|
8604
|
+
if (!existsSync16(ALIAS_DOMAINS_PATH)) return /* @__PURE__ */ new Set();
|
|
10173
8605
|
try {
|
|
10174
|
-
const parsed = JSON.parse(
|
|
8606
|
+
const parsed = JSON.parse(readFileSync12(ALIAS_DOMAINS_PATH, "utf-8"));
|
|
10175
8607
|
if (!Array.isArray(parsed)) return /* @__PURE__ */ new Set();
|
|
10176
8608
|
return new Set(parsed.filter((h) => typeof h === "string"));
|
|
10177
8609
|
} catch {
|
|
@@ -10182,18 +8614,18 @@ function addAliasDomain(hostname2) {
|
|
|
10182
8614
|
const existing = readExisting();
|
|
10183
8615
|
if (existing.has(hostname2)) return;
|
|
10184
8616
|
existing.add(hostname2);
|
|
10185
|
-
|
|
10186
|
-
|
|
8617
|
+
mkdirSync5(dirname4(ALIAS_DOMAINS_PATH), { recursive: true });
|
|
8618
|
+
writeFileSync6(ALIAS_DOMAINS_PATH, JSON.stringify([...existing], null, 2) + "\n", "utf-8");
|
|
10187
8619
|
}
|
|
10188
8620
|
|
|
10189
8621
|
// server/routes/admin/cloudflare.ts
|
|
10190
8622
|
var SETUP_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
10191
8623
|
var DOMAINS_TIMEOUT_MS = 40 * 1e3;
|
|
10192
8624
|
function loadBrandInfo() {
|
|
10193
|
-
const platformRoot2 = process.env.MAXY_PLATFORM_ROOT ??
|
|
10194
|
-
const brandPath =
|
|
8625
|
+
const platformRoot2 = process.env.MAXY_PLATFORM_ROOT ?? resolve14(process.cwd(), "..");
|
|
8626
|
+
const brandPath = resolve14(platformRoot2, "config", "brand.json");
|
|
10195
8627
|
try {
|
|
10196
|
-
const parsed = JSON.parse(
|
|
8628
|
+
const parsed = JSON.parse(readFileSync13(brandPath, "utf-8"));
|
|
10197
8629
|
const hostname2 = typeof parsed.hostname === "string" && parsed.hostname ? parsed.hostname : "maxy";
|
|
10198
8630
|
const configDir2 = typeof parsed.configDir === "string" && parsed.configDir ? parsed.configDir : ".maxy";
|
|
10199
8631
|
return { hostname: hostname2, configDir: configDir2 };
|
|
@@ -10296,7 +8728,7 @@ app21.get("/domains", requireAdminSession, async (c) => {
|
|
|
10296
8728
|
streamLogPath = streamLogPathFor(accountId, correlationId).streamLogPath;
|
|
10297
8729
|
log(`phase=stream-log-resolved path=${streamLogPath}`);
|
|
10298
8730
|
const brand = loadBrandInfo();
|
|
10299
|
-
const scriptPath =
|
|
8731
|
+
const scriptPath = resolve14(homedir(), "list-cf-domains.sh");
|
|
10300
8732
|
const result = await runFormSpawn({
|
|
10301
8733
|
scriptPath,
|
|
10302
8734
|
args: [brand.hostname],
|
|
@@ -10495,17 +8927,17 @@ var cloudflare_default = app21;
|
|
|
10495
8927
|
import { createReadStream as createReadStream3 } from "fs";
|
|
10496
8928
|
import { readdir as readdir2, readFile as readFile4, stat as stat4, mkdir as mkdir3, writeFile as writeFile4, unlink as unlink2 } from "fs/promises";
|
|
10497
8929
|
import { realpathSync as realpathSync4 } from "fs";
|
|
10498
|
-
import { basename as
|
|
8930
|
+
import { basename as basename4, dirname as dirname5, join as join8, resolve as resolve16, sep as sep2 } from "path";
|
|
10499
8931
|
import { Readable as Readable2 } from "stream";
|
|
10500
8932
|
|
|
10501
8933
|
// app/lib/data-path.ts
|
|
10502
8934
|
import { realpathSync as realpathSync3 } from "fs";
|
|
10503
|
-
import { resolve as
|
|
10504
|
-
var PLATFORM_ROOT6 = process.env.MAXY_PLATFORM_ROOT ??
|
|
10505
|
-
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");
|
|
10506
8938
|
function resolveDataPath(raw) {
|
|
10507
8939
|
const cleaned = normalize("/" + (raw ?? "").replace(/\\/g, "/")).replace(/^\/+/, "");
|
|
10508
|
-
const absolute =
|
|
8940
|
+
const absolute = resolve15(DATA_ROOT, cleaned);
|
|
10509
8941
|
let dataRootReal;
|
|
10510
8942
|
try {
|
|
10511
8943
|
dataRootReal = realpathSync3(DATA_ROOT);
|
|
@@ -10853,7 +9285,7 @@ async function cascadeDeleteDocument(params) {
|
|
|
10853
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;
|
|
10854
9286
|
async function readMeta(absDir, baseName) {
|
|
10855
9287
|
try {
|
|
10856
|
-
const raw = await readFile4(
|
|
9288
|
+
const raw = await readFile4(join8(absDir, `${baseName}.meta.json`), "utf8");
|
|
10857
9289
|
const parsed = JSON.parse(raw);
|
|
10858
9290
|
if (typeof parsed?.filename === "string") {
|
|
10859
9291
|
return { filename: parsed.filename, mimeType: typeof parsed.mimeType === "string" ? parsed.mimeType : void 0 };
|
|
@@ -10864,7 +9296,7 @@ async function readMeta(absDir, baseName) {
|
|
|
10864
9296
|
}
|
|
10865
9297
|
async function readAccountNames() {
|
|
10866
9298
|
const map = /* @__PURE__ */ new Map();
|
|
10867
|
-
const accountsDir =
|
|
9299
|
+
const accountsDir = resolve16(DATA_ROOT, "accounts");
|
|
10868
9300
|
let names;
|
|
10869
9301
|
try {
|
|
10870
9302
|
names = await readdir2(accountsDir);
|
|
@@ -10873,7 +9305,7 @@ async function readAccountNames() {
|
|
|
10873
9305
|
}
|
|
10874
9306
|
for (const name of names) {
|
|
10875
9307
|
if (!UUID_RE4.test(name)) continue;
|
|
10876
|
-
const configPath2 =
|
|
9308
|
+
const configPath2 = resolve16(accountsDir, name, "account.json");
|
|
10877
9309
|
try {
|
|
10878
9310
|
const raw = await readFile4(configPath2, "utf8");
|
|
10879
9311
|
const parsed = JSON.parse(raw);
|
|
@@ -10891,7 +9323,7 @@ async function readAccountNames() {
|
|
|
10891
9323
|
}
|
|
10892
9324
|
async function enrich(absolute, entry, accountNames) {
|
|
10893
9325
|
if (entry.kind === "directory" && UUID_RE4.test(entry.name)) {
|
|
10894
|
-
const meta = await readMeta(
|
|
9326
|
+
const meta = await readMeta(join8(absolute, entry.name), entry.name);
|
|
10895
9327
|
if (meta?.filename) {
|
|
10896
9328
|
entry.displayName = meta.filename;
|
|
10897
9329
|
entry.mimeType = meta.mimeType;
|
|
@@ -10950,7 +9382,7 @@ app22.get("/", requireAdminSession, async (c) => {
|
|
|
10950
9382
|
continue;
|
|
10951
9383
|
}
|
|
10952
9384
|
try {
|
|
10953
|
-
const entryPath =
|
|
9385
|
+
const entryPath = join8(absolute, name);
|
|
10954
9386
|
const s = await stat4(entryPath);
|
|
10955
9387
|
entries.push({
|
|
10956
9388
|
name,
|
|
@@ -11005,7 +9437,7 @@ app22.get("/download", requireAdminSession, async (c) => {
|
|
|
11005
9437
|
if (!info.isFile()) {
|
|
11006
9438
|
return c.json({ error: "Path is not a file" }, 400);
|
|
11007
9439
|
}
|
|
11008
|
-
const filename =
|
|
9440
|
+
const filename = basename4(absolute);
|
|
11009
9441
|
const mimeType = detectMimeType(absolute);
|
|
11010
9442
|
const nodeStream = createReadStream3(absolute);
|
|
11011
9443
|
const webStream = Readable2.toWeb(nodeStream);
|
|
@@ -11062,10 +9494,10 @@ app22.post("/upload", requireAdminSession, async (c) => {
|
|
|
11062
9494
|
error: `Unsupported file type: "${file.type}". Supported: ${[...SUPPORTED_MIME_TYPES].join(", ")}.`
|
|
11063
9495
|
}, 422);
|
|
11064
9496
|
}
|
|
11065
|
-
const safeName =
|
|
9497
|
+
const safeName = basename4(file.name).replace(/[\0/\\]/g, "_");
|
|
11066
9498
|
const finalName = `${Date.now()}-${safeName}`;
|
|
11067
|
-
const destDir =
|
|
11068
|
-
const destPath =
|
|
9499
|
+
const destDir = resolve16(DATA_ROOT, "uploads", accountId);
|
|
9500
|
+
const destPath = resolve16(destDir, finalName);
|
|
11069
9501
|
try {
|
|
11070
9502
|
await mkdir3(destDir, { recursive: true });
|
|
11071
9503
|
const dataRootReal = realpathSync4(DATA_ROOT);
|
|
@@ -11107,7 +9539,7 @@ app22.delete("/", requireAdminSession, async (c) => {
|
|
|
11107
9539
|
return c.json({ error: resolution.error }, resolution.status);
|
|
11108
9540
|
}
|
|
11109
9541
|
const { absolute, relative: relPath2 } = resolution;
|
|
11110
|
-
const base =
|
|
9542
|
+
const base = basename4(absolute);
|
|
11111
9543
|
const segments = relPath2.split("/").filter(Boolean);
|
|
11112
9544
|
if (base === "account.json" || segments.includes(".git")) {
|
|
11113
9545
|
console.error(`[data] file-delete blocked path="${relPath2}" reason="protected"`);
|
|
@@ -11123,7 +9555,7 @@ app22.delete("/", requireAdminSession, async (c) => {
|
|
|
11123
9555
|
}
|
|
11124
9556
|
const dot = base.lastIndexOf(".");
|
|
11125
9557
|
const stem = dot === -1 ? base : base.slice(0, dot);
|
|
11126
|
-
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;
|
|
11127
9559
|
await unlink2(absolute);
|
|
11128
9560
|
if (sidecarPath) {
|
|
11129
9561
|
try {
|
|
@@ -11674,11 +10106,6 @@ var GRAPH_LABEL_COLOURS = {
|
|
|
11674
10106
|
// confusion doesn't apply, but kept distinguishable for the legend)
|
|
11675
10107
|
Email: "#6F7F4A",
|
|
11676
10108
|
EmailAccount: "#91A063",
|
|
11677
|
-
// Review signals (Task 626 — previously written by review-detector but
|
|
11678
|
-
// unregistered here, producing an `unknown label` 400 whenever the
|
|
11679
|
-
// filter popover advertised the label. Muted brick — still reads as
|
|
11680
|
-
// alert against the cream background but doesn't shout fire-engine red.)
|
|
11681
|
-
ReviewAlert: "#A85C5C",
|
|
11682
10109
|
// Public-agent projection (Task 837) — burnished bronze sits between
|
|
11683
10110
|
// people-terracotta and email-moss but shares neither hue, signalling
|
|
11684
10111
|
// "operator-defined persona that traverses to KnowledgeDocuments,
|
|
@@ -12603,8 +11030,8 @@ var adherence_default = app30;
|
|
|
12603
11030
|
// server/routes/admin/sidebar-artefacts.ts
|
|
12604
11031
|
import neo4j3 from "neo4j-driver";
|
|
12605
11032
|
import { readFile as readFile5, readdir as readdir3, stat as stat5 } from "fs/promises";
|
|
12606
|
-
import { resolve as
|
|
12607
|
-
import { existsSync as
|
|
11033
|
+
import { resolve as resolve17, relative as relative2, isAbsolute } from "path";
|
|
11034
|
+
import { existsSync as existsSync17 } from "fs";
|
|
12608
11035
|
var LIMIT = 50;
|
|
12609
11036
|
var TEXT_MIME_PREFIXES = ["text/", "application/json", "application/markdown"];
|
|
12610
11037
|
var ADMIN_AGENT_FILES = ["IDENTITY.md", "SOUL.md", "KNOWLEDGE.md"];
|
|
@@ -12620,7 +11047,7 @@ app31.get("/", requireAdminSession, async (c) => {
|
|
|
12620
11047
|
if (docs === null) {
|
|
12621
11048
|
return c.json({ error: "Failed to load artefacts" }, 500);
|
|
12622
11049
|
}
|
|
12623
|
-
const accountDir =
|
|
11050
|
+
const accountDir = resolve17(ACCOUNTS_DIR, accountId);
|
|
12624
11051
|
const agents = await fetchAgentTemplateRows(accountDir);
|
|
12625
11052
|
const artefacts = [...docs, ...agents].sort(
|
|
12626
11053
|
(a, b) => (b.updatedAt ?? "").localeCompare(a.updatedAt ?? "")
|
|
@@ -12683,8 +11110,8 @@ async function readArtefactContent(accountId, attachmentId, mimeType, displayNam
|
|
|
12683
11110
|
logSkip(displayName, "non-text-mime", mimeType);
|
|
12684
11111
|
return { content: "", skipReason: "non-text-mime" };
|
|
12685
11112
|
}
|
|
12686
|
-
const accountDir =
|
|
12687
|
-
const dir =
|
|
11113
|
+
const accountDir = resolve17(ATTACHMENTS_ROOT, accountId);
|
|
11114
|
+
const dir = resolve17(accountDir, attachmentId);
|
|
12688
11115
|
try {
|
|
12689
11116
|
validateFilePathInAccount(dir, accountDir);
|
|
12690
11117
|
} catch {
|
|
@@ -12698,7 +11125,7 @@ async function readArtefactContent(accountId, attachmentId, mimeType, displayNam
|
|
|
12698
11125
|
logSkip(displayName, "missing-on-disk", mimeType);
|
|
12699
11126
|
return { content: "", skipReason: "missing-on-disk" };
|
|
12700
11127
|
}
|
|
12701
|
-
return { content: await readFile5(
|
|
11128
|
+
return { content: await readFile5(resolve17(dir, dataFile), "utf-8"), skipReason: null };
|
|
12702
11129
|
} catch (err) {
|
|
12703
11130
|
const message = err instanceof Error ? err.message : String(err);
|
|
12704
11131
|
console.error(`[admin/sidebar-artefacts] read-failed attachmentId=${attachmentId.slice(0, 8)} error="${message}"`);
|
|
@@ -12714,8 +11141,8 @@ function logSkip(name, reason, mimeType) {
|
|
|
12714
11141
|
async function fetchAgentTemplateRows(accountDir) {
|
|
12715
11142
|
const rows = [];
|
|
12716
11143
|
for (const filename of ADMIN_AGENT_FILES) {
|
|
12717
|
-
const overridePath =
|
|
12718
|
-
const bundledPath =
|
|
11144
|
+
const overridePath = resolve17(accountDir, "agents", "admin", filename);
|
|
11145
|
+
const bundledPath = resolve17(PLATFORM_ROOT, "templates", "agents", "admin", filename);
|
|
12719
11146
|
const labelStem = filename.replace(/\.md$/, "");
|
|
12720
11147
|
const row = await readAgentTemplateRow({
|
|
12721
11148
|
id: `agent-template:admin:${filename}`,
|
|
@@ -12729,12 +11156,12 @@ async function fetchAgentTemplateRows(accountDir) {
|
|
|
12729
11156
|
});
|
|
12730
11157
|
if (row) rows.push(row);
|
|
12731
11158
|
}
|
|
12732
|
-
const overrideDir =
|
|
12733
|
-
const bundledDir =
|
|
11159
|
+
const overrideDir = resolve17(accountDir, "specialists", "agents");
|
|
11160
|
+
const bundledDir = resolve17(PLATFORM_ROOT, "templates", "specialists", "agents");
|
|
12734
11161
|
const specialistNames = await unionSpecialistFilenames(overrideDir, bundledDir);
|
|
12735
11162
|
for (const filename of specialistNames) {
|
|
12736
|
-
const overridePath =
|
|
12737
|
-
const bundledPath =
|
|
11163
|
+
const overridePath = resolve17(overrideDir, filename);
|
|
11164
|
+
const bundledPath = resolve17(bundledDir, filename);
|
|
12738
11165
|
const row = await readAgentTemplateRow({
|
|
12739
11166
|
id: `agent-template:specialist:${filename}`,
|
|
12740
11167
|
displayName: filename.replace(/\.md$/, ""),
|
|
@@ -12752,7 +11179,7 @@ async function fetchAgentTemplateRows(accountDir) {
|
|
|
12752
11179
|
async function unionSpecialistFilenames(overrideDir, bundledDir) {
|
|
12753
11180
|
const names = /* @__PURE__ */ new Set();
|
|
12754
11181
|
for (const dir of [overrideDir, bundledDir]) {
|
|
12755
|
-
if (!
|
|
11182
|
+
if (!existsSync17(dir)) continue;
|
|
12756
11183
|
try {
|
|
12757
11184
|
const entries = await readdir3(dir);
|
|
12758
11185
|
for (const entry of entries) {
|
|
@@ -12767,7 +11194,7 @@ async function unionSpecialistFilenames(overrideDir, bundledDir) {
|
|
|
12767
11194
|
}
|
|
12768
11195
|
async function readAgentTemplateRow(inp) {
|
|
12769
11196
|
let chosenPath = null;
|
|
12770
|
-
if (
|
|
11197
|
+
if (existsSync17(inp.overridePath)) {
|
|
12771
11198
|
try {
|
|
12772
11199
|
validateFilePathInAccount(inp.overridePath, inp.overrideRoot);
|
|
12773
11200
|
chosenPath = inp.overridePath;
|
|
@@ -12778,7 +11205,7 @@ async function readAgentTemplateRow(inp) {
|
|
|
12778
11205
|
);
|
|
12779
11206
|
return null;
|
|
12780
11207
|
}
|
|
12781
|
-
} else if (
|
|
11208
|
+
} else if (existsSync17(inp.bundledPath)) {
|
|
12782
11209
|
if (!isWithin(inp.bundledPath, inp.bundledRoot)) {
|
|
12783
11210
|
console.error(
|
|
12784
11211
|
`[admin/sidebar-artefacts] agent-template-read-failed agent=${inp.displayName} kind=${inp.logName} error="bundled path outside PLATFORM_ROOT"`
|
|
@@ -12819,8 +11246,8 @@ var sidebar_artefacts_default = app31;
|
|
|
12819
11246
|
|
|
12820
11247
|
// server/routes/admin/sidebar-artefact-save.ts
|
|
12821
11248
|
import { mkdir as mkdir4, readdir as readdir4, stat as stat6, writeFile as writeFile5 } from "fs/promises";
|
|
12822
|
-
import { resolve as
|
|
12823
|
-
import { existsSync as
|
|
11249
|
+
import { resolve as resolve18 } from "path";
|
|
11250
|
+
import { existsSync as existsSync18 } from "fs";
|
|
12824
11251
|
var ADMIN_AGENT_FILES2 = /* @__PURE__ */ new Set(["IDENTITY.md", "SOUL.md", "KNOWLEDGE.md"]);
|
|
12825
11252
|
var UUID_RE5 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
12826
11253
|
var app32 = new Hono();
|
|
@@ -12832,7 +11259,7 @@ app32.post("/", requireAdminSession, async (c) => {
|
|
|
12832
11259
|
if (!body || typeof body.id !== "string" || typeof body.content !== "string") {
|
|
12833
11260
|
return c.json({ error: "id and content required" }, 400);
|
|
12834
11261
|
}
|
|
12835
|
-
const accountDir =
|
|
11262
|
+
const accountDir = resolve18(ACCOUNTS_DIR, accountId);
|
|
12836
11263
|
const resolved = await resolveSavePath(body.id, accountId, accountDir);
|
|
12837
11264
|
if (resolved.kind === "reject") {
|
|
12838
11265
|
console.error(
|
|
@@ -12873,22 +11300,22 @@ async function resolveSavePath(id, accountId, accountDir) {
|
|
|
12873
11300
|
if (role !== "admin" || !ADMIN_AGENT_FILES2.has(filename)) {
|
|
12874
11301
|
return { kind: "reject", status: 400, reason: "invalid-id" };
|
|
12875
11302
|
}
|
|
12876
|
-
const parent =
|
|
11303
|
+
const parent = resolve18(accountDir, "agents", "admin");
|
|
12877
11304
|
await mkdir4(parent, { recursive: true });
|
|
12878
11305
|
try {
|
|
12879
11306
|
validateFilePathInAccount(parent, accountDir);
|
|
12880
11307
|
} catch {
|
|
12881
11308
|
return { kind: "reject", status: 400, reason: "containment-rejected" };
|
|
12882
11309
|
}
|
|
12883
|
-
return { kind: "admin-template", path:
|
|
11310
|
+
return { kind: "admin-template", path: resolve18(parent, filename) };
|
|
12884
11311
|
}
|
|
12885
11312
|
if (UUID_RE5.test(id)) {
|
|
12886
|
-
const dir =
|
|
12887
|
-
if (!
|
|
11313
|
+
const dir = resolve18(ATTACHMENTS_ROOT, accountId, id);
|
|
11314
|
+
if (!existsSync18(dir)) {
|
|
12888
11315
|
return { kind: "reject", status: 400, reason: "not-found" };
|
|
12889
11316
|
}
|
|
12890
11317
|
try {
|
|
12891
|
-
validateFilePathInAccount(dir,
|
|
11318
|
+
validateFilePathInAccount(dir, resolve18(ATTACHMENTS_ROOT, accountId));
|
|
12892
11319
|
} catch {
|
|
12893
11320
|
return { kind: "reject", status: 400, reason: "containment-rejected" };
|
|
12894
11321
|
}
|
|
@@ -12897,7 +11324,7 @@ async function resolveSavePath(id, accountId, accountDir) {
|
|
|
12897
11324
|
if (!dataFile) {
|
|
12898
11325
|
return { kind: "reject", status: 400, reason: "not-found" };
|
|
12899
11326
|
}
|
|
12900
|
-
return { kind: "knowledge-doc", path:
|
|
11327
|
+
return { kind: "knowledge-doc", path: resolve18(dir, dataFile) };
|
|
12901
11328
|
}
|
|
12902
11329
|
return { kind: "reject", status: 400, reason: "invalid-id" };
|
|
12903
11330
|
}
|
|
@@ -12908,8 +11335,8 @@ var sidebar_artefact_save_default = app32;
|
|
|
12908
11335
|
|
|
12909
11336
|
// server/routes/admin/sidebar-artefact-content.ts
|
|
12910
11337
|
import { readFile as readFile6, readdir as readdir5 } from "fs/promises";
|
|
12911
|
-
import { existsSync as
|
|
12912
|
-
import { resolve as
|
|
11338
|
+
import { existsSync as existsSync19 } from "fs";
|
|
11339
|
+
import { resolve as resolve19 } from "path";
|
|
12913
11340
|
var UUID_RE6 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
12914
11341
|
var app33 = new Hono();
|
|
12915
11342
|
app33.get("/", requireAdminSession, async (c) => {
|
|
@@ -12921,14 +11348,14 @@ app33.get("/", requireAdminSession, async (c) => {
|
|
|
12921
11348
|
console.error(`[admin/sidebar-artefact-content] not-found id=${id.slice(0, 8)}`);
|
|
12922
11349
|
return new Response("Not found", { status: 404 });
|
|
12923
11350
|
}
|
|
12924
|
-
const dir =
|
|
12925
|
-
if (!
|
|
11351
|
+
const dir = resolve19(ATTACHMENTS_ROOT, accountId, id);
|
|
11352
|
+
if (!existsSync19(dir)) {
|
|
12926
11353
|
console.error(`[admin/sidebar-artefact-content] not-found id=${id.slice(0, 8)}`);
|
|
12927
11354
|
return new Response("Not found", { status: 404 });
|
|
12928
11355
|
}
|
|
12929
11356
|
let meta;
|
|
12930
11357
|
try {
|
|
12931
|
-
meta = JSON.parse(await readFile6(
|
|
11358
|
+
meta = JSON.parse(await readFile6(resolve19(dir, `${id}.meta.json`), "utf-8"));
|
|
12932
11359
|
} catch {
|
|
12933
11360
|
console.error(`[admin/sidebar-artefact-content] not-found id=${id.slice(0, 8)}`);
|
|
12934
11361
|
return new Response("Not found", { status: 404 });
|
|
@@ -12940,7 +11367,7 @@ app33.get("/", requireAdminSession, async (c) => {
|
|
|
12940
11367
|
return new Response("Not found", { status: 404 });
|
|
12941
11368
|
}
|
|
12942
11369
|
const start = Date.now();
|
|
12943
|
-
const buffer = await readFile6(
|
|
11370
|
+
const buffer = await readFile6(resolve19(dir, dataFile));
|
|
12944
11371
|
const ms = Date.now() - start;
|
|
12945
11372
|
console.log(
|
|
12946
11373
|
`[admin/sidebar-artefact-content] account=${accountId} id=${id.slice(0, 8)} mime=${meta.mimeType} bytes=${buffer.length} ms=${ms}`
|
|
@@ -12984,8 +11411,8 @@ app34.route("/sidebar-artefact-content", sidebar_artefact_content_default);
|
|
|
12984
11411
|
var admin_default = app34;
|
|
12985
11412
|
|
|
12986
11413
|
// server/routes/sites.ts
|
|
12987
|
-
import { existsSync as
|
|
12988
|
-
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";
|
|
12989
11416
|
var SAFE_SEG_RE = /^[a-z0-9_][a-z0-9_.-]{0,99}$/i;
|
|
12990
11417
|
var MIME = {
|
|
12991
11418
|
".html": "text/html; charset=utf-8",
|
|
@@ -13043,28 +11470,28 @@ app35.get("/:rel{.*}", (c) => {
|
|
|
13043
11470
|
}
|
|
13044
11471
|
segments.push(seg);
|
|
13045
11472
|
}
|
|
13046
|
-
const rootDir =
|
|
13047
|
-
let filePath = segments.length === 0 ? rootDir :
|
|
11473
|
+
const rootDir = resolve20(account.accountDir, "sites");
|
|
11474
|
+
let filePath = segments.length === 0 ? rootDir : resolve20(rootDir, ...segments);
|
|
13048
11475
|
if (filePath !== rootDir && !filePath.startsWith(rootDir + "/")) {
|
|
13049
11476
|
console.error(`[sites] path-traversal-rejected path=${reqPath} reason=escape status=403`);
|
|
13050
11477
|
return c.text("Forbidden", 403);
|
|
13051
11478
|
}
|
|
13052
11479
|
let stat7;
|
|
13053
11480
|
try {
|
|
13054
|
-
stat7 =
|
|
11481
|
+
stat7 = existsSync20(filePath) ? statSync5(filePath) : null;
|
|
13055
11482
|
} catch {
|
|
13056
11483
|
stat7 = null;
|
|
13057
11484
|
}
|
|
13058
11485
|
if (stat7?.isDirectory()) {
|
|
13059
|
-
filePath =
|
|
11486
|
+
filePath = resolve20(filePath, "index.html");
|
|
13060
11487
|
} else if (stat7 === null && isDirRequest) {
|
|
13061
|
-
filePath =
|
|
11488
|
+
filePath = resolve20(filePath, "index.html");
|
|
13062
11489
|
}
|
|
13063
11490
|
if (!filePath.startsWith(rootDir + "/")) {
|
|
13064
11491
|
console.error(`[sites] path-traversal-rejected path=${reqPath} reason=escape status=403`);
|
|
13065
11492
|
return c.text("Forbidden", 403);
|
|
13066
11493
|
}
|
|
13067
|
-
if (!
|
|
11494
|
+
if (!existsSync20(filePath)) {
|
|
13068
11495
|
console.error(`[sites] not-found path=${reqPath} status=404`);
|
|
13069
11496
|
return c.text("Not found", 404);
|
|
13070
11497
|
}
|
|
@@ -13083,7 +11510,7 @@ app35.get("/:rel{.*}", (c) => {
|
|
|
13083
11510
|
}
|
|
13084
11511
|
let body;
|
|
13085
11512
|
try {
|
|
13086
|
-
body =
|
|
11513
|
+
body = readFileSync14(realPath);
|
|
13087
11514
|
} catch (err) {
|
|
13088
11515
|
const code = err?.code;
|
|
13089
11516
|
if (code === "EISDIR") {
|
|
@@ -13215,14 +11642,14 @@ function clientFrom(c) {
|
|
|
13215
11642
|
);
|
|
13216
11643
|
}
|
|
13217
11644
|
var PLATFORM_ROOT7 = process.env.MAXY_PLATFORM_ROOT || "";
|
|
13218
|
-
var BRAND_JSON_PATH = PLATFORM_ROOT7 ?
|
|
11645
|
+
var BRAND_JSON_PATH = PLATFORM_ROOT7 ? join9(PLATFORM_ROOT7, "config", "brand.json") : "";
|
|
13219
11646
|
var BRAND = { productName: "Maxy", hostname: "maxy", configDir: ".maxy", domain: "getmaxy.com" };
|
|
13220
|
-
if (BRAND_JSON_PATH && !
|
|
11647
|
+
if (BRAND_JSON_PATH && !existsSync21(BRAND_JSON_PATH)) {
|
|
13221
11648
|
console.error(`[brand] WARNING: brand.json not found at ${BRAND_JSON_PATH} \u2014 using Maxy defaults`);
|
|
13222
11649
|
}
|
|
13223
|
-
if (BRAND_JSON_PATH &&
|
|
11650
|
+
if (BRAND_JSON_PATH && existsSync21(BRAND_JSON_PATH)) {
|
|
13224
11651
|
try {
|
|
13225
|
-
const parsed = JSON.parse(
|
|
11652
|
+
const parsed = JSON.parse(readFileSync15(BRAND_JSON_PATH, "utf-8"));
|
|
13226
11653
|
BRAND = { ...BRAND, ...parsed };
|
|
13227
11654
|
} catch (err) {
|
|
13228
11655
|
console.error(`[brand] Failed to parse brand.json: ${err.message}`);
|
|
@@ -13241,11 +11668,11 @@ var brandLoginOpts = {
|
|
|
13241
11668
|
bodyFont: BRAND.defaultFonts?.body,
|
|
13242
11669
|
logoContainsName: !!BRAND.logoContainsName
|
|
13243
11670
|
};
|
|
13244
|
-
var ALIAS_DOMAINS_PATH2 =
|
|
11671
|
+
var ALIAS_DOMAINS_PATH2 = join9(homedir2(), BRAND.configDir, "alias-domains.json");
|
|
13245
11672
|
function loadAliasDomains() {
|
|
13246
11673
|
try {
|
|
13247
|
-
if (!
|
|
13248
|
-
const parsed = JSON.parse(
|
|
11674
|
+
if (!existsSync21(ALIAS_DOMAINS_PATH2)) return null;
|
|
11675
|
+
const parsed = JSON.parse(readFileSync15(ALIAS_DOMAINS_PATH2, "utf-8"));
|
|
13249
11676
|
if (!Array.isArray(parsed)) {
|
|
13250
11677
|
console.error("[alias-domains] malformed alias-domains.json \u2014 expected array");
|
|
13251
11678
|
return null;
|
|
@@ -13585,20 +12012,20 @@ app36.get("/agent-assets/:slug/:filename", (c) => {
|
|
|
13585
12012
|
console.error(`[agent-assets] no-account slug=${slug} file=${filename}`);
|
|
13586
12013
|
return c.text("Not found", 404);
|
|
13587
12014
|
}
|
|
13588
|
-
const filePath =
|
|
13589
|
-
const expectedDir =
|
|
12015
|
+
const filePath = resolve21(account.accountDir, "agents", slug, "assets", filename);
|
|
12016
|
+
const expectedDir = resolve21(account.accountDir, "agents", slug, "assets");
|
|
13590
12017
|
if (!filePath.startsWith(expectedDir + "/")) {
|
|
13591
12018
|
console.error(`[agent-assets] path-traversal-rejected slug=${slug} file=${filename}`);
|
|
13592
12019
|
return c.text("Forbidden", 403);
|
|
13593
12020
|
}
|
|
13594
|
-
if (!
|
|
12021
|
+
if (!existsSync21(filePath)) {
|
|
13595
12022
|
console.error(`[agent-assets] serve slug=${slug} file=${filename} status=404`);
|
|
13596
12023
|
return c.text("Not found", 404);
|
|
13597
12024
|
}
|
|
13598
12025
|
const ext = "." + filename.split(".").pop()?.toLowerCase();
|
|
13599
12026
|
const contentType = IMAGE_MIME[ext] || "application/octet-stream";
|
|
13600
12027
|
console.log(`[agent-assets] serve slug=${slug} file=${filename} status=200`);
|
|
13601
|
-
const body =
|
|
12028
|
+
const body = readFileSync15(filePath);
|
|
13602
12029
|
return c.body(body, 200, {
|
|
13603
12030
|
"Content-Type": contentType,
|
|
13604
12031
|
"Cache-Control": "public, max-age=3600"
|
|
@@ -13615,20 +12042,20 @@ app36.get("/generated/:filename", (c) => {
|
|
|
13615
12042
|
console.error(`[generated] serve file=${filename} status=404`);
|
|
13616
12043
|
return c.text("Not found", 404);
|
|
13617
12044
|
}
|
|
13618
|
-
const filePath =
|
|
13619
|
-
const expectedDir =
|
|
12045
|
+
const filePath = resolve21(account.accountDir, "generated", filename);
|
|
12046
|
+
const expectedDir = resolve21(account.accountDir, "generated");
|
|
13620
12047
|
if (!filePath.startsWith(expectedDir + "/")) {
|
|
13621
12048
|
console.error(`[generated] serve file=${filename} status=403`);
|
|
13622
12049
|
return c.text("Forbidden", 403);
|
|
13623
12050
|
}
|
|
13624
|
-
if (!
|
|
12051
|
+
if (!existsSync21(filePath)) {
|
|
13625
12052
|
console.error(`[generated] serve file=${filename} status=404`);
|
|
13626
12053
|
return c.text("Not found", 404);
|
|
13627
12054
|
}
|
|
13628
12055
|
const ext = "." + filename.split(".").pop()?.toLowerCase();
|
|
13629
12056
|
const contentType = IMAGE_MIME[ext] || "application/octet-stream";
|
|
13630
12057
|
console.log(`[generated] serve file=${filename} status=200`);
|
|
13631
|
-
const body =
|
|
12058
|
+
const body = readFileSync15(filePath);
|
|
13632
12059
|
return c.body(body, 200, {
|
|
13633
12060
|
"Content-Type": contentType,
|
|
13634
12061
|
"Cache-Control": "public, max-age=86400"
|
|
@@ -13638,9 +12065,9 @@ app36.route("/sites", sites_default);
|
|
|
13638
12065
|
var htmlCache = /* @__PURE__ */ new Map();
|
|
13639
12066
|
var brandLogoPath = "/brand/maxy-monochrome.png";
|
|
13640
12067
|
var brandIconPath = "/brand/maxy-monochrome.png";
|
|
13641
|
-
if (BRAND_JSON_PATH &&
|
|
12068
|
+
if (BRAND_JSON_PATH && existsSync21(BRAND_JSON_PATH)) {
|
|
13642
12069
|
try {
|
|
13643
|
-
const fullBrand = JSON.parse(
|
|
12070
|
+
const fullBrand = JSON.parse(readFileSync15(BRAND_JSON_PATH, "utf-8"));
|
|
13644
12071
|
if (fullBrand.assets?.logo) brandLogoPath = `/brand/${fullBrand.assets.logo}`;
|
|
13645
12072
|
brandIconPath = fullBrand.assets?.icon ? `/brand/${fullBrand.assets.icon}` : brandLogoPath;
|
|
13646
12073
|
} catch {
|
|
@@ -13657,9 +12084,9 @@ var brandScript = `<script>window.__BRAND__=${JSON.stringify({
|
|
|
13657
12084
|
function readInstalledVersion() {
|
|
13658
12085
|
try {
|
|
13659
12086
|
if (!PLATFORM_ROOT7) return "unknown";
|
|
13660
|
-
const versionFile =
|
|
13661
|
-
if (!
|
|
13662
|
-
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();
|
|
13663
12090
|
return content || "unknown";
|
|
13664
12091
|
} catch {
|
|
13665
12092
|
return "unknown";
|
|
@@ -13700,7 +12127,7 @@ var clientErrorReporterScript = `<script>
|
|
|
13700
12127
|
function cachedHtml(file) {
|
|
13701
12128
|
let html = htmlCache.get(file);
|
|
13702
12129
|
if (!html) {
|
|
13703
|
-
html =
|
|
12130
|
+
html = readFileSync15(resolve21(process.cwd(), "public", file), "utf-8");
|
|
13704
12131
|
const productNameEsc = escapeHtml(BRAND.productName);
|
|
13705
12132
|
html = html.replace(/<title>([^<]*)<\/title>/, (_match, inner) => `<title>${escapeHtml(inner).replace(/Maxy/g, productNameEsc)}</title>`);
|
|
13706
12133
|
html = html.replace('href="/favicon.ico"', `href="${escapeHtml(brandFaviconPath)}"`);
|
|
@@ -13716,26 +12143,26 @@ ${clientErrorReporterScript}
|
|
|
13716
12143
|
}
|
|
13717
12144
|
var brandedHtmlCache = /* @__PURE__ */ new Map();
|
|
13718
12145
|
function loadBrandingCache(agentSlug) {
|
|
13719
|
-
const configDir2 =
|
|
12146
|
+
const configDir2 = join9(homedir2(), BRAND.configDir);
|
|
13720
12147
|
try {
|
|
13721
|
-
const accountJsonPath =
|
|
13722
|
-
if (!
|
|
13723
|
-
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"));
|
|
13724
12151
|
const accountId = account.accountId;
|
|
13725
12152
|
if (!accountId) return null;
|
|
13726
|
-
const cachePath =
|
|
13727
|
-
if (!
|
|
13728
|
-
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"));
|
|
13729
12156
|
} catch {
|
|
13730
12157
|
return null;
|
|
13731
12158
|
}
|
|
13732
12159
|
}
|
|
13733
12160
|
function resolveDefaultSlug() {
|
|
13734
12161
|
try {
|
|
13735
|
-
const configDir2 =
|
|
13736
|
-
const accountJsonPath =
|
|
13737
|
-
if (!
|
|
13738
|
-
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"));
|
|
13739
12166
|
return account.defaultAgent || null;
|
|
13740
12167
|
} catch {
|
|
13741
12168
|
return null;
|
|
@@ -13808,7 +12235,7 @@ app36.use("/vnc-popout.html", logViewerFetch);
|
|
|
13808
12235
|
app36.get("/vnc-popout.html", (c) => {
|
|
13809
12236
|
let html = htmlCache.get("vnc-popout.html");
|
|
13810
12237
|
if (!html) {
|
|
13811
|
-
html =
|
|
12238
|
+
html = readFileSync15(resolve21(process.cwd(), "public", "vnc-popout.html"), "utf-8");
|
|
13812
12239
|
const name = escapeHtml(BRAND.productName);
|
|
13813
12240
|
html = html.replace("<title>Browser \u2014 Maxy</title>", `<title>${name}</title>`);
|
|
13814
12241
|
html = html.replace("</head>", ` ${brandScript}
|
|
@@ -13898,8 +12325,8 @@ try {
|
|
|
13898
12325
|
(async () => {
|
|
13899
12326
|
try {
|
|
13900
12327
|
let userId = "";
|
|
13901
|
-
if (
|
|
13902
|
-
const users = JSON.parse(
|
|
12328
|
+
if (existsSync21(USERS_FILE)) {
|
|
12329
|
+
const users = JSON.parse(readFileSync15(USERS_FILE, "utf-8").trim() || "[]");
|
|
13903
12330
|
userId = users[0]?.userId ?? "";
|
|
13904
12331
|
}
|
|
13905
12332
|
await backfillNullUserIdConversations(userId);
|
|
@@ -13914,15 +12341,8 @@ try {
|
|
|
13914
12341
|
console.error(`[migration] runBootMigrations rejected: ${err instanceof Error ? err.message : String(err)}`);
|
|
13915
12342
|
}
|
|
13916
12343
|
})();
|
|
13917
|
-
(async () => {
|
|
13918
|
-
try {
|
|
13919
|
-
await startReviewDetector();
|
|
13920
|
-
} catch (err) {
|
|
13921
|
-
console.error(`[review] startReviewDetector rejected: ${err instanceof Error ? err.message : String(err)}`);
|
|
13922
|
-
}
|
|
13923
|
-
})();
|
|
13924
12344
|
startGraphHealthTimer();
|
|
13925
|
-
var configDirForWhatsApp =
|
|
12345
|
+
var configDirForWhatsApp = basename5(MAXY_DIR) || ".maxy";
|
|
13926
12346
|
var bootAccount = resolveAccount();
|
|
13927
12347
|
var bootAccountConfig = bootAccount?.config;
|
|
13928
12348
|
var bootPublicAgent = bootAccount ? resolvePublicAgent(bootAccount.accountDir, { accountId: bootAccount.accountId })?.slug ?? null : null;
|
|
@@ -13966,7 +12386,7 @@ if (bootAccountConfig?.whatsapp) {
|
|
|
13966
12386
|
}
|
|
13967
12387
|
init({
|
|
13968
12388
|
configDir: configDirForWhatsApp,
|
|
13969
|
-
platformRoot:
|
|
12389
|
+
platformRoot: resolve21(process.env.MAXY_PLATFORM_ROOT ?? join9(__dirname, "..")),
|
|
13970
12390
|
accountConfig: bootAccountConfig,
|
|
13971
12391
|
onMessage: async (msg) => {
|
|
13972
12392
|
try {
|
|
@@ -14103,11 +12523,6 @@ process.on("SIGTERM", async () => {
|
|
|
14103
12523
|
} catch (err) {
|
|
14104
12524
|
console.error(`[server] shutdown error: ${String(err)}`);
|
|
14105
12525
|
}
|
|
14106
|
-
try {
|
|
14107
|
-
await shutdownReviewDetector();
|
|
14108
|
-
} catch (err) {
|
|
14109
|
-
console.error(`[server] review detector shutdown error: ${String(err)}`);
|
|
14110
|
-
}
|
|
14111
12526
|
console.error("[server] graceful shutdown complete \u2014 exiting");
|
|
14112
12527
|
process.exit(0);
|
|
14113
12528
|
});
|