Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -201,10 +201,11 @@
# SNAPSHOT_ENABLED=true
# SNAPSHOT_DIR=~/.agentmemory/snapshots

# Team sharing — when set, memories are scoped to (TEAM_ID, USER_ID) tuples.
# Team sharing — when set, memories are scoped to TEAM_ID and filtered by userId.
# TEAM_MODE=shared
# TEAM_ID=acme
# USER_ID=rohit
# USER_ID=rohit # server-level default userId
# AGENTMEMORY_USER_ID=alice # integration-level userId override

# -----------------------------------------------------------------------------
# 7. Ports
Expand Down
17 changes: 16 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1465,6 +1465,7 @@ In multi-agent setups where several roles share one agentmemory server (architec
```env
TEAM_ID=company
USER_ID=engineering-team
AGENTMEMORY_USER_ID=alice
AGENT_ID=architect
AGENTMEMORY_AGENT_SCOPE=isolated # optional; default "shared"
```
Expand All @@ -1486,6 +1487,19 @@ Session `agent` and `metadata` fields are descriptive attribution, not isolation

When `AGENT_ID` is unset, memory remains unscoped (legacy behavior, no tags, no filters).

### Multi-user team memory (`TEAM_MODE` + `userId`)

Team memory uses `TEAM_ID` for the shared team namespace and an effective user identity for team operations. Set `USER_ID` as the server-level default. Set `AGENTMEMORY_USER_ID` in an integration environment when one integration should act as a specific user. MCP and REST callers can also pass `userId` per request for team share, feed, and profile calls.

```env
TEAM_ID=company
USER_ID=server-default
AGENTMEMORY_USER_ID=alice
TEAM_MODE=private
```

In `shared` mode, `memory_team_share` writes with the effective `userId`, and team feed/profile reads return all shared team items. In `private` mode, team feed/profile reads return only items for the effective `userId`. Request bodies are whitelisted; callers cannot set `sharedBy` directly.

### Data Directory

Native iii-engine state is stored under `~/.agentmemory/data` by default, even when `agentmemory` is launched from inside a project repository. This keeps `data/state_store.db` and `data/stream_store` files out of your working tree. Override the location with `--data-dir <path>` or `AGENTMEMORY_DATA_DIR` when you need a separate store:
Expand Down Expand Up @@ -1765,7 +1779,8 @@ Create `~/.agentmemory/.env`:

# Team
# TEAM_ID=
# USER_ID=
# USER_ID= # server-level default userId
# AGENTMEMORY_USER_ID= # integration-level userId override
# TEAM_MODE=private

# Tool visibility: "core" (8 tools, default) or "all" (61 tools)
Expand Down
208 changes: 208 additions & 0 deletions docs/todos/2026-06-18-issue-511-team-private-userid/plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
# Team Private User Identity Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Enforce team-memory user identity per request and make `TEAM_MODE=private` isolate team shares by effective user.

**Architecture:** Keep team memory on the existing iii-engine function/trigger path. Add a small config resolver for team user identity, pass sanitized `userId` through MCP and REST boundaries, and enforce private-mode filtering inside `src/functions/team.ts` where all team storage reads/writes already converge.

**Tech Stack:** TypeScript ESM, iii-sdk trigger functions, Vitest, pnpm.

---

## Files

- Modify: `src/config.ts`
- Add `resolveUserId(override?: string)` and optionally `resolveTeamId()` if needed for consistent env fallback.
- Change `loadTeamConfig()` to use `AGENTMEMORY_USER_ID || USER_ID`.
- Modify: `src/functions/team.ts`
- Accept optional `userId` in team function payloads.
- In shared mode, use request/env/config user identity for `sharedBy`.
- In private mode, read/write only as the effective configured/request user and ignore impersonating write overrides.
- Modify: `src/mcp/server.ts`
- Validate optional `args.userId`, pass whitelisted payloads, and refresh not-enabled messages.
- Route `agentmemory://team/{id}/profile` through enforced team profile behavior or apply equivalent private-mode filtering.
- Modify: `src/mcp/tools-registry.ts`
- Add optional `userId` to team share/feed schemas.
- Modify: `src/triggers/api.ts`
- Whitelist team share fields and support optional `userId` in body/query for share/feed/profile.
- Modify: `README.md`
- Document `USER_ID`, `AGENTMEMORY_USER_ID`, per-request `userId`, and private/shared semantics.
- Modify: `.env.example`
- Add `AGENTMEMORY_USER_ID` guidance near team envs.
- Create: `test/config-resolve.test.ts`
- Cover user resolver behavior.
- Modify: `test/team.test.ts`
- Add shared-mode per-request identity tests and private-mode negative/isolation tests.
- Modify: `test/mcp-server-surface.test.ts`
- Add MCP payload assertions for optional `userId` and private profile-resource filtering.
- Modify: `test/api-boundary-coverage.test.ts`
- Add REST whitelist and optional `userId` assertions.

## Task 1: Red Tests

- [ ] **Step 1: Add config resolver tests**

Create `test/config-resolve.test.ts`:

```typescript
import { describe, it, expect, beforeEach, afterEach } from "vitest";
import { loadTeamConfig, resolveUserId } from "../src/config.js";

const originalEnv = { ...process.env };

beforeEach(() => {
delete process.env.TEAM_ID;
delete process.env.USER_ID;
delete process.env.AGENTMEMORY_USER_ID;
delete process.env.TEAM_MODE;
});

afterEach(() => {
for (const key of Object.keys(process.env)) {
if (!(key in originalEnv)) delete process.env[key];
}
Object.assign(process.env, originalEnv);
});

describe("resolveUserId", () => {
it("prefers a trimmed per-request override", () => {
process.env.AGENTMEMORY_USER_ID = "env-user";
expect(resolveUserId(" request-user ")).toBe("request-user");
});

it("falls back to AGENTMEMORY_USER_ID and then USER_ID", () => {
process.env.USER_ID = "server-user";
expect(resolveUserId()).toBe("server-user");
process.env.AGENTMEMORY_USER_ID = "integration-user";
expect(resolveUserId()).toBe("integration-user");
});

it("ignores blank overrides and caps long overrides", () => {
process.env.USER_ID = "server-user";
expect(resolveUserId(" ")).toBe("server-user");
expect(resolveUserId("x".repeat(200))).toHaveLength(128);
});
});

describe("loadTeamConfig", () => {
it("uses AGENTMEMORY_USER_ID as an integration override", () => {
process.env.TEAM_ID = "team";
process.env.USER_ID = "server-user";
process.env.AGENTMEMORY_USER_ID = "integration-user";
process.env.TEAM_MODE = "shared";
expect(loadTeamConfig()).toEqual({
teamId: "team",
userId: "integration-user",
mode: "shared",
});
});
});
```

- [ ] **Step 2: Add team function tests**

Append tests to `test/team.test.ts` for shared per-request `userId` and private filtering:

```typescript
describe("Team Functions per-request user identity", () => {
// Use existing mockSdk/mockKV/testMemory helpers.
// Shared mode: team-share with userId "user-2" stores sharedBy "user-2".
// Private mode: seed items from user-1 and user-2; feed/profile for user-1
// must not return user-2 data, and a share with userId "admin" must still
// write as the effective private user.
});
```

Write concrete assertions before implementation:
- shared `team-share` honors `userId`.
- shared `team-feed` still returns all shared-visible items.
- private `team-feed` with effective `user-1` excludes stored `user-2`.
- private `team-profile` lists only `user-1`.
- private `team-share` with `userId: "admin"` writes `sharedBy: "user-1"` when config user is `user-1`.

- [ ] **Step 3: Add boundary tests**

Update MCP/API tests to assert `userId` is passed only when it is a string and that REST `api::team-share` no longer forwards raw body extras.

- [ ] **Step 4: Run red tests**

Run:

```bash
corepack pnpm test -- test/team.test.ts test/config-resolve.test.ts test/mcp-server-surface.test.ts test/api-boundary-coverage.test.ts
```

Expected before implementation: failures for missing `resolveUserId`, missing schema/payload userId, and private-mode leakage.

## Task 2: Implement Minimal Identity Enforcement

- [ ] **Step 1: Add config resolver**

In `src/config.ts`, implement:

```typescript
export function resolveUserId(override?: string): string | undefined {
const raw = override?.trim().slice(0, 128);
if (raw) return raw;
const env = getMergedEnv();
return env["AGENTMEMORY_USER_ID"] || env["USER_ID"] || undefined;
}
```

Change `loadTeamConfig()` userId to `env["AGENTMEMORY_USER_ID"] || env["USER_ID"]`.

- [ ] **Step 2: Enforce in team functions**

In `src/functions/team.ts`, import `resolveUserId`, extend payload types with `userId?: string`, and compute effective identity:
- `team-share`: `const requestUserId = resolveUserId(data.userId) ?? config.userId; const sharedBy = config.mode === "private" ? config.userId : requestUserId;`
- `team-feed`: filter `visibility === "shared"` first; in private mode filter `sharedBy === (resolveUserId(data?.userId) ?? config.userId)`.
- `team-profile`: apply the same private-mode filter before aggregating members/concepts/files.

- [ ] **Step 3: Whitelist MCP payloads**

In `src/mcp/server.ts`, add `userId` only if `typeof args.userId === "string"`. Keep existing `itemId`, `itemType`, and limit validation.

- [ ] **Step 4: Whitelist REST payloads**

In `src/triggers/api.ts`, construct explicit payloads for team endpoints:
- share: `itemId`, `itemType`, optional `project`, optional `sessionId`, optional `userId`.
- feed/profile: parse optional `userId` from query params.

- [ ] **Step 5: Update docs**

Add the team env/request explanation to README and `.env.example` without changing counts or externally consumed endpoint/tool totals.

## Task 3: Verify, Review, Commit, And PR Prep

- [ ] **Step 1: Run targeted green tests**

```bash
corepack pnpm test -- test/team.test.ts test/config-resolve.test.ts test/mcp-server-surface.test.ts test/api-boundary-coverage.test.ts
```

- [ ] **Step 2: Run full checks**

```bash
corepack pnpm test
corepack pnpm run lint
corepack pnpm run build
git diff --check
```

- [ ] **Step 3: Run required security gates**

```bash
semgrep scan --config p/default --error --metrics=off .
gitleaks protect --staged --redact
```

- [ ] **Step 4: Commit and prepare PR**

Stage only task-owned files, inspect the staged diff, commit with:

```bash
git commit -m "feat: enforce team private user identity"
```

Then follow `github-push-prepare` for base capture and PR readiness. Push/PR/merge/issue close remain remote-write approval boundaries.
Loading
Loading