Skip to content

security: harden local defaults, viewer CSP, mesh auth, and export confinement#108

Merged
rohitg00 merged 3 commits intorohitg00:mainfrom
packetandframe:codex/upstream-security-pr
Apr 12, 2026
Merged

security: harden local defaults, viewer CSP, mesh auth, and export confinement#108
rohitg00 merged 3 commits intorohitg00:mainfrom
packetandframe:codex/upstream-security-pr

Conversation

@eng-pf
Copy link
Copy Markdown
Contributor

@eng-pf eng-pf commented Apr 9, 2026

Summary

This PR addresses a set of security issues in agentmemory’s default deployment, viewer, mesh sync, export path, and startup flow.

It specifically fixes:

  1. network exposure by default
  2. stored XSS risk in the viewer
  3. arbitrary filesystem writes via Obsidian export
  4. unauthenticated mesh sync
  5. incomplete secret redaction
  6. remote shell script execution in the CLI startup path

Findings Addressed

1. Default REST/stream exposure on 0.0.0.0

Previously, the default iii-config.yaml exposed REST and stream services on all interfaces.

Changes:

  • bind default REST and stream services to 127.0.0.1
  • restrict Docker port publishing to localhost
  • add a dedicated iii-config.docker.yaml for Docker usage

Security impact:

  • default local installs are no longer network-exposed by default

2. Stored XSS via inline viewer handlers

The viewer previously rendered attacker-controlled values into HTML that used inline event handlers, while CSP allowed inline scripts.

Changes:

  • remove inline event handlers from the viewer
  • replace them with delegated data-action event handling
  • switch to a nonce-based CSP for viewer HTML
  • add script-src-attr 'none'
  • update the theme toggle to follow the same CSP-safe pattern

Security impact:

  • closes the inline-handler XSS path and removes dependence on unsafe-inline script execution

3. Arbitrary path writes in Obsidian export

vaultDir was previously accepted directly, allowing export to arbitrary filesystem paths.

Changes:

  • confine exports to AGENTMEMORY_EXPORT_ROOT
  • default export root remains under ~/.agentmemory
  • reject vaultDir values outside the allowed root

Security impact:

  • removes the arbitrary-write primitive from the export endpoint

4. Mesh sync without peer auth

Mesh push/pull requests previously ran without authorization headers, and mesh endpoints were usable without a configured shared secret.

Changes:

  • require AGENTMEMORY_SECRET for mesh endpoints
  • require AGENTMEMORY_SECRET for mesh sync operations
  • send Authorization: Bearer <secret> on mesh push/pull requests

Security impact:

  • removes unauthenticated mesh federation behavior and enforces an explicit trust boundary

5. Secret redaction misses modern token formats

The privacy filter did not catch several current credential shapes.

Changes:

  • add redaction coverage for:
    • bearer tokens
    • sk-proj-*
    • broader sk/pk/rk/ak token variants
    • GitHub ghs_ / ghu_ style tokens

Security impact:

  • reduces the chance of modern credentials being stored or surfaced downstream

6. CLI executes remote shell script directly

The CLI startup path previously supported curl ... | sh installation for iii-engine.

Changes:

  • remove remote shell-script execution from CLI startup
  • use an existing local iii-engine binary if available
  • otherwise fall back to Docker
  • update help text and docs accordingly

Security impact:

  • removes a high-risk remote code execution / supply-chain pattern from startup

Additional Hardening

  • make /agentmemory/viewer follow normal bearer-token auth rules when AGENTMEMORY_SECRET is set
  • keep the localhost viewer usable while enforcing CSP-safe rendering
  • add regression coverage for viewer security, mesh auth, export confinement, privacy redaction, and viewer auth behavior

Validation

Ran successfully:

  • npm test
  • npm run build

Notes

This branch was rebased/cherry-picked onto current upstream/main and conflicts were resolved against recent upstream README/CLI/viewer changes.

Summary by CodeRabbit

  • Security

    • Viewer CSP now uses per-request nonces, forbids inline handlers/attributes, and viewer/mesh endpoints enforce AGENTMEMORY_SECRET when configured; services default to binding on 127.0.0.1.
  • New Features

    • Configurable AGENTMEMORY_EXPORT_ROOT (default ~/.agentmemory); viewer served dynamically with nonce-protected scripts.
  • Documentation

    • Clarified quick-start, startup (local engine vs Docker), networking, viewer, export, and mesh auth docs.
  • Chores

    • Removed automatic engine install flow; added Docker-specific config and build file copy.
  • Tests

    • New/updated tests for viewer security, mesh-sync auth, privacy redaction, and export-root behavior.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 9, 2026

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough

Walkthrough

This PR tightens local network bindings, hardens viewer CSP using per-response nonces and delegated event handling, requires AGENTMEMORY_SECRET for mesh sync and includes it on outbound mesh requests, enforces an export root for Obsidian exports, and removes the automatic iii-engine installer from the CLI.

Changes

Cohort / File(s) Summary
Network & Deployment
docker-compose.yml, iii-config.yaml, iii-config.docker.yaml, package.json, README.md
Bind engine-published ports to 127.0.0.1, add iii-config.docker.yaml for Docker runtime, copy it into dist/ during build, and update README to prefer local iii-engine when available or fall back to Docker; mention localhost defaults.
Viewer Rendering & CSP
src/auth.ts, src/viewer/document.ts, src/viewer/server.ts, src/viewer/index.html, src/triggers/api.ts
Introduce per-response viewer nonces (createViewerNonce / buildViewerCsp), move HTML rendering to renderViewerDocument() (nonce substitution + CSP), serve rendered HTML with dynamic CSP header, remove on-disk static discovery, and convert inline event handlers to delegated data-action routing.
Mesh Sync & Auth
src/functions/mesh.ts, src/index.ts, src/triggers/api.ts, test/mesh.test.ts
Add optional meshAuthToken param to mesh registration, require AGENTMEMORY_SECRET for mesh operations (early return when missing), and include Authorization: Bearer <token> on outbound mesh push/pull requests; wire env secret into registration and tests.
Obsidian Export Safety
src/functions/obsidian-export.ts, test/obsidian-export.test.ts
Introduce AGENTMEMORY_EXPORT_ROOT (default ~/.agentmemory), compute/resolve export root, validate vaultDir is inside that root, and return failure for out-of-root export requests; tests updated to set a fixed export root.
CLI / Engine Startup
src/cli.ts, README.md
Remove installIii() auto-install flow and curl installer; only attempt to start a local iii when a binary and config are discovered in PATH or known locations; update CLI help/setup text.
Privacy / Redaction
src/functions/privacy.ts, test/privacy.test.ts
Broaden regexes to detect more secret formats (e.g., sk-proj-..., ghs_..., Bearer tokens) and add corresponding unit tests.
Tests & Integration
test/integration.test.ts, test/viewer-security.test.ts, test/mesh.test.ts, test/obsidian-export.test.ts
Adjust viewer integration auth behavior when AGENTMEMORY_SECRET is set, add renderer CSP/nonces and inline-handler absence tests, add mesh-secret enforcement and header inclusion tests, and add export-root validation tests.
Docker iii config
iii-config.docker.yaml
Add Docker-specific iii configuration with modules list (StateModule, RestApiModule bound for Docker, StreamModule, OtelModule, ExecModule, etc.).

Sequence Diagram(s)

sequenceDiagram
  participant Local as Local AgentMemory
  participant HTTP as HTTP Fetch
  participant Peer as Remote Peer (https://peer)

  Local->>Local: mem::mesh-sync invoked
  Local->>Local: require AGENTMEMORY_SECRET check
  alt secret missing
    Local-->>Local: return { success: false, error: "mesh sync requires AGENTMEMORY_SECRET" }
  else secret present
    Local->>HTTP: POST https://peer/agentmemory/mesh/receive
    Note right of HTTP: Authorization: Bearer <AGENTMEMORY_SECRET>\nContent-Type: application/json
    HTTP->>Peer: deliver push payload
    Peer-->>HTTP: 200 OK
    HTTP-->>Local: response
    Local->>HTTP: GET https://peer/agentmemory/mesh/export?...
    Note right of HTTP: Authorization: Bearer <AGENTMEMORY_SECRET>
    HTTP->>Peer: request export
    Peer-->>HTTP: export payload
    HTTP-->>Local: response (process results)
    Local-->>Local: aggregate results and return success
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐇 I baked a nonce inside a cozy shell,
localhost gates where viewers dwell,
peers now bow with tokens held tight,
vaults fenced in moonlit export light,
hop, delegate, click — secure at night!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title precisely summarizes the four main security hardening areas addressed in the changeset: local defaults, viewer CSP, mesh auth, and export confinement.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/viewer/server.ts (1)

86-100: ⚠️ Potential issue | 🟠 Major

Do not serve /agentmemory/viewer as an unauthenticated local alias.

Line 90 makes the exact /agentmemory/viewer route bypass the authenticated API handler and return HTML directly on the viewer port. When AGENTMEMORY_SECRET is set, that path is still public on 127.0.0.1:${port} even though the REST API returns 401 for the same route.

Proposed fix
-    if (
-      method === "GET" &&
-      (pathname === "/" ||
-        pathname === "/viewer" ||
-        pathname === "/agentmemory/viewer")
-    ) {
+    if (method === "GET" && (pathname === "/" || pathname === "/viewer")) {
       const rendered = renderViewerDocument();
       if (rendered.found) {
         res.writeHead(200, {
           "Content-Type": "text/html; charset=utf-8",
           "Content-Security-Policy": rendered.csp,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/viewer/server.ts` around lines 86 - 100, The route check in the HTTP
handler is incorrectly allowing the exact path "/agentmemory/viewer" to bypass
authentication and serve renderViewerDocument() HTML directly; update the
condition in the request handling logic (the GET path check that currently
references pathname === "/agentmemory/viewer") to only allow "/" and "/viewer"
(keep renderViewerDocument() usage intact for those) and remove
"/agentmemory/viewer" so that requests to "/agentmemory/viewer" are handled by
the authenticated API flow/authorization checks already used for other
agentmemory endpoints (ensure any authentication logic that runs for other
agentmemory routes also runs for "/agentmemory/viewer").
src/cli.ts (1)

100-140: ⚠️ Potential issue | 🟠 Major

Check known local iii locations before falling back to Docker.

Lines 100-115 start Docker before Lines 117-140 scan ~/.local/bin/iii and /usr/local/bin/iii, so a valid local install outside PATH is ignored whenever Docker is available. That reverses the intended precedence.

Proposed fix
-  const dockerBin = whichBinary("docker");
-  const dockerCompose = join(__dirname, "..", "docker-compose.yml");
-  const dcExists = existsSync(dockerCompose) || existsSync(join(process.cwd(), "docker-compose.yml"));
-
-  if (dockerBin && dcExists) {
-    const composeFile = existsSync(dockerCompose) ? dockerCompose : join(process.cwd(), "docker-compose.yml");
-    const s = p.spinner();
-    s.start("Starting iii-engine via Docker...");
-    const child = spawn(dockerBin, ["compose", "-f", composeFile, "up", "-d"], {
-      detached: true,
-      stdio: "ignore",
-    });
-    child.unref();
-    s.stop("Docker compose started");
-    return true;
-  }
-
   const iiiPaths = [
     join(process.env["HOME"] || "", ".local", "bin", "iii"),
     "/usr/local/bin/iii",
   ];
   for (const iiiPath of iiiPaths) {
@@
   if (iiiBin && configPath) {
     const s = p.spinner();
     s.start(`Starting iii-engine: ${iiiBin}`);
     const child = spawn(iiiBin, ["--config", configPath], {
       detached: true,
       stdio: "ignore",
     });
     child.unref();
     s.stop("iii-engine process started");
     return true;
   }
+
+  const dockerBin = whichBinary("docker");
+  const dockerCompose = join(__dirname, "..", "docker-compose.yml");
+  const dcExists =
+    existsSync(dockerCompose) ||
+    existsSync(join(process.cwd(), "docker-compose.yml"));
+
+  if (dockerBin && dcExists) {
+    const composeFile = existsSync(dockerCompose)
+      ? dockerCompose
+      : join(process.cwd(), "docker-compose.yml");
+    const s = p.spinner();
+    s.start("Starting iii-engine via Docker...");
+    const child = spawn(dockerBin, ["compose", "-f", composeFile, "up", "-d"], {
+      detached: true,
+      stdio: "ignore",
+    });
+    child.unref();
+    s.stop("Docker compose started");
+    return true;
+  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cli.ts` around lines 100 - 140, The start logic currently prefers Docker
when dockerBin && dcExists, which causes local installs in iiiPaths
(~/.local/bin/iii and /usr/local/bin/iii) to be ignored; move or run the
iiiPaths discovery before the Docker branch (or change the condition to prefer
iiiBin when found) so that the loop that sets iiiBin (checking iiiPaths, logging
via p.log.info and updating process.env.PATH) executes before deciding to spawn
Docker; then if iiiBin && configPath spawn the local iii process (using the same
spinner/child.unref flow) and only fall back to the docker compose spawn
(dockerBin + dcExists) if no local iiiBin was found.
🧹 Nitpick comments (5)
test/privacy.test.ts (1)

65-69: Add explicit ghu_ regression coverage

Line [65] adds ghs_ coverage, but the regex in src/functions/privacy.ts Line [11] also targets ghu_. Add one ghu_... case to lock that behavior.

➕ Suggested test addition
   it("strips GitHub fine-grained service tokens", () => {
     expect(
       stripPrivateData("ghs_1234567890abcdefghijklmnopqrstuvwxyzAB"),
     ).toBe("[REDACTED_SECRET]");
   });
+
+  it("strips GitHub user tokens", () => {
+    expect(
+      stripPrivateData("ghu_1234567890abcdefghijklmnopqrstuvwxyzAB"),
+    ).toBe("[REDACTED_SECRET]");
+  });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/privacy.test.ts` around lines 65 - 69, Add an explicit test case
exercising the `ghu_` prefix to prevent a regression: in the `privacy.test.ts`
suite add an `it` case (similar to the existing `ghs_` test) that calls
`stripPrivateData("ghu_1234567890abcdefghijklmnopqrstuvwxyzAB")` and asserts it
returns `"[REDACTED_SECRET]"`; this ensures the `stripPrivateData` behavior (the
regex in `src/functions/privacy.ts` that also targets `ghu_`) stays covered.
src/viewer/document.ts (1)

10-23: Consider caching the template after first load.

loadViewerTemplate() uses readFileSync on every call. For high-traffic scenarios, consider caching the template content after the first successful read.

💡 Optional: Cache template after first load
+let cachedTemplate: string | null = null;
+
 function loadViewerTemplate(): string | null {
+  if (cachedTemplate !== null) {
+    return cachedTemplate;
+  }
   const base = dirname(fileURLToPath(import.meta.url));
   const candidates = [
     join(base, "..", "src", "viewer", "index.html"),
     join(base, "..", "viewer", "index.html"),
     join(base, "viewer", "index.html"),
   ];
   for (const path of candidates) {
     try {
-      return readFileSync(path, "utf-8");
+      cachedTemplate = readFileSync(path, "utf-8");
+      return cachedTemplate;
     } catch {}
   }
   return null;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/viewer/document.ts` around lines 10 - 23, The loadViewerTemplate function
currently calls readFileSync on each invocation; add a module-level cache (e.g.,
a let cachedViewerTemplate: string | null = undefined) and modify
loadViewerTemplate to return the cached value if defined, otherwise read the
file once, assign the result to cachedViewerTemplate, and return it; keep the
existing candidates loop and null return behavior if no file found. Ensure the
cache variable is exported or visible to the module scope so subsequent calls to
loadViewerTemplate reuse the cached content.
test/obsidian-export.test.ts (1)

123-132: Consider cleaning up the environment variable after tests.

Setting process.env.AGENTMEMORY_EXPORT_ROOT without cleanup could affect other test suites if they run in the same process.

💡 Optional: Clean up env in afterEach
+import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
...
 describe("Obsidian Export", () => {
   let sdk: ReturnType<typeof mockSdk>;
   let kv: ReturnType<typeof mockKV>;
   const exportRoot = "/tmp/agentmemory-export-root";
+  let originalExportRoot: string | undefined;

   beforeEach(() => {
+    originalExportRoot = process.env.AGENTMEMORY_EXPORT_ROOT;
     process.env.AGENTMEMORY_EXPORT_ROOT = exportRoot;
     ...
   });

+  afterEach(() => {
+    if (originalExportRoot === undefined) {
+      delete process.env.AGENTMEMORY_EXPORT_ROOT;
+    } else {
+      process.env.AGENTMEMORY_EXPORT_ROOT = originalExportRoot;
+    }
+  });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/obsidian-export.test.ts` around lines 123 - 132, The test sets
process.env.AGENTMEMORY_EXPORT_ROOT in the beforeEach block but doesn't restore
it; add an afterEach that deletes or restores
process.env.AGENTMEMORY_EXPORT_ROOT to avoid leaking into other tests.
Specifically, in the same test file where beforeEach sets exportRoot and calls
registerObsidianExportFunction(sdk, kv), add an afterEach that does delete
process.env.AGENTMEMORY_EXPORT_ROOT (or restores a saved previous value) and
retains existing cleanup of writtenFiles and createdDirs so the environment is
fully cleaned between tests.
package.json (1)

13-13: Build script correctly updated for Docker configuration.

The additional copy step for iii-config.docker.yaml aligns with the new Docker-specific config. For consistency, consider adding iii-config.docker.yaml to the files array (lines 33-41) alongside iii-config.yaml and docker-compose.yml.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@package.json` at line 13, The package.json build script copies
iii-config.docker.yaml into dist but the package "files" array does not include
it; update the "files" array to include "iii-config.docker.yaml" alongside
"iii-config.yaml" and "docker-compose.yml" so the Docker-specific config is
published with the package (locate the "files" array in package.json and add the
filename).
test/viewer-security.test.ts (1)

17-24: Make the inline-handler assertion generic.

This only guards three attribute names, so onload=, onerror=, etc. can slip back in without failing the test. A single regex keeps the regression aligned with the XSS goal.

Proposed fix
-    expect(rendered.html).not.toContain("onclick=");
-    expect(rendered.html).not.toContain("onmouseover=");
-    expect(rendered.html).not.toContain("onmouseout=");
+    expect(rendered.html).not.toMatch(/\son[a-z]+\s*=/i);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/viewer-security.test.ts` around lines 17 - 24, The test for inline DOM
event handlers should be made generic: in the "does not contain inline DOM event
handlers" case (using renderViewerDocument() and the rendered.html result),
replace the three specific toContain checks with a single assertion that
rendered.html does not match a regex for any on* attribute (e.g. use a
case-insensitive pattern like /\bon\w+\s*=/i or similar) so any inline event
handlers (onload, onerror, onclick, etc.) will fail the test.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@test/mesh.test.ts`:
- Around line 204-233: The test currently stubs global fetch with
vi.stubGlobal("fetch", fetchMock) but only calls vi.unstubAllGlobals() at the
end of the test body so a failing expectation can leave fetch stubbed; fix by
guaranteeing cleanup: wrap the test body that sets fetchMock and calls
authedSdk.trigger(...) in a try/finally where vi.unstubAllGlobals() is invoked
in finally, or move the cleanup into an afterEach hook that calls
vi.unstubAllGlobals(); update references to fetchMock/vi.stubGlobal and ensure
the finally/afterEach runs regardless of test failures.

---

Outside diff comments:
In `@src/cli.ts`:
- Around line 100-140: The start logic currently prefers Docker when dockerBin
&& dcExists, which causes local installs in iiiPaths (~/.local/bin/iii and
/usr/local/bin/iii) to be ignored; move or run the iiiPaths discovery before the
Docker branch (or change the condition to prefer iiiBin when found) so that the
loop that sets iiiBin (checking iiiPaths, logging via p.log.info and updating
process.env.PATH) executes before deciding to spawn Docker; then if iiiBin &&
configPath spawn the local iii process (using the same spinner/child.unref flow)
and only fall back to the docker compose spawn (dockerBin + dcExists) if no
local iiiBin was found.

In `@src/viewer/server.ts`:
- Around line 86-100: The route check in the HTTP handler is incorrectly
allowing the exact path "/agentmemory/viewer" to bypass authentication and serve
renderViewerDocument() HTML directly; update the condition in the request
handling logic (the GET path check that currently references pathname ===
"/agentmemory/viewer") to only allow "/" and "/viewer" (keep
renderViewerDocument() usage intact for those) and remove "/agentmemory/viewer"
so that requests to "/agentmemory/viewer" are handled by the authenticated API
flow/authorization checks already used for other agentmemory endpoints (ensure
any authentication logic that runs for other agentmemory routes also runs for
"/agentmemory/viewer").

---

Nitpick comments:
In `@package.json`:
- Line 13: The package.json build script copies iii-config.docker.yaml into dist
but the package "files" array does not include it; update the "files" array to
include "iii-config.docker.yaml" alongside "iii-config.yaml" and
"docker-compose.yml" so the Docker-specific config is published with the package
(locate the "files" array in package.json and add the filename).

In `@src/viewer/document.ts`:
- Around line 10-23: The loadViewerTemplate function currently calls
readFileSync on each invocation; add a module-level cache (e.g., a let
cachedViewerTemplate: string | null = undefined) and modify loadViewerTemplate
to return the cached value if defined, otherwise read the file once, assign the
result to cachedViewerTemplate, and return it; keep the existing candidates loop
and null return behavior if no file found. Ensure the cache variable is exported
or visible to the module scope so subsequent calls to loadViewerTemplate reuse
the cached content.

In `@test/obsidian-export.test.ts`:
- Around line 123-132: The test sets process.env.AGENTMEMORY_EXPORT_ROOT in the
beforeEach block but doesn't restore it; add an afterEach that deletes or
restores process.env.AGENTMEMORY_EXPORT_ROOT to avoid leaking into other tests.
Specifically, in the same test file where beforeEach sets exportRoot and calls
registerObsidianExportFunction(sdk, kv), add an afterEach that does delete
process.env.AGENTMEMORY_EXPORT_ROOT (or restores a saved previous value) and
retains existing cleanup of writtenFiles and createdDirs so the environment is
fully cleaned between tests.

In `@test/privacy.test.ts`:
- Around line 65-69: Add an explicit test case exercising the `ghu_` prefix to
prevent a regression: in the `privacy.test.ts` suite add an `it` case (similar
to the existing `ghs_` test) that calls
`stripPrivateData("ghu_1234567890abcdefghijklmnopqrstuvwxyzAB")` and asserts it
returns `"[REDACTED_SECRET]"`; this ensures the `stripPrivateData` behavior (the
regex in `src/functions/privacy.ts` that also targets `ghu_`) stays covered.

In `@test/viewer-security.test.ts`:
- Around line 17-24: The test for inline DOM event handlers should be made
generic: in the "does not contain inline DOM event handlers" case (using
renderViewerDocument() and the rendered.html result), replace the three specific
toContain checks with a single assertion that rendered.html does not match a
regex for any on* attribute (e.g. use a case-insensitive pattern like
/\bon\w+\s*=/i or similar) so any inline event handlers (onload, onerror,
onclick, etc.) will fail the test.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: cb278fb2-3322-4e21-9c82-bde3e02aa535

📥 Commits

Reviewing files that changed from the base of the PR and between 122efde and bb8ced2.

📒 Files selected for processing (20)
  • README.md
  • docker-compose.yml
  • iii-config.docker.yaml
  • iii-config.yaml
  • package.json
  • src/auth.ts
  • src/cli.ts
  • src/functions/mesh.ts
  • src/functions/obsidian-export.ts
  • src/functions/privacy.ts
  • src/index.ts
  • src/triggers/api.ts
  • src/viewer/document.ts
  • src/viewer/index.html
  • src/viewer/server.ts
  • test/integration.test.ts
  • test/mesh.test.ts
  • test/obsidian-export.test.ts
  • test/privacy.test.ts
  • test/viewer-security.test.ts

Comment thread test/mesh.test.ts
Comment on lines +204 to +233
const fetchMock = vi.fn(async () =>
new Response(JSON.stringify({ accepted: 0 }), {
status: 200,
headers: { "Content-Type": "application/json" },
}),
);
vi.stubGlobal("fetch", fetchMock);

const regResult = (await authedSdk.trigger("mem::mesh-register", {
url: "https://peer2.example.com",
name: "peer-2",
})) as { success: boolean; peer: MeshPeer };

const result = (await authedSdk.trigger("mem::mesh-sync", {
peerId: regResult.peer.id,
direction: "push",
})) as { success: boolean; results: Array<{ errors: string[] }> };

expect(result.success).toBe(true);
expect(result.results[0].errors).toEqual([]);
expect(fetchMock).toHaveBeenCalledWith(
"https://peer2.example.com/agentmemory/mesh/receive",
expect.objectContaining({
headers: expect.objectContaining({
Authorization: "Bearer mesh-secret",
}),
}),
);

vi.unstubAllGlobals();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's see the full test file structure and the specific test in question
cat -n test/mesh.test.ts | head -250

Repository: rohitg00/agentmemory

Length of output: 9632


🏁 Script executed:

# Check for other uses of vi.stubGlobal in the file to see patterns
rg "vi\.stubGlobal|vi\.unstubAllGlobals" test/mesh.test.ts -n -B 2 -A 2

Repository: rohitg00/agentmemory

Length of output: 295


🏁 Script executed:

# Check the test/crystallize.test.ts pattern mentioned in guidelines to understand expected patterns
rg "vi\.stubGlobal|try.*finally|afterEach" test/crystallize.test.ts -n -B 1 -A 1

Repository: rohitg00/agentmemory

Length of output: 46


🏁 Script executed:

# Check test/crystallize.test.ts for patterns
head -100 test/crystallize.test.ts

Repository: rohitg00/agentmemory

Length of output: 3176


🏁 Script executed:

# Check if there are other vi.stubGlobal usages in the test suite
rg "vi\.stubGlobal" test/ -n

Repository: rohitg00/agentmemory

Length of output: 126


🏁 Script executed:

# Check the broader test structure - look for other tests using mocks or stubs
rg "afterEach|try.*finally" test/mesh.test.ts -n

Repository: rohitg00/agentmemory

Length of output: 46


🏁 Script executed:

# Check if the test suite uses afterEach anywhere for cleanup
grep -n "afterEach" test/mesh.test.ts

Repository: rohitg00/agentmemory

Length of output: 46


🏁 Script executed:

# Check the broader vitest setup and patterns in the codebase
find test -name "*.test.ts" -type f | head -5

Repository: rohitg00/agentmemory

Length of output: 174


🏁 Script executed:

# See if there are any examples of proper global stub cleanup in the test suite
rg "unstubAllGlobals|afterEach" test/ -B 3 -A 1

Repository: rohitg00/agentmemory

Length of output: 1564


🏁 Script executed:

# Let's check if there are any other tests in mesh.test.ts that might be affected by the fetch stub
wc -l test/mesh.test.ts

Repository: rohitg00/agentmemory

Length of output: 85


🏁 Script executed:

# Look at the complete afterEach pattern in other test files for consistency
rg -A 5 "afterEach" test/index-persistence.test.ts test/embedding-provider.test.ts test/circuit-breaker.test.ts | head -30

Repository: rohitg00/agentmemory

Length of output: 1883


🏁 Script executed:

# Verify the actual test structure - is the mesh-sync test the last test or are there tests after it?
sed -n '183,250p' test/mesh.test.ts

Repository: rohitg00/agentmemory

Length of output: 2316


Always restore the global fetch stub with guaranteed cleanup.

If any expectation before line 233 fails, fetch stays stubbed and can cascade failures into the subsequent mesh-receive test suite. Wrap the test body in try/finally or move cleanup to afterEach.

Proposed fix
       vi.stubGlobal("fetch", fetchMock);
-
-      const regResult = (await authedSdk.trigger("mem::mesh-register", {
-        url: "https://peer2.example.com",
-        name: "peer-2",
-      })) as { success: boolean; peer: MeshPeer };
-
-      const result = (await authedSdk.trigger("mem::mesh-sync", {
-        peerId: regResult.peer.id,
-        direction: "push",
-      })) as { success: boolean; results: Array<{ errors: string[] }> };
-
-      expect(result.success).toBe(true);
-      expect(result.results[0].errors).toEqual([]);
-      expect(fetchMock).toHaveBeenCalledWith(
-        "https://peer2.example.com/agentmemory/mesh/receive",
-        expect.objectContaining({
-          headers: expect.objectContaining({
-            Authorization: "Bearer mesh-secret",
-          }),
-        }),
-      );
-
-      vi.unstubAllGlobals();
+      try {
+        const regResult = (await authedSdk.trigger("mem::mesh-register", {
+          url: "https://peer2.example.com",
+          name: "peer-2",
+        })) as { success: boolean; peer: MeshPeer };
+
+        const result = (await authedSdk.trigger("mem::mesh-sync", {
+          peerId: regResult.peer.id,
+          direction: "push",
+        })) as { success: boolean; results: Array<{ errors: string[] }> };
+
+        expect(result.success).toBe(true);
+        expect(result.results[0].errors).toEqual([]);
+        expect(fetchMock).toHaveBeenCalledWith(
+          "https://peer2.example.com/agentmemory/mesh/receive",
+          expect.objectContaining({
+            headers: expect.objectContaining({
+              Authorization: "Bearer mesh-secret",
+            }),
+          }),
+        );
+      } finally {
+        vi.unstubAllGlobals();
+      }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const fetchMock = vi.fn(async () =>
new Response(JSON.stringify({ accepted: 0 }), {
status: 200,
headers: { "Content-Type": "application/json" },
}),
);
vi.stubGlobal("fetch", fetchMock);
const regResult = (await authedSdk.trigger("mem::mesh-register", {
url: "https://peer2.example.com",
name: "peer-2",
})) as { success: boolean; peer: MeshPeer };
const result = (await authedSdk.trigger("mem::mesh-sync", {
peerId: regResult.peer.id,
direction: "push",
})) as { success: boolean; results: Array<{ errors: string[] }> };
expect(result.success).toBe(true);
expect(result.results[0].errors).toEqual([]);
expect(fetchMock).toHaveBeenCalledWith(
"https://peer2.example.com/agentmemory/mesh/receive",
expect.objectContaining({
headers: expect.objectContaining({
Authorization: "Bearer mesh-secret",
}),
}),
);
vi.unstubAllGlobals();
const fetchMock = vi.fn(async () =>
new Response(JSON.stringify({ accepted: 0 }), {
status: 200,
headers: { "Content-Type": "application/json" },
}),
);
vi.stubGlobal("fetch", fetchMock);
try {
const regResult = (await authedSdk.trigger("mem::mesh-register", {
url: "https://peer2.example.com",
name: "peer-2",
})) as { success: boolean; peer: MeshPeer };
const result = (await authedSdk.trigger("mem::mesh-sync", {
peerId: regResult.peer.id,
direction: "push",
})) as { success: boolean; results: Array<{ errors: string[] }> };
expect(result.success).toBe(true);
expect(result.results[0].errors).toEqual([]);
expect(fetchMock).toHaveBeenCalledWith(
"https://peer2.example.com/agentmemory/mesh/receive",
expect.objectContaining({
headers: expect.objectContaining({
Authorization: "Bearer mesh-secret",
}),
}),
);
} finally {
vi.unstubAllGlobals();
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/mesh.test.ts` around lines 204 - 233, The test currently stubs global
fetch with vi.stubGlobal("fetch", fetchMock) but only calls
vi.unstubAllGlobals() at the end of the test body so a failing expectation can
leave fetch stubbed; fix by guaranteeing cleanup: wrap the test body that sets
fetchMock and calls authedSdk.trigger(...) in a try/finally where
vi.unstubAllGlobals() is invoked in finally, or move the cleanup into an
afterEach hook that calls vi.unstubAllGlobals(); update references to
fetchMock/vi.stubGlobal and ensure the finally/afterEach runs regardless of test
failures.

@eng-pf eng-pf force-pushed the codex/upstream-security-pr branch from bb8ced2 to 8843485 Compare April 9, 2026 18:55
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
README.md (1)

720-725: ⚠️ Potential issue | 🟡 Minor

Update the /agentmemory/health auth docs.

The table still says /agentmemory/health is “always public”, but src/triggers/api.ts runs checkAuth(req, secret) in api::health, so it returns 401 once AGENTMEMORY_SECRET is configured.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` around lines 720 - 725, The README table incorrectly states
`/agentmemory/health` is always public; update the documentation entry for the
`GET /agentmemory/health` row to reflect that `api::health` calls
`checkAuth(req, secret)` and therefore requires `Authorization: Bearer <secret>`
when `AGENTMEMORY_SECRET` is configured (otherwise remains public). Edit the
table description to something like: "Health check with metrics; requires
Authorization when AGENTMEMORY_SECRET is set" so it matches the behavior
implemented in `api::health`/`checkAuth`.
src/triggers/api.ts (1)

1434-1446: ⚠️ Potential issue | 🟠 Major

Whitelist mesh payloads before calling sdk.trigger().

These handlers still pass req.body / req.body || {} straight through to the internal functions. Since mesh endpoints are peer-exposed system boundaries, construct explicit payloads from known fields/scopes here and validate their types before dispatching.

As per coding guidelines "REST endpoint handlers must validate inputs, whitelist request body fields, and never pass raw request body directly to sdk.trigger()" and "Input validation must occur at system boundaries (MCP handlers, REST endpoints) in TypeScript".

Also applies to: 1473-1482, 1491-1499

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/triggers/api.ts` around lines 1434 - 1446, The api::mesh-register REST
handler currently forwards req.body directly into
sdk.trigger("mem::mesh-register"); instead, validate and whitelist the expected
fields (url: string, name: string, sharedScopes?: string[]), coerce/verify
types, build a new payload object (e.g., { url, name, sharedScopes }) and pass
that to sdk.trigger; keep the existing secret/auth checks
(requireConfiguredSecret and checkAuth) but do not pass req.body or allow extra
properties through. Apply the same pattern to the other mesh-related handlers
that call sdk.trigger for mesh operations to ensure all inputs are validated and
only whitelisted fields are sent to mem::mesh-* triggers.
src/viewer/server.ts (1)

70-108: ⚠️ Potential issue | 🔴 Critical

Don't turn the viewer port into an auth-bypassing REST proxy.

This server no longer authenticates proxied requests, but proxyToRestApi() still injects Authorization: Bearer ${secret} upstream. That makes port 3113 an unauthenticated gateway to every protected /agentmemory/* route for any local caller, including browser-driven CSRF-style requests to loopback services. Keep /viewer itself local-friendly, but require auth or proxy only a tight allowlist for the rest.

Also applies to: 145-148

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/viewer/server.ts` around lines 70 - 108, The server currently proxies
arbitrary requests to proxyToRestApi(resolvedRestPort, pathname, qs, method,
req, res, secret) and always injects Authorization: Bearer ${secret}, turning
the viewer port into an auth-bypassing gateway; fix by enforcing auth or an
allowlist before calling proxyToRestApi: either (A) require the incoming request
to present valid credentials (e.g., check req.headers.authorization matches the
expected secret) and only then forward using proxyToRestApi, or (B) restrict
proxied paths to a small explicit allowlist (e.g., only specific safe routes)
and refuse/404 everything else; do not call proxyToRestApi with the secret for
unauthenticated requests and do not blindly add Authorization when the incoming
request lacks valid auth. Ensure the checks are implemented in the createServer
handler around the proxyToRestApi invocation and reference the symbols
proxyToRestApi, resolvedRestPort, secret, pathname, and
req.headers.authorization.
🧹 Nitpick comments (1)
test/obsidian-export.test.ts (1)

234-241: Assert that the rejected export is side-effect free.

This regression only checks the returned error. Add assertions that no directories or files were created, so the test fails if a future change performs partial writes before returning.

Suggested assertion additions
   expect(result.success).toBe(false);
   expect(result.error).toContain(exportRoot);
+  expect(createdDirs.size).toBe(0);
+  expect(writtenFiles.size).toBe(0);
 });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/obsidian-export.test.ts` around lines 234 - 241, The test "rejects
vaultDir outside the export root" currently only checks the returned error;
update it to also assert the operation is side-effect free by verifying no files
or directories were created under the export root after calling
sdk.trigger("mem::obsidian-export"). After the
expect(result.success).toBe(false) line, add assertions that the exportRoot (and
any expected child export path used by this test) either does not exist or
remains empty/unmodified (use fs.existsSync/fs.readdirSync or the test suite's
FS helpers) so the test will fail if mem::obsidian-export performs any partial
writes before returning.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/functions/obsidian-export.ts`:
- Around line 20-27: The current resolveVaultDir allows escaping via symlinks (a
path under export root that is a symlink to outside), so update resolveVaultDir
to validate real filesystem paths: compute realRoot =
realpathSync(getExportRoot()) and realResolved = realpathSync(resolved) and
ensure realResolved === realRoot or realResolved.startsWith(realRoot + sep);
alternatively, explicitly walk each path segment from root to resolved using
lstatSync and reject if any segment is a symlink before permitting later
writeFile() operations. Use resolveVaultDir as the single place to enforce this
check so writeFile() never receives an approved but symlinked path.
- Around line 16-18: The getExportRoot function should expand a leading "~" in
the AGENTMEMORY_EXPORT_ROOT env var before resolving so "~/.agentmemory" maps to
the user's home; update getExportRoot to check
process.env["AGENTMEMORY_EXPORT_ROOT"], and if it starts with "~" replace that
leading "~" with os.homedir() (or homedir()) then call resolve on the resulting
path; keep fallback to DEFAULT_EXPORT_ROOT unchanged so existing behavior using
DEFAULT_EXPORT_ROOT and homedir() remains intact.

---

Outside diff comments:
In `@README.md`:
- Around line 720-725: The README table incorrectly states `/agentmemory/health`
is always public; update the documentation entry for the `GET
/agentmemory/health` row to reflect that `api::health` calls `checkAuth(req,
secret)` and therefore requires `Authorization: Bearer <secret>` when
`AGENTMEMORY_SECRET` is configured (otherwise remains public). Edit the table
description to something like: "Health check with metrics; requires
Authorization when AGENTMEMORY_SECRET is set" so it matches the behavior
implemented in `api::health`/`checkAuth`.

In `@src/triggers/api.ts`:
- Around line 1434-1446: The api::mesh-register REST handler currently forwards
req.body directly into sdk.trigger("mem::mesh-register"); instead, validate and
whitelist the expected fields (url: string, name: string, sharedScopes?:
string[]), coerce/verify types, build a new payload object (e.g., { url, name,
sharedScopes }) and pass that to sdk.trigger; keep the existing secret/auth
checks (requireConfiguredSecret and checkAuth) but do not pass req.body or allow
extra properties through. Apply the same pattern to the other mesh-related
handlers that call sdk.trigger for mesh operations to ensure all inputs are
validated and only whitelisted fields are sent to mem::mesh-* triggers.

In `@src/viewer/server.ts`:
- Around line 70-108: The server currently proxies arbitrary requests to
proxyToRestApi(resolvedRestPort, pathname, qs, method, req, res, secret) and
always injects Authorization: Bearer ${secret}, turning the viewer port into an
auth-bypassing gateway; fix by enforcing auth or an allowlist before calling
proxyToRestApi: either (A) require the incoming request to present valid
credentials (e.g., check req.headers.authorization matches the expected secret)
and only then forward using proxyToRestApi, or (B) restrict proxied paths to a
small explicit allowlist (e.g., only specific safe routes) and refuse/404
everything else; do not call proxyToRestApi with the secret for unauthenticated
requests and do not blindly add Authorization when the incoming request lacks
valid auth. Ensure the checks are implemented in the createServer handler around
the proxyToRestApi invocation and reference the symbols proxyToRestApi,
resolvedRestPort, secret, pathname, and req.headers.authorization.

---

Nitpick comments:
In `@test/obsidian-export.test.ts`:
- Around line 234-241: The test "rejects vaultDir outside the export root"
currently only checks the returned error; update it to also assert the operation
is side-effect free by verifying no files or directories were created under the
export root after calling sdk.trigger("mem::obsidian-export"). After the
expect(result.success).toBe(false) line, add assertions that the exportRoot (and
any expected child export path used by this test) either does not exist or
remains empty/unmodified (use fs.existsSync/fs.readdirSync or the test suite's
FS helpers) so the test will fail if mem::obsidian-export performs any partial
writes before returning.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 32a08d2c-2e84-4d75-b8c3-3abf6c35ceed

📥 Commits

Reviewing files that changed from the base of the PR and between bb8ced2 and 8843485.

📒 Files selected for processing (20)
  • README.md
  • docker-compose.yml
  • iii-config.docker.yaml
  • iii-config.yaml
  • package.json
  • src/auth.ts
  • src/cli.ts
  • src/functions/mesh.ts
  • src/functions/obsidian-export.ts
  • src/functions/privacy.ts
  • src/index.ts
  • src/triggers/api.ts
  • src/viewer/document.ts
  • src/viewer/index.html
  • src/viewer/server.ts
  • test/integration.test.ts
  • test/mesh.test.ts
  • test/obsidian-export.test.ts
  • test/privacy.test.ts
  • test/viewer-security.test.ts
✅ Files skipped from review due to trivial changes (5)
  • package.json
  • iii-config.yaml
  • test/privacy.test.ts
  • docker-compose.yml
  • iii-config.docker.yaml
🚧 Files skipped from review as they are similar to previous changes (8)
  • src/index.ts
  • test/integration.test.ts
  • test/viewer-security.test.ts
  • src/viewer/document.ts
  • src/functions/privacy.ts
  • src/functions/mesh.ts
  • src/cli.ts
  • src/viewer/index.html

Comment on lines +16 to +18
function getExportRoot(): string {
return resolve(process.env["AGENTMEMORY_EXPORT_ROOT"] || DEFAULT_EXPORT_ROOT);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
node - <<'NODE'
const { resolve, join } = require("node:path");
const { homedir } = require("node:os");

console.log("path.resolve('~/.agentmemory') =", resolve("~/.agentmemory"));
console.log("expected home path            =", join(homedir(), ".agentmemory"));
NODE

Repository: rohitg00/agentmemory

Length of output: 190


🏁 Script executed:

cat -n src/functions/obsidian-export.ts | head -50

Repository: rohitg00/agentmemory

Length of output: 1895


🏁 Script executed:

grep -n "getExportRoot\|AGENTMEMORY_EXPORT_ROOT" src/functions/obsidian-export.ts

Repository: rohitg00/agentmemory

Length of output: 287


🏁 Script executed:

rg "AGENTMEMORY_EXPORT_ROOT" --type md --type txt

Repository: rohitg00/agentmemory

Length of output: 236


Expand ~ before resolving AGENTMEMORY_EXPORT_ROOT.

The README documents AGENTMEMORY_EXPORT_ROOT=~/.agentmemory as the recommended value, but path.resolve("~/.agentmemory") treats ~ literally, resolving to a literal ~ directory under the current working directory instead of the user's home. When AGENTMEMORY_EXPORT_ROOT is not set, DEFAULT_EXPORT_ROOT correctly uses homedir(), but explicit env var values with ~ will not expand.

Suggested fix
 function getExportRoot(): string {
-  return resolve(process.env["AGENTMEMORY_EXPORT_ROOT"] || DEFAULT_EXPORT_ROOT);
+  const configured =
+    process.env["AGENTMEMORY_EXPORT_ROOT"] || DEFAULT_EXPORT_ROOT;
+  const expanded =
+    configured === "~"
+      ? homedir()
+      : configured.startsWith(`~${sep}`)
+        ? join(homedir(), configured.slice(2))
+        : configured;
+  return resolve(expanded);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/functions/obsidian-export.ts` around lines 16 - 18, The getExportRoot
function should expand a leading "~" in the AGENTMEMORY_EXPORT_ROOT env var
before resolving so "~/.agentmemory" maps to the user's home; update
getExportRoot to check process.env["AGENTMEMORY_EXPORT_ROOT"], and if it starts
with "~" replace that leading "~" with os.homedir() (or homedir()) then call
resolve on the resulting path; keep fallback to DEFAULT_EXPORT_ROOT unchanged so
existing behavior using DEFAULT_EXPORT_ROOT and homedir() remains intact.

Comment on lines +20 to +27
function resolveVaultDir(vaultDir?: string): string | null {
const root = getExportRoot();
const resolved = resolve(vaultDir || join(root, "vault"));
if (resolved === root || resolved.startsWith(root + sep)) {
return resolved;
}
return null;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

vaultDir confinement is still bypassable via symlinks.

A path like <exportRoot>/link passes this prefix check even when link is a symlink to a directory outside the export root, so the later writeFile() calls can still escape the intended sandbox. Revalidate against real paths, or reject symlinked path segments before writing.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/functions/obsidian-export.ts` around lines 20 - 27, The current
resolveVaultDir allows escaping via symlinks (a path under export root that is a
symlink to outside), so update resolveVaultDir to validate real filesystem
paths: compute realRoot = realpathSync(getExportRoot()) and realResolved =
realpathSync(resolved) and ensure realResolved === realRoot or
realResolved.startsWith(realRoot + sep); alternatively, explicitly walk each
path segment from root to resolved using lstatSync and reject if any segment is
a symlink before permitting later writeFile() operations. Use resolveVaultDir as
the single place to enforce this check so writeFile() never receives an approved
but symlinked path.

Resolved README.md conflicts by taking the new benchmarks/competitor
table layout from main and re-applying the security-related text
changes from this branch:

- "starts with local iii-engine or Docker" in npx command
- From source section: local iii-engine or Docker fallback note
- Real-Time Viewer: 127.0.0.1 binding + nonce-based CSP description
- AGENTMEMORY_EXPORT_ROOT env var
- API section: 127.0.0.1 default + mesh AGENTMEMORY_SECRET requirement
- Prerequisites: Node.js + iii-engine or Docker (no curl|sh)

src/cli.ts auto-merged cleanly: install_iii() function and curl|sh
removed by this branch, token savings status command preserved from
main.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants