Skip to content
12 changes: 8 additions & 4 deletions harnesses/codex/.codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
104 changes: 104 additions & 0 deletions harnesses/codex/SUBMISSION.md
Original file line number Diff line number Diff line change
@@ -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 <topic that was never discussed>?"
- 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).
6 changes: 3 additions & 3 deletions harnesses/codex/commands/login.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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 <email> <role>`
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 <email> <role>`
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.
12 changes: 6 additions & 6 deletions harnesses/codex/hooks/hooks.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
]
Expand All @@ -17,7 +17,7 @@
"hooks": [
{
"type": "command",
"command": "node \"$CODEX_PLUGIN_ROOT/bundle/capture.js\"",
"command": "node \"$PLUGIN_ROOT/bundle/capture.js\"",
"timeout": 10
}
]
Expand All @@ -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
}
]
Expand All @@ -40,7 +40,7 @@
"hooks": [
{
"type": "command",
"command": "node \"$CODEX_PLUGIN_ROOT/bundle/capture.js\"",
"command": "node \"$PLUGIN_ROOT/bundle/capture.js\"",
"timeout": 15
}
]
Expand All @@ -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
}
]
Expand Down
2 changes: 1 addition & 1 deletion harnesses/codex/skills/deeplake-memory/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 "<path>/auth-login.js" login` — SSO login
- `node "<path>/auth-login.js" whoami` — show current user/org
- `node "<path>/auth-login.js" org list` — list organizations
Expand Down
1 change: 1 addition & 0 deletions scripts/sync-versions.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion src/hooks/codex/session-start-setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ async function main(): Promise<void> {
// 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";
Expand Down
50 changes: 50 additions & 0 deletions src/hooks/shared/autoupdate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<codexHome>/plugins/cache/
* <marketplace>/<plugin>/<ref>/bundle/` (e.g. the `openai-curated` directory).
* Our npm installer instead copies bundles to `<codexHome>/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
Expand Down Expand Up @@ -133,6 +179,10 @@ export async function autoUpdate(
): Promise<void> {
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; }

Expand Down
96 changes: 95 additions & 1 deletion tests/claude-code/autoupdate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

/**
Expand Down Expand Up @@ -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/<marketplace>/hivemind/<ref>/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();
Expand Down
Loading