agentic-browser 2.6.1 → 2.6.2

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.
@@ -8,7 +8,33 @@ import WebSocket from "ws";
8
8
  import crypto from "node:crypto";
9
9
  import { URL as URL$1 } from "node:url";
10
10
  import { z } from "zod";
11
- //#region src/session/chrome-launcher.ts
11
+ /**
12
+ * Create a directory (recursively) and tighten it to owner-only if it already existed
13
+ * with wider permissions (skips home, filesystem root, and dirs owned by another user).
14
+ */
15
+ function ensurePrivateDir(dir) {
16
+ fs.mkdirSync(dir, {
17
+ recursive: true,
18
+ mode: 448
19
+ });
20
+ try {
21
+ if (process.platform === "win32" || typeof process.getuid !== "function") return;
22
+ const resolved = path.resolve(dir);
23
+ if (resolved === path.parse(resolved).root || resolved === path.resolve(os.homedir())) return;
24
+ const st = fs.statSync(resolved);
25
+ if (!st.isDirectory() || st.uid !== process.getuid()) return;
26
+ if ((st.mode & 63) !== 0) fs.chmodSync(resolved, 448);
27
+ } catch {}
28
+ }
29
+ /** Write a file with owner-only permissions, tightening any pre-existing file. */
30
+ function writePrivateFileSync(file, data) {
31
+ fs.writeFileSync(file, data, {
32
+ encoding: "utf8",
33
+ mode: 384
34
+ });
35
+ fs.chmodSync(file, 384);
36
+ }
37
+ // src/session/chrome-launcher.ts
12
38
  const CANDIDATES = [
13
39
  "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
14
40
  "/usr/bin/google-chrome",
@@ -21,8 +47,36 @@ function discoverChrome(explicitPath) {
21
47
  if (!found) throw new Error("No supported Chrome installation found.");
22
48
  return found;
23
49
  }
24
- //#endregion
25
- //#region src/session/browser-controller.ts
50
+ // src/session/browser-controller.ts
51
+ /**
52
+ * Detect a running Chrome that owns `profileDir`. On macOS/Linux `SingletonLock`
53
+ * is a dangling symlink to `<hostname>-<pid>`, so existsSync (which follows
54
+ * symlinks) cannot see it; inspect the link itself and probe the pid.
55
+ */
56
+ function isProfileLocked(profileDir) {
57
+ const lockFile = path.join(profileDir, "SingletonLock");
58
+ let target;
59
+ try {
60
+ if (!fs.lstatSync(lockFile).isSymbolicLink()) return { locked: true };
61
+ target = fs.readlinkSync(lockFile);
62
+ } catch {
63
+ return { locked: false };
64
+ }
65
+ const pid = Number(target.slice(target.lastIndexOf("-") + 1));
66
+ if (!Number.isInteger(pid) || pid <= 0) return { locked: true };
67
+ try {
68
+ process.kill(pid, 0);
69
+ return {
70
+ locked: true,
71
+ pid
72
+ };
73
+ } catch (e) {
74
+ return {
75
+ locked: e.code === "EPERM",
76
+ pid
77
+ };
78
+ }
79
+ }
26
80
  var CdpConnection = class CdpConnection {
27
81
  ws;
28
82
  nextId = 0;
@@ -749,7 +803,7 @@ function buildKeyDispatchPayload(inputKey) {
749
803
  };
750
804
  }
751
805
  /** Connection pool configuration */
752
- const CONNECTION_IDLE_TIMEOUT_MS = 300 * 1e3;
806
+ const CONNECTION_IDLE_TIMEOUT_MS = 3e5;
753
807
  const CONNECTION_CLEANUP_INTERVAL_MS = 6e4;
754
808
  /**
755
809
  * True when the error means the socket was closed/not-open — i.e. the request
@@ -765,6 +819,7 @@ var ChromeCdpBrowserController = class {
765
819
  baseDir;
766
820
  connectionFactory;
767
821
  connections = /* @__PURE__ */ new Map();
822
+ pendingConnections = /* @__PURE__ */ new Map();
768
823
  cleanupInterval;
769
824
  /**
770
825
  * User-agent override resolved at target init, keyed by target ws URL.
@@ -793,17 +848,27 @@ var ChromeCdpBrowserController = class {
793
848
  cached.lastUsedAt = Date.now();
794
849
  return cached.conn;
795
850
  }
796
- const conn = await this.connectionFactory(targetWsUrl);
797
- await this.applySessionOverrides(conn, targetWsUrl);
798
- this.connections.set(targetWsUrl, {
799
- conn,
800
- enabled: {
801
- page: false,
802
- runtime: false
803
- },
804
- lastUsedAt: Date.now()
805
- });
806
- return conn;
851
+ const pending = this.pendingConnections.get(targetWsUrl);
852
+ if (pending) return pending;
853
+ const p = (async () => {
854
+ const conn = await this.connectionFactory(targetWsUrl);
855
+ await this.applySessionOverrides(conn, targetWsUrl);
856
+ this.connections.set(targetWsUrl, {
857
+ conn,
858
+ enabled: {
859
+ page: false,
860
+ runtime: false
861
+ },
862
+ lastUsedAt: Date.now()
863
+ });
864
+ return conn;
865
+ })();
866
+ this.pendingConnections.set(targetWsUrl, p);
867
+ try {
868
+ return await p;
869
+ } finally {
870
+ this.pendingConnections.delete(targetWsUrl);
871
+ }
807
872
  }
808
873
  /**
809
874
  * Re-apply session-scoped stealth patches + UA override on a fresh connection.
@@ -883,9 +948,11 @@ var ChromeCdpBrowserController = class {
883
948
  if (userProfileDir === "default") profileDir = resolveDefaultProfileDir();
884
949
  else if (userProfileDir) profileDir = userProfileDir;
885
950
  else profileDir = path.join(this.baseDir, "profiles", sessionId);
886
- fs.mkdirSync(profileDir, { recursive: true });
951
+ if (userProfileDir) fs.mkdirSync(profileDir, { recursive: true });
952
+ else ensurePrivateDir(profileDir);
887
953
  const lockFile = path.join(profileDir, "SingletonLock");
888
- if (fs.existsSync(lockFile)) throw new Error(`Chrome profile is already in use (lock file exists: ${lockFile}). Quit the running Chrome instance first, or use --cdp-url to connect to it instead.`);
954
+ const lock = isProfileLocked(profileDir);
955
+ if (lock.locked) throw new Error(`Chrome profile is already in use (lock file exists: ${lockFile}${lock.pid ? `, pid ${lock.pid}` : ""}). Quit the running Chrome instance first, or use --cdp-url to connect to it instead.`);
889
956
  const launchAttempts = headless ? [{ headless: true }] : [{ headless: false }, { headless: true }];
890
957
  let lastError;
891
958
  for (const attempt of launchAttempts) {
@@ -1078,6 +1145,40 @@ var ChromeCdpBrowserController = class {
1078
1145
  if (value !== "ready" && value !== "cleared") throw new Error("Unable to resolve type target");
1079
1146
  return value;
1080
1147
  }
1148
+ /**
1149
+ * Arm a wait for a main-frame navigation BEFORE the click is dispatched, so
1150
+ * the start event cannot be missed. Resolves "navigated" once the main frame
1151
+ * started loading and then committed/loaded (each stage timeout-tolerant),
1152
+ * or "none" when no main-frame load started within 500 ms.
1153
+ */
1154
+ async armMainFrameNavigationWait(conn) {
1155
+ const matching = conn.waitForEventMatching?.bind(conn);
1156
+ if (!matching) return { result: (async () => {
1157
+ try {
1158
+ await conn.waitForEvent("Page.frameNavigated", 50);
1159
+ } catch {
1160
+ return "none";
1161
+ }
1162
+ try {
1163
+ await Promise.race([conn.waitForEvent("Page.loadEventFired", 3e3), conn.waitForEvent("Page.frameStoppedLoading", 3e3)]);
1164
+ } catch {}
1165
+ return "navigated";
1166
+ })() };
1167
+ let mainFrameId;
1168
+ try {
1169
+ mainFrameId = (await conn.send("Page.getFrameTree")).frameTree?.frame?.id;
1170
+ } catch {}
1171
+ const isMain = (params) => mainFrameId === void 0 || params.frameId === mainFrameId;
1172
+ const started = matching("Page.frameStartedLoading", isMain, 500).then(() => true, () => false);
1173
+ return { result: (async () => {
1174
+ if (!await started) return "none";
1175
+ const navigated = matching("Page.frameNavigated", (params) => !params.frame?.parentId, 5e3).catch(() => void 0);
1176
+ const loaded = Promise.race([matching("Page.frameStoppedLoading", isMain, 5e3), matching("Page.loadEventFired", () => true, 5e3)]).catch(() => void 0);
1177
+ await navigated;
1178
+ await loaded;
1179
+ return "navigated";
1180
+ })() };
1181
+ }
1081
1182
  async dispatchMouseClick(conn, x, y) {
1082
1183
  await conn.send("Input.dispatchMouseEvent", {
1083
1184
  type: "mouseMoved",
@@ -1177,13 +1278,9 @@ var ChromeCdpBrowserController = class {
1177
1278
  await this.ensureEnabled(targetWsUrl);
1178
1279
  if (payload.action === "click") {
1179
1280
  const point = await this.preparePointerTarget(conn, payload);
1281
+ const nav = await this.armMainFrameNavigationWait(conn);
1180
1282
  await this.dispatchMouseClick(conn, point.x, point.y);
1181
- try {
1182
- await conn.waitForEvent("Page.frameNavigated", 50);
1183
- try {
1184
- await Promise.race([conn.waitForEvent("Page.loadEventFired", 3e3), conn.waitForEvent("Page.frameStoppedLoading", 3e3)]);
1185
- } catch {}
1186
- } catch {}
1283
+ await nav.result;
1187
1284
  return "clicked";
1188
1285
  }
1189
1286
  if (payload.action === "hover") {
@@ -2640,8 +2737,7 @@ var MockBrowserController = class {
2640
2737
  terminate(_pid) {}
2641
2738
  closeConnection(_targetWsUrl) {}
2642
2739
  };
2643
- //#endregion
2644
- //#region src/lib/domain-schemas.ts
2740
+ // src/lib/domain-schemas.ts
2645
2741
  const InteractActionSchema = z.enum([
2646
2742
  "click",
2647
2743
  "type",
@@ -2747,12 +2843,11 @@ z.object({
2747
2843
  createdAt: z.string().datetime(),
2748
2844
  severity: EventSeveritySchema
2749
2845
  });
2750
- //#endregion
2751
- //#region src/session/session-store.ts
2846
+ // src/session/session-store.ts
2752
2847
  var SessionStore = class {
2753
2848
  filePath;
2754
2849
  constructor(baseDir) {
2755
- fs.mkdirSync(baseDir, { recursive: true });
2850
+ ensurePrivateDir(baseDir);
2756
2851
  this.filePath = path.join(baseDir, "sessions.json");
2757
2852
  if (!fs.existsSync(this.filePath)) this.write({ sessions: {} });
2758
2853
  }
@@ -2838,12 +2933,68 @@ var SessionStore = class {
2838
2933
  }
2839
2934
  write(state) {
2840
2935
  const tmp = `${this.filePath}.${process.pid}.tmp`;
2841
- fs.writeFileSync(tmp, JSON.stringify(state, null, 2), "utf8");
2936
+ writePrivateFileSync(tmp, JSON.stringify(state, null, 2));
2842
2937
  fs.renameSync(tmp, this.filePath);
2843
2938
  }
2844
2939
  };
2845
- //#endregion
2846
- //#region src/session/session-manager.ts
2940
+ // src/session/url-policy.ts
2941
+ function assertNavigableUrl(raw, opts) {
2942
+ let parsed;
2943
+ try {
2944
+ parsed = new URL(raw);
2945
+ } catch {
2946
+ throw new Error(`Invalid URL: ${raw}`);
2947
+ }
2948
+ const { protocol } = parsed;
2949
+ if (protocol === "http:" || protocol === "https:") return;
2950
+ if (raw === "about:blank") return;
2951
+ if (protocol === "file:" && opts.allowFileUrls) return;
2952
+ throw new Error(`Navigation to ${protocol} URLs is not allowed${protocol === "file:" ? " (set AGENTIC_BROWSER_ALLOW_FILE_URLS=true to allow file: URLs)" : ""}`);
2953
+ }
2954
+ // src/memory/redact-step-payload.ts
2955
+ const ALLOWED_KEYS = /* @__PURE__ */ new Set([
2956
+ "action",
2957
+ "selector",
2958
+ "fallbackSelectors",
2959
+ "ref",
2960
+ "key",
2961
+ "direction",
2962
+ "amount",
2963
+ "deltaY",
2964
+ "scrollX",
2965
+ "scrollY",
2966
+ "timeoutMs",
2967
+ "redacted"
2968
+ ]);
2969
+ const URL_IN_TEXT = /\bhttps?:\/\/[^\s"'<>]+/gi;
2970
+ /** Reduce a URL to origin + pathname, dropping query string and fragment. */
2971
+ function stripUrl(rawUrl) {
2972
+ try {
2973
+ const parsed = new URL(rawUrl);
2974
+ return `${parsed.origin}${parsed.pathname}`;
2975
+ } catch {
2976
+ return;
2977
+ }
2978
+ }
2979
+ /** Strip query strings and fragments from every http(s) URL embedded in free text. */
2980
+ function redactUrlsInText(text) {
2981
+ return text.replace(URL_IN_TEXT, (match) => stripUrl(match) ?? "[url]");
2982
+ }
2983
+ /** Keep only structural, non-secret keys of a step payload; typed text and values are dropped. */
2984
+ function redactStepPayload(payload) {
2985
+ const result = {};
2986
+ for (const [key, value] of Object.entries(payload)) {
2987
+ if (key === "url") {
2988
+ const stripped = typeof value === "string" ? stripUrl(value) : void 0;
2989
+ if (stripped !== void 0) result.url = stripped;
2990
+ continue;
2991
+ }
2992
+ if (ALLOWED_KEYS.has(key)) result[key] = value;
2993
+ }
2994
+ if ("text" in payload || "value" in payload) result.redacted = true;
2995
+ return result;
2996
+ }
2997
+ // src/session/session-manager.ts
2847
2998
  var SessionManager = class {
2848
2999
  ctx;
2849
3000
  browser;
@@ -2926,6 +3077,7 @@ var SessionManager = class {
2926
3077
  if (input.type === "navigate") {
2927
3078
  const url = String(input.payload.url ?? "");
2928
3079
  if (!url) throw new Error("navigate command requires payload.url");
3080
+ assertNavigableUrl(url, { allowFileUrls: this.ctx.config.allowFileUrls });
2929
3081
  const finalUrl = await this.browser.navigate(record.targetWsUrl, url);
2930
3082
  this.store.setLastUrl(sessionId, finalUrl);
2931
3083
  resultMessage = `Navigated to ${finalUrl}`;
@@ -3219,7 +3371,7 @@ var SessionManager = class {
3219
3371
  type: input.type === "navigate" ? "navigate" : input.type === "interact" ? "interact" : "assert",
3220
3372
  summary: input.type === "interact" ? `interact:${action ?? "unknown"}` : input.type,
3221
3373
  selector,
3222
- payload: input.payload
3374
+ payload: redactStepPayload(input.payload)
3223
3375
  },
3224
3376
  selector,
3225
3377
  url: inputUrl
@@ -3246,8 +3398,7 @@ var SessionManager = class {
3246
3398
  }
3247
3399
  }
3248
3400
  };
3249
- //#endregion
3250
- //#region src/transport/control-api.ts
3401
+ // src/transport/control-api.ts
3251
3402
  var ControlApi = class {
3252
3403
  sessions;
3253
3404
  constructor(sessions) {
@@ -3305,8 +3456,7 @@ var ControlApi = class {
3305
3456
  return this.sessions.cleanupSessions(input);
3306
3457
  }
3307
3458
  };
3308
- //#endregion
3309
- //#region src/lib/config.ts
3459
+ // src/lib/config.ts
3310
3460
  function loadConfig(env = process.env) {
3311
3461
  const userProfile = env.AGENTIC_BROWSER_USER_PROFILE;
3312
3462
  let userProfileDir;
@@ -3318,11 +3468,11 @@ function loadConfig(env = process.env) {
3318
3468
  cdpUrl: env.AGENTIC_BROWSER_CDP_URL,
3319
3469
  userProfileDir,
3320
3470
  headless: env.AGENTIC_BROWSER_HEADLESS === "true" || env.AGENTIC_BROWSER_HEADLESS === "1",
3321
- userAgent: env.AGENTIC_BROWSER_USER_AGENT || void 0
3471
+ userAgent: env.AGENTIC_BROWSER_USER_AGENT || void 0,
3472
+ allowFileUrls: env.AGENTIC_BROWSER_ALLOW_FILE_URLS === "true" || env.AGENTIC_BROWSER_ALLOW_FILE_URLS === "1"
3322
3473
  };
3323
3474
  }
3324
- //#endregion
3325
- //#region src/observability/logger.ts
3475
+ // src/observability/logger.ts
3326
3476
  var Logger = class {
3327
3477
  namespace;
3328
3478
  constructor(namespace) {
@@ -3353,8 +3503,7 @@ var Logger = class {
3353
3503
  this.emit("error", message, context);
3354
3504
  }
3355
3505
  };
3356
- //#endregion
3357
- //#region src/observability/event-store.ts
3506
+ // src/observability/event-store.ts
3358
3507
  const MAX_EVENTS_PER_SESSION = 500;
3359
3508
  var EventStore = class {
3360
3509
  events = /* @__PURE__ */ new Map();
@@ -3364,8 +3513,11 @@ var EventStore = class {
3364
3513
  pendingFlushDelayMs = Number.POSITIVE_INFINITY;
3365
3514
  flushInFlight;
3366
3515
  constructor(baseDir) {
3367
- fs.mkdirSync(baseDir, { recursive: true });
3516
+ ensurePrivateDir(baseDir);
3368
3517
  this.filePath = path.join(baseDir, "session-events.log");
3518
+ try {
3519
+ if (fs.existsSync(this.filePath)) fs.chmodSync(this.filePath, 384);
3520
+ } catch {}
3369
3521
  process.on("beforeExit", this.flushSync);
3370
3522
  process.on("exit", this.flushSync);
3371
3523
  }
@@ -3416,7 +3568,10 @@ var EventStore = class {
3416
3568
  async flushAsync() {
3417
3569
  if (this.flushInFlight || this.bufferedLines.length === 0) return;
3418
3570
  const chunk = this.bufferedLines.splice(0, this.bufferedLines.length).join("");
3419
- this.flushInFlight = fs.promises.appendFile(this.filePath, chunk, "utf8").catch(() => {}).finally(() => {
3571
+ this.flushInFlight = fs.promises.appendFile(this.filePath, chunk, {
3572
+ encoding: "utf8",
3573
+ mode: 384
3574
+ }).catch(() => {}).finally(() => {
3420
3575
  this.flushInFlight = void 0;
3421
3576
  if (this.bufferedLines.length > 0) this.flushSoon(0);
3422
3577
  });
@@ -3430,12 +3585,14 @@ var EventStore = class {
3430
3585
  if (this.bufferedLines.length === 0) return;
3431
3586
  const chunk = this.bufferedLines.splice(0, this.bufferedLines.length).join("");
3432
3587
  try {
3433
- fs.appendFileSync(this.filePath, chunk, "utf8");
3588
+ fs.appendFileSync(this.filePath, chunk, {
3589
+ encoding: "utf8",
3590
+ mode: 384
3591
+ });
3434
3592
  } catch {}
3435
3593
  };
3436
3594
  };
3437
- //#endregion
3438
- //#region src/auth/session-token.ts
3595
+ // src/auth/session-token.ts
3439
3596
  var SessionTokenService = class {
3440
3597
  tokens = /* @__PURE__ */ new Map();
3441
3598
  issue(sessionId) {
@@ -3461,8 +3618,7 @@ var SessionTokenService = class {
3461
3618
  this.tokens.set(sessionId, token);
3462
3619
  }
3463
3620
  };
3464
- //#endregion
3465
- //#region src/memory/memory-index.ts
3621
+ // src/memory/memory-index.ts
3466
3622
  function normalize(value) {
3467
3623
  return value.trim().toLowerCase();
3468
3624
  }
@@ -3571,8 +3727,7 @@ var MemoryIndex = class {
3571
3727
  return candidates.map((insight) => scoreInsight(insight, normalizedIntent, normalizedDomain, profileLookup)).filter(relevant).toSorted((a, b) => b.result.score - a.result.score).slice(0, limit).map((scored) => scored.result);
3572
3728
  }
3573
3729
  };
3574
- //#endregion
3575
- //#region src/memory/staleness-detector.ts
3730
+ // src/memory/staleness-detector.ts
3576
3731
  const MAX_SUSPECT_STRIKES = 2;
3577
3732
  function detectStalenessSignal(errorMessage, selector) {
3578
3733
  const lowered = errorMessage.toLowerCase();
@@ -3600,7 +3755,7 @@ function applyFailure(insight, signal) {
3600
3755
  };
3601
3756
  }
3602
3757
  function applyAgeStaleness(insight) {
3603
- const ageDays = (Date.now() - Date.parse(insight.lastVerifiedAt)) / (1440 * 60 * 1e3);
3758
+ const ageDays = (Date.now() - Date.parse(insight.lastVerifiedAt)) / 864e5;
3604
3759
  if (insight.freshness === "fresh" && ageDays > 30) return {
3605
3760
  ...insight,
3606
3761
  freshness: "suspect",
@@ -3627,8 +3782,57 @@ function applySuccess(insight) {
3627
3782
  updatedAt: now
3628
3783
  };
3629
3784
  }
3630
- //#endregion
3631
- //#region src/memory/memory-schemas.ts
3785
+ // src/lib/flush-registry.ts
3786
+ const REGISTRY = Symbol.for("agentic-browser.flush-registry");
3787
+ function flushAll(items) {
3788
+ for (const item of items) try {
3789
+ item.flushSync();
3790
+ } catch {}
3791
+ }
3792
+ function createRegistry() {
3793
+ const items = /* @__PURE__ */ new Set();
3794
+ const onSignal = (signal) => {
3795
+ flushAll(items);
3796
+ if (process.listenerCount(signal) === 1) process.exit(0);
3797
+ };
3798
+ return {
3799
+ items,
3800
+ onExit: () => flushAll(items),
3801
+ onSigint: () => onSignal("SIGINT"),
3802
+ onSigterm: () => onSignal("SIGTERM")
3803
+ };
3804
+ }
3805
+ /**
3806
+ * Register an item to be flushed synchronously on process exit / SIGINT / SIGTERM.
3807
+ * Returns an idempotent unregister function; the process listeners are removed
3808
+ * once the last item is unregistered.
3809
+ */
3810
+ function registerFlushable(item) {
3811
+ const proc = process;
3812
+ let registry = proc[REGISTRY];
3813
+ if (!registry) {
3814
+ registry = createRegistry();
3815
+ proc[REGISTRY] = registry;
3816
+ process.on("exit", registry.onExit);
3817
+ process.on("SIGINT", registry.onSigint);
3818
+ process.on("SIGTERM", registry.onSigterm);
3819
+ }
3820
+ registry.items.add(item);
3821
+ const owner = registry;
3822
+ let done = false;
3823
+ return () => {
3824
+ if (done) return;
3825
+ done = true;
3826
+ owner.items.delete(item);
3827
+ if (owner.items.size === 0 && proc[REGISTRY] === owner) {
3828
+ process.off("exit", owner.onExit);
3829
+ process.off("SIGINT", owner.onSigint);
3830
+ process.off("SIGTERM", owner.onSigterm);
3831
+ delete proc[REGISTRY];
3832
+ }
3833
+ };
3834
+ }
3835
+ // src/memory/memory-schemas.ts
3632
3836
  const InsightFreshnessSchema = z.enum([
3633
3837
  "fresh",
3634
3838
  "suspect",
@@ -3705,27 +3909,25 @@ const SiteProfileSchema = z.object({
3705
3909
  createdAt: z.string().datetime()
3706
3910
  });
3707
3911
  const SiteProfileStateSchema = z.object({ profiles: z.array(SiteProfileSchema) });
3708
- //#endregion
3709
- //#region src/memory/site-profile-store.ts
3912
+ // src/memory/site-profile-store.ts
3710
3913
  const EMPTY_STATE$1 = { profiles: [] };
3711
3914
  const FLUSH_DELAY_MS$1 = 500;
3712
- const REGISTERED_EXIT_HANDLERS$1 = Symbol.for("agentic-browser.site-profile-store.exit-handlers");
3713
- const REGISTERED_STORES$1 = Symbol.for("agentic-browser.site-profile-store.instances");
3714
3915
  var SiteProfileStore = class {
3715
3916
  filePath;
3716
3917
  cached = null;
3717
3918
  dirty = false;
3718
3919
  flushTimer = null;
3920
+ unregister;
3719
3921
  constructor(baseDir) {
3720
3922
  const memoryDir = path.join(baseDir, "memory");
3721
- fs.mkdirSync(memoryDir, { recursive: true });
3923
+ ensurePrivateDir(memoryDir);
3722
3924
  this.filePath = path.join(memoryDir, "site-profiles.json");
3723
3925
  const tmpPath = `${this.filePath}.tmp`;
3724
3926
  try {
3725
3927
  if (fs.existsSync(tmpPath)) fs.unlinkSync(tmpPath);
3726
3928
  } catch {}
3727
3929
  if (!fs.existsSync(this.filePath)) this.writeDisk(EMPTY_STATE$1);
3728
- this.registerProcessHandlers();
3930
+ this.unregister = registerFlushable(this);
3729
3931
  }
3730
3932
  list() {
3731
3933
  return this.getCache();
@@ -3746,6 +3948,11 @@ var SiteProfileStore = class {
3746
3948
  else profiles.push(normalized);
3747
3949
  this.markDirty();
3748
3950
  }
3951
+ /** Flush pending writes and stop flushing on process exit. Idempotent. */
3952
+ dispose() {
3953
+ this.flushSync();
3954
+ this.unregister();
3955
+ }
3749
3956
  flushSync() {
3750
3957
  if (this.flushTimer) {
3751
3958
  clearTimeout(this.flushTimer);
@@ -3802,51 +4009,30 @@ var SiteProfileStore = class {
3802
4009
  }
3803
4010
  writeDisk(state) {
3804
4011
  const tempPath = `${this.filePath}.tmp`;
3805
- fs.writeFileSync(tempPath, JSON.stringify(state, null, 2), "utf8");
4012
+ writePrivateFileSync(tempPath, JSON.stringify(state, null, 2));
3806
4013
  fs.renameSync(tempPath, this.filePath);
3807
4014
  }
3808
- registerProcessHandlers() {
3809
- const proc = process;
3810
- const stores = proc[REGISTERED_STORES$1] ??= /* @__PURE__ */ new Set();
3811
- stores.add(this);
3812
- if (proc[REGISTERED_EXIT_HANDLERS$1]) return;
3813
- proc[REGISTERED_EXIT_HANDLERS$1] = true;
3814
- const flushAll = () => {
3815
- for (const store of stores) store.flushSync();
3816
- };
3817
- process.on("exit", flushAll);
3818
- process.on("SIGINT", () => {
3819
- flushAll();
3820
- process.exit(0);
3821
- });
3822
- process.on("SIGTERM", () => {
3823
- flushAll();
3824
- process.exit(0);
3825
- });
3826
- }
3827
4015
  };
3828
- //#endregion
3829
- //#region src/memory/task-insight-store.ts
4016
+ // src/memory/task-insight-store.ts
3830
4017
  const EMPTY_STATE = { insights: [] };
3831
4018
  const FLUSH_DELAY_MS = 500;
3832
- const REGISTERED_EXIT_HANDLERS = Symbol.for("agentic-browser.task-insight-store.exit-handlers");
3833
- const REGISTERED_STORES = Symbol.for("agentic-browser.task-insight-store.instances");
3834
4019
  var TaskInsightStore = class {
3835
4020
  filePath;
3836
4021
  /** In-memory cache – authoritative after first load. */
3837
4022
  cached = null;
3838
4023
  dirty = false;
3839
4024
  flushTimer = null;
4025
+ unregister;
3840
4026
  constructor(baseDir) {
3841
4027
  const memoryDir = path.join(baseDir, "memory");
3842
- fs.mkdirSync(memoryDir, { recursive: true });
4028
+ ensurePrivateDir(memoryDir);
3843
4029
  this.filePath = path.join(memoryDir, "insights.json");
3844
4030
  const tmpPath = `${this.filePath}.tmp`;
3845
4031
  try {
3846
4032
  if (fs.existsSync(tmpPath)) fs.unlinkSync(tmpPath);
3847
4033
  } catch {}
3848
4034
  if (!fs.existsSync(this.filePath)) this.writeDisk(EMPTY_STATE);
3849
- this.registerProcessHandlers();
4035
+ this.unregister = registerFlushable(this);
3850
4036
  }
3851
4037
  list() {
3852
4038
  return this.getCache();
@@ -3876,6 +4062,11 @@ var TaskInsightStore = class {
3876
4062
  this.cached = insights;
3877
4063
  this.markDirty();
3878
4064
  }
4065
+ /** Flush pending writes and stop flushing on process exit. Idempotent. */
4066
+ dispose() {
4067
+ this.flushSync();
4068
+ this.unregister();
4069
+ }
3879
4070
  /** Force an immediate synchronous flush (used at shutdown). */
3880
4071
  flushSync() {
3881
4072
  if (this.flushTimer) {
@@ -3892,8 +4083,52 @@ var TaskInsightStore = class {
3892
4083
  if (this.cached) return this.cached;
3893
4084
  const state = this.readDisk();
3894
4085
  this.cached = state.insights;
4086
+ if (this.sanitizeCached(this.cached)) this.markDirty();
3895
4087
  return this.cached;
3896
4088
  }
4089
+ /**
4090
+ * Redact already-persisted step payloads and URL query strings/fragments in place.
4091
+ * Returns true if anything changed.
4092
+ */
4093
+ sanitizeCached(insights) {
4094
+ let changed = false;
4095
+ for (const insight of insights) {
4096
+ const expectedOutcome = redactUrlsInText(insight.expectedOutcome);
4097
+ if (expectedOutcome !== insight.expectedOutcome) {
4098
+ insight.expectedOutcome = expectedOutcome;
4099
+ changed = true;
4100
+ }
4101
+ const taskIntent = redactUrlsInText(insight.taskIntent);
4102
+ if (taskIntent !== insight.taskIntent) {
4103
+ insight.taskIntent = taskIntent;
4104
+ changed = true;
4105
+ }
4106
+ for (const evidence of insight.evidence) {
4107
+ if (evidence.url !== void 0) {
4108
+ const url = stripUrl(evidence.url);
4109
+ if (url !== evidence.url) {
4110
+ evidence.url = url;
4111
+ changed = true;
4112
+ }
4113
+ }
4114
+ if (evidence.reason !== void 0) {
4115
+ const reason = redactUrlsInText(evidence.reason);
4116
+ if (reason !== evidence.reason) {
4117
+ evidence.reason = reason;
4118
+ changed = true;
4119
+ }
4120
+ }
4121
+ }
4122
+ for (const step of insight.actionRecipe) {
4123
+ const redacted = redactStepPayload(step.payload);
4124
+ if (JSON.stringify(redacted) !== JSON.stringify(step.payload)) {
4125
+ step.payload = redacted;
4126
+ changed = true;
4127
+ }
4128
+ }
4129
+ }
4130
+ return changed;
4131
+ }
3897
4132
  markDirty() {
3898
4133
  this.dirty = true;
3899
4134
  if (!this.flushTimer) this.flushTimer = setTimeout(() => {
@@ -3935,31 +4170,11 @@ var TaskInsightStore = class {
3935
4170
  }
3936
4171
  writeDisk(state) {
3937
4172
  const tempPath = `${this.filePath}.tmp`;
3938
- fs.writeFileSync(tempPath, JSON.stringify(state, null, 2), "utf8");
4173
+ writePrivateFileSync(tempPath, JSON.stringify(state, null, 2));
3939
4174
  fs.renameSync(tempPath, this.filePath);
3940
4175
  }
3941
- registerProcessHandlers() {
3942
- const proc = process;
3943
- const stores = proc[REGISTERED_STORES] ??= /* @__PURE__ */ new Set();
3944
- stores.add(this);
3945
- if (proc[REGISTERED_EXIT_HANDLERS]) return;
3946
- proc[REGISTERED_EXIT_HANDLERS] = true;
3947
- const flushAll = () => {
3948
- for (const store of stores) store.flushSync();
3949
- };
3950
- process.on("exit", flushAll);
3951
- process.on("SIGINT", () => {
3952
- flushAll();
3953
- process.exit(0);
3954
- });
3955
- process.on("SIGTERM", () => {
3956
- flushAll();
3957
- process.exit(0);
3958
- });
3959
- }
3960
4176
  };
3961
- //#endregion
3962
- //#region src/memory/memory-service.ts
4177
+ // src/memory/memory-service.ts
3963
4178
  /** Memory service configuration */
3964
4179
  const SEARCH_CACHE_TTL_MS = 2e3;
3965
4180
  const MAX_SELECTOR_ALIASES = 10;
@@ -3987,6 +4202,11 @@ var MemoryService = class {
3987
4202
  this.store.flushSync();
3988
4203
  this.profileStore.flushSync();
3989
4204
  }
4205
+ /** Flush pending writes and release the stores' process exit hooks. */
4206
+ dispose() {
4207
+ this.store.dispose();
4208
+ this.profileStore.dispose();
4209
+ }
3990
4210
  search(input) {
3991
4211
  const cacheKey = `${input.taskIntent}\0${input.siteDomain ?? ""}\0${input.limit ?? 10}`;
3992
4212
  const now = Date.now();
@@ -4048,7 +4268,8 @@ var MemoryService = class {
4048
4268
  this.invalidateSearchCache();
4049
4269
  return verified;
4050
4270
  }
4051
- recordSuccess(input) {
4271
+ recordSuccess(rawInput) {
4272
+ const input = this.sanitizeInput(rawInput);
4052
4273
  this.updateSiteProfile(input.siteDomain, input.selector, input.url, input.sitePathPattern);
4053
4274
  const insights = this.store.list();
4054
4275
  const matched = this.findBestExactMatch(insights, input);
@@ -4107,13 +4328,14 @@ var MemoryService = class {
4107
4328
  this.invalidateSearchCache();
4108
4329
  return withAliases;
4109
4330
  }
4110
- recordFailure(input, errorMessage) {
4331
+ recordFailure(rawInput, errorMessage) {
4332
+ const input = this.sanitizeInput(rawInput);
4111
4333
  this.updateSiteProfile(input.siteDomain, input.selector, input.url, input.sitePathPattern);
4112
4334
  const insights = this.store.list();
4113
4335
  const matched = this.findBestExactMatch(insights, input);
4114
4336
  if (!matched) return;
4115
4337
  const signal = detectStalenessSignal(errorMessage, input.selector);
4116
- const evidence = this.createEvidence(input, "failure", errorMessage);
4338
+ const evidence = this.createEvidence(input, "failure", redactUrlsInText(errorMessage));
4117
4339
  const failed = applyFailure({
4118
4340
  ...matched,
4119
4341
  useCount: matched.useCount + 1,
@@ -4235,6 +4457,15 @@ var MemoryService = class {
4235
4457
  const rightSegment = normalizePathSegment(right);
4236
4458
  return Boolean(leftSegment) && leftSegment === rightSegment;
4237
4459
  }
4460
+ /** Strip query strings and fragments from every URL-bearing field before it is persisted. */
4461
+ sanitizeInput(input) {
4462
+ return {
4463
+ ...input,
4464
+ url: input.url ? stripUrl(input.url) ?? void 0 : void 0,
4465
+ expectedOutcome: redactUrlsInText(input.expectedOutcome),
4466
+ taskIntent: redactUrlsInText(input.taskIntent)
4467
+ };
4468
+ }
4238
4469
  createEvidence(input, result, reason) {
4239
4470
  return {
4240
4471
  evidenceId: crypto.randomUUID(),
@@ -4287,8 +4518,7 @@ var MemoryService = class {
4287
4518
  return [...recipe, step].slice(-8);
4288
4519
  }
4289
4520
  };
4290
- //#endregion
4291
- //#region src/cli/app.ts
4521
+ // src/cli/app.ts
4292
4522
  function createAppContext(env = process.env) {
4293
4523
  const config = loadConfig(env);
4294
4524
  return {
@@ -4299,17 +4529,31 @@ function createAppContext(env = process.env) {
4299
4529
  memoryService: new MemoryService(config.dataDir)
4300
4530
  };
4301
4531
  }
4302
- //#endregion
4303
- //#region src/cli/runtime.ts
4532
+ // src/cli/runtime.ts
4304
4533
  var AgenticBrowserCore = class {
4305
4534
  context;
4306
4535
  sessions;
4307
4536
  api;
4537
+ browserController;
4538
+ disposed = false;
4308
4539
  constructor(context, browserController) {
4309
4540
  this.context = context;
4541
+ this.browserController = browserController;
4310
4542
  this.sessions = new SessionManager(context, browserController);
4311
4543
  this.api = new ControlApi(this.sessions);
4312
4544
  }
4545
+ /**
4546
+ * Release process-level resources (event store and memory store exit handlers,
4547
+ * controller timers). Flushes pending memory writes.
4548
+ * Does not terminate browser sessions. Idempotent.
4549
+ */
4550
+ async dispose() {
4551
+ if (this.disposed) return;
4552
+ this.disposed = true;
4553
+ this.browserController.dispose?.();
4554
+ this.context.eventStore.dispose();
4555
+ this.context.memoryService.dispose();
4556
+ }
4313
4557
  async startSession(input = { browser: "chrome" }) {
4314
4558
  return await this.api.createSession(input);
4315
4559
  }
@@ -4380,5 +4624,4 @@ function createMockAgenticBrowserCore(env) {
4380
4624
  function createCliRuntime() {
4381
4625
  return createAgenticBrowserCore();
4382
4626
  }
4383
- //#endregion
4384
- export { createMockAgenticBrowserCore as i, createAgenticBrowserCore as n, createCliRuntime as r, AgenticBrowserCore as t };
4627
+ export { isConnectionError as a, createMockAgenticBrowserCore as i, createAgenticBrowserCore as n, ensurePrivateDir as o, createCliRuntime as r, writePrivateFileSync as s, AgenticBrowserCore as t };