@remembrance-ai/opencode-plugin 0.1.46 → 0.1.48

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/README.md CHANGED
@@ -49,9 +49,10 @@ chmod 600 ~/.config/remembrance/config.json
49
49
  local MCP server both read `~/.config/remembrance/config.json`, an unset
50
50
  `REMEMBRANCE_API_KEY` does **not** prove this install is anonymous.
51
51
 
52
- After restarting, call MCP `get_connection_status`. It should report
53
- `local_stdio_mcp`, the resolved credential source, and the expected organization
54
- scope. Never infer scope from `REMEMBRANCE_API_KEY` alone, from an environment
52
+ After restarting, run MCP `run_connection_doctor` and require
53
+ `safe_to_query: true`. It verifies the active connection and gives one exact
54
+ next step if attention is required. Use `get_connection_status` only for lower-
55
+ level fields. Never infer scope from `REMEMBRANCE_API_KEY` alone, from an environment
55
56
  variable, or from an anonymous REST/browser probe — the diagnostic verifies the
56
57
  process that will actually serve opencode's tools, without exposing the key.
57
58
 
@@ -66,6 +67,11 @@ printf '{"apiKey":"YOUR_ORG_KEY","apiUrl":"https://dev.remembrance.dev"}\n' \
66
67
  > ~/.config/remembrance/config.json
67
68
  ```
68
69
 
70
+ This paired file binds the key to that destination. If environment variables
71
+ are used instead, set `REMEMBRANCE_API_KEY_ORIGIN` equal to the exact custom
72
+ `REMEMBRANCE_API_URL`. Remote registries require HTTPS; a trusted private or
73
+ link-local self-host also requires `REMEMBRANCE_ALLOW_PRIVATE_REGISTRY=true`.
74
+
69
75
  ## What the plugin does
70
76
 
71
77
  | Event | Behavior |
package/opencode.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "command": [
7
7
  "npx",
8
8
  "-y",
9
- "@remembrance-ai/mcp-server@0.1.46"
9
+ "@remembrance-ai/mcp-server@0.1.48"
10
10
  ],
11
11
  "enabled": true,
12
12
  "environment": {
@@ -15,6 +15,6 @@
15
15
  }
16
16
  },
17
17
  "plugin": [
18
- "@remembrance-ai/opencode-plugin@0.1.46"
18
+ "@remembrance-ai/opencode-plugin@0.1.48"
19
19
  ]
20
20
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remembrance-ai/opencode-plugin",
3
- "version": "0.1.46",
3
+ "version": "0.1.48",
4
4
  "license": "Apache-2.0",
5
5
  "description": "opencode plugin for Remembrance session hooks, tool observation, and contribution nudges.",
6
6
  "type": "module",
@@ -17,15 +17,23 @@
17
17
 
18
18
  import { createHash, randomBytes } from "node:crypto";
19
19
  import {
20
+ chmodSync,
21
+ closeSync,
22
+ constants,
20
23
  existsSync,
24
+ fstatSync,
25
+ lstatSync,
21
26
  mkdirSync,
27
+ openSync,
22
28
  readFileSync,
29
+ readSync,
23
30
  readdirSync,
24
31
  renameSync,
25
32
  rmSync,
26
33
  statSync,
27
34
  writeFileSync,
28
35
  } from "node:fs";
36
+ import { isIP } from "node:net";
29
37
  import { homedir, tmpdir } from "node:os";
30
38
  import { dirname, join } from "node:path";
31
39
  import process from "node:process";
@@ -43,7 +51,8 @@ const VALUE_EPISODE_MARKER_LIMIT = 20;
43
51
  const VALUE_EPISODE_MARKER_TTL_MS = 30 * 24 * 60 * 60 * 1000;
44
52
  const DIRECT_SELECTION_MARKER_LIMIT = 20;
45
53
  const DIRECT_SELECTION_MARKER_TTL_MS = 24 * 60 * 60 * 1000;
46
- const PLUGIN_HEALTH_DIR = "remembrance-plugin-health";
54
+ const MAX_LOCAL_CONFIG_BYTES = 64 * 1024;
55
+ const MAX_LOCAL_HEALTH_MARKER_BYTES = 16 * 1024;
47
56
  const PLUGIN_HEALTH_COMPONENTS = new Set([
48
57
  "session_start",
49
58
  "prompt_hook",
@@ -2281,7 +2290,7 @@ export function contributeDisabled(value) {
2281
2290
  function pluginHealthDir(env = process.env) {
2282
2291
  return env?.REMEMBRANCE_PLUGIN_HEALTH_DIR
2283
2292
  ? String(env.REMEMBRANCE_PLUGIN_HEALTH_DIR)
2284
- : join(tmpdir(), PLUGIN_HEALTH_DIR);
2293
+ : join(homedir(), ".cache", "remembrance", "plugin-health");
2285
2294
  }
2286
2295
 
2287
2296
  function normalizedPluginHealthSurface(value) {
@@ -2317,7 +2326,9 @@ export function readPluginLifecycleHealth(
2317
2326
  const path = pluginHealthSessionPath(surface, sessionId, env);
2318
2327
  if (!path) return null;
2319
2328
  try {
2320
- const parsed = JSON.parse(readFileSync(path, "utf8"));
2329
+ const parsed = JSON.parse(
2330
+ readSecureHookFile(path, MAX_LOCAL_HEALTH_MARKER_BYTES),
2331
+ );
2321
2332
  return parsed && typeof parsed === "object" && !Array.isArray(parsed)
2322
2333
  ? parsed
2323
2334
  : null;
@@ -2329,7 +2340,7 @@ export function readPluginLifecycleHealth(
2329
2340
  // Record that a native lifecycle component actually executed. The marker is
2330
2341
  // intentionally local and content-free: it contains only surface/version,
2331
2342
  // component timestamps, and the credential source category. Local MCP reads
2332
- // it through get_connection_status so partial activation cannot look healthy.
2343
+ // it through run_connection_doctor so partial activation cannot look healthy.
2333
2344
  export function recordPluginLifecycleHealth(
2334
2345
  {
2335
2346
  surface,
@@ -2390,6 +2401,11 @@ export function recordPluginLifecycleHealth(
2390
2401
  // healthy for as long as the old marker remains fresh.
2391
2402
  const currentComponents =
2392
2403
  normalizedComponent === "session_start" ? {} : existingComponents;
2404
+ const apiConfiguration = resolveApiConfiguration(env);
2405
+ const apiDestinationFingerprint = createHash("sha256")
2406
+ .update(apiConfiguration.apiUrl.replace(/\/+$/, ""), "utf8")
2407
+ .digest("hex")
2408
+ .slice(0, 16);
2393
2409
  const payload = {
2394
2410
  schema_version: 1,
2395
2411
  surface: normalizedSurface,
@@ -2414,6 +2430,8 @@ export function recordPluginLifecycleHealth(
2414
2430
  )
2415
2431
  ? existing.credential_source
2416
2432
  : "none",
2433
+ api_destination_source: apiConfiguration.source,
2434
+ api_destination_fingerprint: apiDestinationFingerprint,
2417
2435
  components: {
2418
2436
  ...currentComponents,
2419
2437
  [normalizedComponent]: now,
@@ -2422,9 +2440,10 @@ export function recordPluginLifecycleHealth(
2422
2440
  };
2423
2441
  const temporaryPath = `${path}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
2424
2442
  try {
2425
- mkdirSync(dirname(path), { recursive: true });
2443
+ ensureSecureHookDirectory(dirname(path));
2426
2444
  writeFileSync(temporaryPath, `${JSON.stringify(payload)}\n`, {
2427
2445
  mode: 0o600,
2446
+ flag: "wx",
2428
2447
  });
2429
2448
  renameSync(temporaryPath, path);
2430
2449
  prunePluginHealthSessionMarkers(normalizedSurface, path, env);
@@ -2481,7 +2500,9 @@ export function remembranceConfigPath(env = process.env) {
2481
2500
 
2482
2501
  export function readRemembranceConfig(env = process.env) {
2483
2502
  try {
2484
- const parsed = JSON.parse(readFileSync(remembranceConfigPath(env), "utf8"));
2503
+ const parsed = JSON.parse(
2504
+ readSecureHookFile(remembranceConfigPath(env), MAX_LOCAL_CONFIG_BYTES),
2505
+ );
2485
2506
  return parsed && typeof parsed === "object" && !Array.isArray(parsed)
2486
2507
  ? parsed
2487
2508
  : {};
@@ -2497,32 +2518,59 @@ export function resolveApiCredential(env = process.env) {
2497
2518
  }
2498
2519
  const environmentKey = String(env.REMEMBRANCE_API_KEY ?? "").trim();
2499
2520
  if (environmentKey) {
2500
- return {
2501
- apiKey: environmentKey,
2502
- source: "environment",
2503
- };
2521
+ const explicitBinding = String(
2522
+ env.REMEMBRANCE_API_KEY_ORIGIN ?? "",
2523
+ ).trim();
2524
+ const binding = explicitBinding
2525
+ ? normalizeApiUrl(explicitBinding, env)
2526
+ : { apiUrl: DEFAULT_API_URL, issue: null };
2527
+ return credentialForApiDestination(
2528
+ binding.apiUrl && !binding.issue
2529
+ ? {
2530
+ apiKey: environmentKey,
2531
+ source: "environment",
2532
+ boundApiUrl: binding.apiUrl,
2533
+ }
2534
+ : unusableDestinationCredential(),
2535
+ apiConfiguration,
2536
+ );
2504
2537
  }
2505
2538
  const path = remembranceConfigPath(env);
2506
2539
  if (!existsSync(path)) {
2507
2540
  return { apiKey: "", source: "none" };
2508
2541
  }
2509
2542
  try {
2510
- const parsed = JSON.parse(readFileSync(path, "utf8"));
2543
+ const parsed = JSON.parse(
2544
+ readSecureHookFile(path, MAX_LOCAL_CONFIG_BYTES),
2545
+ );
2511
2546
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
2512
- return { apiKey: "", source: "unusable_shared_config" };
2547
+ return unusableSharedConfigCredential();
2513
2548
  }
2514
2549
  if (
2515
2550
  Object.prototype.hasOwnProperty.call(parsed, "apiKey") &&
2516
2551
  (typeof parsed.apiKey !== "string" || !parsed.apiKey.trim())
2517
2552
  ) {
2518
- return { apiKey: "", source: "unusable_shared_config" };
2553
+ return unusableSharedConfigCredential();
2519
2554
  }
2520
2555
  const fromFile = parsed.apiKey;
2521
- return typeof fromFile === "string" && fromFile.trim()
2522
- ? { apiKey: fromFile.trim(), source: "shared_config" }
2523
- : { apiKey: "", source: "none" };
2556
+ if (typeof fromFile !== "string" || !fromFile.trim()) {
2557
+ return { apiKey: "", source: "none" };
2558
+ }
2559
+ const binding = Object.prototype.hasOwnProperty.call(parsed, "apiUrl")
2560
+ ? normalizeApiUrl(parsed.apiUrl, env)
2561
+ : { apiUrl: DEFAULT_API_URL, issue: null };
2562
+ return credentialForApiDestination(
2563
+ binding.apiUrl && !binding.issue
2564
+ ? {
2565
+ apiKey: fromFile.trim(),
2566
+ source: "shared_config",
2567
+ boundApiUrl: binding.apiUrl,
2568
+ }
2569
+ : unusableSharedConfigCredential(),
2570
+ apiConfiguration,
2571
+ );
2524
2572
  } catch {
2525
- return { apiKey: "", source: "unusable_shared_config" };
2573
+ return unusableSharedConfigCredential();
2526
2574
  }
2527
2575
  }
2528
2576
 
@@ -2538,8 +2586,10 @@ export function sharedConfigCredentialNotice(env = process.env) {
2538
2586
  return (
2539
2587
  "Remembrance setup needs attention: REMEMBRANCE_API_URL is invalid. " +
2540
2588
  "Remote Remembrance calls are paused locally instead of changing " +
2541
- "destinations. Set it to an absolute HTTP(S) registry URL or " +
2542
- "intentionally remove it, then call get_connection_status."
2589
+ "destinations. Set it to an absolute HTTPS registry URL (HTTP is allowed " +
2590
+ "only for loopback development) without credentials, query parameters, " +
2591
+ "or fragments, or " +
2592
+ "intentionally remove it, then run run_connection_doctor."
2543
2593
  );
2544
2594
  }
2545
2595
  if (source === "unusable_shared_config") {
@@ -2547,7 +2597,16 @@ export function sharedConfigCredentialNotice(env = process.env) {
2547
2597
  "Remembrance setup needs attention: the shared config file exists but " +
2548
2598
  "is unreadable or invalid. Remote Remembrance calls are paused locally " +
2549
2599
  "instead of falling back to anonymous scope. Fix or intentionally remove " +
2550
- "~/.config/remembrance/config.json, then call get_connection_status."
2600
+ "~/.config/remembrance/config.json, then run run_connection_doctor."
2601
+ );
2602
+ }
2603
+ if (source === "unusable_destination_binding") {
2604
+ return (
2605
+ "Remembrance setup needs attention: the API key is not bound to the " +
2606
+ "configured registry destination. For a custom registry, store apiKey " +
2607
+ "and apiUrl together in the shared config, or set " +
2608
+ "REMEMBRANCE_API_KEY_ORIGIN to the exact REMEMBRANCE_API_URL value. " +
2609
+ "Remote calls are paused so the key cannot be forwarded elsewhere."
2551
2610
  );
2552
2611
  }
2553
2612
  if (source !== "shared_config") {
@@ -2558,24 +2617,30 @@ export function sharedConfigCredentialNotice(env = process.env) {
2558
2617
  `the shared Remembrance config file (normally ` +
2559
2618
  `~/.config/remembrance/config.json). REMEMBRANCE_API_KEY may be unset; do not ` +
2560
2619
  `use an anonymous REST/browser probe to infer this hook's scope. Call ` +
2561
- `get_connection_status for the MCP transport you will use.`
2620
+ `run_connection_doctor for the MCP transport you will use.`
2562
2621
  );
2563
2622
  }
2564
2623
 
2565
2624
  export function resolveApiConfiguration(env = process.env) {
2566
2625
  const environmentUrl = String(env.REMEMBRANCE_API_URL ?? "").trim();
2567
2626
  if (environmentUrl) {
2568
- const normalized = normalizeApiUrl(environmentUrl);
2569
- return normalized
2570
- ? { apiUrl: normalized, source: "environment" }
2571
- : { apiUrl: DEFAULT_API_URL, source: "unusable_environment" };
2627
+ const normalized = normalizeApiUrl(environmentUrl, env);
2628
+ return normalized.apiUrl && !normalized.issue
2629
+ ? { apiUrl: normalized.apiUrl, source: "environment" }
2630
+ : {
2631
+ apiUrl: DEFAULT_API_URL,
2632
+ source: "unusable_environment",
2633
+ issue: normalized.issue ?? "invalid_url",
2634
+ };
2572
2635
  }
2573
2636
  const path = remembranceConfigPath(env);
2574
2637
  if (!existsSync(path)) {
2575
2638
  return { apiUrl: DEFAULT_API_URL, source: "default" };
2576
2639
  }
2577
2640
  try {
2578
- const parsed = JSON.parse(readFileSync(path, "utf8"));
2641
+ const parsed = JSON.parse(
2642
+ readSecureHookFile(path, MAX_LOCAL_CONFIG_BYTES),
2643
+ );
2579
2644
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
2580
2645
  return {
2581
2646
  apiUrl: DEFAULT_API_URL,
@@ -2585,10 +2650,14 @@ export function resolveApiConfiguration(env = process.env) {
2585
2650
  if (!Object.prototype.hasOwnProperty.call(parsed, "apiUrl")) {
2586
2651
  return { apiUrl: DEFAULT_API_URL, source: "default" };
2587
2652
  }
2588
- const normalized = normalizeApiUrl(parsed.apiUrl);
2589
- return normalized
2590
- ? { apiUrl: normalized, source: "shared_config" }
2591
- : { apiUrl: DEFAULT_API_URL, source: "unusable_shared_config" };
2653
+ const normalized = normalizeApiUrl(parsed.apiUrl, env);
2654
+ return normalized.apiUrl && !normalized.issue
2655
+ ? { apiUrl: normalized.apiUrl, source: "shared_config" }
2656
+ : {
2657
+ apiUrl: DEFAULT_API_URL,
2658
+ source: "unusable_shared_config",
2659
+ issue: normalized.issue ?? "invalid_url",
2660
+ };
2592
2661
  } catch {
2593
2662
  return { apiUrl: DEFAULT_API_URL, source: "unusable_shared_config" };
2594
2663
  }
@@ -2598,8 +2667,10 @@ function apiUrl(env) {
2598
2667
  return resolveApiConfiguration(env).apiUrl;
2599
2668
  }
2600
2669
 
2601
- function normalizeApiUrl(value) {
2602
- if (typeof value !== "string" || !value.trim()) return null;
2670
+ function normalizeApiUrl(value, env = process.env) {
2671
+ if (typeof value !== "string" || !value.trim()) {
2672
+ return { apiUrl: null, issue: "invalid_url" };
2673
+ }
2603
2674
  const candidate = value.trim();
2604
2675
  try {
2605
2676
  const parsed = new URL(candidate);
@@ -2610,17 +2681,193 @@ function normalizeApiUrl(value) {
2610
2681
  parsed.search ||
2611
2682
  parsed.hash
2612
2683
  ) {
2613
- return null;
2684
+ return { apiUrl: null, issue: "invalid_url" };
2685
+ }
2686
+ const loopback = isLoopbackHostname(parsed.hostname);
2687
+ const privateDestination = isPrivateDestinationHostname(parsed.hostname);
2688
+ if (parsed.protocol === "http:" && !loopback) {
2689
+ return { apiUrl: null, issue: "insecure_remote_http" };
2690
+ }
2691
+ if (
2692
+ !loopback &&
2693
+ privateDestination &&
2694
+ env.REMEMBRANCE_ALLOW_PRIVATE_REGISTRY !== "true"
2695
+ ) {
2696
+ return {
2697
+ apiUrl: null,
2698
+ issue: "private_destination_requires_opt_in",
2699
+ };
2614
2700
  }
2615
- return candidate.replace(/\/+$/, "");
2701
+ return { apiUrl: candidate.replace(/\/+$/, ""), issue: null };
2616
2702
  } catch {
2617
- return null;
2703
+ return { apiUrl: null, issue: "invalid_url" };
2704
+ }
2705
+ }
2706
+
2707
+ function credentialForApiDestination(credential, configuration) {
2708
+ if (!credential.apiKey || isUnusableConfigurationSource(configuration.source)) {
2709
+ return credential.apiKey
2710
+ ? unusableDestinationCredential()
2711
+ : { apiKey: "", source: credential.source };
2618
2712
  }
2713
+ return credential.boundApiUrl === configuration.apiUrl
2714
+ ? { apiKey: credential.apiKey, source: credential.source }
2715
+ : unusableDestinationCredential();
2716
+ }
2717
+
2718
+ function unusableSharedConfigCredential() {
2719
+ return {
2720
+ apiKey: "",
2721
+ source: "unusable_shared_config",
2722
+ };
2723
+ }
2724
+
2725
+ function unusableDestinationCredential() {
2726
+ return {
2727
+ apiKey: "",
2728
+ source: "unusable_destination_binding",
2729
+ };
2730
+ }
2731
+
2732
+ function isLoopbackHostname(hostname) {
2733
+ const normalized = normalizeHostname(hostname);
2734
+ if (normalized === "localhost" || normalized.endsWith(".localhost")) {
2735
+ return true;
2736
+ }
2737
+ if (normalized === "::1") return true;
2738
+ if (isIP(normalized) === 4) {
2739
+ return Number(normalized.split(".")[0]) === 127;
2740
+ }
2741
+ return false;
2742
+ }
2743
+
2744
+ function isPrivateDestinationHostname(hostname) {
2745
+ const normalized = normalizeHostname(hostname);
2746
+ if (
2747
+ normalized.endsWith(".local") ||
2748
+ normalized.endsWith(".internal") ||
2749
+ normalized === "localhost"
2750
+ ) {
2751
+ return true;
2752
+ }
2753
+ const version = isIP(normalized);
2754
+ if (version === 4) {
2755
+ const [a, b] = normalized.split(".").map(Number);
2756
+ return (
2757
+ a === 0 ||
2758
+ a === 10 ||
2759
+ a === 127 ||
2760
+ (a === 100 && b >= 64 && b <= 127) ||
2761
+ (a === 169 && b === 254) ||
2762
+ (a === 172 && b >= 16 && b <= 31) ||
2763
+ (a === 192 && b === 168) ||
2764
+ (a === 198 && (b === 18 || b === 19)) ||
2765
+ a >= 224
2766
+ );
2767
+ }
2768
+ if (version === 6) {
2769
+ return (
2770
+ normalized === "::" ||
2771
+ normalized === "::1" ||
2772
+ normalized.startsWith("fc") ||
2773
+ normalized.startsWith("fd") ||
2774
+ /^fe[89ab]/.test(normalized)
2775
+ );
2776
+ }
2777
+ return false;
2778
+ }
2779
+
2780
+ function normalizeHostname(hostname) {
2781
+ return String(hostname).replace(/^\[|\]$/g, "").toLowerCase();
2782
+ }
2783
+
2784
+ function readSecureHookFile(path, maxBytes) {
2785
+ const parent = lstatSync(dirname(path));
2786
+ assertSecureHookDirectory(parent);
2787
+ const before = lstatSync(path);
2788
+ assertSecureHookFile(before, maxBytes);
2789
+ const descriptor = openSync(
2790
+ path,
2791
+ constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0),
2792
+ );
2793
+ try {
2794
+ const opened = fstatSync(descriptor);
2795
+ assertSecureHookFile(opened, maxBytes);
2796
+ if (before.dev !== opened.dev || before.ino !== opened.ino) {
2797
+ throw new Error("Remembrance local state changed while opening.");
2798
+ }
2799
+ return readBoundedHookDescriptor(descriptor, maxBytes);
2800
+ } finally {
2801
+ closeSync(descriptor);
2802
+ }
2803
+ }
2804
+
2805
+ function ensureSecureHookDirectory(path) {
2806
+ mkdirSync(path, { recursive: true, mode: 0o700 });
2807
+ const metadata = lstatSync(path);
2808
+ if (metadata.isSymbolicLink() || !metadata.isDirectory()) {
2809
+ throw new Error("Remembrance health state is not a regular directory.");
2810
+ }
2811
+ assertCurrentHookUser(metadata.uid);
2812
+ if (isPosixHookRuntime() && (metadata.mode & 0o077) !== 0) {
2813
+ chmodSync(path, 0o700);
2814
+ }
2815
+ }
2816
+
2817
+ function assertSecureHookDirectory(metadata) {
2818
+ if (metadata.isSymbolicLink() || !metadata.isDirectory()) {
2819
+ throw new Error("Remembrance local state parent is unsafe.");
2820
+ }
2821
+ assertCurrentHookUser(metadata.uid);
2822
+ if (isPosixHookRuntime() && (metadata.mode & 0o022) !== 0) {
2823
+ throw new Error("Remembrance local state parent is writable by others.");
2824
+ }
2825
+ }
2826
+
2827
+ function assertSecureHookFile(metadata, maxBytes) {
2828
+ if (metadata.isSymbolicLink() || !metadata.isFile()) {
2829
+ throw new Error("Remembrance local state is not a regular file.");
2830
+ }
2831
+ assertCurrentHookUser(metadata.uid);
2832
+ if (metadata.size > maxBytes) {
2833
+ throw new Error("Remembrance local state exceeds its size limit.");
2834
+ }
2835
+ if (isPosixHookRuntime() && (metadata.mode & 0o077) !== 0) {
2836
+ throw new Error("Remembrance local state permissions are not private.");
2837
+ }
2838
+ }
2839
+
2840
+ function assertCurrentHookUser(uid) {
2841
+ if (isPosixHookRuntime() && uid !== process.getuid()) {
2842
+ throw new Error("Remembrance local state is owned by another user.");
2843
+ }
2844
+ }
2845
+
2846
+ function readBoundedHookDescriptor(descriptor, maxBytes) {
2847
+ const chunks = [];
2848
+ let total = 0;
2849
+ while (total <= maxBytes) {
2850
+ const chunk = Buffer.allocUnsafe(Math.min(8 * 1024, maxBytes + 1 - total));
2851
+ const bytesRead = readSync(descriptor, chunk, 0, chunk.length, null);
2852
+ if (bytesRead === 0) break;
2853
+ chunks.push(chunk.subarray(0, bytesRead));
2854
+ total += bytesRead;
2855
+ }
2856
+ if (total > maxBytes) {
2857
+ throw new Error("Remembrance local state exceeds its size limit.");
2858
+ }
2859
+ return Buffer.concat(chunks, total).toString("utf8");
2860
+ }
2861
+
2862
+ function isPosixHookRuntime() {
2863
+ return typeof process.getuid === "function";
2619
2864
  }
2620
2865
 
2621
2866
  function isUnusableConfigurationSource(source) {
2622
2867
  return (
2623
- source === "unusable_environment" || source === "unusable_shared_config"
2868
+ source === "unusable_environment" ||
2869
+ source === "unusable_shared_config" ||
2870
+ source === "unusable_destination_binding"
2624
2871
  );
2625
2872
  }
2626
2873