diff --git a/harnesses/codex/.codex-plugin/plugin.json b/harnesses/codex/.codex-plugin/plugin.json index 4d72341e6..44b77406c 100644 --- a/harnesses/codex/.codex-plugin/plugin.json +++ b/harnesses/codex/.codex-plugin/plugin.json @@ -1,10 +1,14 @@ { "name": "hivemind", - "version": "0.6.7", + "version": "0.7.123", "description": "Cloud-backed persistent memory powered by Deeplake — read, write, and share memory across Codex sessions and agents", - "skills": [], - "mcpServers": [], - "apps": [], + "skills": "./skills/", + "author": { + "name": "Activeloop", + "url": "https://deeplake.ai" + }, + "repository": "https://github.com/activeloopai/hivemind", + "keywords": ["memory", "deeplake", "persistent-memory", "session-capture", "search"], "interface": { "displayName": "Hivemind Memory", "shortDescription": "Persistent shared memory for AI agents powered by Deeplake", diff --git a/harnesses/codex/SUBMISSION.md b/harnesses/codex/SUBMISSION.md new file mode 100644 index 000000000..b36f53a31 --- /dev/null +++ b/harnesses/codex/SUBMISSION.md @@ -0,0 +1,104 @@ +# Codex Plugin Directory — Submission Worksheet (hivemind) + +Portal: https://platform.openai.com/plugins +Submission type: **Skills only** (no MCP server, no app). + +Paste the sections below into the corresponding fields of the submission form. + +--- + +## 1. Listing (Info tab) + +| Field | Value | +|---|---| +| Plugin name | hivemind | +| Display name | Hivemind Memory | +| Short description | Persistent shared memory for AI agents powered by Deeplake | +| Category | productivity | +| Developer | Activeloop | +| Website URL | https://deeplake.ai | +| Repository | https://github.com/activeloopai/hivemind | + +### TODO before submit (need your confirmation — not filled in the manifest) +- [ ] Support URL +- [ ] Privacy Policy URL +- [ ] Terms of Service URL +- [ ] License (add to `plugin.json` if you want it in the listing) +- [ ] Logo asset (square, per portal size spec) + +--- + +## 2. Starter prompts (4-5, key workflows) + +1. Recall what our team decided about the auth refactor last week. +2. Save this as a memory: we standardized on Deeplake managed tables for all capture. +3. What has been worked on in this repo recently across all sessions? +4. Search our shared memory for anything about rate limits. +5. Pick up where I left off on the branch-aware docs work. + +--- + +## 3. Positive test cases (exactly 5) + +Each includes the prompt, expected behavior, and any required test data. + +1. **Cross-session recall** + - Prompt: "What did we decide about X last week?" + - Expected: reads `~/.deeplake/memory/index.md` / `summaries/` and returns a grounded answer citing a summary file. + - Test data: at least one prior session summary present in the org table. + +2. **Write a memory** + - Prompt: "Remember that we use Deeplake managed tables for capture." + - Expected: writes to the memory VFS (INSERT into the memory table); confirms the write. + - Test data: authenticated org/workspace. + +3. **Keyword search over shared memory** + - Prompt: "Search our memory for rate limit." + - Expected: `grep -r` over `summaries/` routed through hybrid lexical+semantic search; returns ranked matches. + - Test data: a summary containing the term. + +4. **Code-graph structural query** + - Prompt: "What calls `sqlStr`?" + - Expected: hivemind-graph skill queries `memory/graph/query/...`; returns callers/callees. + - Test data: a built graph snapshot for the repo. + +5. **Goal/KPI tracking** + - Prompt: "Track a goal: ship the Codex plugin submission this month." + - Expected: hivemind-goals skill writes to `memory/goal/`; confirms creation. + - Test data: authenticated workspace. + +--- + +## 4. Negative test cases (exactly 3) + +1. **No credentials** + - Prompt: "Recall our team memory" with no Deeplake auth configured. + - Expected: safe fallback — clear message to run `hivemind login`; no crash, no fabricated recall. + +2. **Empty / no matching memory** + - Prompt: "What did we decide about ?" + - Expected: reports "no matching memory found" rather than inventing an answer. + +3. **Disallowed interpreter on the memory mount** + - Prompt: "Run a python script against ~/.deeplake/memory/." + - Expected: refuses / routes only bash (cat/ls/grep) per the mount contract; does not execute python/node/curl on the VFS. + +--- + +## 5. Hooks declaration (review notes — be explicit) + +This plugin's core is **hook-based**, not just skills. Declare this plainly to reviewers: + +- Hooks bundled: SessionStart, UserPromptSubmit (capture), PreToolUse (Bash intercept for the memory mount), PostToolUse (capture), Stop (summary + graph). +- Hooks are **not auto-trusted**: per Codex policy, installing/enabling the plugin does not trust its hooks — the user approves them via the standard trust-review flow on first run. +- What the hooks do: capture session activity to a Deeplake managed table, and intercept Bash/Read/Write targeting `~/.deeplake/memory/` to route through the virtual filesystem. No arbitrary network egress beyond the Deeplake API. +- Data handling: session prompts/tool calls/responses are stored in the org's Deeplake tables and shared across the org. State this in the privacy policy. + +--- + +## 6. Account prerequisites (you must do these) + +- [ ] OpenAI org identity verification (individual or business) completed in Platform settings. +- [ ] Org role has **Apps Management: Write**. +- [ ] Select countries for availability, add release notes, attest to policies. +- [ ] After approval: manually publish (approval does not auto-publish). diff --git a/harnesses/codex/commands/login.md b/harnesses/codex/commands/login.md index d91d085ee..d97264f0d 100644 --- a/harnesses/codex/commands/login.md +++ b/harnesses/codex/commands/login.md @@ -6,7 +6,7 @@ allowed-tools: Bash Run: ```bash -node "$CODEX_PLUGIN_ROOT/bundle/commands/auth-login.js" login +node "$PLUGIN_ROOT/bundle/commands/auth-login.js" login ``` If login succeeds, show this welcome message. Include the organization name from the command output: @@ -19,7 +19,7 @@ Your Codex agents can now share memory across sessions, teammates, and machines. Get started: 1. Verify sync: spin up multiple sessions and confirm agents share context -2. Invite a teammate: run `node "$CODEX_PLUGIN_ROOT/bundle/commands/auth-login.js" invite ` -3. Switch orgs: run `node "$CODEX_PLUGIN_ROOT/bundle/commands/auth-login.js" org list` +2. Invite a teammate: run `node "$PLUGIN_ROOT/bundle/commands/auth-login.js" invite ` +3. Switch orgs: run `node "$PLUGIN_ROOT/bundle/commands/auth-login.js" org list` If login fails, show the error and suggest the user check their internet connection or try again. diff --git a/harnesses/codex/hooks/hooks.json b/harnesses/codex/hooks/hooks.json index 5889c1b07..a8b46fa29 100644 --- a/harnesses/codex/hooks/hooks.json +++ b/harnesses/codex/hooks/hooks.json @@ -6,7 +6,7 @@ "hooks": [ { "type": "command", - "command": "node \"$CODEX_PLUGIN_ROOT/bundle/session-start.js\"", + "command": "node \"$PLUGIN_ROOT/bundle/session-start.js\"", "timeout": 10 } ] @@ -17,7 +17,7 @@ "hooks": [ { "type": "command", - "command": "node \"$CODEX_PLUGIN_ROOT/bundle/capture.js\"", + "command": "node \"$PLUGIN_ROOT/bundle/capture.js\"", "timeout": 10 } ] @@ -29,7 +29,7 @@ "hooks": [ { "type": "command", - "command": "node \"$CODEX_PLUGIN_ROOT/bundle/pre-tool-use.js\"", + "command": "node \"$PLUGIN_ROOT/bundle/pre-tool-use.js\"", "timeout": 10 } ] @@ -40,7 +40,7 @@ "hooks": [ { "type": "command", - "command": "node \"$CODEX_PLUGIN_ROOT/bundle/capture.js\"", + "command": "node \"$PLUGIN_ROOT/bundle/capture.js\"", "timeout": 15 } ] @@ -51,12 +51,12 @@ "hooks": [ { "type": "command", - "command": "node \"$CODEX_PLUGIN_ROOT/bundle/stop.js\"", + "command": "node \"$PLUGIN_ROOT/bundle/stop.js\"", "timeout": 30 }, { "type": "command", - "command": "node \"$CODEX_PLUGIN_ROOT/bundle/graph-on-stop.js\"", + "command": "node \"$PLUGIN_ROOT/bundle/graph-on-stop.js\"", "timeout": 30 } ] diff --git a/harnesses/codex/skills/deeplake-memory/SKILL.md b/harnesses/codex/skills/deeplake-memory/SKILL.md index 47a52d6a6..cd13f5630 100644 --- a/harnesses/codex/skills/deeplake-memory/SKILL.md +++ b/harnesses/codex/skills/deeplake-memory/SKILL.md @@ -33,7 +33,7 @@ Do NOT jump straight to reading raw JSONL files. Always start with index.md and ## Organization Management -Each argument is separate — do NOT quote subcommands together. The auth command is at `$CODEX_PLUGIN_ROOT/bundle/commands/auth-login.js` (or check the session context for the resolved path): +Each argument is separate — do NOT quote subcommands together. The auth command is at `$PLUGIN_ROOT/bundle/commands/auth-login.js` (or check the session context for the resolved path): - `node "/auth-login.js" login` — SSO login - `node "/auth-login.js" whoami` — show current user/org - `node "/auth-login.js" org list` — list organizations diff --git a/scripts/sync-versions.mjs b/scripts/sync-versions.mjs index 26d101ea9..e783dc625 100644 --- a/scripts/sync-versions.mjs +++ b/scripts/sync-versions.mjs @@ -25,6 +25,7 @@ export const SCALAR_TARGETS = [ "harnesses/openclaw/openclaw.plugin.json", "harnesses/openclaw/package.json", "harnesses/codex/package.json", + "harnesses/codex/.codex-plugin/plugin.json", ]; // Marketplace target: has BOTH metadata.version AND every plugins[].version. diff --git a/src/hooks/codex/session-start-setup.ts b/src/hooks/codex/session-start-setup.ts index b5fef14a0..2490b809c 100644 --- a/src/hooks/codex/session-start-setup.ts +++ b/src/hooks/codex/session-start-setup.ts @@ -65,7 +65,7 @@ async function main(): Promise { // can stall for tens of seconds against a slow/unreachable backend, and // autoUpdate has no dependency on table state. Run it first so the user // sees the upgrade notice promptly even when the API is down. - await autoUpdate(creds, { agent: "codex" }); + await autoUpdate(creds, { agent: "codex", bundleDir: __bundleDir }); // Table setup + sync — always sync, only skip placeholder when capture disabled const captureEnabled = process.env.HIVEMIND_CAPTURE !== "false"; diff --git a/src/hooks/shared/autoupdate.ts b/src/hooks/shared/autoupdate.ts index bfb58817e..4ba5ff5d2 100644 --- a/src/hooks/shared/autoupdate.ts +++ b/src/hooks/shared/autoupdate.ts @@ -65,12 +65,58 @@ export type AgentId = "claude" | "codex" | "cursor" | "hermes" | "pi" | "opencla export interface AutoUpdateOpts { agent: AgentId; + /** + * Directory the running bundle lives in (`dirname(fileURLToPath(import.meta.url))` + * at the call site). When it identifies a Codex-managed (plugin directory) + * install, the npm self-update is skipped — Codex owns updates there via + * marketplace snapshots. Omit for channels that have no directory install. + */ + bundleDir?: string; /** Test override: resolved hivemind binary path or null. When provided, skips the PATH walk. */ hivemindBinaryPath?: string | null; /** Test override: replaces the actual subprocess spawn with a fake. Must return the spawned child's pid (or 0). */ spawn?: (cmd: string, args: string[]) => { pid?: number }; } +/** + * Is the running bundle a Codex-managed (plugin directory) install rather + * than our npm `hivemind install`? + * + * Codex extracts marketplace plugins under `/plugins/cache/ + * ///bundle/` (e.g. the `openai-curated` directory). + * Our npm installer instead copies bundles to `/hivemind/bundle/` + * and bakes absolute paths. ONLY the managed channel must skip the npm + * self-update: there, Codex refreshes the plugin from marketplace snapshots + * and an `npm install -g` + `hivemind install` would fight its file + * management. + * + * Deliberately CONSERVATIVE. Returns true only on positive identification of + * the managed cache path. Every other location — our npm path, a dev + * checkout, an unrecognized layout — returns false so the update STILL runs. + * The dangerous failure mode is a false "managed" verdict that silently + * freezes updates for npm users; the safe default is therefore always + * "not managed → update". A missed managed detection only costs a redundant, + * self-serializing update attempt (see the flock in `runUpdate`). + * + * `plugins/cache` is anchored to a `.codex` segment (or an explicit + * `CODEX_HOME`), so an unrelated path that merely contains `plugins/cache` + * does not trip the skip. + */ +export function isCodexManagedInstall( + bundleDir: string | undefined, + env: NodeJS.ProcessEnv = process.env, +): boolean { + if (!bundleDir) return false; + const norm = bundleDir.replace(/\\/g, "/"); + if (norm.includes("/.codex/plugins/cache/")) return true; + const codexHome = env.CODEX_HOME; + if (codexHome && codexHome.trim() !== "") { + const h = codexHome.replace(/\\/g, "/").replace(/\/+$/, ""); + if (norm.includes(`${h}/plugins/cache/`)) return true; + } + return false; +} + /** * Default detached spawn — fire-and-forget. The child process inherits no * stdio, becomes its own session leader (`detached: true`), and is @@ -133,6 +179,10 @@ export async function autoUpdate( ): Promise { const t0 = Date.now(); log(`agent=${opts.agent} entered`); + if (isCodexManagedInstall(opts.bundleDir)) { + log(`agent=${opts.agent} skip: Codex-managed install (${opts.bundleDir}); updates come via marketplace snapshots (${Date.now() - t0}ms)`); + return; + } if (!creds?.token) { log(`agent=${opts.agent} skip: no creds.token (${Date.now() - t0}ms)`); return; } if (creds.autoupdate === false) { log(`agent=${opts.agent} skip: autoupdate=false (${Date.now() - t0}ms)`); return; } diff --git a/tests/claude-code/autoupdate.test.ts b/tests/claude-code/autoupdate.test.ts index 0807644c5..edf509941 100644 --- a/tests/claude-code/autoupdate.test.ts +++ b/tests/claude-code/autoupdate.test.ts @@ -3,7 +3,7 @@ import { mkdtempSync, mkdirSync, rmSync, writeFileSync, existsSync } from "node: import { tmpdir } from "node:os"; import { delimiter, join } from "node:path"; -import { autoUpdate } from "../../src/hooks/shared/autoupdate.js"; +import { autoUpdate, isCodexManagedInstall } from "../../src/hooks/shared/autoupdate.js"; import { setFakeHome, clearFakeHome } from "../shared/fake-home.js"; /** @@ -56,6 +56,100 @@ afterEach(() => { vi.restoreAllMocks(); }); +describe("isCodexManagedInstall — Codex-managed detection", () => { + // These paths are real layouts observed on disk (2026-07-10): + // managed: ~/.codex/plugins/cache//hivemind//bundle + // npm: ~/.codex/hivemind/bundle + // The overriding safety requirement (efenocchi): NEVER report "managed" + // for an npm install — a false managed verdict silently freezes updates. + + // --- MUST be detected as managed (skip npm self-update) --- + it.each([ + ["/home/emanuele/.codex/plugins/cache/openai-curated/hivemind/1.2.3/bundle"], + ["/home/emanuele/.codex/plugins/cache/openai-curated-remote/hivemind/abc123/bundle"], + ["/home/emanuele/.codex/plugins/cache/local/hivemind/local/bundle"], + ["/Users/x/.codex/plugins/cache/openai-curated/hivemind/9.9.9/bundle"], + ])("managed cache path -> true: %s", (p) => { + expect(isCodexManagedInstall(p)).toBe(true); + }); + + it("managed cache path on Windows (backslashes) -> true", () => { + expect( + isCodexManagedInstall("C:\\Users\\x\\.codex\\plugins\\cache\\openai-curated\\hivemind\\1.0.0\\bundle"), + ).toBe(true); + }); + + it("honours a non-default CODEX_HOME for the managed cache", () => { + expect( + isCodexManagedInstall("/opt/codexhome/plugins/cache/openai-curated/hivemind/1/bundle", { + CODEX_HOME: "/opt/codexhome", + } as NodeJS.ProcessEnv), + ).toBe(true); + }); + + // --- MUST NOT be detected as managed (update MUST still run) --- + // This is the false-negative-of-update guard the user is worried about. + it.each([ + ["npm install bundle", "/home/emanuele/.codex/hivemind/bundle"], + ["npm install on macOS", "/Users/x/.codex/hivemind/bundle"], + ["dev checkout", "/home/emanuele/repo/harnesses/codex/bundle"], + ["global npm node_modules", "/usr/lib/node_modules/@deeplake/hivemind/harnesses/codex/bundle"], + ["unknown layout", "/opt/whatever/bundle"], + ["plugins/cache but NOT under .codex", "/home/x/some-tool/plugins/cache/foo/bundle"], + ["home dir literally named plugins cache-ish", "/home/emanuele/.codex/hivemind/bundle/subdir"], + ])("not managed -> false (%s)", (_label, p) => { + expect(isCodexManagedInstall(p)).toBe(false); + }); + + it("undefined bundleDir -> false (no crash, update proceeds)", () => { + expect(isCodexManagedInstall(undefined)).toBe(false); + }); + + it("empty CODEX_HOME does not widen the match", () => { + expect( + isCodexManagedInstall("/home/x/.codex/hivemind/bundle", { CODEX_HOME: "" } as NodeJS.ProcessEnv), + ).toBe(false); + }); +}); + +describe("autoUpdate — Codex-managed guard integration", () => { + const MANAGED = "/home/emanuele/.codex/plugins/cache/openai-curated/hivemind/1.2.3/bundle"; + const NPM = "/home/emanuele/.codex/hivemind/bundle"; + + it("SKIPS the update on a Codex-managed install (even with valid creds + binary)", async () => { + const spawnFn = vi.fn().mockReturnValue({ pid: 1 }); + await autoUpdate(VALID_CREDS, { + agent: "codex", bundleDir: MANAGED, spawn: spawnFn, hivemindBinaryPath: "/u/bin/hivemind", + }); + expect(spawnFn).not.toHaveBeenCalled(); + }); + + it("RUNS the update on an npm install path (the false-skip regression guard)", async () => { + const spawnFn = vi.fn().mockReturnValue({ pid: 1 }); + await autoUpdate(VALID_CREDS, { + agent: "codex", bundleDir: NPM, spawn: spawnFn, hivemindBinaryPath: "/u/bin/hivemind", + }); + expect(spawnFn).toHaveBeenCalledTimes(1); + }); + + it("RUNS the update when bundleDir is omitted (other agents unaffected)", async () => { + const spawnFn = vi.fn().mockReturnValue({ pid: 1 }); + await autoUpdate(VALID_CREDS, { + agent: "claude", spawn: spawnFn, hivemindBinaryPath: "/u/bin/hivemind", + }); + expect(spawnFn).toHaveBeenCalledTimes(1); + }); + + it("RUNS the update on a dev-checkout bundleDir (not a managed cache)", async () => { + const spawnFn = vi.fn().mockReturnValue({ pid: 1 }); + await autoUpdate(VALID_CREDS, { + agent: "codex", bundleDir: "/home/x/repo/harnesses/codex/bundle", + spawn: spawnFn, hivemindBinaryPath: "/u/bin/hivemind", + }); + expect(spawnFn).toHaveBeenCalledTimes(1); + }); +}); + describe("autoUpdate — gating", () => { it("no-op when creds are null", async () => { const spawnFn = vi.fn(); diff --git a/tests/codex/codex-hooks.test.ts b/tests/codex/codex-hooks.test.ts index 94b6a4b2d..c9500c229 100644 --- a/tests/codex/codex-hooks.test.ts +++ b/tests/codex/codex-hooks.test.ts @@ -57,6 +57,17 @@ describe("codex hooks.json", () => { } }); + it("hook commands reference $PLUGIN_ROOT, never the unset $CODEX_PLUGIN_ROOT", () => { + // Codex injects PLUGIN_ROOT / CLAUDE_PLUGIN_ROOT into hook commands, NOT + // CODEX_PLUGIN_ROOT. A directory (Codex-managed) install runs this file + // verbatim, so referencing the unset var would expand to `/bundle/...` + // and every hook would ENOENT silently. Our npm installer bakes absolute + // paths and is unaffected — this guard protects the directory channel. + const raw = readFileSync(join(codexRoot, "hooks", "hooks.json"), "utf-8"); + expect(raw).not.toContain("CODEX_PLUGIN_ROOT"); + expect(raw).toContain("$PLUGIN_ROOT/bundle/"); + }); + it("Stop hook uses a separate stop.js (not capture.js)", () => { const stop = hooks.hooks.Stop; expect(stop[0].hooks[0].command).toContain("stop.js"); @@ -80,10 +91,18 @@ describe("codex plugin.json", () => { expect(plugin.interface.category).toBeTruthy(); }); - it("has arrays for skills, mcpServers, apps", () => { - expect(Array.isArray(plugin.skills)).toBe(true); - expect(Array.isArray(plugin.mcpServers)).toBe(true); - expect(Array.isArray(plugin.apps)).toBe(true); + it("points skills at the bundled skills dir (official schema is a path, not an array)", () => { + // The official Codex plugin schema expects `skills` to be a string path + // to the skills directory (e.g. "./skills/"), NOT an array. An empty + // array — the previous value — exported zero skills to the directory. + expect(plugin.skills).toBe("./skills/"); + }); + + it("omits mcpServers/apps (skills-only plugin — no empty-array pointers)", () => { + // Component pointers are only declared for components we ship. This is a + // skills-only plugin, so mcpServers/apps must be absent, not `[]`. + expect(plugin.mcpServers).toBeUndefined(); + expect(plugin.apps).toBeUndefined(); }); }); diff --git a/tests/codex/codex-session-start-setup-hook.test.ts b/tests/codex/codex-session-start-setup-hook.test.ts index 427777977..d29108cce 100644 --- a/tests/codex/codex-session-start-setup-hook.test.ts +++ b/tests/codex/codex-session-start-setup-hook.test.ts @@ -167,10 +167,16 @@ describe("codex session-start-setup hook — placeholder branching", () => { }); describe("codex session-start-setup hook — centralized autoupdate", () => { - it("invokes autoUpdate exactly once with agent: 'codex'", async () => { + it("invokes autoUpdate exactly once with agent: 'codex' and the running bundleDir", async () => { await runHook(); expect(autoUpdateMock).toHaveBeenCalledTimes(1); - expect(autoUpdateMock.mock.calls[0][1]).toEqual({ agent: "codex" }); + const opts = autoUpdateMock.mock.calls[0][1]; + expect(opts.agent).toBe("codex"); + // bundleDir must be threaded so the Codex-managed guard can detect a + // plugin-directory install and skip the npm self-update there. It is + // dirname(fileURLToPath(import.meta.url)) of session-start-setup, so it + // ends with hooks/codex — assert the suffix to catch wrong-dir regressions. + expect(opts.bundleDir).toMatch(/[\\/]hooks[\\/]codex$/); }); it("passes the loaded creds (so the helper can read creds.autoupdate)", async () => {