npm package: @sethwebster/agent-password-manager
Description: Local-first encrypted password manager for agents and operators
Latest version: 0.4.0

# Agent Password Manager

A local-first password manager for agents and operators.

Now packaged for npm as `@sethwebster/agent-password-manager`, while keeping the same conservative vault, policy, audit, CLI, and MCP behavior intact.

## Why

Because agents and operators still need secrets, but the usual options are full of nonsense:

- shell history leaks
- plaintext `.env` sprawl
- shared browser password stores
- overbuilt SaaS vaults when local control is enough
- no clear audit trail for who revealed what and why

This repo exists to provide a boring, local-first secret layer for agent workflows:

- encrypted at rest
- masked reads by default
- explicit about purpose when revealing secrets
- policy-gated for sensitive reads/exports
- usable from both CLI and MCP integrations

## What it does

- encrypted local vault
- masked reads by default
- explicit-purpose secret reveal
- policy-gated approvals for reveal/export
- append-only local audit log with hash chaining
- MCP tool surface for local integrations

## Install / run

### One-shot via bunx

After publish:

```bash
export APM_MASTER_PASSPHRASE='your-passphrase'
bunx @sethwebster/agent-password-manager doctor
bunx @sethwebster/agent-password-manager init
```

### One-shot via npx

After publish:

```bash
export APM_MASTER_PASSPHRASE='your-passphrase'
npx @sethwebster/agent-password-manager doctor
npx @sethwebster/agent-password-manager init
```

### Install globally

```bash
npm install -g @sethwebster/agent-password-manager
apm doctor
```

### Run from this repo during development

```bash
cd /Users/admin/.openclaw/workspace/repos/agent-password-manager
export APM_MASTER_PASSPHRASE='your-passphrase'
bun run build
node dist/apm.js doctor
```

## CLI entrypoints after publish

Published package bins:

- `agent-password-manager` — package-name-matched bin for `npx`/`bunx @sethwebster/agent-password-manager ...`
- `apm` — short CLI alias after install
- `apm-mcp` — MCP stdio server after install

Examples:

```bash
npx @sethwebster/agent-password-manager doctor
bunx @sethwebster/agent-password-manager get github-dougbot
apm doctor
apm-mcp
```

## MCP server

### Run directly from the published package

```bash
export APM_MASTER_PASSPHRASE='your-passphrase'
npx apm-mcp
```

If you do not want a global install, `npx` can invoke the named bin from the scoped package explicitly:

```bash
npx --package @sethwebster/agent-password-manager apm-mcp
```

Equivalent with Bun:

```bash
bunx --package @sethwebster/agent-password-manager apm-mcp
```

### Run from this repo

```bash
export APM_MASTER_PASSPHRASE='your-passphrase'
bun run build
node dist/mcp-server.js
```

Or through the package script:

```bash
bun run mcp
```

Or through the CLI shim:

```bash
node dist/apm.js mcp-server
```

## Vault path resolution

Default behavior is intentionally repo-local / working-directory-local:

- CLI and MCP both use `./vault` relative to the current working directory.
- If you launch them from the same repo or project directory, they will use the same vault.
- To pin the vault somewhere else explicitly, set `APM_VAULT_DIR` to an absolute or relative path.

Examples:

```bash
# default: use ./vault under the current directory
apm doctor

# explicit override: force a shared vault path
APM_VAULT_DIR=~/.apm/main-vault apm doctor
APM_VAULT_DIR=~/.apm/main-vault apm-mcp
```

## Directory layout

- `vault/vault.enc.json` — encrypted vault
- `vault/audit.log` — append-only audit log with integrity hashes
- `vault/policy.json` — local policy file for reveal/export/read rules
- `vault/sync-state.json` — peer sync config and per-peer sync baselines
- `src/core.ts` — shared vault, policy, and audit logic
- `src/apm.ts` — CLI source
- `src/mcp-server.ts` — stdio MCP server source
- `dist/apm.js` — compiled CLI entrypoint
- `dist/mcp-server.js` — compiled MCP entrypoint
- `spec.md` — product spec
- `threat-model.md` — security assumptions and risks

## CLI usage

### Doctor

```bash
npx @sethwebster/agent-password-manager doctor
```

### Add an entry safely

Preferred order:
1. `--password-stdin`
2. interactive secret prompt on a TTY
3. `--password` only if you knowingly accept shell history / process list exposure

```bash
printf 'super-secret\n' | npx @sethwebster/agent-password-manager add github-dougbot \
  --username 'sethwebster+dougbot@gmail.com' \
  --password-stdin \
  --url 'https://github.com' \
  --tag github --tag doug \
  --ownerType shared
```

### Get an entry

Masked by default:

```bash
npx @sethwebster/agent-password-manager get github-dougbot
```

Reveal only with explicit purpose, and with approval when policy requires it:

```bash
npx @sethwebster/agent-password-manager get github-dougbot --reveal --purpose 'log into GitHub CLI'
npx @sethwebster/agent-password-manager get github-dougbot --reveal --approve --purpose 'break-glass production fix'
```

### Edit an entry

```bash
printf 'rotated-secret\n' | npx @sethwebster/agent-password-manager edit github-dougbot \
  --password-stdin \
  --purpose 'rotate credential'
```

### Search / list

```bash
npx @sethwebster/agent-password-manager list
npx @sethwebster/agent-password-manager list --tag github
npx @sethwebster/agent-password-manager search --query doug
```

### Export / import

Plaintext export is intentionally annoying:

```bash
npx @sethwebster/agent-password-manager export \
  --output ./vault-export.json \
  --allow-plaintext-export \
  --approve \
  --purpose 'offline encrypted backup prep'

npx @sethwebster/agent-password-manager import --input ./vault-export.json --mode merge
```

### Audit log

```bash
npx @sethwebster/agent-password-manager audit --limit 20
npx @sethwebster/agent-password-manager audit --verify
```

### Secure peer sync

There are now two sync modes:

1. **Network start/connect** for an actual live machine-to-machine exchange.
2. **Package push/pull** for removable media / shared-folder workflows.

Both use the same conservative merge rules:

- vault passphrases stay local
- sync uses a separate shared secret/key
- deletes replicate as tombstones
- if both sides changed the same id since the last baseline, sync aborts instead of guessing
- sync actions are written to the audit log

#### Live network sync

Intended UX:

```bash
# source machine
apm sync start --secret 'something'

# safer equivalents
printf 'something\n' | apm sync start --secret-stdin
APM_SYNC_SECRET='something' apm sync start

# client machine
apm sync connect http://source-host:8787 --secret 'something'

# safer equivalents
printf 'something\n' | apm sync connect http://source-host:8787 --secret-stdin
APM_SYNC_SECRET='something' apm sync connect http://source-host:8787
```

What `sync connect` does:

- sends the client's current encrypted sync snapshot to the source server
- the source applies only fast-forwardable changes from the client
- the source returns its current encrypted snapshot
- the client applies only fast-forwardable changes from the source
- if either side detects a same-record/same-baseline divergence, the sync fails with the conflicting ids

The server is intentionally simple: an HTTP process you start when you want it, not a permanent secret daemon.

#### Legacy/local-first package sync

File-package sync still exists for conservative offline workflows:

```bash
# machine A: connect peer B to a package path
printf 'shared-sync-key\n' | npx @sethwebster/agent-password-manager sync connect laptop-b \
  --package ./laptop-b.apmsync \
  --sync-key-stdin

# write my current state for the peer to consume
printf 'shared-sync-key\n' | npx @sethwebster/agent-password-manager sync push laptop-b --sync-key-stdin

# read the peer package and apply only non-conflicting changes
printf 'shared-sync-key\n' | npx @sethwebster/agent-password-manager sync pull laptop-b --sync-key-stdin
```

Safety semantics:

- vault passphrases are never transmitted by either sync mode
- network sync authenticates with a shared secret and encrypts the exchanged sync body with that same secret-derived key material
- package sync encrypts the sync package with the shared sync key
- package contents and network payloads contain vault records/tombstones, not the master vault passphrase
- conflict handling stays conservative on purpose; you reconcile locally, then retry

## MCP tool surface

Minimal and conservative on purpose:

- `apm_doctor`
- `apm_list`
- `apm_search`
- `apm_get`
- `apm_add`
- `apm_edit`
- `apm_policy_show`
- `apm_audit_tail`

## MCP safety model

- `apm_get` returns masked passwords by default.
- Reveals require `reveal=true` and `purpose="..."`.
- If policy says approval is required, also pass `approve=true`.
- `apm_add` and `apm_edit` do **not** take inline plaintext passwords.
- Secret writes must use one of:
  - `passwordEnvVar`: environment variable name containing the password
  - `passwordFile`: local file path containing the password
- MCP calls reuse the same policy checks and audit log as the CLI.
- MCP audit records are tagged with `transport: "mcp"`.

## Example MCP config

Example for a local MCP client that supports stdio servers:

```json
{
  "mcpServers": {
    "agent-password-manager": {
      "command": "npx",
      "args": [
        "--package",
        "@sethwebster/agent-password-manager",
        "apm-mcp"
      ],
      "env": {
        "APM_MASTER_PASSPHRASE": "your-passphrase"
      }
    }
  }
}
```

If your client supports inherited environment instead of literal secrets in config, use that. Better than hardcoding the passphrase in JSON.

## Example MCP tool calls

Masked read:

```json
{
  "name": "apm_get",
  "arguments": {
    "id": "github-dougbot"
  }
}
```

Reveal with explicit purpose:

```json
{
  "name": "apm_get",
  "arguments": {
    "id": "github-dougbot",
    "reveal": true,
    "purpose": "log into GitHub CLI"
  }
}
```

Add using an environment variable instead of inline plaintext:

```json
{
  "name": "apm_add",
  "arguments": {
    "id": "github-dougbot",
    "username": "sethwebster+dougbot@gmail.com",
    "ownerType": "shared",
    "tags": ["github", "doug"],
    "passwordEnvVar": "GITHUB_DOUGBOT_PASSWORD",
    "purpose": "initial vault import"
  }
}
```

## Policy

Default local policy file:

```json
{
  "version": 1,
  "defaults": {
    "requireApprovalForReveal": false,
    "requireApprovalForMaskedRead": false,
    "requireApprovalForExport": true,
    "denyReveal": false,
    "allowedOwnerTypes": ["agent", "operator", "shared", "service"]
  },
  "rules": [
    {
      "name": "prod secrets",
      "tag": "prod",
      "requireApprovalForReveal": true,
      "denyReveal": false,
      "allowedOwnerTypes": ["service", "operator"]
    }
  ]
}
```

## Scripts

- `bun run build` — compile TypeScript entrypoints to `dist/` for Node execution
- `bun run build:clean` — rebuild from scratch
- `bun run prepack` — rebuild before publishing/packing
- `bun run pack:check` — rebuild and inspect the npm tarball contents
- `bun test` — run all Bun-native tests
- `bun run test` — rebuild, then run all tests
- `bun run test:noninteractive` — rebuild, then run CLI tests only
- `bun run test:mcp` — rebuild, then run MCP tests only
- `bun run test:smoke` — rebuild, then run the doctor command under Node
- `bun run mcp` — rebuild and launch the MCP server under Node
- `bun run cli -- <args>` — rebuild and run the CLI with arguments under Node

## Testing

```bash
bun run test
bun run test:mcp
bun run test:noninteractive
bun run pack:check
```

## Tradeoffs

- This stays local-first and simple.
- Approval is local friction, not a remote workflow system.
- The MCP server is intentionally stdio-only and short-lived by default.
- `--password` still exists in the CLI for compatibility, but MCP blocks inline plaintext secret writes on purpose.
- Policy is a local guardrail, not a defense against a hostile machine owner.