From d029a6c952463e14d8ba95d617003e65fd598339 Mon Sep 17 00:00:00 2001 From: ChronodeAi Date: Mon, 24 Aug 2026 17:29:59 -0500 Subject: [PATCH 01/15] fix(security): constrain client-supplied graphify import path to the project cwd An explicit path on mem::graph::import-graphify could point anywhere the daemon can read. Explicit paths must now resolve inside the requested cwd and keep the graph.json basename; violations return a generic error that does not echo the attempted path. Stat failures for explicit paths keep the same no-echo discipline while the default computed path retains its specific pointer. --- src/functions/graph-import.ts | 24 +++++- test/graph-import-scope.test.ts | 135 ++++++++++++++++++++++++++++++++ test/graph-import.test.ts | 6 +- 3 files changed, 159 insertions(+), 6 deletions(-) create mode 100644 test/graph-import-scope.test.ts diff --git a/src/functions/graph-import.ts b/src/functions/graph-import.ts index f479bb4aa..5f6930f95 100644 --- a/src/functions/graph-import.ts +++ b/src/functions/graph-import.ts @@ -1,5 +1,5 @@ import { readFile, stat } from "node:fs/promises"; -import { join } from "node:path"; +import { basename, join, resolve, sep } from "node:path"; import type { ISdk } from "iii-sdk"; import type { GraphEdge, @@ -221,6 +221,22 @@ export function registerGraphImportFunction(sdk: ISdk, kv: StateKV): void { typeof data?.path === "string" ? data.path : undefined; const cwd = typeof data?.cwd === "string" ? data.cwd : process.cwd(); const path = explicitPath ?? join(cwd, "graphify-out", "graph.json"); + if (explicitPath !== undefined) { + // The explicit path is client-supplied, so constrain it to the + // graphify artifact inside the project checkout before any stat, + // read, or error echo can touch it. + const resolvedExplicit = resolve(explicitPath); + const resolvedCwd = resolve(cwd); + const insideCwd = + resolvedExplicit === resolvedCwd || + resolvedExplicit.startsWith(resolvedCwd + sep); + if (basename(resolvedExplicit) !== "graph.json" || !insideCwd) { + return { + success: false, + error: "path must be a graph.json inside the project cwd", + }; + } + } const project = scope.kind === "project" ? scope.project : undefined; try { @@ -230,8 +246,10 @@ export function registerGraphImportFunction(sdk: ISdk, kv: StateKV): void { } catch { return { success: false, - error: `graph.json not found at ${path}. Run graphify first, or pass an explicit path.`, - path, + error: explicitPath + ? "graph.json not found at the requested location. Run graphify first, or pass an explicit path." + : `graph.json not found at ${path}. Run graphify first, or pass an explicit path.`, + ...(explicitPath ? {} : { path }), }; } if (size > MAX_FILE_BYTES) { diff --git a/test/graph-import-scope.test.ts b/test/graph-import-scope.test.ts new file mode 100644 index 000000000..6a199b5b5 --- /dev/null +++ b/test/graph-import-scope.test.ts @@ -0,0 +1,135 @@ +import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; +import { mkdtempSync, rmSync, writeFileSync, mkdirSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, join } from "node:path"; + +vi.mock("../src/logger.js", () => ({ + logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() }, + bootLog: vi.fn(), +})); + +import { + registerGraphImportFunction, +} from "../src/functions/graph-import.js"; +import type { GraphifyImportResult } from "../src/functions/graph-import.js"; +import { KV } from "../src/state/schema.js"; + +// Client-supplied explicit paths must stay inside the project cwd and keep +// the graph.json basename; violations are rejected with a generic message +// that does not echo the attempted path. +const FIXTURE = JSON.stringify({ + nodes: [{ id: "n1", label: "extract", file_type: "code" }], + links: [], +}); +const PROJECT = "github.com/example/repository"; + +function mockKV() { + const store = new Map>(); + return { + get: async (scope: string, key: string): Promise => + (store.get(scope)?.get(key) as T) ?? null, + set: async (scope: string, key: string, data: T): Promise => { + if (!store.has(scope)) store.set(scope, new Map()); + store.get(scope)!.set(key, data); + return data; + }, + delete: async (scope: string, key: string): Promise => { + store.get(scope)?.delete(key); + }, + list: async (scope: string): Promise => + Array.from(store.get(scope)?.values() ?? []) as T[], + _store: store, + }; +} + +describe("mem::graph::import-graphify path scoping", () => { + let tmp: string; + let kv: ReturnType; + let trigger: (payload: unknown) => Promise; + + beforeEach(() => { + tmp = mkdtempSync(join(tmpdir(), "am-graphify-scope-")); + mkdirSync(join(tmp, "graphify-out"), { recursive: true }); + writeFileSync(join(tmp, "graphify-out", "graph.json"), FIXTURE); + kv = mockKV(); + const sdk = { + registerFunction: ( + _id: string, + handler: (payload?: unknown) => Promise, + ) => { + trigger = (payload: unknown) => handler(payload); + }, + registerTrigger: () => {}, + } as never; + registerGraphImportFunction(sdk, kv as never); + }); + + afterEach(() => { + rmSync(tmp, { recursive: true, force: true }); + }); + + it("rejects an absolute graph.json outside the cwd without echoing the path", async () => { + const other = mkdtempSync(join(tmpdir(), "am-graphify-outside-")); + try { + const outside = join(other, "graph.json"); + writeFileSync(outside, FIXTURE); + const result = await trigger({ path: outside, cwd: tmp, project: PROJECT }); + expect(result.success).toBe(false); + expect(result.error).toBe( + "path must be a graph.json inside the project cwd", + ); + expect(JSON.stringify(result)).not.toContain(outside); + expect(await kv.list(KV.graphNodes)).toHaveLength(0); + } finally { + rmSync(other, { recursive: true, force: true }); + } + }); + + it("rejects a wrong basename inside the cwd", async () => { + const sneaky = join(tmp, "export-graph.json"); + writeFileSync(sneaky, FIXTURE); + const result = await trigger({ path: sneaky, cwd: tmp, project: PROJECT }); + expect(result.success).toBe(false); + expect(result.error).toBe( + "path must be a graph.json inside the project cwd", + ); + expect(JSON.stringify(result)).not.toContain(sneaky); + }); + + it("still imports a valid /graphify-out/graph.json passed explicitly", async () => { + const result = await trigger({ + path: join(tmp, "graphify-out", "graph.json"), + cwd: tmp, + project: PROJECT, + }); + expect(result.success).toBe(true); + expect(result.nodesImported).toBe(1); + expect(await kv.list(KV.graphNodes)).toHaveLength(1); + }); + + it("keeps the stat-failure pointer generic for explicit paths and specific for the default", async () => { + const missingExplicit = join(tmp, "missing-dir", "graph.json"); + const explicitResult = await trigger({ + path: missingExplicit, + cwd: tmp, + project: PROJECT, + }); + expect(explicitResult.success).toBe(false); + expect(explicitResult.error).toContain("Run graphify first"); + expect(explicitResult.error).not.toContain(missingExplicit); + expect(explicitResult.path).toBeUndefined(); + + const defaultResult = await trigger({ + cwd: join(tmp, "nowhere"), + project: PROJECT, + }); + expect(defaultResult.success).toBe(false); + expect(defaultResult.error).toContain( + join(tmp, "nowhere", "graphify-out", "graph.json"), + ); + expect(defaultResult.path).toBe( + join(tmp, "nowhere", "graphify-out", "graph.json"), + ); + expect(dirname(missingExplicit)).toBeTruthy(); + }); +}); diff --git a/test/graph-import.test.ts b/test/graph-import.test.ts index 3ab9d23d9..9077399e7 100644 --- a/test/graph-import.test.ts +++ b/test/graph-import.test.ts @@ -217,12 +217,12 @@ describe("mem::graph::import-graphify", () => { expect(result.error).toContain("Run graphify first"); }); - it("accepts an explicit path", async () => { - const alt = join(tmp, "custom.json"); + it("accepts an explicit path inside the project cwd", async () => { + const alt = join(tmp, "graph.json"); writeFileSync(alt, JSON.stringify(FIXTURE)); const result = (await (sdk as any).trigger({ function_id: "mem::graph::import-graphify", - payload: { path: alt, project: PROJECT }, + payload: { path: alt, cwd: tmp, project: PROJECT }, })) as { success: boolean; nodesImported: number }; expect(result.success).toBe(true); expect(result.nodesImported).toBe(4); From 3553b49001276b21134a8be7f2cf60c63b5468a1 Mon Sep 17 00:00:00 2001 From: ChronodeAi Date: Mon, 24 Aug 2026 17:31:19 -0500 Subject: [PATCH 02/15] fix(security): redact credentials before echoing an unnormalizable git remote to stderr The identity-fallback warning wrote the raw `git remote get-url` output, leaking embedded passwords for remotes URL parsing rejected. New redactRemoteForLog helper keeps everything from the last @ onward and masks the credential segment; credential-free remotes pass through unchanged. --- src/project-config.ts | 18 +++++- test/project-config-redaction.test.ts | 84 +++++++++++++++++++++++++++ 2 files changed, 101 insertions(+), 1 deletion(-) create mode 100644 test/project-config-redaction.test.ts diff --git a/src/project-config.ts b/src/project-config.ts index 00c8992af..9c6a47e3c 100644 --- a/src/project-config.ts +++ b/src/project-config.ts @@ -214,6 +214,22 @@ export function normalizeGitRemote(remote: string): string | undefined { // checkout; the hashed canonical path keeps that identity stable instead of // failing every hook process that runs inside such a clone. The warning // fires once per process so per-event hooks stay quiet. + +/** + * Mask credentials before a raw git remote reaches stderr. Everything from + * the last "@" onward is kept (host/path), everything between the scheme + * and that "@" is replaced; remotes without "@" pass through unchanged. + * + * @param value - Raw `git remote get-url` output, in any git-supported form. + * @returns A log-safe rendering of the remote. + */ +export function redactRemoteForLog(value: string): string { + const at = value.lastIndexOf("@"); + if (at === -1) return value; + const scheme = value.match(/^[a-z][a-z0-9+.-]*:\/\//i)?.[0] ?? ""; + return `${scheme}***@${value.slice(at + 1)}`; +} + let warnedUnnormalizableRemote = false; export function inferProjectId(root: string): string { @@ -225,7 +241,7 @@ export function inferProjectId(root: string): string { if (!warnedUnnormalizableRemote) { warnedUnnormalizableRemote = true; process.stderr.write( - `[agentmemory] Git remote "${remote}" cannot be normalized to a canonical project id; using local/${projectPathHash(root)} for this checkout\n`, + `[agentmemory] Git remote "${redactRemoteForLog(remote)}" cannot be normalized to a canonical project id; using local/${projectPathHash(root)} for this checkout\n`, ); } } diff --git a/test/project-config-redaction.test.ts b/test/project-config-redaction.test.ts new file mode 100644 index 000000000..ab1a681bf --- /dev/null +++ b/test/project-config-redaction.test.ts @@ -0,0 +1,84 @@ +import { describe, expect, it, vi, afterEach } from "vitest"; +import { execFileSync } from "node:child_process"; +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { + inferProjectId, + redactRemoteForLog, +} from "../src/project-config.js"; + +const roots: string[] = []; + +afterEach(() => { + for (const root of roots.splice(0)) rmSync(root, { recursive: true }); +}); + +function gitProject(remote?: string): string { + const root = mkdtempSync(join(tmpdir(), "agentmemory-redaction-")); + roots.push(root); + execFileSync("git", ["init", "-q"], { cwd: root }); + if (remote) { + execFileSync( + "git", + ["-C", root, "remote", "add", "origin", remote], + ); + } + return root; +} + +describe("redactRemoteForLog", () => { + it("masks credentials on https remotes", () => { + expect(redactRemoteForLog("https://alice:s3cret@example.org/org/repo.git")).toBe( + "https://***@example.org/org/repo.git", + ); + }); + + it("masks credentials on scp-style remotes", () => { + expect(redactRemoteForLog("deploy:s3cret@git.internal:org/repo.git")).toBe( + "***@git.internal:org/repo.git", + ); + }); + + it("preserves the scheme and masks everything through the last @", () => { + expect(redactRemoteForLog("ssh://git@host.example:2222/org/repo.git")).toBe( + "ssh://***@host.example:2222/org/repo.git", + ); + expect(redactRemoteForLog("https://a@b:c@d.example/x/y")).toBe( + "https://***@d.example/x/y", + ); + }); + + it("leaves credential-free remotes untouched", () => { + expect(redactRemoteForLog("https://github.com/org/repo.git")).toBe( + "https://github.com/org/repo.git", + ); + expect(redactRemoteForLog("/srv/git/repo")).toBe("/srv/git/repo"); + // A username alone is still material after the marker position rule. + expect(redactRemoteForLog("git@github.com:org/repo.git")).toBe( + "***@github.com:org/repo.git", + ); + }); +}); + +describe("inferProjectId identity-fallback warning", () => { + it("never writes raw remote credentials to stderr", () => { + const root = gitProject("https://alice:s3cret@example.org/solo.git"); + const writes: string[] = []; + const spy = vi + .spyOn(process.stderr, "write") + .mockImplementation(((chunk: unknown) => { + writes.push(String(chunk)); + return true; + }) as never); + try { + expect(inferProjectId(root)).toMatch(/^local\/[a-f0-9]{24}$/); + } finally { + spy.mockRestore(); + } + const output = writes.join(""); + expect(output).toContain("***@example.org/solo.git"); + expect(output).not.toContain("s3cret"); + expect(output).not.toContain("alice:"); + }); +}); From 1ce0b66a2dd228dba3031cdbade5e6d65156c9c6 Mon Sep 17 00:00:00 2001 From: ChronodeAi Date: Mon, 24 Aug 2026 17:33:46 -0500 Subject: [PATCH 03/15] fix(security): harden project capability secret provisioning mkdirSync now requests 0700 for the credential directory. A symlink at the credential path is refused via lstat instead of being followed to a foreign target. A chmod failure on a pre-existing populated credential logs one stderr line and continues (the file predates this run), while a chmod failure on the freshly written credential removes the partial file and fails loud instead of leaving an unsecured secret on disk. --- src/cli/connect/capability-secret.ts | 38 +++++++- test/capability-secret-hardening.test.ts | 106 +++++++++++++++++++++++ 2 files changed, 141 insertions(+), 3 deletions(-) create mode 100644 test/capability-secret-hardening.test.ts diff --git a/src/cli/connect/capability-secret.ts b/src/cli/connect/capability-secret.ts index 027c41e01..5c2b2355f 100644 --- a/src/cli/connect/capability-secret.ts +++ b/src/cli/connect/capability-secret.ts @@ -1,8 +1,10 @@ import { chmodSync, existsSync, + lstatSync, mkdirSync, readFileSync, + unlinkSync, writeFileSync, } from "node:fs"; import { homedir } from "node:os"; @@ -45,13 +47,30 @@ export function generateCapabilitySecret(): string { * Return the existing credential, or generate one (64 hex chars from 32 * random bytes) written with mode 0600. A file that exists but holds no * value is provisioned too — there is no user data to clobber — while a - * populated file is left byte-for-byte untouched. + * populated file is left byte-for-byte untouched (permissions are tightened + * best-effort). A symlink at the credential path is refused outright. */ export function ensureProjectCapabilitySecret(): CapabilityProvisionResult { const path = projectCapabilitySecretFile(); if (existsSync(path)) { + // lstat, not stat: a symlink parked at the credential path must be + // refused before any read or write can follow it to another target. + if (lstatSync(path).isSymbolicLink()) { + throw new Error( + "project capability secret path is a symlink; refusing", + ); + } try { if (readFileSync(path, "utf8").trim()) { + // The file pre-existed under the operator's home; tighten it when + // the filesystem allows, but never fail connect over a chmod. + try { + chmodSync(path, 0o600); + } catch (err) { + process.stderr.write( + `[agentmemory] could not tighten permissions on ${path}: ${err instanceof Error ? err.message : String(err)}\n`, + ); + } return { path, provisioned: false, reused: true }; } } catch { @@ -61,10 +80,23 @@ export function ensureProjectCapabilitySecret(): CapabilityProvisionResult { } const secret = generateCapabilitySecret(); - mkdirSync(dirname(path), { recursive: true }); + mkdirSync(dirname(path), { recursive: true, mode: 0o700 }); // The mode flag applies only at file creation; chmod covers the // pre-existing-but-empty case so the credential is never world-readable. writeFileSync(path, `${secret}\n`, { encoding: "utf8", mode: 0o600 }); - chmodSync(path, 0o600); + try { + chmodSync(path, 0o600); + } catch { + // A partial credential that could not be secured must not stay on + // disk for another process to read; remove it and fail loud. + try { + unlinkSync(path); + } catch { + // Nothing further to clean up; the chmod failure below is reported. + } + throw new Error( + "could not secure project capability secret (chmod failed); removed partial file", + ); + } return { path, provisioned: true, reused: false }; } diff --git a/test/capability-secret-hardening.test.ts b/test/capability-secret-hardening.test.ts new file mode 100644 index 000000000..7ad5c4c43 --- /dev/null +++ b/test/capability-secret-hardening.test.ts @@ -0,0 +1,106 @@ +import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; +import { + existsSync, + mkdirSync, + mkdtempSync, + readFileSync, + rmSync, + statSync, + symlinkSync, + writeFileSync, +} from "node:fs"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; + +// chmod is the failure surface under test; every other fs call must hit the +// real filesystem. The flag toggles the fault injection per test. +const chmodFault = vi.hoisted(() => ({ fail: false })); +vi.mock("node:fs", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + chmodSync: ((path: Parameters[0], mode: Parameters[1]) => { + if (chmodFault.fail) { + throw Object.assign(new Error("EPERM: operation not permitted, chmod"), { + code: "EPERM", + }); + } + return actual.chmodSync(path, mode); + }) as typeof actual.chmodSync, + }; +}); + +import { + ensureProjectCapabilitySecret, + projectCapabilitySecretFile, +} from "../src/cli/connect/capability-secret.js"; + +const POSIX = process.platform !== "win32"; +const ORIGINAL_HOME = process.env["HOME"]; +const stderrWrites: string[] = []; + +describe("project capability secret hardening", () => { + let sandboxHome: string; + + beforeEach(() => { + sandboxHome = mkdtempSync(join(tmpdir(), "am-capability-hardening-")); + process.env["HOME"] = sandboxHome; + delete process.env["AGENTMEMORY_PROJECT_CAPABILITY_SECRET_FILE"]; + chmodFault.fail = false; + stderrWrites.length = 0; + vi.spyOn(process.stderr, "write").mockImplementation(((chunk: unknown) => { + stderrWrites.push(String(chunk)); + return true; + }) as never); + }); + + afterEach(() => { + vi.restoreAllMocks(); + if (ORIGINAL_HOME === undefined) delete process.env["HOME"]; + else process.env["HOME"] = ORIGINAL_HOME; + rmSync(sandboxHome, { recursive: true, force: true }); + }); + + it.runIf(POSIX)("creates the credential directory with mode 0700", () => { + const path = projectCapabilitySecretFile(); + const result = ensureProjectCapabilitySecret(); + expect(result.provisioned).toBe(true); + expect(statSync(join(path, "..")).mode & 0o777).toBe(0o700); + expect(statSync(result.path).mode & 0o777).toBe(0o600); + }); + + it.runIf(POSIX)("refuses a symlink parked at the credential path", () => { + const target = join(sandboxHome, "real-secret"); + writeFileSync(target, "operator-secret\n", { mode: 0o600 }); + const path = projectCapabilitySecretFile(); + mkdirSync(join(path, ".."), { recursive: true }); + symlinkSync(target, path); + + expect(() => ensureProjectCapabilitySecret()).toThrow( + "project capability secret path is a symlink; refusing", + ); + // The symlink target was never read or rewritten. + expect(readFileSync(target, "utf8")).toBe("operator-secret\n"); + }); + + it("removes a partially written secret when the creation chmod fails", () => { + chmodFault.fail = true; + const path = projectCapabilitySecretFile(); + expect(() => ensureProjectCapabilitySecret()).toThrow( + "could not secure project capability secret (chmod failed); removed partial file", + ); + expect(existsSync(path)).toBe(false); + }); + + it("keeps a pre-existing populated secret when its tightening chmod fails", () => { + const path = projectCapabilitySecretFile(); + mkdirSync(join(path, ".."), { recursive: true }); + writeFileSync(path, "user-chosen-secret\n", { mode: 0o600 }); + chmodFault.fail = true; + + const result = ensureProjectCapabilitySecret(); + expect(result).toEqual({ path, provisioned: false, reused: true }); + expect(readFileSync(path, "utf8")).toBe("user-chosen-secret\n"); + expect(stderrWrites.join("")).toContain("could not tighten permissions"); + }); +}); From d2497d3bc8db8eea397720512bb2f806e6207882 Mon Sep 17 00:00:00 2001 From: ChronodeAi Date: Mon, 24 Aug 2026 17:34:56 -0500 Subject: [PATCH 04/15] fix(correctness): fold --data-dir into the environment before .env hydration The CLI hydrated /.env before folding the --data-dir flag into AGENTMEMORY_DATA_DIR, so a flagged run read its .env from the default ~/.agentmemory instead of the requested location. The fold (and the legacy-store warning that consumes the resolved dir) now runs before hydrateProcessEnvFromFile(); --version keeps its pre-side-effect exit. --- src/cli.ts | 31 +++++++++++++++++-------------- test/data-dir.test.ts | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 14 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index 47fce12c9..e2f9dad30 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -116,10 +116,26 @@ if (args.includes("--version") || args.includes("-V")) { process.exit(0); } +// --data-dir relocates the agentmemory data dir (default ~/.agentmemory). +// The flag wins over a pre-set AGENTMEMORY_DATA_DIR, so folding it into the +// environment here keeps one precedence story — flag > env > default — for +// everything downstream of this process (worker, engine, hooks inherit env). +// The fold must happen BEFORE hydrateProcessEnvFromFile(): the .env file is +// read from /.env, so a --data-dir run has to hydrate the +// flagged location, not the default one. +const dataDirFlagValue = readDataDirFlag(args); +if (dataDirFlagValue !== undefined && dataDirFlagValue.trim().length > 0) { + process.env["AGENTMEMORY_DATA_DIR"] = dataDirFlagValue.trim(); +} +// Upstream adopts a legacy cwd ./data store automatically; this fork does +// not. When state would silently land somewhere different from an existing +// local ./data store, tell the operator how to opt in explicitly. +warnOnLegacyDataDir(); + // Fold ~/.agentmemory/.env into process.env before anything reads config // from the environment (the iii version pin, --tools/--port/--instance // handling, engine boot). Fill-missing-only: a real process.env value — -// including one set by a CLI flag below — always wins over the file. +// including one set by a CLI flag above — always wins over the file. hydrateProcessEnvFromFile(); // Pinned iii-engine version. The unpinned `install.iii.dev/iii/main/install.sh` @@ -291,19 +307,6 @@ if (instanceIdx !== -1 && args[instanceIdx + 1]) { } } -// --data-dir relocates the agentmemory data dir (default ~/.agentmemory). -// The flag wins over a pre-set AGENTMEMORY_DATA_DIR, so folding it into the -// environment here keeps one precedence story — flag > env > default — for -// everything downstream of this process (worker, engine, hooks inherit env). -const dataDirFlagValue = readDataDirFlag(args); -if (dataDirFlagValue !== undefined && dataDirFlagValue.trim().length > 0) { - process.env["AGENTMEMORY_DATA_DIR"] = dataDirFlagValue.trim(); -} -// Upstream adopts a legacy cwd ./data store automatically; this fork does -// not. When state would silently land somewhere different from an existing -// local ./data store, tell the operator how to opt in explicitly. -warnOnLegacyDataDir(); - const skipEngine = args.includes("--no-engine"); function getRestPort(): number { diff --git a/test/data-dir.test.ts b/test/data-dir.test.ts index 476c592e6..b3e9238a0 100644 --- a/test/data-dir.test.ts +++ b/test/data-dir.test.ts @@ -291,4 +291,42 @@ describe("config integration", () => { join(join(sandboxHome, "relocated"), "standalone.json"), ); }); + + it("STANDALONE_PERSIST_PATH keeps precedence over the data-dir default", async () => { + process.env[DATA_DIR_ENV] = join(sandboxHome, "relocated"); + process.env["STANDALONE_PERSIST_PATH"] = join(sandboxHome, "custom.json"); + const cfg = await freshConfig(); + expect(cfg.getStandalonePersistPath()).toBe(join(sandboxHome, "custom.json")); + }); + + it("hydrates from the flag-folded data dir when AGENTMEMORY_DATA_DIR is set before boot", async () => { + // Mirrors the CLI boot order: the --data-dir flag is folded into + // process.env first, then hydrateProcessEnvFromFile() reads + // /.env. A stale memoized file cache must not pin + // the old location after __resetEnvFileCache(). + const relocated = join(sandboxHome, "flagged"); + mkdirSync(relocated, { recursive: true }); + writeFileSync( + join(relocated, ".env"), + "AM_FLAGFOLD_PROBE=from-flagged\n", + ); + + process.env[DATA_DIR_ENV] = relocated; + let cfg = await freshConfig(); + cfg.hydrateProcessEnvFromFile(); + expect(cfg.getEnvVar("AM_FLAGFOLD_PROBE")).toBe("from-flagged"); + + // Same-process refold: a new --data-dir on a restarted CLI maps to a + // fresh module; within one process the reset hook must unlock the new + // path. + const second = join(sandboxHome, "flagged-two"); + mkdirSync(second, { recursive: true }); + writeFileSync(join(second, ".env"), "AM_FLAGFOLD_PROBE=from-second\n"); + process.env[DATA_DIR_ENV] = second; + cfg.__resetEnvFileCache(); + delete process.env["AM_FLAGFOLD_PROBE"]; + cfg.hydrateProcessEnvFromFile(); + expect(process.env["AM_FLAGFOLD_PROBE"]).toBe("from-second"); + delete process.env["AM_FLAGFOLD_PROBE"]; + }); }); From 8df48f7a51d66aea84db7947509637d43a36f59a Mon Sep 17 00:00:00 2001 From: ChronodeAi Date: Mon, 24 Aug 2026 17:36:27 -0500 Subject: [PATCH 05/15] fix(correctness): standalone MCP persist path delegates to the data-dir-aware config resolver The shim kept its own copy of the persist-path resolution pinned to ~/.agentmemory, ignoring AGENTMEMORY_DATA_DIR and duplicating config.ts. It now calls hydrateProcessEnvFromFile() at boot and delegates getStandalonePersistPath to the shared export, preserving the STANDALONE_PERSIST_PATH override precedence while honoring a relocated data dir. --- src/mcp/standalone.ts | 15 +++++++++------ test/mcp-standalone.test.ts | 19 +++++++++++++++++++ 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/src/mcp/standalone.ts b/src/mcp/standalone.ts index 8a29fcc06..d5ae53b03 100644 --- a/src/mcp/standalone.ts +++ b/src/mcp/standalone.ts @@ -10,8 +10,7 @@ import { loadRetrievalQuarantine, retrievalQuarantineKey, } from "../context-eligibility.js"; -import { homedir } from "node:os"; -import { join } from "node:path"; +import { getStandalonePersistPath as resolvePersistPath, hydrateProcessEnvFromFile } from "../config.js"; import { resolveHandle, invalidateHandle, @@ -54,11 +53,15 @@ const SERVER_INFO = { protocolVersion: SUPPORTED_PROTOCOL_VERSIONS[0], }; +// Fold /.env before the persist path is computed so a +// .env-declared STANDALONE_PERSIST_PATH or AGENTMEMORY_DATA_DIR steers the +// fallback store location. Fill-missing-only: real environment wins. +hydrateProcessEnvFromFile(); + +// Single source of truth with config.ts: STANDALONE_PERSIST_PATH override, +// else /standalone.json. function getStandalonePersistPath(): string { - return ( - process.env["STANDALONE_PERSIST_PATH"]?.trim() || - join(homedir(), ".agentmemory", "standalone.json") - ); + return resolvePersistPath(); } const kv = new InMemoryKV(getStandalonePersistPath()); diff --git a/test/mcp-standalone.test.ts b/test/mcp-standalone.test.ts index a4f0a2ebc..937f808fd 100644 --- a/test/mcp-standalone.test.ts +++ b/test/mcp-standalone.test.ts @@ -17,6 +17,7 @@ vi.mock("../src/mcp/transport.js", () => ({ vi.mock("../src/config.js", () => ({ getStandalonePersistPath: vi.fn(() => "/tmp/test-standalone.json"), + hydrateProcessEnvFromFile: vi.fn(), })); import { @@ -32,6 +33,10 @@ import { resetHandleForTests, setLivezProbe, } from "../src/mcp/rest-proxy.js"; +import { + getStandalonePersistPath, + hydrateProcessEnvFromFile, +} from "../src/config.js"; import { writeFileSync } from "node:fs"; const PROJECT_SCOPED_TOOLS = new Set([ @@ -92,6 +97,20 @@ const fetchTrap = vi.fn(async (url: unknown) => { }); describe("Tools Registry", () => { + it("resolves its persist path via the shared config resolver, after env hydration", () => { + // The module-level fallback KV is constructed from config.ts's + // data-dir-aware getStandalonePersistPath; .env-declared + // STANDALONE_PERSIST_PATH / AGENTMEMORY_DATA_DIR must be folded into + // the environment before that path is computed. + const persistMock = vi.mocked(getStandalonePersistPath); + const hydrateMock = vi.mocked(hydrateProcessEnvFromFile); + expect(persistMock).toHaveBeenCalled(); + expect(hydrateMock).toHaveBeenCalled(); + expect(hydrateMock.mock.invocationCallOrder[0]).toBeLessThan( + persistMock.mock.invocationCallOrder[0], + ); + }); + it("getAllTools returns all tools with unique names", () => { const tools = getAllTools(); expect(tools.length).toBeGreaterThanOrEqual(41); From 875cf5bda318224c94509dc230a2ec61f0a63e43 Mon Sep 17 00:00:00 2001 From: ChronodeAi Date: Mon, 24 Aug 2026 17:37:33 -0500 Subject: [PATCH 06/15] fix(perf): mark the memory index ready after a clean startup reconciliation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit reconcileCanonicalSearchIndex walks the full KV.memories corpus, so a successful run leaves the keyword index covering the memory corpus just like performRebuildIndex does — but the ready flag stayed false, forcing mem::remember onto the full-scan fallback until an explicit rebuild ran. Set the same flag on the reconcile success path; a failed walk leaves it untouched. --- src/functions/search.ts | 6 +++ test/search-reconcile-ready.test.ts | 72 +++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+) create mode 100644 test/search-reconcile-ready.test.ts diff --git a/src/functions/search.ts b/src/functions/search.ts index 95c3ba723..6637b34e4 100644 --- a/src/functions/search.ts +++ b/src/functions/search.ts @@ -566,6 +566,12 @@ export async function reconcileCanonicalSearchIndex(kv: StateKV): Promise<{ } await flush() + // The memory walk above covered every current KV.memories row without + // throwing, so the keyword index covers the corpus exactly as the + // equivalent point in a full rebuild does; live index maintenance in + // mem::remember can trust it instead of forcing another rebuild. + memoryIndexReady = true + return { canonicalEntries: canonical.size, addedKeywordEntries, diff --git a/test/search-reconcile-ready.test.ts b/test/search-reconcile-ready.test.ts new file mode 100644 index 000000000..4bdd2ee67 --- /dev/null +++ b/test/search-reconcile-ready.test.ts @@ -0,0 +1,72 @@ +import { describe, expect, it, vi } from "vitest"; + +vi.mock("../src/logger.js", () => ({ + logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() }, +})); + +import { + getSearchIndex, + isMemoryIndexReady, + reconcileCanonicalSearchIndex, +} from "../src/functions/search.js"; +import { KV } from "../src/state/schema.js"; +import type { Memory } from "../src/types.js"; + +function mockKV() { + const store = new Map>(); + return { + get: async (scope: string, key: string): Promise => + (store.get(scope)?.get(key) as T) ?? null, + set: async (scope: string, key: string, data: T): Promise => { + if (!store.has(scope)) store.set(scope, new Map()); + store.get(scope)!.set(key, data); + return data; + }, + list: async (scope: string): Promise => { + const entries = store.get(scope); + return entries ? (Array.from(entries.values()) as T[]) : []; + }, + }; +} + +// Startup reconciliation walks the full KV.memories corpus exactly like a +// rebuild does, so success must leave the same memoryIndexReady guarantee +// behind — and a failed walk must not. +describe("reconcileCanonicalSearchIndex readiness", () => { + it("marks the memory index ready only after a clean reconciliation", async () => { + const kv = mockKV(); + const memory: Memory = { + id: "mem_ready_1", + title: "Deploy runbook", + content: "Ship via the staged rollout pipeline.", + concepts: ["deploy"], + files: [], + sessionIds: [], + createdAt: "2026-01-01T00:00:00.000Z", + updatedAt: "2026-01-01T00:00:00.000Z", + strength: 7, + version: 1, + isLatest: true, + } as Memory; + await kv.set(KV.memories, memory.id, memory); + + expect(isMemoryIndexReady()).toBe(false); + + const failingKv = { + ...kv, + list: async (scope: string): Promise => { + if (scope === KV.memories) throw new Error("memory corpus unavailable"); + return kv.list(scope); + }, + }; + await expect( + reconcileCanonicalSearchIndex(failingKv as never), + ).rejects.toThrow("memory corpus unavailable"); + expect(isMemoryIndexReady()).toBe(false); + + const result = await reconcileCanonicalSearchIndex(kv as never); + expect(result).toMatchObject({ canonicalEntries: 1 }); + expect(getSearchIndex().has(memory.id)).toBe(true); + expect(isMemoryIndexReady()).toBe(true); + }); +}); From de1ab83259b6aba6aca603002a0a75f74d1c7ce5 Mon Sep 17 00:00:00 2001 From: ChronodeAi Date: Mon, 24 Aug 2026 17:38:43 -0500 Subject: [PATCH 07/15] fix(correctness): release the consolidation cooldown when the dispatch is rejected The cooldown marker was written before the Void fan-out, so a rejected mem::consolidate-pipeline / mem::auto-crystallize dispatch pinned the debounce for the whole window with no pipeline behind it. Rejection and sync-throw handlers now best-effort delete the marker (plain KV write, no pipeline lock) without touching the fire-and-forget latency profile, letting the next eligible stop retry. --- src/triggers/events.ts | 16 ++++++++-- test/consolidation-lifecycle.test.ts | 44 ++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 3 deletions(-) diff --git a/src/triggers/events.ts b/src/triggers/events.ts index 30b674508..8e075c0fd 100644 --- a/src/triggers/events.ts +++ b/src/triggers/events.ts @@ -865,6 +865,14 @@ export function registerEventTriggers(sdk: ISdk, kv: StateKV): void { // fan-outs above: tolerate synchronous throws and non-promise // returns from sdk.trigger, log async rejections without failing // the stop lifecycle. + const releaseConsolidationCooldown = (): void => { + // The marker was written before the dispatch, so a rejected + // (or never-started) pipeline would otherwise pin the cooldown + // for the whole window with no work behind it. Plain kv delete: + // the marker is debounce bookkeeping, not pipeline state, and + // needs no lock. Best-effort only — the next stop re-checks. + kv.delete(KV.config, CONSOLIDATION_MARKER_KEY).catch(() => {}); + }; const fireVoid = ( function_id: string, payload: Record, @@ -875,14 +883,15 @@ export function registerEventTriggers(sdk: ISdk, kv: StateKV): void { payload, action: TriggerAction.Void(), }); - Promise.resolve(dispatched).catch((err: unknown) => + Promise.resolve(dispatched).catch((err: unknown) => { logger.warn(function_id + " trigger failed", { sessionId: data.sessionId, project: data.project, pipelineRunId, error: err instanceof Error ? err.message : String(err), - }), - ); + }); + releaseConsolidationCooldown(); + }); } catch (err) { logger.warn(function_id + " trigger failed", { sessionId: data.sessionId, @@ -890,6 +899,7 @@ export function registerEventTriggers(sdk: ISdk, kv: StateKV): void { pipelineRunId, error: err instanceof Error ? err.message : String(err), }); + releaseConsolidationCooldown(); } }; fireVoid("mem::consolidate-pipeline", { diff --git a/test/consolidation-lifecycle.test.ts b/test/consolidation-lifecycle.test.ts index e01169010..3cd8fbb54 100644 --- a/test/consolidation-lifecycle.test.ts +++ b/test/consolidation-lifecycle.test.ts @@ -345,4 +345,48 @@ describe("session-stop consolidation lifecycle", () => { expect(consolidationCalls()).toHaveLength(1); expect(crystallizeCalls()).toHaveLength(1); }); + + it("releases the cooldown marker when the consolidation dispatch is rejected", async () => { + await kv.set(KV.sessions, "s-r1", sessionRow("s-r1")); + await kv.set(KV.sessions, "s-r2", sessionRow("s-r2")); + registerRuntime(); + // First dispatch is rejected by the provider/engine; the retry lands. + let consolidateDispatches = 0; + (sdk as unknown as { fns: Map Promise> }).fns.set( + "mem::consolidate-pipeline", + async (payload) => { + consolidateDispatches += 1; + if (consolidateDispatches === 1) { + throw new Error("SIMULATED_PROVIDER_REJECTION"); + } + calls.push({ + functionId: "mem::consolidate-pipeline", + payload: payload as Record, + }); + return { success: true }; + }, + ); + + await stop({ sessionId: "s-r1" }); + await flush(); + + expect(consolidateDispatches).toBe(1); + // The marker was written before the dispatch; the rejection handler + // must have cleared it best-effort. + expect(await marker()).toBeNull(); + + // The next stop inside the cooldown window retries consolidation + // instead of being debounced by a marker with no pipeline behind it. + await stop({ sessionId: "s-r2" }); + await flush(); + + expect(consolidateDispatches).toBe(2); + expect(consolidationCalls()).toEqual([ + { + functionId: "mem::consolidate-pipeline", + payload: { tier: "all", force: true, project: PROJECT }, + }, + ]); + expect(await marker()).not.toBeNull(); + }); }); From 174d6085d68096d2659276eb14b8ec1b00bc5e6a Mon Sep 17 00:00:00 2001 From: ChronodeAi Date: Mon, 24 Aug 2026 17:40:02 -0500 Subject: [PATCH 08/15] fix(security): stamp shared-channel origin on peer Memory upserts mem::mesh-receive and the mesh pull path wrote peer memories straight into KV.memories, so records arriving without provenance bypassed the write-time Origin rule every local capture surface follows. Incoming memories now go through the types.js importOrigin factory: a peer-provided origin is preserved, anything else is marked with the shared channel at receive time. --- src/functions/mesh.ts | 22 ++++++++++++-- src/types.ts | 11 +++++-- test/mesh-origin.test.ts | 64 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 93 insertions(+), 4 deletions(-) create mode 100644 test/mesh-origin.test.ts diff --git a/src/functions/mesh.ts b/src/functions/mesh.ts index 7b4a26849..b06300ebd 100644 --- a/src/functions/mesh.ts +++ b/src/functions/mesh.ts @@ -13,6 +13,7 @@ import type { GraphNode, GraphEdge, } from "../types.js"; +import { importOrigin } from "../types.js"; import { lookup } from "node:dns/promises"; import { isIP } from "node:net"; @@ -109,6 +110,21 @@ function graphNodeTs(node: GraphNode): string { return node.updatedAt || node.createdAt; } +// Peer Memory upserts bypass every local capture surface, so records +// arriving without an Origin would enter the store with no provenance. +// Keep-or-mark via the shared factory: a peer-provided origin is preserved, +// everything else is marked with the shared channel at receive time. +function stampPeerMemoryOrigins( + memories: Memory[] | undefined, + receivedAt: string, +): Memory[] | undefined { + if (!memories || !Array.isArray(memories)) return memories; + return memories.map((memory) => ({ + ...memory, + origin: importOrigin(memory.origin, receivedAt, undefined, "shared"), + })); +} + async function lwwMergeGraphNodes( kv: StateKV, items: GraphNode[] | undefined, @@ -341,8 +357,9 @@ export function registerMeshFunction( return { success: false, error: "payload required" }; } let accepted = 0; + const receivedAt = new Date().toISOString(); - accepted += await lwwMergeList(kv, KV.memories, data.memories, "mem:memory", "updatedAt"); + accepted += await lwwMergeList(kv, KV.memories, stampPeerMemoryOrigins(data.memories, receivedAt), "mem:memory", "updatedAt"); accepted += await lwwMergeList(kv, KV.actions, data.actions, "mem:action", "updatedAt"); accepted += await lwwMergeList(kv, KV.semantic, data.semantic, "mem:semantic", "updatedAt"); accepted += await lwwMergeList(kv, KV.procedural, data.procedural, "mem:procedural", "updatedAt"); @@ -495,9 +512,10 @@ async function applySyncData( scopes: string[], ): Promise { let applied = 0; + const receivedAt = new Date().toISOString(); if (scopes.includes("memories")) { - applied += await lwwMergeList(kv, KV.memories, data.memories, "mem:memory", "updatedAt"); + applied += await lwwMergeList(kv, KV.memories, stampPeerMemoryOrigins(data.memories, receivedAt), "mem:memory", "updatedAt"); } if (scopes.includes("actions")) { applied += await lwwMergeList(kv, KV.actions, data.actions, "mem:action", "updatedAt"); diff --git a/src/types.ts b/src/types.ts index 59a7d2352..5fb1747fb 100644 --- a/src/types.ts +++ b/src/types.ts @@ -102,16 +102,23 @@ export interface Origin { /** * Keep-or-mark rule for imported records: an origin already present on the - * record is preserved, otherwise the record is marked as import-channel + * record is preserved, otherwise the record is marked with `channel` * provenance at `capturedAt`. + * + * @param existing - Origin already carried by the record, if any. + * @param capturedAt - Capture timestamp stamped onto new origins. + * @param detail - Optional free-form detail recorded alongside the channel. + * @param channel - Provenance channel for newly created origins; peer-synced + * records pass "shared", every other surface keeps the "import" default. */ export function importOrigin( existing: Origin | undefined, capturedAt: string, detail?: string, + channel: Origin["channel"] = "import", ): Origin { if (existing) return existing; - return { channel: "import", capturedAt, ...(detail ? { detail } : {}) }; + return { channel, capturedAt, ...(detail ? { detail } : {}) }; } export interface RawObservation { diff --git a/test/mesh-origin.test.ts b/test/mesh-origin.test.ts new file mode 100644 index 000000000..e0162bc71 --- /dev/null +++ b/test/mesh-origin.test.ts @@ -0,0 +1,64 @@ +import { describe, it, expect } from "vitest"; +import { InMemoryKV } from "../src/mcp/in-memory-kv.js"; +import { KV } from "../src/state/schema.js"; +import { registerMeshFunction } from "../src/functions/mesh.js"; +import type { Memory } from "../src/types.js"; +import { mockSdk } from "./helpers/mocks.js"; + +// Peer Memory upserts bypass every local capture surface. Records without +// an Origin must gain shared-channel provenance at receive time; records +// that already carry one keep it untouched. +function peerMemory(overrides: Partial = {}): Memory { + return { + id: "mem-peer-1", + title: "Peer memory", + content: "Synced from a mesh peer.", + concepts: [], + files: [], + sessionIds: [], + createdAt: "2026-01-01T00:00:00.000Z", + updatedAt: "2026-01-01T00:00:00.000Z", + strength: 7, + version: 1, + isLatest: true, + ...overrides, + } as Memory; +} + +describe("mesh receive origin stamping", () => { + it("marks a peer record without origin with the shared channel", async () => { + const kv = new InMemoryKV(); + const sdk = mockSdk(); + registerMeshFunction(sdk as never, kv as never, undefined); + + const result = (await sdk.trigger("mem::mesh-receive", { + memories: [peerMemory()], + })) as { success: boolean; accepted: number }; + + expect(result).toMatchObject({ success: true, accepted: 1 }); + const stored = await kv.get(KV.memories, "mem-peer-1"); + expect(stored?.origin?.channel).toBe("shared"); + expect(stored?.origin?.capturedAt).toBeTruthy(); + expect(Number.isNaN(new Date(stored!.origin!.capturedAt).getTime())).toBe( + false, + ); + }); + + it("preserves an origin the peer already provided", async () => { + const kv = new InMemoryKV(); + const sdk = mockSdk(); + registerMeshFunction(sdk as never, kv as never, undefined); + const provided = { + channel: "user" as const, + detail: "typed by the operator", + capturedAt: "2025-12-31T23:59:59.000Z", + }; + + await sdk.trigger("mem::mesh-receive", { + memories: [peerMemory({ id: "mem-peer-2", origin: provided })], + }); + + const stored = await kv.get(KV.memories, "mem-peer-2"); + expect(stored?.origin).toEqual(provided); + }); +}); From e3413ab588050104dc21db0679f4d2b0a1234ed7 Mon Sep 17 00:00:00 2001 From: ChronodeAi Date: Mon, 24 Aug 2026 17:41:53 -0500 Subject: [PATCH 09/15] fix(correctness): read user project overrides from the data-dir projects dir getUserProjectConfigPath was pinned to ~/.agentmemory/projects and ignored a relocated data dir. It now resolves /projects/.yaml first and falls back to the legacy ~/.agentmemory copy when the data-dir file is absent, so existing overrides keep working; nothing writes this file today, so no write path changes. --- src/project-config.ts | 14 +++--- test/project-config.test.ts | 88 +++++++++++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+), 6 deletions(-) diff --git a/src/project-config.ts b/src/project-config.ts index 9c6a47e3c..a28513a49 100644 --- a/src/project-config.ts +++ b/src/project-config.ts @@ -9,6 +9,7 @@ import { homedir } from "node:os"; import { basename, isAbsolute, join, relative, resolve } from "node:path"; import { parse as parseYaml } from "yaml"; import { hydrateProcessEnvFromFile } from "./config.js"; +import { resolveDataDir } from "./data-dir.js"; export type ProjectPrivacy = "standard" | "private" | "strict"; export type CaptureProfile = "minimal" | "balanced" | "full"; @@ -275,12 +276,13 @@ function readConfigFile(path: string): ConfigLayer | undefined { } export function getUserProjectConfigPath(root: string): string { - return join( - userHome(), - ".agentmemory", - "projects", - `${projectPathHash(root)}.yaml`, - ); + const hashed = `${projectPathHash(root)}.yaml`; + // The resolved data dir is the canonical override location; the + // ~/.agentmemory path is the pre-data-dir layout and keeps working as a + // fallback so existing overrides survive unchanged. + const dataDirOverride = join(resolveDataDir(), "projects", hashed); + if (existsSync(dataDirOverride)) return dataDirOverride; + return join(userHome(), ".agentmemory", "projects", hashed); } export function loadAgentmemoryEnvironment(): Record { diff --git a/test/project-config.test.ts b/test/project-config.test.ts index dbbec1615..1f0d562fc 100644 --- a/test/project-config.test.ts +++ b/test/project-config.test.ts @@ -8,12 +8,14 @@ import { import { tmpdir } from "node:os"; import { dirname, join } from "node:path"; import { afterEach, describe, expect, it } from "vitest"; +import { DATA_DIR_ENV } from "../src/data-dir.js"; import { getUserProjectConfigPath, inferProjectId, isProjectPathExcluded, normalizeGitRemote, normalizedProjectPath, + projectPathHash, resolveProjectConfig, } from "../src/project-config.js"; @@ -207,4 +209,90 @@ describe("canonical project configuration", () => { "github.com/example/second", ); }); + + it("reads user overrides from the data-dir projects dir when present", () => { + const root = gitProject(); + const home = mkdtempSync(join(tmpdir(), "agentmemory-home-")); + roots.push(home); + const dataDir = join(home, "relocated-data"); + const dataDirCopy = join( + dataDir, + "projects", + `${projectPathHash(root)}.yaml`, + ); + process.env["HOME"] = home; + process.env[DATA_DIR_ENV] = dataDir; + + // Nothing exists yet: the resolver still points at the legacy layout. + expect(getUserProjectConfigPath(root)).toBe( + join(home, ".agentmemory", "projects", `${projectPathHash(root)}.yaml`), + ); + + mkdirSync(dirname(dataDirCopy), { recursive: true }); + writeFileSync( + dataDirCopy, + ["schema_version: 1", "project_id: datadir/project"].join("\n"), + ); + expect(getUserProjectConfigPath(root)).toBe(dataDirCopy); + + const config = resolveProjectConfig(root); + expect(config.override_path).toBe(dataDirCopy); + expect(config.project_id).toBe("datadir/project"); + }); + + it("falls back to the legacy ~/.agentmemory override when the data-dir copy is absent", () => { + const root = gitProject(); + const home = mkdtempSync(join(tmpdir(), "agentmemory-home-")); + roots.push(home); + process.env["HOME"] = home; + delete process.env[DATA_DIR_ENV]; + + const legacyPath = getUserProjectConfigPath(root); + expect(legacyPath).toBe( + join(home, ".agentmemory", "projects", `${projectPathHash(root)}.yaml`), + ); + mkdirSync(dirname(legacyPath), { recursive: true }); + writeFileSync( + legacyPath, + ["schema_version: 1", "project_id: legacy/project"].join("\n"), + ); + + const config = resolveProjectConfig(root); + expect(config.override_path).toBe(legacyPath); + expect(config.project_id).toBe("legacy/project"); + }); + + it("prefers the data-dir override over a colliding legacy copy", () => { + const root = gitProject(); + const home = mkdtempSync(join(tmpdir(), "agentmemory-home-")); + roots.push(home); + const dataDir = join(home, "relocated-data"); + process.env["HOME"] = home; + process.env[DATA_DIR_ENV] = dataDir; + delete process.env["AGENTMEMORY_PROJECT_CONFIG"]; + + const dataDirCopy = join( + dataDir, + "projects", + `${projectPathHash(root)}.yaml`, + ); + mkdirSync(dirname(dataDirCopy), { recursive: true }); + writeFileSync( + dataDirCopy, + ["schema_version: 1", "project_id: datadir/wins"].join("\n"), + ); + const legacyCopy = join( + home, + ".agentmemory", + "projects", + `${projectPathHash(root)}.yaml`, + ); + mkdirSync(dirname(legacyCopy), { recursive: true }); + writeFileSync( + legacyCopy, + ["schema_version: 1", "project_id: legacy/loses"].join("\n"), + ); + + expect(resolveProjectConfig(root).project_id).toBe("datadir/wins"); + }); }); From 569ef69f75f0b64a266be78dcb97680b4621cbfe Mon Sep 17 00:00:00 2001 From: ChronodeAi Date: Mon, 24 Aug 2026 17:43:59 -0500 Subject: [PATCH 10/15] docs(changelog): record the adversarial-fixes train under Unreleased Fixed bullets for graphify import path scoping, remote credential redaction, capability secret hardening, --data-dir hydration order, standalone persist-path delegation, consolidation cooldown release on rejected dispatch, mesh peer-origin stamping, reconciliation readiness, and data-dir-aware user overrides; Changed notes that .env is parsed once per process. Strict-session heuristic graph extraction stays documented solely in the 0.9.30-chronode.1 notes. --- CHANGELOG.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index dd86e76ba..af7d0f804 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,22 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), ## [Unreleased] +### Fixed + +- **Graphify import path scoping** (security). An explicit path on `mem::graph::import-graphify` (`POST /agentmemory/graph/import-graphify`) could point anywhere the daemon can read. Explicit paths must now resolve inside the requested project cwd and keep the `graph.json` basename; violations return a generic error that never echoes the attempted path, and stat failures for explicit paths stay equally non-specific. +- **Credential redaction in the identity-fallback warning** (security). The stderr warning for an unnormalizable git remote wrote the raw `git remote get-url` output, leaking embedded passwords; remotes are masked before logging (scheme and host/path preserved, credentials replaced). +- **Project capability secret hardening** (security). Zero-touch provisioning creates the credential directory with mode 0700, refuses a symlink parked at the credential path instead of following it, keeps a pre-existing populated secret when only its permission tightening fails, and removes a freshly written secret whose securing chmod failed rather than leaving it readable. +- **`--data-dir` honored during `.env` hydration** (correctness). The CLI hydrated `/.env` before folding the flag into the environment, so a flagged run silently read its `.env` from the default `~/.agentmemory`; the fold now happens first. +- **Standalone MCP persist path follows the data dir** (correctness). The shim kept a duplicate resolver pinned to `~/.agentmemory`; it now delegates to the shared config resolver after folding `.env`, preserving the `STANDALONE_PERSIST_PATH` override while honoring `AGENTMEMORY_DATA_DIR`. +- **Consolidation cooldown released on rejected dispatch** (correctness). A rejected `mem::consolidate-pipeline` / `mem::auto-crystallize` Void dispatch left the cooldown marker standing for the whole window with no pipeline behind it; rejection handlers clear the marker best-effort so the next eligible stop retries. +- **Origin provenance on peer Memory upserts** (security). Mesh receive/pull wrote peer memories without provenance; records lacking an Origin now gain shared-channel provenance via the shared keep-or-mark factory, and peer-provided origins are preserved. +- **Search-index readiness after startup reconciliation** (perf). Reconciliation walks the full memory corpus but never marked the index ready, forcing live saves onto full-scan fallbacks until an explicit rebuild; success now sets the same readiness flag a rebuild does. +- **Per-project user overrides follow the data dir** (info). User project config overrides are read from `/projects/.yaml`, falling back to the legacy `~/.agentmemory/projects/.yaml` copy so existing overrides keep working. + +### Changed + +- `/.env` is parsed once per daemon process; edits require a daemon restart to take effect. + ## [0.9.30-chronode.1] — 2026-08-24 Upstream v0.9.29 sync wave, staged across five reviewed trains (#5–#9). Fork architecture of record unchanged: remote-derived canonical project identity, exclusive project scope, fail-closed governance, self-contained bundled hooks. From 24f42b8fbf8c5fd62b4c03b02725c58c5298ec02 Mon Sep 17 00:00:00 2001 From: ChronodeAi Date: Mon, 24 Aug 2026 17:45:22 -0500 Subject: [PATCH 11/15] chore(build): regenerate hook bundles for the adversarial-fixes train npm run build after the project-config, cli.ts, and mcp/standalone.ts changes: redactRemoteForLog rides in the shared auth/project chunks and the standalone shim now hydrates .env before resolving its persist path through the shared config resolver. Shared-chunk hashes rotate accordingly. --- ...{_auth-8LRLcG2I.mjs => _auth-1Z57rc-e.mjs} | 107 +++------------ ...ure-CdxWrCwc.mjs => _capture-Ba1NCNW7.mjs} | 2 +- ...ry-C1jg9u5N.mjs => _delivery-BPnYIm56.mjs} | 2 +- ...Ekr.mjs => _observe-delivery-BSYpE5r3.mjs} | 2 +- ...ect-VjQrnNqc.mjs => _project-BqDfPlX6.mjs} | 2 +- plugin/scripts/auth-DkiaFluQ.mjs | 126 ++++++++++++++++++ plugin/scripts/auth-Evr8deOq.mjs | 31 ----- plugin/scripts/notification.mjs | 4 +- plugin/scripts/post-commit.mjs | 6 +- plugin/scripts/post-tool-failure.mjs | 6 +- plugin/scripts/post-tool-use.mjs | 6 +- plugin/scripts/pre-compact.mjs | 4 +- plugin/scripts/pre-tool-use.mjs | 4 +- plugin/scripts/prompt-submit.mjs | 4 +- plugin/scripts/session-end.mjs | 4 +- plugin/scripts/session-start.mjs | 6 +- plugin/scripts/standalone.mjs | 5 +- plugin/scripts/stop.mjs | 4 +- plugin/scripts/subagent-start.mjs | 4 +- plugin/scripts/subagent-stop.mjs | 4 +- plugin/scripts/task-completed.mjs | 4 +- 21 files changed, 183 insertions(+), 154 deletions(-) rename plugin/scripts/{_auth-8LRLcG2I.mjs => _auth-1Z57rc-e.mjs} (98%) rename plugin/scripts/{_capture-CdxWrCwc.mjs => _capture-Ba1NCNW7.mjs} (99%) rename plugin/scripts/{_delivery-C1jg9u5N.mjs => _delivery-BPnYIm56.mjs} (97%) rename plugin/scripts/{_observe-delivery-BYrh4Ekr.mjs => _observe-delivery-BSYpE5r3.mjs} (97%) rename plugin/scripts/{_project-VjQrnNqc.mjs => _project-BqDfPlX6.mjs} (95%) create mode 100644 plugin/scripts/auth-DkiaFluQ.mjs delete mode 100644 plugin/scripts/auth-Evr8deOq.mjs diff --git a/plugin/scripts/_auth-8LRLcG2I.mjs b/plugin/scripts/_auth-1Z57rc-e.mjs similarity index 98% rename from plugin/scripts/_auth-8LRLcG2I.mjs rename to plugin/scripts/_auth-1Z57rc-e.mjs index 2077e64b9..823def7d0 100644 --- a/plugin/scripts/_auth-8LRLcG2I.mjs +++ b/plugin/scripts/_auth-1Z57rc-e.mjs @@ -1,4 +1,4 @@ -import { i as isStrictCapabilityMode, n as PROJECT_CAPABILITY_PROJECT_HEADER, r as createProjectCapabilityToken } from "./auth-Evr8deOq.mjs"; +import { i as isStrictCapabilityMode, n as PROJECT_CAPABILITY_PROJECT_HEADER, o as hydrateProcessEnvFromFile, r as createProjectCapabilityToken, s as resolveDataDir } from "./auth-DkiaFluQ.mjs"; import { createRequire } from "node:module"; import { existsSync, readFileSync, realpathSync } from "node:fs"; import { isAbsolute, join, relative, resolve } from "node:path"; @@ -6574,7 +6574,7 @@ var require_public_api = /* @__PURE__ */ __commonJSMin(((exports) => { exports.stringify = stringify; })); //#endregion -//#region src/data-dir.ts +//#region src/project-config.ts var import_dist = (/* @__PURE__ */ __commonJSMin(((exports) => { var composer = require_composer(); var Document = require_Document(); @@ -6621,89 +6621,6 @@ var import_dist = (/* @__PURE__ */ __commonJSMin(((exports) => { exports.visit = visit.visit; exports.visitAsync = visit.visitAsync; })))(); -const DATA_DIR_FLAG = "--data-dir"; -const DATA_DIR_ENV = "AGENTMEMORY_DATA_DIR"; -function readDataDirFlag(argv) { - const equalsPrefix = `${DATA_DIR_FLAG}=`; - for (const arg of argv) if (arg.startsWith(equalsPrefix)) return arg.slice(equalsPrefix.length); - const idx = argv.indexOf(DATA_DIR_FLAG); - if (idx !== -1) return argv[idx + 1]; -} -function expandHomePath(pathValue, home) { - if (pathValue === "~") return home; - if (pathValue.startsWith("~/") || pathValue.startsWith("~\\")) return join(home, pathValue.slice(2)); - return pathValue; -} -function defaultDataDir(home = homedir()) { - return join(home, ".agentmemory"); -} -function toAbsoluteDataDir(raw, cwd, home) { - const expanded = expandHomePath(raw.trim(), home); - return isAbsolute(expanded) ? expanded : resolve(cwd, expanded); -} -function resolveDataDirDetailed(options = {}) { - const argv = options.argv ?? process.argv.slice(2); - const env = options.env ?? process.env; - const cwd = options.cwd ?? process.cwd(); - const home = options.home ?? homedir(); - const flagValue = readDataDirFlag(argv); - if (flagValue !== void 0 && flagValue.trim().length > 0) return { - dir: toAbsoluteDataDir(flagValue, cwd, home), - source: "flag" - }; - const envValue = env[DATA_DIR_ENV]; - if (envValue !== void 0 && envValue.trim().length > 0) return { - dir: toAbsoluteDataDir(envValue, cwd, home), - source: "env" - }; - return { - dir: defaultDataDir(home), - source: "default" - }; -} -function resolveDataDir(options = {}) { - return resolveDataDirDetailed(options).dir; -} -//#endregion -//#region src/config.ts -let envFileCache; -function envFilePath() { - return join(resolveDataDir(), ".env"); -} -function loadEnvFile() { - if (envFileCache) return envFileCache; - const envFile = envFilePath(); - if (!existsSync(envFile)) { - envFileCache = {}; - return envFileCache; - } - const content = readFileSync(envFile, "utf-8"); - const vars = {}; - for (const line of content.split("\n")) { - const trimmed = line.trim(); - if (!trimmed || trimmed.startsWith("#")) continue; - const eqIdx = trimmed.indexOf("="); - if (eqIdx === -1) continue; - const key = trimmed.slice(0, eqIdx).trim(); - let val = trimmed.slice(eqIdx + 1).trim(); - const quoteChar = val[0] === "\"" || val[0] === "'" ? val[0] : ""; - if (quoteChar) { - const closeIdx = val.indexOf(quoteChar, 1); - if (closeIdx !== -1) val = val.slice(1, closeIdx); - } else { - const hashIdx = val.indexOf(" #"); - if (hashIdx !== -1) val = val.slice(0, hashIdx).trim(); - } - vars[key] = val; - } - envFileCache = vars; - return envFileCache; -} -function hydrateProcessEnvFromFile() { - for (const [k, v] of Object.entries(loadEnvFile())) if (process.env[k] === void 0) process.env[k] = v; -} -//#endregion -//#region src/project-config.ts const PRIVACY_ORDER = { standard: 0, private: 1, @@ -6826,6 +6743,19 @@ function normalizeGitRemote(remote) { return; } } +/** +* Mask credentials before a raw git remote reaches stderr. Everything from +* the last "@" onward is kept (host/path), everything between the scheme +* and that "@" is replaced; remotes without "@" pass through unchanged. +* +* @param value - Raw `git remote get-url` output, in any git-supported form. +* @returns A log-safe rendering of the remote. +*/ +function redactRemoteForLog(value) { + const at = value.lastIndexOf("@"); + if (at === -1) return value; + return `${value.match(/^[a-z][a-z0-9+.-]*:\/\//i)?.[0] ?? ""}***@${value.slice(at + 1)}`; +} let warnedUnnormalizableRemote = false; function inferProjectId(root) { const remote = git(root, [ @@ -6842,7 +6772,7 @@ function inferProjectId(root) { if (remote && !normalizedRemote) { if (!warnedUnnormalizableRemote) { warnedUnnormalizableRemote = true; - process.stderr.write(`[agentmemory] Git remote "${remote}" cannot be normalized to a canonical project id; using local/${projectPathHash(root)} for this checkout\n`); + process.stderr.write(`[agentmemory] Git remote "${redactRemoteForLog(remote)}" cannot be normalized to a canonical project id; using local/${projectPathHash(root)} for this checkout\n`); } } return normalizedRemote ?? `local/${projectPathHash(root)}`; @@ -6868,7 +6798,10 @@ function readConfigFile(path) { } } function getUserProjectConfigPath(root) { - return join(userHome(), ".agentmemory", "projects", `${projectPathHash(root)}.yaml`); + const hashed = `${projectPathHash(root)}.yaml`; + const dataDirOverride = join(resolveDataDir(), "projects", hashed); + if (existsSync(dataDirOverride)) return dataDirOverride; + return join(userHome(), ".agentmemory", "projects", hashed); } function loadAgentmemoryEnvironment() { hydrateProcessEnvFromFile(); diff --git a/plugin/scripts/_capture-CdxWrCwc.mjs b/plugin/scripts/_capture-Ba1NCNW7.mjs similarity index 99% rename from plugin/scripts/_capture-CdxWrCwc.mjs rename to plugin/scripts/_capture-Ba1NCNW7.mjs index bd5454584..07d866062 100644 --- a/plugin/scripts/_capture-CdxWrCwc.mjs +++ b/plugin/scripts/_capture-Ba1NCNW7.mjs @@ -1,4 +1,4 @@ -import { a as normalizedProjectPath, r as isProjectPathExcluded } from "./_auth-8LRLcG2I.mjs"; +import { a as normalizedProjectPath, r as isProjectPathExcluded } from "./_auth-1Z57rc-e.mjs"; import { resolve } from "node:path"; import { createHash } from "node:crypto"; import { execFileSync } from "node:child_process"; diff --git a/plugin/scripts/_delivery-C1jg9u5N.mjs b/plugin/scripts/_delivery-BPnYIm56.mjs similarity index 97% rename from plugin/scripts/_delivery-C1jg9u5N.mjs rename to plugin/scripts/_delivery-BPnYIm56.mjs index ba5c8dd92..57e463c17 100644 --- a/plugin/scripts/_delivery-C1jg9u5N.mjs +++ b/plugin/scripts/_delivery-BPnYIm56.mjs @@ -1,4 +1,4 @@ -import { n as projectAuthHeaders } from "./_auth-8LRLcG2I.mjs"; +import { n as projectAuthHeaders } from "./_auth-1Z57rc-e.mjs"; //#region src/hooks/_delivery.ts var HookDeliveryError = class extends Error { retryable; diff --git a/plugin/scripts/_observe-delivery-BYrh4Ekr.mjs b/plugin/scripts/_observe-delivery-BSYpE5r3.mjs similarity index 97% rename from plugin/scripts/_observe-delivery-BYrh4Ekr.mjs rename to plugin/scripts/_observe-delivery-BSYpE5r3.mjs index 6f5640946..6e821def0 100644 --- a/plugin/scripts/_observe-delivery-BYrh4Ekr.mjs +++ b/plugin/scripts/_observe-delivery-BSYpE5r3.mjs @@ -1,4 +1,4 @@ -import { n as projectAuthHeaders } from "./_auth-8LRLcG2I.mjs"; +import { n as projectAuthHeaders } from "./_auth-1Z57rc-e.mjs"; //#region src/hooks/_observe-delivery.ts const MAX_ATTEMPTS = 2; const REQUEST_TIMEOUT_MS = 250; diff --git a/plugin/scripts/_project-VjQrnNqc.mjs b/plugin/scripts/_project-BqDfPlX6.mjs similarity index 95% rename from plugin/scripts/_project-VjQrnNqc.mjs rename to plugin/scripts/_project-BqDfPlX6.mjs index c9ad15dde..6d2cd31eb 100644 --- a/plugin/scripts/_project-VjQrnNqc.mjs +++ b/plugin/scripts/_project-BqDfPlX6.mjs @@ -1,4 +1,4 @@ -import { i as loadAgentmemoryEnvironment, o as resolveProjectConfig } from "./_auth-8LRLcG2I.mjs"; +import { i as loadAgentmemoryEnvironment, o as resolveProjectConfig } from "./_auth-1Z57rc-e.mjs"; //#region src/hooks/_project.ts loadAgentmemoryEnvironment(); /** diff --git a/plugin/scripts/auth-DkiaFluQ.mjs b/plugin/scripts/auth-DkiaFluQ.mjs new file mode 100644 index 000000000..86bdd5292 --- /dev/null +++ b/plugin/scripts/auth-DkiaFluQ.mjs @@ -0,0 +1,126 @@ +import { existsSync, readFileSync } from "node:fs"; +import { isAbsolute, join, resolve } from "node:path"; +import { createHmac, randomBytes } from "node:crypto"; +import { homedir } from "node:os"; +//#region src/data-dir.ts +const DATA_DIR_FLAG = "--data-dir"; +const DATA_DIR_ENV = "AGENTMEMORY_DATA_DIR"; +function readDataDirFlag(argv) { + const equalsPrefix = `${DATA_DIR_FLAG}=`; + for (const arg of argv) if (arg.startsWith(equalsPrefix)) return arg.slice(equalsPrefix.length); + const idx = argv.indexOf(DATA_DIR_FLAG); + if (idx !== -1) return argv[idx + 1]; +} +function expandHomePath(pathValue, home) { + if (pathValue === "~") return home; + if (pathValue.startsWith("~/") || pathValue.startsWith("~\\")) return join(home, pathValue.slice(2)); + return pathValue; +} +function defaultDataDir(home = homedir()) { + return join(home, ".agentmemory"); +} +function toAbsoluteDataDir(raw, cwd, home) { + const expanded = expandHomePath(raw.trim(), home); + return isAbsolute(expanded) ? expanded : resolve(cwd, expanded); +} +function resolveDataDirDetailed(options = {}) { + const argv = options.argv ?? process.argv.slice(2); + const env = options.env ?? process.env; + const cwd = options.cwd ?? process.cwd(); + const home = options.home ?? homedir(); + const flagValue = readDataDirFlag(argv); + if (flagValue !== void 0 && flagValue.trim().length > 0) return { + dir: toAbsoluteDataDir(flagValue, cwd, home), + source: "flag" + }; + const envValue = env[DATA_DIR_ENV]; + if (envValue !== void 0 && envValue.trim().length > 0) return { + dir: toAbsoluteDataDir(envValue, cwd, home), + source: "env" + }; + return { + dir: defaultDataDir(home), + source: "default" + }; +} +function resolveDataDir(options = {}) { + return resolveDataDirDetailed(options).dir; +} +//#endregion +//#region src/config.ts +let envFileCache; +function envFilePath() { + return join(resolveDataDir(), ".env"); +} +function loadEnvFile() { + if (envFileCache) return envFileCache; + const envFile = envFilePath(); + if (!existsSync(envFile)) { + envFileCache = {}; + return envFileCache; + } + const content = readFileSync(envFile, "utf-8"); + const vars = {}; + for (const line of content.split("\n")) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith("#")) continue; + const eqIdx = trimmed.indexOf("="); + if (eqIdx === -1) continue; + const key = trimmed.slice(0, eqIdx).trim(); + let val = trimmed.slice(eqIdx + 1).trim(); + const quoteChar = val[0] === "\"" || val[0] === "'" ? val[0] : ""; + if (quoteChar) { + const closeIdx = val.indexOf(quoteChar, 1); + if (closeIdx !== -1) val = val.slice(1, closeIdx); + } else { + const hashIdx = val.indexOf(" #"); + if (hashIdx !== -1) val = val.slice(0, hashIdx).trim(); + } + vars[key] = val; + } + envFileCache = vars; + return envFileCache; +} +function hydrateProcessEnvFromFile() { + for (const [k, v] of Object.entries(loadEnvFile())) if (process.env[k] === void 0) process.env[k] = v; +} +function getMergedEnv(overrides) { + return { + ...loadEnvFile(), + ...process.env, + ...overrides + }; +} +function getStandalonePersistPath() { + return getMergedEnv()["STANDALONE_PERSIST_PATH"] || join(resolveDataDir(), "standalone.json"); +} +randomBytes(32); +const PROJECT_CAPABILITY_TOKEN_VERSION = "amcap1"; +const DEFAULT_PROJECT_CAPABILITY_AUDIENCE = "agentmemory"; +const PROJECT_CAPABILITY_PROJECT_HEADER = "x-agentmemory-project"; +function hmac(value, secret) { + return createHmac("sha256", secret).update(value).digest("base64url"); +} +function createProjectCapabilityToken(claims, signingSecret) { + if (!signingSecret) throw new Error("project capability signing secret is required"); + const normalized = { + version: 1, + audience: claims.audience.trim(), + project: claims.project.trim(), + expiresAt: claims.expiresAt, + ...claims.issuedAt !== void 0 ? { issuedAt: claims.issuedAt } : {}, + ...claims.capabilityId ? { capabilityId: claims.capabilityId.trim() } : {} + }; + if (!normalized.audience || !normalized.project || !Number.isSafeInteger(normalized.expiresAt)) throw new Error("invalid project capability claims"); + const signed = `${PROJECT_CAPABILITY_TOKEN_VERSION}.${Buffer.from(JSON.stringify(normalized)).toString("base64url")}`; + return `${signed}.${hmac(signed, signingSecret)}`; +} +function isStrictCapabilityMode(value = process.env["AGENTMEMORY_STRICT_CAPABILITY_MODE"]) { + return ![ + "false", + "0", + "off" + ].includes((value ?? "").trim().toLowerCase()); +} +//#endregion +export { getStandalonePersistPath as a, isStrictCapabilityMode as i, PROJECT_CAPABILITY_PROJECT_HEADER as n, hydrateProcessEnvFromFile as o, createProjectCapabilityToken as r, resolveDataDir as s, DEFAULT_PROJECT_CAPABILITY_AUDIENCE as t }; diff --git a/plugin/scripts/auth-Evr8deOq.mjs b/plugin/scripts/auth-Evr8deOq.mjs deleted file mode 100644 index ac50695ff..000000000 --- a/plugin/scripts/auth-Evr8deOq.mjs +++ /dev/null @@ -1,31 +0,0 @@ -import { createHmac, randomBytes } from "node:crypto"; -randomBytes(32); -const PROJECT_CAPABILITY_TOKEN_VERSION = "amcap1"; -const DEFAULT_PROJECT_CAPABILITY_AUDIENCE = "agentmemory"; -const PROJECT_CAPABILITY_PROJECT_HEADER = "x-agentmemory-project"; -function hmac(value, secret) { - return createHmac("sha256", secret).update(value).digest("base64url"); -} -function createProjectCapabilityToken(claims, signingSecret) { - if (!signingSecret) throw new Error("project capability signing secret is required"); - const normalized = { - version: 1, - audience: claims.audience.trim(), - project: claims.project.trim(), - expiresAt: claims.expiresAt, - ...claims.issuedAt !== void 0 ? { issuedAt: claims.issuedAt } : {}, - ...claims.capabilityId ? { capabilityId: claims.capabilityId.trim() } : {} - }; - if (!normalized.audience || !normalized.project || !Number.isSafeInteger(normalized.expiresAt)) throw new Error("invalid project capability claims"); - const signed = `${PROJECT_CAPABILITY_TOKEN_VERSION}.${Buffer.from(JSON.stringify(normalized)).toString("base64url")}`; - return `${signed}.${hmac(signed, signingSecret)}`; -} -function isStrictCapabilityMode(value = process.env["AGENTMEMORY_STRICT_CAPABILITY_MODE"]) { - return ![ - "false", - "0", - "off" - ].includes((value ?? "").trim().toLowerCase()); -} -//#endregion -export { isStrictCapabilityMode as i, PROJECT_CAPABILITY_PROJECT_HEADER as n, createProjectCapabilityToken as r, DEFAULT_PROJECT_CAPABILITY_AUDIENCE as t }; diff --git a/plugin/scripts/notification.mjs b/plugin/scripts/notification.mjs index b9fc3af01..24f6164df 100755 --- a/plugin/scripts/notification.mjs +++ b/plugin/scripts/notification.mjs @@ -1,6 +1,6 @@ #!/usr/bin/env node -import { t as resolveProject } from "./_project-VjQrnNqc.mjs"; -import { n as reportObservationDeliveryFailure, t as deliverObservation } from "./_observe-delivery-BYrh4Ekr.mjs"; +import { t as resolveProject } from "./_project-BqDfPlX6.mjs"; +import { n as reportObservationDeliveryFailure, t as deliverObservation } from "./_observe-delivery-BSYpE5r3.mjs"; //#region src/hooks/notification.ts function isSdkChildContext(payload) { if (process.env["AGENTMEMORY_SDK_CHILD"] === "1") return true; diff --git a/plugin/scripts/post-commit.mjs b/plugin/scripts/post-commit.mjs index f4bd21bae..ce823e21b 100755 --- a/plugin/scripts/post-commit.mjs +++ b/plugin/scripts/post-commit.mjs @@ -1,7 +1,7 @@ #!/usr/bin/env node -import { t as resolveProject } from "./_project-VjQrnNqc.mjs"; -import { n as reportHookDeliveryFailure, t as deliverProjectRequest } from "./_delivery-C1jg9u5N.mjs"; -import { n as credentialFreeWorktreeId, r as parseCommitTransitions } from "./_capture-CdxWrCwc.mjs"; +import { t as resolveProject } from "./_project-BqDfPlX6.mjs"; +import { n as reportHookDeliveryFailure, t as deliverProjectRequest } from "./_delivery-BPnYIm56.mjs"; +import { n as credentialFreeWorktreeId, r as parseCommitTransitions } from "./_capture-Ba1NCNW7.mjs"; import { resolve } from "node:path"; import { execFile } from "node:child_process"; import { pathToFileURL } from "node:url"; diff --git a/plugin/scripts/post-tool-failure.mjs b/plugin/scripts/post-tool-failure.mjs index a697c80e2..ead2742e3 100755 --- a/plugin/scripts/post-tool-failure.mjs +++ b/plugin/scripts/post-tool-failure.mjs @@ -1,7 +1,7 @@ #!/usr/bin/env node -import { o as resolveProjectConfig } from "./_auth-8LRLcG2I.mjs"; -import { n as reportObservationDeliveryFailure, t as deliverObservation } from "./_observe-delivery-BYrh4Ekr.mjs"; -import { t as captureToolEvent } from "./_capture-CdxWrCwc.mjs"; +import { o as resolveProjectConfig } from "./_auth-1Z57rc-e.mjs"; +import { n as reportObservationDeliveryFailure, t as deliverObservation } from "./_observe-delivery-BSYpE5r3.mjs"; +import { t as captureToolEvent } from "./_capture-Ba1NCNW7.mjs"; //#region src/hooks/post-tool-failure.ts function isSdkChildContext(payload) { if (process.env["AGENTMEMORY_SDK_CHILD"] === "1") return true; diff --git a/plugin/scripts/post-tool-use.mjs b/plugin/scripts/post-tool-use.mjs index 6939e2ead..db2d8c3f6 100755 --- a/plugin/scripts/post-tool-use.mjs +++ b/plugin/scripts/post-tool-use.mjs @@ -1,7 +1,7 @@ #!/usr/bin/env node -import { o as resolveProjectConfig } from "./_auth-8LRLcG2I.mjs"; -import { n as reportObservationDeliveryFailure, t as deliverObservation } from "./_observe-delivery-BYrh4Ekr.mjs"; -import { t as captureToolEvent } from "./_capture-CdxWrCwc.mjs"; +import { o as resolveProjectConfig } from "./_auth-1Z57rc-e.mjs"; +import { n as reportObservationDeliveryFailure, t as deliverObservation } from "./_observe-delivery-BSYpE5r3.mjs"; +import { t as captureToolEvent } from "./_capture-Ba1NCNW7.mjs"; //#region src/hooks/post-tool-use.ts function isSdkChildContext(payload) { if (process.env["AGENTMEMORY_SDK_CHILD"] === "1") return true; diff --git a/plugin/scripts/pre-compact.mjs b/plugin/scripts/pre-compact.mjs index d38083c8c..0d245fcb4 100755 --- a/plugin/scripts/pre-compact.mjs +++ b/plugin/scripts/pre-compact.mjs @@ -1,6 +1,6 @@ #!/usr/bin/env node -import { i as loadAgentmemoryEnvironment, n as projectAuthHeaders, t as contextAcknowledgementSecret } from "./_auth-8LRLcG2I.mjs"; -import { t as resolveProject } from "./_project-VjQrnNqc.mjs"; +import { i as loadAgentmemoryEnvironment, n as projectAuthHeaders, t as contextAcknowledgementSecret } from "./_auth-1Z57rc-e.mjs"; +import { t as resolveProject } from "./_project-BqDfPlX6.mjs"; import { createHash, createHmac, randomUUID } from "node:crypto"; //#region src/hooks/pre-compact.ts function isSdkChildContext(payload) { diff --git a/plugin/scripts/pre-tool-use.mjs b/plugin/scripts/pre-tool-use.mjs index eef51c53b..10c5ba596 100755 --- a/plugin/scripts/pre-tool-use.mjs +++ b/plugin/scripts/pre-tool-use.mjs @@ -1,6 +1,6 @@ #!/usr/bin/env node -import { i as loadAgentmemoryEnvironment, n as projectAuthHeaders } from "./_auth-8LRLcG2I.mjs"; -import { t as resolveProject } from "./_project-VjQrnNqc.mjs"; +import { i as loadAgentmemoryEnvironment, n as projectAuthHeaders } from "./_auth-1Z57rc-e.mjs"; +import { t as resolveProject } from "./_project-BqDfPlX6.mjs"; //#region src/hooks/pre-tool-use.ts function isSdkChildContext(payload) { if (process.env["AGENTMEMORY_SDK_CHILD"] === "1") return true; diff --git a/plugin/scripts/prompt-submit.mjs b/plugin/scripts/prompt-submit.mjs index 97a4649b8..ecf6d6593 100755 --- a/plugin/scripts/prompt-submit.mjs +++ b/plugin/scripts/prompt-submit.mjs @@ -1,6 +1,6 @@ #!/usr/bin/env node -import { t as resolveProject } from "./_project-VjQrnNqc.mjs"; -import { n as reportObservationDeliveryFailure, t as deliverObservation } from "./_observe-delivery-BYrh4Ekr.mjs"; +import { t as resolveProject } from "./_project-BqDfPlX6.mjs"; +import { n as reportObservationDeliveryFailure, t as deliverObservation } from "./_observe-delivery-BSYpE5r3.mjs"; //#region src/hooks/prompt-submit.ts function isSdkChildContext(payload) { if (process.env["AGENTMEMORY_SDK_CHILD"] === "1") return true; diff --git a/plugin/scripts/session-end.mjs b/plugin/scripts/session-end.mjs index 7452e1929..8e0c13794 100755 --- a/plugin/scripts/session-end.mjs +++ b/plugin/scripts/session-end.mjs @@ -1,6 +1,6 @@ #!/usr/bin/env node -import { t as resolveProject } from "./_project-VjQrnNqc.mjs"; -import { n as reportHookDeliveryFailure, t as deliverProjectRequest } from "./_delivery-C1jg9u5N.mjs"; +import { t as resolveProject } from "./_project-BqDfPlX6.mjs"; +import { n as reportHookDeliveryFailure, t as deliverProjectRequest } from "./_delivery-BPnYIm56.mjs"; //#region src/hooks/session-end.ts function isSdkChildContext(payload) { if (process.env["AGENTMEMORY_SDK_CHILD"] === "1") return true; diff --git a/plugin/scripts/session-start.mjs b/plugin/scripts/session-start.mjs index 01cc31ff6..66863564d 100755 --- a/plugin/scripts/session-start.mjs +++ b/plugin/scripts/session-start.mjs @@ -1,7 +1,7 @@ #!/usr/bin/env node -import { o as resolveProjectConfig } from "./_auth-8LRLcG2I.mjs"; -import "./_project-VjQrnNqc.mjs"; -import { n as reportHookDeliveryFailure, t as deliverProjectRequest } from "./_delivery-C1jg9u5N.mjs"; +import { o as resolveProjectConfig } from "./_auth-1Z57rc-e.mjs"; +import "./_project-BqDfPlX6.mjs"; +import { n as reportHookDeliveryFailure, t as deliverProjectRequest } from "./_delivery-BPnYIm56.mjs"; //#region src/hooks/session-start.ts function isSdkChildContext(payload) { if (process.env["AGENTMEMORY_SDK_CHILD"] === "1") return true; diff --git a/plugin/scripts/standalone.mjs b/plugin/scripts/standalone.mjs index a2fe1f910..25c4ad71f 100755 --- a/plugin/scripts/standalone.mjs +++ b/plugin/scripts/standalone.mjs @@ -1,6 +1,6 @@ #!/usr/bin/env node import { n as generateId, t as KV } from "./schema-Dttua2Zo.mjs"; -import { i as isStrictCapabilityMode, n as PROJECT_CAPABILITY_PROJECT_HEADER, r as createProjectCapabilityToken } from "./auth-Evr8deOq.mjs"; +import { a as getStandalonePersistPath$1, i as isStrictCapabilityMode, n as PROJECT_CAPABILITY_PROJECT_HEADER, o as hydrateProcessEnvFromFile, r as createProjectCapabilityToken } from "./auth-DkiaFluQ.mjs"; import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { dirname, join } from "node:path"; import { homedir } from "node:os"; @@ -2074,8 +2074,9 @@ const SERVER_INFO = { version: VERSION, protocolVersion: SUPPORTED_PROTOCOL_VERSIONS[0] }; +hydrateProcessEnvFromFile(); function getStandalonePersistPath() { - return process.env["STANDALONE_PERSIST_PATH"]?.trim() || join(homedir(), ".agentmemory", "standalone.json"); + return getStandalonePersistPath$1(); } const kv = new InMemoryKV(getStandalonePersistPath()); let modeAnnounced = false; diff --git a/plugin/scripts/stop.mjs b/plugin/scripts/stop.mjs index b0c2dfb8b..2f94ce8ec 100755 --- a/plugin/scripts/stop.mjs +++ b/plugin/scripts/stop.mjs @@ -1,6 +1,6 @@ #!/usr/bin/env node -import { t as resolveProject } from "./_project-VjQrnNqc.mjs"; -import { n as reportHookDeliveryFailure, t as deliverProjectRequest } from "./_delivery-C1jg9u5N.mjs"; +import { t as resolveProject } from "./_project-BqDfPlX6.mjs"; +import { n as reportHookDeliveryFailure, t as deliverProjectRequest } from "./_delivery-BPnYIm56.mjs"; //#region src/hooks/stop.ts function isSdkChildContext(payload) { if (process.env["AGENTMEMORY_SDK_CHILD"] === "1") return true; diff --git a/plugin/scripts/subagent-start.mjs b/plugin/scripts/subagent-start.mjs index dbdff3abe..d8f0d716d 100755 --- a/plugin/scripts/subagent-start.mjs +++ b/plugin/scripts/subagent-start.mjs @@ -1,6 +1,6 @@ #!/usr/bin/env node -import { t as resolveProject } from "./_project-VjQrnNqc.mjs"; -import { n as reportObservationDeliveryFailure, t as deliverObservation } from "./_observe-delivery-BYrh4Ekr.mjs"; +import { t as resolveProject } from "./_project-BqDfPlX6.mjs"; +import { n as reportObservationDeliveryFailure, t as deliverObservation } from "./_observe-delivery-BSYpE5r3.mjs"; //#region src/hooks/subagent-start.ts function isSdkChildContext(payload) { if (process.env["AGENTMEMORY_SDK_CHILD"] === "1") return true; diff --git a/plugin/scripts/subagent-stop.mjs b/plugin/scripts/subagent-stop.mjs index bdab11c24..f87a72123 100755 --- a/plugin/scripts/subagent-stop.mjs +++ b/plugin/scripts/subagent-stop.mjs @@ -1,6 +1,6 @@ #!/usr/bin/env node -import { t as resolveProject } from "./_project-VjQrnNqc.mjs"; -import { n as reportObservationDeliveryFailure, t as deliverObservation } from "./_observe-delivery-BYrh4Ekr.mjs"; +import { t as resolveProject } from "./_project-BqDfPlX6.mjs"; +import { n as reportObservationDeliveryFailure, t as deliverObservation } from "./_observe-delivery-BSYpE5r3.mjs"; //#region src/hooks/subagent-stop.ts function isSdkChildContext(payload) { if (process.env["AGENTMEMORY_SDK_CHILD"] === "1") return true; diff --git a/plugin/scripts/task-completed.mjs b/plugin/scripts/task-completed.mjs index 5bf09a9bd..aa8e1aba9 100755 --- a/plugin/scripts/task-completed.mjs +++ b/plugin/scripts/task-completed.mjs @@ -1,6 +1,6 @@ #!/usr/bin/env node -import { t as resolveProject } from "./_project-VjQrnNqc.mjs"; -import { n as reportObservationDeliveryFailure, t as deliverObservation } from "./_observe-delivery-BYrh4Ekr.mjs"; +import { t as resolveProject } from "./_project-BqDfPlX6.mjs"; +import { n as reportObservationDeliveryFailure, t as deliverObservation } from "./_observe-delivery-BSYpE5r3.mjs"; //#region src/hooks/task-completed.ts function isSdkChildContext(payload) { if (process.env["AGENTMEMORY_SDK_CHILD"] === "1") return true; From 4a0a888b4a277ee30204fd93390959012758065f Mon Sep 17 00:00:00 2001 From: ChronodeAi Date: Mon, 24 Aug 2026 17:45:57 -0500 Subject: [PATCH 12/15] chore(gate): refresh R-13 test manifest for five new test files Adds graph-import-scope, project-config-redaction, capability-secret-hardening, search-reconcile-ready, and mesh-origin suites (171 -> 176 files) with recomputed manifest and content hashes per scripts/r13/run.mjs. --- ci/r13-test-manifest.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ci/r13-test-manifest.json b/ci/r13-test-manifest.json index 9436e070d..67712212b 100644 --- a/ci/r13-test-manifest.json +++ b/ci/r13-test-manifest.json @@ -1,5 +1,5 @@ { - "count": 171, - "sha256": "dfb0a43ae58136529042da617518eddb884a379233b038ca6970aa46d993ac8f", - "content_sha256": "4a9a6cfa26b4796eadf14ec294559de28dfdfba7f7083c65fc7bbd479c6326b8" + "count": 176, + "sha256": "4ef314289367720f8cbe199ca8f2823eea416ec3fa0c861fefcbaf0550d530f1", + "content_sha256": "542b9f58957d1ac9fa9421abb47bbd4a985dae5a3a1fe6ee4b2f1916d527693e" } From d7e30a94582bf9d3f574c101bed6a4e4d8785e04 Mon Sep 17 00:00:00 2001 From: ChronodeAi Date: Mon, 24 Aug 2026 17:46:31 -0500 Subject: [PATCH 13/15] chore(evidence): refresh interface inventory for the adversarial-fixes train Measured surface counts are unchanged (137 REST routes, 60 MCP tools, 13 hooks, 19 connectors); only the source-identity hashes rotate with the branch head. --- .aiwg/reports/g-icm-01-interface-inventory.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.aiwg/reports/g-icm-01-interface-inventory.json b/.aiwg/reports/g-icm-01-interface-inventory.json index ab477a93d..9f948b5d3 100644 --- a/.aiwg/reports/g-icm-01-interface-inventory.json +++ b/.aiwg/reports/g-icm-01-interface-inventory.json @@ -3,9 +3,9 @@ "control_id": "G-ICM-01", "project_id": "github.com/chronodeai/agentmemory", "source_identity": { - "commit_sha": "78e11a0b67c66f9e848001a889fe02300360ec5c", - "commit_tree_sha": "dd211619484da34de4ba4958f0940ea8c2601cbb", - "inventory_input_sha256": "948e6690e0c490edefca5c834f6afb812e38b18ca38fdd56ff28ecd959e16979" + "commit_sha": "4a0a888b4a277ee30204fd93390959012758065f", + "commit_tree_sha": "47b78aff1804c95a07b55562c1334367ef89e820", + "inventory_input_sha256": "6b75682a2d2ceb2c8d9848a0cd04dc4a91708967268e2c08d50f377af2af13b6" }, "public_route_allowlist": [ "GET /agentmemory/livez" From 5df4a9441d0c2d583498b953c6d3f1f0b78803bb Mon Sep 17 00:00:00 2001 From: ChronodeAi Date: Mon, 24 Aug 2026 17:51:29 -0500 Subject: [PATCH 14/15] chore(release): 0.9.30-chronode.2 --- CHANGELOG.md | 4 ++++ package-lock.json | 4 ++-- package.json | 2 +- packages/mcp/package.json | 4 ++-- plugin/.claude-plugin/plugin.json | 2 +- plugin/.codex-plugin/plugin.json | 2 +- plugin/plugin.json | 2 +- plugin/scripts/standalone.mjs | 2 +- src/version.ts | 2 +- 9 files changed, 14 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index af7d0f804..dff405935 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), ## [Unreleased] +## [0.9.30-chronode.2] — 2026-08-24 + +Adversarial-review fixes over the shipped sync wave. + ### Fixed - **Graphify import path scoping** (security). An explicit path on `mem::graph::import-graphify` (`POST /agentmemory/graph/import-graphify`) could point anywhere the daemon can read. Explicit paths must now resolve inside the requested project cwd and keep the `graph.json` basename; violations return a generic error that never echoes the attempted path, and stat failures for explicit paths stay equally non-specific. diff --git a/package-lock.json b/package-lock.json index 5ae67ad7c..fa874c898 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@agentmemory/agentmemory", - "version": "0.9.30-chronode.1", + "version": "0.9.30-chronode.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@agentmemory/agentmemory", - "version": "0.9.30-chronode.1", + "version": "0.9.30-chronode.2", "license": "Apache-2.0", "dependencies": { "@anthropic-ai/claude-agent-sdk": "^0.3.142", diff --git a/package.json b/package.json index ed2903b62..022a09199 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@agentmemory/agentmemory", - "version": "0.9.30-chronode.1", + "version": "0.9.30-chronode.2", "description": "Persistent memory for AI coding agents, powered by iii-engine's three primitives", "type": "module", "main": "dist/index.mjs", diff --git a/packages/mcp/package.json b/packages/mcp/package.json index afafc2024..66a08efb9 100644 --- a/packages/mcp/package.json +++ b/packages/mcp/package.json @@ -1,6 +1,6 @@ { "name": "@agentmemory/mcp", - "version": "0.9.30-chronode.1", + "version": "0.9.30-chronode.2", "description": "Standalone MCP server for agentmemory — thin shim that re-exposes @agentmemory/agentmemory's MCP entrypoint", "type": "module", "bin": { @@ -28,7 +28,7 @@ "homepage": "https://github.com/rohitg00/agentmemory#readme", "bugs": "https://github.com/rohitg00/agentmemory/issues", "dependencies": { - "@agentmemory/agentmemory": "0.9.30-chronode.1" + "@agentmemory/agentmemory": "0.9.30-chronode.2" }, "publishConfig": { "access": "public", diff --git a/plugin/.claude-plugin/plugin.json b/plugin/.claude-plugin/plugin.json index 2bde6f6cf..beb559157 100644 --- a/plugin/.claude-plugin/plugin.json +++ b/plugin/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "agentmemory", - "version": "0.9.30-chronode.1", + "version": "0.9.30-chronode.2", "description": "Persistent memory for AI coding agents -- captures tool usage, compresses via LLM, injects context into future sessions. 12 hooks, 60 MCP tools, 17 skills, real-time viewer.", "author": { "name": "Rohit Ghumare", diff --git a/plugin/.codex-plugin/plugin.json b/plugin/.codex-plugin/plugin.json index e3f9d46dd..bdbb95f81 100644 --- a/plugin/.codex-plugin/plugin.json +++ b/plugin/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "agentmemory", - "version": "0.9.30-chronode.1", + "version": "0.9.30-chronode.2", "description": "Persistent memory for AI coding agents -- captures tool usage, compresses via LLM, injects context into future sessions. 11 hooks, 60 MCP tools, 17 skills, real-time viewer.", "author": { "name": "Rohit Ghumare", diff --git a/plugin/plugin.json b/plugin/plugin.json index f4e28cc36..3c662ec7f 100644 --- a/plugin/plugin.json +++ b/plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "agentmemory", - "version": "0.9.30-chronode.1", + "version": "0.9.30-chronode.2", "description": "Persistent memory for AI coding agents -- captures tool usage, compresses via LLM, injects context into future sessions. 12 hooks, 60 MCP tools, 17 skills, real-time viewer.", "author": { "name": "Rohit Ghumare", diff --git a/plugin/scripts/standalone.mjs b/plugin/scripts/standalone.mjs index 25c4ad71f..24c8c243b 100755 --- a/plugin/scripts/standalone.mjs +++ b/plugin/scripts/standalone.mjs @@ -1761,7 +1761,7 @@ function getAllTools() { } //#endregion //#region src/version.ts -const VERSION = "0.9.30-chronode.1"; +const VERSION = "0.9.30-chronode.2"; process.env["AGENTMEMORY_BUILD_ID"]; process.env["AGENTMEMORY_VIEWER_BUILD_ID"]; //#endregion diff --git a/src/version.ts b/src/version.ts index cc68ce282..60413cef4 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1,4 +1,4 @@ -export const VERSION = "0.9.30-chronode.1"; +export const VERSION = "0.9.30-chronode.2"; export const EXPORT_FORMAT_VERSION = "0.9.28" as const; export const API_CONTRACT_VERSION = 1; export const BACKEND_BUILD_ID = From f155a618e9bb638afa97459a127d95c49a575aaf Mon Sep 17 00:00:00 2001 From: ChronodeAi Date: Mon, 24 Aug 2026 17:51:30 -0500 Subject: [PATCH 15/15] chore(evidence): refresh interface inventory for the release commit --- .aiwg/reports/g-icm-01-interface-inventory.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.aiwg/reports/g-icm-01-interface-inventory.json b/.aiwg/reports/g-icm-01-interface-inventory.json index 9f948b5d3..aa108649e 100644 --- a/.aiwg/reports/g-icm-01-interface-inventory.json +++ b/.aiwg/reports/g-icm-01-interface-inventory.json @@ -3,9 +3,9 @@ "control_id": "G-ICM-01", "project_id": "github.com/chronodeai/agentmemory", "source_identity": { - "commit_sha": "4a0a888b4a277ee30204fd93390959012758065f", - "commit_tree_sha": "47b78aff1804c95a07b55562c1334367ef89e820", - "inventory_input_sha256": "6b75682a2d2ceb2c8d9848a0cd04dc4a91708967268e2c08d50f377af2af13b6" + "commit_sha": "5df4a9441d0c2d583498b953c6d3f1f0b78803bb", + "commit_tree_sha": "38f5054811be14f96cf7950b09649a2a2d710002", + "inventory_input_sha256": "fe2dea65299cdc4eeba6e309dccce74b9a001fcd71f9edcee0656897c65e1c4c" }, "public_route_allowlist": [ "GET /agentmemory/livez"