# Development (https://hindclaw.pro/docs/development) ## Setup ```bash git clone git@github.com:mrkhachaturov/hindclaw.git cd hindclaw npm install npm run build # TypeScript -> dist/ npm test # unit tests ``` ## Source Structure ``` src/ ├── index.ts # Plugin entry: init + hook registration ├── client.ts # Stateless Hindsight HTTP client (bankId per-call) ├── types.ts # Full type system ├── config.ts # Config resolver + bank file parser + $include ├── utils.ts # Shared utilities ├── hooks/ │ ├── recall.ts # before_prompt_build (single + multi-bank + reflect) │ ├── retain.ts # agent_end (tags, context, observation_scopes) │ └── session-start.ts # session_start (mental models) ├── embed-manager.ts # Local daemon lifecycle ├── derive-bank-id.ts # Bank ID derivation └── format.ts # Memory formatting ``` ## Key Patterns **Two-level config.** Plugin defaults in `openclaw.json` + per-agent overrides. Shallow merge, agent entry wins. **Stateless client.** Every client method takes `bankId` as first parameter. No instance-level bank state. Enables multi-bank operations. **Server-side vs behavioral.** `snake_case` fields = server-side (managed via Terraform). `camelCase` fields = behavioral (used by hooks at runtime). **Graceful degradation.** All hooks catch errors and log warnings. Never crash the gateway. ## Testing ```bash npm test # unit tests (vitest) npm run test:integration # needs running Hindsight API ``` Integration test environment: | Variable | Default | Description | |----------|---------|-------------| | `HINDSIGHT_API_URL` | `http://localhost:8888` | Hindsight server URL | | `HINDSIGHT_API_TOKEN` | -- | Auth token (optional) | ## Publishing Push a `v*` tag -- GitHub Actions publishes to npm via OIDC trusted publisher. Before tagging: 1. Bump version in `package.json` 2. Add changelog entry in `CHANGELOG.md` with the exact same version (workflow reads it by tag) 3. Commit both files 4. Tag and push ```bash git tag v0.2.0 git push origin main --tags ``` # Examples (https://hindclaw.pro/docs/examples) These pages exist to verify navigation behaviour, not to document anything. The sidebar demonstrates two different shapes, both driven by `meta.json`: - **Separator sections** — written as `"---NAME---"` followed by `"...folder"`. The folder's pages are spread flat under a heading and are always visible. - **Collapsible folders** — the folder listed by name only, e.g. `"nested"`. It renders with a chevron and can be opened and closed. `Examples` itself is a collapsible folder, and `Nested Folder` inside it is a second level that opens independently. # Flat Page (https://hindclaw.pro/docs/examples/flat-page) One level deep: `content/docs/examples/flat-page.mdx` → `/docs/examples/flat-page`. ## A heading Present so the table of contents has something to show. ## Another heading Enough content for the "On this page" rail to render two entries. # Third Level Page (https://hindclaw.pro/docs/examples/nested/deeper/third) Path: `content/docs/examples/nested/deeper/third.mdx` → `/docs/examples/nested/deeper/third`. If this is reachable by opening `Examples` → `Nested Folder` → `Deeper Still`, then arbitrarily deep nesting works. # First Nested Page (https://hindclaw.pro/docs/examples/nested/first) Path: `content/docs/examples/nested/first.mdx` → `/docs/examples/nested/first`. ## Why this page exists If this appears under a folder that opens and closes, two-level nesting works. # Second Nested Page (https://hindclaw.pro/docs/examples/nested/second) This page is listed after `first` in the folder's `meta.json`, so it must appear second regardless of filesystem order. # Installation (https://hindclaw.pro/docs/getting-started/installation) This guide walks you through installing hindclaw and its dependencies, configuring the Hindsight daemon, enabling the plugin in your OpenClaw gateway, and (for multi-user setups) installing the server-side extension. ## Prerequisites Before you begin, make sure you have: - **Python 3.11+** -- Hindsight's daemon (`hindsight-embed`) is a Python application - **uv** -- Python package installer and tool manager ([docs.astral.sh/uv](https://docs.astral.sh/uv/)) - **Node.js 22+** -- hindclaw is a Node.js plugin - **OpenClaw** -- a running gateway with `openclaw` CLI available Check your versions: ```bash python3 --version # 3.11 or higher uv --version # any recent version node --version # 22 or higher openclaw --version # any recent version ``` ## Step 1: Install and configure hindsight-embed `hindsight-embed` is the local Hindsight daemon. It runs the memory engine -- storing facts, building the knowledge graph, and serving recall queries. Install it as a uv tool (globally available, isolated environment): ```bash uv tool install hindsight-embed ``` Then create a named profile for OpenClaw. The profile stores the daemon's port, database path, and LLM settings: ```bash hindsight-embed configure -p openclaw ``` This walks you through an interactive setup. The key settings: | Setting | Description | Typical value | |---------|-------------|---------------| | API port | Port the daemon listens on | `9077` | | Database | Where memories are stored | `~/.hindsight/openclaw/` | | LLM provider | Used for fact extraction and reflection | `anthropic`, `openai`, etc. | The profile name `openclaw` matters -- hindclaw uses it to manage the daemon lifecycle. ## Step 2: Install the hindclaw plugin ```bash openclaw plugins install hindclaw-openclaw ``` This downloads the plugin from npm and registers it in your OpenClaw config. ## Step 3: Configure the plugin Add the hindclaw plugin to your `openclaw.json` (or to a `$include`'d plugins config file). The minimal configuration: ```json5 { "plugins": { "slots": { "memory": "hindclaw" }, "entries": { "hindclaw": { "enabled": true, "config": { "dynamicBankGranularity": ["agent"], "bootstrap": true } } } } } ``` What each field does: - **`slots.memory`** -- tells OpenClaw that hindclaw occupies the memory slot (only one memory plugin can be active) - **`dynamicBankGranularity`** -- controls how bank IDs are derived from context. `["agent"]` means one bank per agent (recommended starting point). Other options include `["agent", "channel"]` or `["agent", "channel", "user"]` for finer granularity. - **`bootstrap`** -- when `true`, the plugin automatically applies bank configuration to Hindsight on first run. ### Optional: external Hindsight server (single-user) If you are running a remote Hindsight server and do not need multi-user access control, add the server URL and a static API token: ```json5 { "plugins": { "entries": { "hindclaw": { "enabled": true, "config": { "dynamicBankGranularity": ["agent"], "bootstrap": true, "hindsightApiUrl": "https://hindsight.your-server.local", "hindsightApiToken": "your-api-token" } } } } } ``` When `hindsightApiUrl` is set, the plugin connects to that server directly and does not start a local daemon. ### Optional: external Hindsight server (multi-user with hindclaw-extension) If the server is running the hindclaw-extension (see Step 4 below), use `jwtSecret` instead of `hindsightApiToken`. The plugin will generate short-lived JWTs for each request: ```json5 { "plugins": { "entries": { "hindclaw": { "enabled": true, "config": { "dynamicBankGranularity": ["agent"], "bootstrap": true, "hindsightApiUrl": "https://hindsight.your-server.local", "jwtSecret": "shared-secret-between-plugin-and-server" } } } } } ``` The `jwtSecret` must match the `HINDSIGHT_API_TENANT_JWT_SECRET` environment variable configured on the server. ## Step 4: Install hindclaw-extension on the server (multi-user only) This step is **required for multi-user setups** where you need access control, per-user permissions, and server-side enrichment. For single-user setups, the extension is optional -- the plugin works without it using a static API token. The hindclaw-extension is a Python package that installs three Hindsight server extensions (tenant authentication, operation validation, and an HTTP management API). Install it on the machine running the Hindsight API server: ```bash pip install hindclaw-extension ``` Then configure the server environment variables to load the extensions: ```bash # Extension classes HINDSIGHT_API_TENANT_EXTENSION=hindclaw_ext.tenant:HindclawTenant HINDSIGHT_API_OPERATION_VALIDATOR_EXTENSION=hindclaw_ext.validator:HindclawValidator HINDSIGHT_API_HTTP_EXTENSION=hindclaw_ext.http:HindclawHttp # Shared secret for JWT validation (must match jwtSecret in plugin config) HINDSIGHT_API_TENANT_JWT_SECRET=shared-secret-between-plugin-and-server # Root user bootstrapped automatically on startup (gets iam:admin + bank:admin policies) HINDCLAW_ROOT_USER=admin@example.com HINDCLAW_ROOT_API_KEY=hc_u_root_ ``` The extensions use the same PostgreSQL database as Hindsight core (`HINDSIGHT_API_DATABASE_URL`). Tables are created automatically on startup via `CREATE TABLE IF NOT EXISTS`. After configuring the env vars, restart the Hindsight API server. You should see startup logs confirming the extensions loaded. Once the extension is running, manage users, groups, and permissions via the [Terraform provider](../guides/terraform). See the [Access Control guide](../guides/access-control) and the [Configuration Reference](../reference/configuration) for details. ## Step 5: Restart the gateway ```bash openclaw restart ``` Watch the logs for confirmation that hindclaw initialized: ```bash openclaw logs ``` You should see lines like: ``` [Hindsight] Plugin initialized [Hindsight] Bootstrap: checking bank my-agent config... [Hindsight] Bootstrap: applying 5 config fields to bank my-agent ``` If you see errors about the daemon not running, verify that `hindsight-embed` is installed and the `openclaw` profile was configured correctly: ```bash hindsight-embed -p openclaw status ``` ## Next steps Your gateway is running with hindclaw enabled. To configure how agents extract and organize memories, define bank configs via the [Terraform provider](../guides/terraform). Next: [Verify memory is working](./verify.md) to confirm retain, recall, and the Hindsight UI. # Verify Memory Works (https://hindclaw.pro/docs/getting-started/verify) After installing hindclaw and configuring your banks (via Terraform), you need to confirm that memory operations are running end to end. This guide covers four checkpoints: gateway logs, a live message test, CLI recall, and the Hindsight UI. ## Checkpoint 1: Gateway logs Start (or restart) the gateway and watch the logs: ```bash openclaw restart && openclaw logs --follow ``` On startup, look for these `[Hindsight]` lines: ``` [Hindsight] Plugin initialized [Hindsight] Bootstrap: checking bank my-agent config... [Hindsight] Bootstrap: applying 5 config fields to bank my-agent [Hindsight] Bootstrap: creating 1 directives for bank my-agent ``` This confirms: - The plugin loaded and connected to the Hindsight daemon (or remote server) - It found the bank configuration - It applied the config to the server (first run only -- subsequent starts will show "already has N overrides -- skipping") If you do not see `[Hindsight]` lines at all, the plugin may not be enabled. Check that your plugins config has `"enabled": true` and `"slots": { "memory": "hindclaw" }`. ## Checkpoint 2: Send a test message Send a message to your agent through Telegram (or whichever channel you use). Something with a concrete fact works best: > "Remember that our next deployment is scheduled for Friday at 3pm." Watch the gateway logs. You should see two phases: **Recall (before the agent responds):** ``` [Hindsight] before_prompt_build - bank: my-agent, channel: telegram/12345 [Hindsight] Auto-recall for bank my-agent, full query: --- Remember that our next deployment is scheduled for Friday at 3pm. --- ``` **Retain (after the agent responds):** ``` [Hindsight] Retaining to bank my-agent, document: session-abc123, chars: 342 [Hindsight] Retained 2 messages to bank my-agent for session session-abc123 ``` The retain step means the conversation was sent to Hindsight for fact extraction. The extraction happens asynchronously on the Hindsight side -- the gateway does not wait for it to complete. ## Checkpoint 3: Query memories with the CLI Wait about 10-15 seconds after sending your test message (Hindsight needs time to extract and index the facts). Then use `hindsight-embed recall` to query directly: ```bash hindsight-embed -p openclaw recall --bank my-agent "deployment schedule" ``` You should see the extracted fact about the Friday deployment in the results. If you get no results, wait a bit longer -- extraction latency depends on the LLM provider and model. You can also verify the bank configuration on the server using Terraform: ```bash terraform plan ``` If the plan shows no changes, your Terraform state and server state are in sync (which is what you want after a successful apply). ## Checkpoint 4: Open the Hindsight UI Hindsight includes a web UI for browsing banks, facts, and the knowledge graph. Launch it with: ```bash hindsight-embed -p openclaw ui ``` This opens the UI in your browser, connected to the correct daemon port from your `openclaw` profile. You can: - Browse your agent's bank and see extracted facts - View the knowledge graph relationships - Check entity label tags on individual facts - Verify that directives were applied Use `hindsight-embed -p openclaw ui`, not `hindsight ui`. The `-p openclaw` flag ensures the UI connects to the right daemon port. Without it, you may connect to a different profile or get a connection error. ## Common issues ### Daemon not running **Symptom:** Gateway logs show connection errors like `ECONNREFUSED` on port 9077. **Fix:** The daemon should start automatically when the gateway initializes. If it does not, start it manually: ```bash hindsight-embed -p openclaw serve ``` Then restart the gateway. Check that `hindsight-embed` is installed and the `openclaw` profile exists: ```bash hindsight-embed -p openclaw status ``` ### Wrong API port **Symptom:** Plugin initializes but all recall/retain calls fail with connection errors. **Fix:** The default daemon port is `9077`. If your profile uses a different port, set it in the plugin config: ```json5 { "config": { "apiPort": 9077 } } ``` The port must match what `hindsight-embed configure -p openclaw` set. You can check your profile's port with: ```bash hindsight-embed -p openclaw status ``` ### Bootstrap did not fire **Symptom:** Gateway starts, but you see "already has N overrides -- skipping" even though the bank is new. **Cause:** Bootstrap checks for any existing overrides on the server. If a previous run partially applied config, or if you manually configured the bank via the API or Terraform, bootstrap considers it already set up. **Fix:** Use Terraform to apply your config explicitly: ```bash terraform plan # see what differs terraform apply # apply the changes ``` ### No retain lines in logs **Symptom:** Recall works, but you never see "Retaining to bank" in the logs. **Possible causes:** - `autoRetain` is set to `false` in your plugin or bank config - The agent's response did not include any retainable roles (check `retainRoles` -- default is `["user", "assistant"]`) - `retainEveryNTurns` is set to a value greater than 1, so retention only fires every Nth turn ### Recall returns empty results **Symptom:** Retain lines appear in logs, but recall always returns nothing. **Possible causes:** - Extraction has not completed yet -- wait 10-15 seconds and try again - The LLM provider for extraction is not configured or the API key is missing - The `recallTypes` filter is too restrictive -- default is `["world", "experience"]` - `autoRecall` is set to `false` ## The full message flow Here is what happens for every message, end to end: ```mermaid sequenceDiagram participant U as User (Telegram) participant G as OpenClaw Gateway participant H as hindclaw plugin participant D as Hindsight daemon U->>G: Send message G->>H: before_prompt_build hook H->>D: Recall query (user's message) D-->>H: Relevant memories H-->>G: Inject memories into prompt G->>G: LLM generates response G->>U: Send response G->>H: agent_end hook H->>D: Retain (conversation transcript) D->>D: Extract facts (async) ``` The recall step adds context before the LLM sees the prompt. The retain step captures facts after the turn completes. Both run automatically on every turn (unless configured otherwise). ## Next steps Memory is working. From here you can: - Add more agents with their own bank configs via the [Terraform provider](/docs/guides/terraform) - Set up named retain strategies to route different topics to different extraction rules - Configure [access control](/docs/guides/access-control) for multi-user deployments - Enable [cross-agent recall](/docs/guides/multi-bank-recall) so agents can read each other's memories # Access Control (https://hindclaw.pro/docs/guides/access-control) hindclaw provides per-principal memory access control enforced server-side through the `hindclaw-extension` -- a set of Hindsight server extensions that authenticate requests, evaluate policies, and enrich operations via `accept_with()`. The same user can get different behavior on different banks, channels, and topics -- different actions allowed, different recall budgets, different retain strategies. All identity, policy, and bank configuration data lives in the Hindsight PostgreSQL database and is managed through the [Terraform provider](https://registry.terraform.io/providers/mrkhachaturov/hindclaw/latest). The plugin itself is a thin adapter: it generates a JWT from the OpenClaw context and sends standard Hindsight API calls. It does not store or evaluate policies. Policy evaluation runs entirely on the Hindsight server via the `hindclaw-extension`. There is no client-side permission logic in the plugin. The plugin sends a JWT with sender context -- the server resolves the principal, evaluates attached policies, and accepts or rejects the operation. ## Mental model The model is inspired by MinIO's IAM system: - **Banks are buckets** -- each bank has its own policy for context-level configuration - **Users are human principals** -- identified via channel mappings (e.g., `telegram:123456` → `alice`) - **Groups are identity containers** -- no permission columns; they carry policies instead - **Access policies are reusable documents** -- define allowed/denied actions on banks, with behavioral parameters - **Service accounts are machine credentials** -- owned by a user, scoped by optional policy - **Bank policies are per-bank configuration** -- default strategy, context overrides, public access rules ``` User: alice Groups: [default, executive] Access policies (direct): alice-overrides Access policies (via default): default-access Access policies (via executive): executive-upgrade Service account: alice-claude Owned by: alice Scoping policy: claude-readonly → intersected with alice's effective policy Bank policy: advisor default_strategy: "advisor-default" strategy_overrides: channel=telegram → "advisor-telegram" topic=99001 → "advisor-project-alpha" public_access: null ``` ## How it works Three Hindsight extensions in one pip package, sharing the same database: ``` Client (OpenClaw plugin) | | Authorization: Bearer v Hindsight API Server | +-- HindclawTenant (TenantExtension) | JWT / API key -> principal identity | +-- HindclawValidator (OperationValidatorExtension) | principal -> policies -> evaluate -> accept_with(enrichment) | +-- HindclawHttp (HttpExtension) | /ext/hindclaw/* CRUD endpoints (used by Terraform provider) | +-- Hindsight Core (retain/recall/reflect) ``` Install the extension package on the Hindsight server: ```bash pip install hindclaw-extension ``` Configure via environment variables: ```bash HINDSIGHT_API_TENANT_EXTENSION=hindclaw_ext.tenant:HindclawTenant HINDSIGHT_API_OPERATION_VALIDATOR_EXTENSION=hindclaw_ext.validator:HindclawValidator HINDSIGHT_API_HTTP_EXTENSION=hindclaw_ext.http:HindclawHttp # Shared secret for JWT validation (must match plugin config) HINDSIGHT_API_TENANT_JWT_SECRET=shared-secret # Root user bootstrap (created on first start) HINDCLAW_ROOT_USER=admin HINDCLAW_ROOT_API_KEY=hc_u_your-root-key ``` ### Authentication The extension accepts two token formats in the `Authorization` header: **JWT** (for plugins acting on behalf of users): ```json { "client_id": "openclaw-prod", "sender": "telegram:123456", "agent": "my-agent", "channel": "telegram", "topic": "99001", "iat": 1711000000, "exp": 1711000300 } ``` | Claim | Description | |---|---| | `client_id` | Identifies the trusted client (for audit logs) | | `sender` | Raw sender ID from the channel, format `provider:id` | | `agent` | Agent (bank) ID from OpenClaw context | | `channel` | Channel type (telegram, slack, etc.) | | `topic` | Topic ID within the channel (optional) | | `iat` / `exp` | Issued-at and expiration. Short-lived (5 min). HMAC-SHA256 signed. | The plugin generates this JWT from the OpenClaw message context and signs it with a shared secret. Plugin config is minimal: ```json5 { "hindsightApiUrl": "https://hindsight.home.local", "jwtSecret": "shared-secret-between-plugin-and-server" } ``` **API keys** (for service accounts and user direct access): - `hc_sa_` prefix -- service account key. Looked up in `hindclaw_service_account_keys`. Inherits parent user's effective policy, optionally narrowed by `scoping_policy_id`. - `hc_u_` prefix -- user API key. Looked up in `hindclaw_api_keys`. Carries the user's full effective policy. API key prefixes allow fast routing in `HindclawTenant` without cross-table scanning. ### Request flow ```mermaid sequenceDiagram participant C as Client (Plugin) participant T as HindclawTenant participant V as HindclawValidator participant H as Hindsight Core C->>T: POST /v1/.../recall
Authorization: Bearer T->>T: Decode JWT, validate signature T->>T: sender telegram:123456 -> DB lookup -> user "alice" T->>T: Set tenant_id = "alice" T->>V: Pass to validator V->>V: Gather alice's policies (direct + groups) V->>V: Evaluate bank:recall on "advisor" V->>V: Merge behavioral params from matching statements alt Permitted V->>H: accept_with(tag_groups=..., retain_tags=...) H->>C: Filtered recall results else Denied V->>C: 403 reject("bank:recall denied") end ``` ## Setting up users and channels Users are identity records that map platform-specific sender IDs to a canonical principal. Manage users and their channel mappings via Terraform. ### Create a user ```hcl resource "hindclaw_user" "alice" { id = "alice" display_name = "Alice" email = "alice@example.com" disable_user = false force_destroy = false } ``` The `disable_user` attribute deactivates the user without deleting their policies, memberships, or service accounts. All service accounts owned by a deactivated user are automatically denied. Re-activating the user restores all access. ### Add channel mappings Channel mappings link platform sender IDs to the canonical user. When a message arrives from Telegram user `123456`, the extension resolves it to user `alice`. ```hcl resource "hindclaw_user_channel" "alice_telegram" { user_id = hindclaw_user.alice.id channel_provider = "telegram" sender_id = "123456" } resource "hindclaw_user_channel" "alice_slack" { user_id = hindclaw_user.alice.id channel_provider = "slack" sender_id = "U123456" } ``` ## Creating groups Groups collect users for shared policy attachment. They are identity-only -- no permission columns. Access is granted by attaching policies to groups, not by setting fields on the group itself. ```hcl resource "hindclaw_group" "default" { id = "default" display_name = "Default" force_destroy = false } resource "hindclaw_group" "executive" { id = "executive" display_name = "Executive" force_destroy = false } ``` ### Add members to groups ```hcl resource "hindclaw_group_membership" "alice_default" { group_id = hindclaw_group.default.id user_id = hindclaw_user.alice.id } resource "hindclaw_group_membership" "alice_executive" { group_id = hindclaw_group.executive.id user_id = hindclaw_user.alice.id } resource "hindclaw_group_membership" "bob_default" { group_id = hindclaw_group.default.id user_id = hindclaw_user.bob.id } ``` ## Access policies Access policies are reusable JSON documents with `version` and `statements[]`. Each statement has an `effect` (allow or deny), a list of `actions`, a list of `banks`, and optional behavioral parameters. ### Policy document shape ```json { "version": "2026-03-24", "statements": [ { "effect": "allow", "actions": ["bank:recall", "bank:reflect"], "banks": ["*"], "recall_budget": "mid", "recall_max_tokens": 1024 }, { "effect": "allow", "actions": ["bank:retain"], "banks": ["*"], "retain_roles": ["user", "assistant"], "retain_tags": ["role:staff"], "retain_every_n_turns": 1 }, { "effect": "deny", "actions": ["bank:recall", "bank:retain"], "banks": ["restricted-bank"] } ] } ``` ### Actions Three core bank actions: | Action | Meaning | |---|---| | `bank:recall` | Retrieve raw memories | | `bank:reflect` | LLM-synthesized answers (independent of recall) | | `bank:retain` | Store new memories | `reflect` is a separate action -- it can be granted without `recall` (e.g., a service account that synthesizes answers but cannot see raw memory entries). `bank:*` matches all bank actions. ### Bank matching - `"*"` matches all banks - Exact match: `"advisor"` matches only the `advisor` bank - Prefix wildcard: `"yoda::*"` matches banks whose ID starts with `yoda::` (e.g., `yoda::group:-100...::42`). Does not match the exact bank `yoda` -- list both `["yoda", "yoda::*"]` to cover the base bank and all derived children. ### Behavioral parameters Optional on any `allow` statement. Define how the granted action behaves for this principal: | Field | Applies to | Merge rule | |---|---|---| | `recall_budget` | recall | Most permissive wins (`high` > `mid` > `low`) | | `recall_max_tokens` | recall | Highest value wins | | `recall_tag_groups` | recall | AND-ed together (all filters must pass) | | `retain_roles` | retain | Union across all sources | | `retain_tags` | retain | Union across all sources | | `retain_every_n_turns` | retain | Lowest value wins (most frequent) | | `retain_strategy` | retain | Most specific principal wins | | `llm_model` | reflect | Most specific statement wins | | `llm_provider` | reflect | Most specific statement wins | | `exclude_providers` | recall | Union (more exclusions) | ### Creating and attaching policies Use `data "hindclaw_policy_document"` to build policy JSON from HCL, then `hindclaw_policy` to store it, then `hindclaw_policy_attachment` to attach it to a user or group. ```hcl # Build policy JSON from HCL blocks data "hindclaw_policy_document" "default_access" { statement { effect = "allow" actions = ["bank:recall", "bank:reflect", "bank:retain"] banks = ["*"] recall_budget = "mid" recall_max_tokens = 1024 retain_roles = ["user", "assistant"] retain_every_n_turns = 1 } } # Store as a named policy resource "hindclaw_policy" "default_access" { id = "default-access" display_name = "Default fleet access" document = data.hindclaw_policy_document.default_access.json } # Attach to the default group resource "hindclaw_policy_attachment" "default_access" { principal_type = "group" principal_id = hindclaw_group.default.id policy_id = hindclaw_policy.default_access.id priority = 0 } ``` Multiple policies can be attached to the same principal. Attach an upgrade policy to a group at higher priority to override single-value fields (like `llm_model`) for that group's members: ```hcl # Executive upgrade: higher recall budget and token limit data "hindclaw_policy_document" "executive_upgrade" { statement { effect = "allow" actions = ["bank:recall"] banks = ["*"] recall_budget = "high" recall_max_tokens = 2048 } } resource "hindclaw_policy" "executive_upgrade" { id = "executive-upgrade" display_name = "Executive recall upgrade" document = data.hindclaw_policy_document.executive_upgrade.json } resource "hindclaw_policy_attachment" "executive_upgrade" { principal_type = "group" principal_id = hindclaw_group.executive.id policy_id = hindclaw_policy.executive_upgrade.id priority = 10 # higher than default-access, wins on single-value fields } ``` ### Built-in policies The server ships these built-in policies. They cannot be modified or deleted: | Policy | Grants | |---|---| | `bank:readwrite` | `bank:recall`, `bank:reflect`, `bank:retain` on `*` | | `bank:readonly` | `bank:recall`, `bank:reflect` on `*` | | `bank:retain-only` | `bank:retain` on `*` | | `bank:admin` | All `bank:*` actions on `*` | | `iam:admin` | All `iam:*` control plane actions | Attach them by ID: `policy_id = "bank:readwrite"`. ## Policy evaluation ### For users ```mermaid graph TD MSG["Incoming request with JWT"] --> AUTH["HindclawTenant: authenticate"] AUTH --> ID["Resolve identity
sender → user via channel mapping"] ID --> GATHER["Gather access policies
direct + from all groups"] GATHER --> EVAL["Evaluate statements
for action + bank"] EVAL --> DENY{"Explicit deny?"} DENY -->|yes| BLOCK["403 reject"] DENY -->|no| ALLOW{"Allow statement
matches?"} ALLOW -->|no| BLOCK ALLOW -->|yes| MERGE["Merge behavioral params
per merge rules"] MERGE --> ENRICH["accept_with() enrichment
tags, tag_groups, strategy"] style MSG fill:#1d4ed8,color:#fff,stroke:#1d4ed8 style AUTH fill:#475569,color:#fff,stroke:#475569 style ID fill:#8b5cf6,color:#fff,stroke:#8b5cf6 style GATHER fill:#8b5cf6,color:#fff,stroke:#8b5cf6 style EVAL fill:#c2410c,color:#fff,stroke:#c2410c style DENY fill:#c2410c,color:#fff,stroke:#c2410c style ALLOW fill:#c2410c,color:#fff,stroke:#c2410c style BLOCK fill:#ef4444,color:#fff,stroke:#ef4444 style MERGE fill:#0f766e,color:#fff,stroke:#0f766e style ENRICH fill:#10b981,color:#fff,stroke:#10b981 ``` Steps: 1. **Resolve identity** -- `HindclawTenant` decodes the JWT, extracts `sender` (e.g., `telegram:123456`), looks up `hindclaw_user_channels` to find the canonical user. If no match, the request is `_unmapped` -- checked against bank public access. 2. **Gather policies** -- Collect all policies directly attached to the user, plus all policies attached to each of the user's groups. 3. **Evaluate statements** -- Find all `allow` and `deny` statements that match the requested action and bank. Deny takes absolute precedence -- any matching deny blocks the request regardless of priority. 4. **Merge behavioral parameters** -- When multiple allow statements match, merge their behavioral parameters using the per-field rules. For single-value fields (`llm_model`, `retain_strategy`): most specific principal wins (user-attached > group-attached), then exact bank > wildcard, then higher `priority` value on the attachment, then lexical policy ID as final tiebreaker. 5. **Enrich** -- The validator calls `accept_with()` to inject resolved `tag_groups`, `retain_tags` (including auto-injected `user:` and `agent:` tags), and `retain_strategy` into the Hindsight operation. ### Precedence for single-value fields | Priority | Source | Example | |---|---|---| | 1 (highest) | User-attached policy, exact bank | User policy with `"banks": ["advisor"]` | | 2 | User-attached policy, wildcard bank | User policy with `"banks": ["*"]` | | 3 | Group-attached policy, exact bank | Group policy with `"banks": ["advisor"]` | | 4 (lowest) | Group-attached policy, wildcard bank | Group policy with `"banks": ["*"]` | Within the same level, higher `priority` on the attachment wins. Ties break on lexical policy ID. The `priority` field only affects single-value fields (`llm_model`, `llm_provider`, `retain_strategy`). Additive fields (tags, tag_groups, exclude_providers) union across all sources. Max/min fields (budget, max_tokens, every_n_turns) use their merge rules regardless of priority. ## Service accounts Service accounts are machine principals -- for MCP clients, Terraform, CI/CD, dashboards. Each is owned by one user and inherits the parent user's full effective policy by default. ```hcl # SA with full inheritance -- used for Terraform resource "hindclaw_service_account" "alice_terraform" { id = "alice-terraform" owner_user_id = hindclaw_user.alice.id display_name = "Alice — Terraform" } resource "hindclaw_service_account_key" "alice_terraform" { service_account_id = hindclaw_service_account.alice_terraform.id description = "Terraform provider key" } output "alice_terraform_key" { value = hindclaw_service_account_key.alice_terraform.api_key sensitive = true } ``` ### Scoping a service account Use `scoping_policy_id` to narrow the SA's access below its parent user's effective policy. The SA gets only permissions that appear in both the parent's effective policy and the scoping policy (intersection). The SA can never exceed the parent's permissions even if the scoping policy is broader. ```hcl # Scoping policy: read-only on specific banks data "hindclaw_policy_document" "claude_readonly" { statement { effect = "allow" actions = ["bank:recall", "bank:reflect"] banks = ["advisor", "ops-agent"] } } resource "hindclaw_policy" "claude_readonly" { id = "claude-readonly" display_name = "Claude MCP read-only" document = data.hindclaw_policy_document.claude_readonly.json } # SA scoped to read-only on two banks only resource "hindclaw_service_account" "alice_claude" { id = "alice-claude" owner_user_id = hindclaw_user.alice.id display_name = "Alice — Claude Code MCP" scoping_policy_id = hindclaw_policy.claude_readonly.id } resource "hindclaw_service_account_key" "alice_claude" { service_account_id = hindclaw_service_account.alice_claude.id description = "Claude Code MCP key" } ``` A service account has at most one scoping policy. This prevents accidental privilege broadening through multiple policy union. ### Intersection rules When a scoping policy is set, behavioral parameters resolve to the more restrictive of the two sources: | Field | Intersection rule | |---|---| | `recall_budget` | Lower budget wins | | `recall_max_tokens` | Lower value wins | | `recall_tag_groups` | Union (AND-ed -- more filtering) | | `retain_roles` | Intersection (only roles in both) | | `retain_every_n_turns` | Higher value wins (less frequent) | | `retain_tags` | Union (more tags) | | `retain_strategy` | Scoping policy wins if set | | `llm_model` | Scoping policy wins if set | | `exclude_providers` | Union (more exclusions) | ## Bank policies Bank policies are per-bank configuration documents. They define the default retain strategy, context-level strategy overrides (per-channel, per-topic), and public access rules for unmapped senders. ```hcl resource "hindclaw_bank_policy" "advisor" { bank_id = "advisor" document = jsonencode({ version = "2026-03-24" default_strategy = "advisor-default" strategy_overrides = [ { scope = "channel", value = "telegram", strategy = "advisor-telegram" }, { scope = "topic", value = "99001", strategy = "advisor-project-alpha" }, ] public_access = { default = null # no public access by default overrides = [ { scope = "provider" value = "web" actions = ["bank:recall", "bank:reflect"] recall_budget = "low" recall_max_tokens = 512 } ] } }) } ``` ### Strategy resolution When the validator needs a retain strategy for a request: ```mermaid graph LR A["Principal policy
retain_strategy on user"] --> B["Principal policy
retain_strategy on group"] B --> C["Bank policy
topic override"] C --> D["Bank policy
channel override"] D --> E["Bank policy
default_strategy"] style A fill:#c026d3,color:#fff,stroke:#c026d3 style B fill:#a855f7,color:#fff,stroke:#a855f7 style C fill:#8b5cf6,color:#fff,stroke:#8b5cf6 style D fill:#6366f1,color:#fff,stroke:#6366f1 style E fill:#64748b,color:#fff,stroke:#64748b ``` 1. Check the principal's effective access policy for `retain_strategy` on this bank -- most specific principal wins (user-attached > group-attached) 2. If no principal-level strategy: check the bank policy for context overrides -- most specific context wins (topic > channel) 3. If neither: use the bank policy's `default_strategy`, or Hindsight's built-in default if none is set ### Public access for unmapped senders When a sender has no channel mapping (`_unmapped`), the validator checks the bank policy's `public_access` section. This is how agents can serve unknown customers or web visitors without creating HindClaw user accounts for them. Resolution order for `_unmapped`: 1. Match context against `public_access.overrides` -- most specific wins (topic > channel > provider) 2. If no match and `public_access.default` is null -- denied 3. If match found -- grant only the listed actions with the listed parameters A non-null `default` grants access to all unmapped senders regardless of context: ```json "public_access": { "default": { "actions": ["bank:recall"], "recall_budget": "low", "recall_max_tokens": 256 } } ``` Banks without a bank policy deny all unmapped senders by default. ## Tag-based filtering `recall_tag_groups` in policy statements uses Hindsight's tag filtering API to control what memories a principal can see during recall. The extension passes the resolved `tag_groups` to `accept_with()`, and Hindsight core applies the filter. Tags on facts come from two sources: 1. **Extension-injected tags** -- `retain_tags` from policy statements (e.g., `role:executive`) plus automatic `user:` and `agent:` tags injected during retain 2. **LLM-extracted tags** -- Entity labels with `tag: true` in the bank config (e.g., `department:sales`, `sensitivity:restricted`) Filter examples in policy statements: ```json5 // See everything (no filter) -- omit recall_tag_groups or set to null { "effect": "allow", "actions": ["bank:recall"], "banks": ["*"] } // Exclude restricted content { "effect": "allow", "actions": ["bank:recall"], "banks": ["*"], "recall_tag_groups": [ { "not": { "tags": ["sensitivity:restricted"], "match": "any_strict" } } ] } // Only see sales department content { "effect": "allow", "actions": ["bank:recall"], "banks": ["*"], "recall_tag_groups": [ { "tags": ["department:sales"], "match": "any" } ] } ``` ## Bootstrap On first install, hindclaw creates the root user automatically from environment variables -- the same approach as MinIO's `MINIO_ROOT_USER` / `MINIO_ROOT_PASSWORD`. ```bash HINDCLAW_ROOT_USER=admin HINDCLAW_ROOT_API_KEY=hc_u_your-root-key-here ``` On startup, the server ensures the root user exists with the built-in `iam:admin` and `bank:admin` policies attached. The root user is a real user -- not a special principal type. It authenticates the same way as any other user, can own service accounts, and is managed through the same API. The root API key acts as break-glass access. ## Debug endpoint The debug endpoint resolves the effective access policy for a given context without executing an operation. Available at `/ext/hindclaw/debug/resolve`: ```json { "tenant_id": "alice", "principal_type": "user", "access": { "allowed": true, "resolved_user_id": "alice", "recall_budget": "high", "recall_max_tokens": 2048, "recall_tag_groups": null, "retain_roles": ["user", "assistant"], "retain_tags": ["role:executive", "user:alice"], "retain_strategy": "project-alpha", "retain_every_n_turns": 1, "llm_model": null, "llm_provider": null, "exclude_providers": [] }, "bank_policy": { "default_strategy": "advisor-default", "strategy_overrides": [ { "scope": "topic", "value": "99001", "strategy": "advisor-project-alpha" } ], "public_access": { "default": null, "overrides": [] } } } ``` The `access` section shows the fully merged effective permissions for the principal on this bank and action. The `bank_policy` section shows the resolved bank configuration. ## Practical example Three users, two agents, different access: | | advisor (strategic) | ops-agent (operations) | |---|---|---| | **alice** (executive) | recall + retain + reflect, high budget, no tag filter | recall + retain, high budget | | **bob** (staff) | recall + reflect only, mid budget | recall + retain, mid budget | | **anonymous** | blocked (no public access) | blocked | ```hcl # 1. Users resource "hindclaw_user" "alice" { id = "alice" display_name = "Alice" } resource "hindclaw_user" "bob" { id = "bob" display_name = "Bob" } # 2. Channel mappings resource "hindclaw_user_channel" "alice_telegram" { user_id = hindclaw_user.alice.id channel_provider = "telegram" sender_id = "111111" } resource "hindclaw_user_channel" "bob_telegram" { user_id = hindclaw_user.bob.id channel_provider = "telegram" sender_id = "222222" } # 3. Groups (identity-only, no permission columns) resource "hindclaw_group" "default" { id = "default" display_name = "Default" } resource "hindclaw_group" "executive" { id = "executive" display_name = "Executive" } # 4. Group memberships resource "hindclaw_group_membership" "alice_default" { group_id = hindclaw_group.default.id; user_id = hindclaw_user.alice.id } resource "hindclaw_group_membership" "alice_executive" { group_id = hindclaw_group.executive.id; user_id = hindclaw_user.alice.id } resource "hindclaw_group_membership" "bob_default" { group_id = hindclaw_group.default.id; user_id = hindclaw_user.bob.id } # 5. Baseline policy: recall + reflect + retain on all banks data "hindclaw_policy_document" "default_access" { statement { effect = "allow" actions = ["bank:recall", "bank:reflect", "bank:retain"] banks = ["*"] recall_budget = "mid" recall_max_tokens = 1024 retain_roles = ["user", "assistant"] } } resource "hindclaw_policy" "default_access" { id = "default-access" display_name = "Default fleet access" document = data.hindclaw_policy_document.default_access.json } resource "hindclaw_policy_attachment" "default_access" { principal_type = "group" principal_id = hindclaw_group.default.id policy_id = hindclaw_policy.default_access.id priority = 0 } # 6. Executive upgrade: higher recall budget data "hindclaw_policy_document" "executive_upgrade" { statement { effect = "allow" actions = ["bank:recall"] banks = ["*"] recall_budget = "high" recall_max_tokens = 2048 } } resource "hindclaw_policy" "executive_upgrade" { id = "executive-upgrade" display_name = "Executive recall upgrade" document = data.hindclaw_policy_document.executive_upgrade.json } resource "hindclaw_policy_attachment" "executive_upgrade" { principal_type = "group" principal_id = hindclaw_group.executive.id policy_id = hindclaw_policy.executive_upgrade.id priority = 10 } # 7. Alice override: deny retain on advisor data "hindclaw_policy_document" "alice_overrides" { statement { effect = "deny" actions = ["bank:retain"] banks = ["advisor"] } } resource "hindclaw_policy" "alice_overrides" { id = "alice-overrides" display_name = "Alice per-bank overrides" document = data.hindclaw_policy_document.alice_overrides.json } resource "hindclaw_policy_attachment" "alice_overrides" { principal_type = "user" principal_id = hindclaw_user.alice.id policy_id = hindclaw_policy.alice_overrides.id } # 8. Bob override: deny retain on advisor (recall + reflect only on advisor) data "hindclaw_policy_document" "bob_overrides" { statement { effect = "deny" actions = ["bank:retain"] banks = ["advisor"] } } resource "hindclaw_policy" "bob_overrides" { id = "bob-overrides" display_name = "Bob per-bank overrides" document = data.hindclaw_policy_document.bob_overrides.json } resource "hindclaw_policy_attachment" "bob_overrides" { principal_type = "user" principal_id = hindclaw_user.bob.id policy_id = hindclaw_policy.bob_overrides.id } ``` Result: - **alice**: default-access (mid budget, all actions) + executive-upgrade (high budget) + alice-overrides (deny retain on advisor). On advisor: recall + reflect only, high budget. On ops-agent: recall + retain + reflect, high budget. - **bob**: default-access (mid budget, all actions) + bob-overrides (deny retain on advisor). On advisor: recall + reflect only, mid budget. On ops-agent: recall + retain + reflect, mid budget. - **anonymous**: no channel mapping → `_unmapped`. No bank policy with public access → denied on both banks. # Cross-Agent Recall (https://hindclaw.pro/docs/guides/multi-bank-recall) An agent can recall memories from multiple banks in parallel. This lets one agent draw on knowledge from across the fleet -- a strategic advisor reading from the operations agent's bank, or a knowledge librarian pulling from every agent in the system. ## How it works When an agent with `recallFrom` configured receives a message, it sends the same recall query to all listed banks simultaneously. Results from each bank are interleaved using round-robin and injected into the prompt as a single combined context. ```mermaid graph LR Q["Agent recall query"] --> B1["bank: my-agent"] Q --> B2["bank: ops-agent"] Q --> B3["bank: kb-agent"] B1 -->|recall: true| R1["results"] B2 -->|recall: true| R2["results"] B3 -->|recall: false| SKIP["skipped"] R1 --> MERGE["Merge + interleave"] R2 --> MERGE MERGE --> INJECT["Inject into prompt"] style Q fill:#1d4ed8,color:#fff,stroke:#1d4ed8 style B1 fill:#8b5cf6,color:#fff,stroke:#8b5cf6 style B2 fill:#8b5cf6,color:#fff,stroke:#8b5cf6 style B3 fill:#8b5cf6,color:#fff,stroke:#8b5cf6 style R1 fill:#10b981,color:#fff,stroke:#10b981 style R2 fill:#10b981,color:#fff,stroke:#10b981 style SKIP fill:#ef4444,color:#fff,stroke:#ef4444 style MERGE fill:#0f766e,color:#fff,stroke:#0f766e style INJECT fill:#0f766e,color:#fff,stroke:#0f766e ``` ## Configuration Add `recallFrom` to the agent's config in the plugin config. Each entry specifies a bank ID and optional per-bank overrides: ```json5 // In openclaw.json plugin config, agents section { "recallFrom": [ { "bankId": "my-agent" }, { "bankId": "ops-agent", "budget": "low", "maxTokens": 512 }, { "bankId": "kb-agent", "budget": "low", "maxTokens": 512 } ], "recallBudget": "high", "recallMaxTokens": 2048 } ``` Note that the agent's own bank should be included in the list if you want it to recall from itself. The `recallFrom` list replaces the default single-bank behavior -- it does not add to it. ### Per-bank overrides Each entry in `recallFrom` accepts: | Field | Type | Description | |---|---|---| | `bankId` | string | Target bank ID (required) | | `budget` | `low` / `mid` / `high` | Override recall effort for this bank | | `maxTokens` | number | Override max tokens for this bank | | `types` | string[] | Override memory types (`world`, `experience`, `observation`) | | `tagGroups` | TagGroup[] | Override tag filter for this bank | Fields not specified fall back to the agent's top-level `recallBudget` and `recallMaxTokens`. ## Permission checks When access control is active, permissions are checked independently for each target bank. The requesting user must have `recall: true` on each bank they read from. If the current user does not have `bank:recall` allowed on bank `kb-agent` (resolved through the [policy evaluation engine](./access-control.md#policy-evaluation)), that bank is silently skipped. The agent still recalls from the remaining permitted banks. This means cross-agent recall respects the same access control rules as single-bank recall. No unauthorized cross-reads. ## Round-robin interleave Results from multiple banks are merged using round-robin interleaving -- one result from each bank in turn, cycling until all results are exhausted. This prevents any single bank from dominating the context window. Given three banks returning 4, 2, and 3 results respectively: ``` Bank A: [A1, A2, A3, A4] Bank B: [B1, B2] Bank C: [C1, C2, C3] Interleaved: [A1, B1, C1, A2, B2, C2, A3, C3, A4] ``` The interleaved results are then formatted and injected into the prompt as a single `` block. ## Budget distribution Each bank in `recallFrom` makes its own recall request with its own budget and token limit. The budgets are not split from a global pool -- each bank gets the full budget specified in its entry (or the agent-level default). A practical pattern is to give the agent's own bank a higher budget and secondary banks a lower one: ```json5 // In openclaw.json plugin config, agents section { "recallFrom": [ { "bankId": "my-agent", "budget": "high", "maxTokens": 2048 }, { "bankId": "ops-agent", "budget": "low", "maxTokens": 256 }, { "bankId": "kb-agent", "budget": "low", "maxTokens": 256 } ] } ``` This prioritizes the agent's own memories while still pulling relevant context from other banks. ## In-flight deduplication Concurrent recall requests for the same bank and query are deduplicated automatically. If two hooks trigger recall for the same bank with the same query text, only one HTTP request is made. This is transparent and requires no configuration. ## Practical example An advisor agent (`my-agent`) that draws from the operations agent (`ops-agent`) and the knowledge base agent (`kb-agent`): The bank's retain mission is managed via Terraform: ```hcl resource "hindclaw_bank_config" "my_agent" { bank_id = "my-agent" config = jsonencode({ retain_mission = "Extract strategic decisions and cross-departmental patterns." }) } ``` And the multi-bank recall is configured in the plugin config: ```json5 // In openclaw.json plugin config, agents section for my-agent { "recallFrom": [ { "bankId": "my-agent" }, { "bankId": "ops-agent", "budget": "low", "maxTokens": 512 }, { "bankId": "kb-agent", "budget": "low", "maxTokens": 512 } ], "recallBudget": "high", "recallMaxTokens": 2048 } ``` When a user asks the advisor "What did we decide about the office expansion?", the agent sends the query to all three banks in parallel, checks the user's permissions on each, interleaves the results, and injects them into the prompt. The response draws on the advisor's strategic context, the operations agent's details, and the knowledge base agent's documented knowledge. # Multi-Server Setup (https://hindclaw.pro/docs/guides/multi-server) hindclaw supports per-agent infrastructure routing -- different agents in the same gateway can connect to different Hindsight servers. This enables scenarios like separating personal and company memory, or mixing a local daemon with remote servers. ## How it works The plugin resolves `hindsightApiUrl` per-agent. The default is the local daemon (started automatically), but any agent can override it to point at a different server. ```mermaid graph LR GW["Gateway"] --> A1["agent-1"] GW --> A2["agent-2"] GW --> A3["agent-3"] GW --> A4["agent-4"] A1 --> HOME["Home server"] A2 --> HOME A3 --> OFFICE["Office server"] A4 --> LOCAL["Local daemon"] style GW fill:#0f766e,color:#fff,stroke:#0f766e style A1 fill:#1d4ed8,color:#fff,stroke:#1d4ed8 style A2 fill:#1d4ed8,color:#fff,stroke:#1d4ed8 style A3 fill:#1d4ed8,color:#fff,stroke:#1d4ed8 style A4 fill:#1d4ed8,color:#fff,stroke:#1d4ed8 style HOME fill:#8b5cf6,color:#fff,stroke:#8b5cf6 style OFFICE fill:#c2410c,color:#fff,stroke:#c2410c style LOCAL fill:#f59e0b,color:#fff,stroke:#f59e0b ``` One gateway, one plugin instance, multiple Hindsight backends. ## Configuration ### Plugin-level default Set a default Hindsight server in the plugin config. All agents use this unless overridden: ```json5 // In openclaw.json (or $include'd config) "hindclaw": { "enabled": true, "config": { "hindsightApiUrl": "https://hindsight.home.local", "hindsightApiToken": "home-token-here" } } ``` ### Per-agent override Override the URL in the plugin config's agent entry to point an agent at a different server: ```json5 // In openclaw.json plugin config, agents section { "agents": { "ops-agent": { "hindsightApiUrl": "https://hindsight.office.local", "hindsightApiToken": "office-token-here" } } } ``` The bank's server-side settings (retain mission, entity labels, etc.) are managed via Terraform on the target server. ### Mixing local daemon and remote servers If some agents should use the local daemon (no URL override) and others should use a remote server, simply omit `hindsightApiUrl` for the local agents: ```json5 // In openclaw.json plugin config { "agents": { "my-agent": {}, // uses local daemon (default) "ops-agent": { "hindsightApiUrl": "https://hindsight.office.local", "hindsightApiToken": "office-token-here" } } } ``` The local daemon starts automatically when any agent uses it. Agents pointing at remote servers do not trigger the daemon. ## Use cases ### Home vs office separation Keep personal memories on a home server and company memories on an office server: ``` Gateway ├── agent-1 (private) --> https://hindsight.home.local ├── agent-2 (private) --> https://hindsight.home.local ├── agent-3 (company) --> https://hindsight.office.local ├── agent-4 (company) --> https://hindsight.office.local └── agent-5 (health) --> local daemon (no URL) ``` This keeps data physically separated. Personal conversations never leave the home network; company data stays on the office server. ### Development and production Use a local daemon for development agents while production agents connect to a stable remote server: ```json5 // In openclaw.json plugin config { "agents": { "dev-agent": {}, // uses local daemon "prod-agent": { "hindsightApiUrl": "https://hindsight.prod.internal", "hindsightApiToken": "prod-token" } } } ``` ### Multi-tenant deployment In a shared gateway serving multiple organizations, each tenant's agents can point to their own Hindsight instance: ```json5 // In openclaw.json plugin config { "agents": { "tenant-a-agent": { "hindsightApiUrl": "https://hindsight.tenant-a.local", "hindsightApiToken": "tenant-a-token" }, "tenant-b-agent": { "hindsightApiUrl": "https://hindsight.tenant-b.local", "hindsightApiToken": "tenant-b-token" } } } ``` ## Cross-agent recall across servers When an agent uses `recallFrom` to recall from another agent's bank, the target bank must be accessible from the same Hindsight server. Cross-server recall (agent on server A recalling from a bank on server B) is not supported -- all banks in a `recallFrom` list must be on the same server as the requesting agent. If you need cross-server knowledge sharing, consider using [session start models](./session-context.md) to pre-load context from different servers at session start. ## Authentication Each server can have its own authentication token. Set `hindsightApiToken` alongside `hindsightApiUrl` in either the plugin config (for the default) or the bank config (for per-agent overrides). Tokens should be stored securely. If your gateway config supports environment variable references, use those instead of hardcoding tokens: ```json5 { "hindsightApiUrl": "https://hindsight.office.local", "hindsightApiToken": "${HINDSIGHT_OFFICE_TOKEN}" } ``` ## Terraform with multi-server When using Terraform to manage bank configs across multiple servers, use separate provider aliases: ```hcl provider "hindclaw" { alias = "home" api_url = "https://hindsight.home.local" api_key = var.home_api_key } provider "hindclaw" { alias = "office" api_url = "https://hindsight.office.local" api_key = var.office_api_key } resource "hindclaw_bank_config" "advisor" { provider = hindclaw.home bank_id = "advisor" config = jsonencode({ retain_mission = "Strategic decisions." }) } resource "hindclaw_bank_config" "ops_agent" { provider = hindclaw.office bank_id = "ops-agent" config = jsonencode({ retain_mission = "Operational decisions." }) } ``` # Reflect on Recall (https://hindclaw.pro/docs/guides/reflect) By default, hindclaw uses **recall** -- it retrieves individual memory facts and injects them as a raw list into the prompt. **Reflect** is an alternative mode where the Hindsight server first reasons over the retrieved memories using an LLM, then returns a synthesized response instead of raw facts. ## Recall vs reflect ```mermaid graph LR Q["User question"] --> MODE{"Reflect enabled?"} MODE -->|yes| REFLECT["Hindsight reflect API"] MODE -->|no| RECALL["Hindsight recall API"] REFLECT --> REASON["LLM reasons over memories"] REASON --> ANSWER["Grounded response"] RECALL --> RAW["Raw memory list"] RAW --> ANSWER style Q fill:#1d4ed8,color:#fff,stroke:#1d4ed8 style REFLECT fill:#8b5cf6,color:#fff,stroke:#8b5cf6 style RECALL fill:#c2410c,color:#fff,stroke:#c2410c style REASON fill:#0f766e,color:#fff,stroke:#0f766e style RAW fill:#f59e0b,color:#fff,stroke:#f59e0b style ANSWER fill:#10b981,color:#fff,stroke:#10b981 ``` With **recall**, the agent receives a list of individual facts like: ``` - User prefers morning meetings - Q3 revenue was $2.4M, up 15% from Q2 - Decision: delay expansion until cash reserves reach $500K - Bob raised concerns about staffing in the Sept 12 review ``` The agent must then synthesize these into a coherent response. With **reflect**, the Hindsight server retrieves the same memories internally, passes them to an LLM with the user's query, and returns a pre-reasoned response. The agent receives something like: > Based on stored knowledge: The Q3 revenue increase to $2.4M suggests the expansion conditions are approaching, but the $500K cash reserve threshold from the September decision has not been confirmed as met. Bob's staffing concerns from the Sept 12 review remain unresolved and could impact expansion timelines. The reflect response is grounded in the same memories, but the reasoning happens server-side before reaching the agent. ## How reflect works 1. The user sends a message 2. hindclaw extracts a recall query from the message (same as normal recall) 3. Instead of calling the recall API, it calls the reflect API on the primary bank 4. The Hindsight server retrieves relevant memories, passes them to its configured LLM along with the query, and generates a reasoned response 5. The response is injected into the agent's prompt in a `` block The reflect mission defined on the bank (`reflect_mission`) guides how the server-side LLM reasons. Set it via the Terraform `hindclaw_bank_config` resource: ```hcl resource "hindclaw_bank_config" "my_agent" { bank_id = "my-agent" config = jsonencode({ reflect_mission = "You are the strategic advisor. Reason critically over stored knowledge. Challenge assumptions, surface contradictions, and connect facts across time periods." }) } ``` ## Configuration Enable reflect in the agent's plugin config (behavioral fields) and set the reflect mission on the bank via Terraform: ```json5 // In openclaw.json plugin config, agents section { "reflectOnRecall": true, "reflectBudget": "high" } ``` ```hcl # Server-side: how the reflect LLM reasons resource "hindclaw_bank_config" "my_agent" { bank_id = "my-agent" config = jsonencode({ reflect_mission = "You are the strategic advisor. Challenge assumptions and surface non-obvious connections." }) } ``` ### Fields | Field | Type | Default | Description | |---|---|---|---| | `reflectOnRecall` | boolean | `false` | Use reflect instead of recall | | `reflectBudget` | `low` / `mid` / `high` | Falls back to `recallBudget` | Effort level for reflect | | `reflectMaxTokens` | number | Falls back to `recallMaxTokens` | Max tokens for reflect response | | `reflect_mission` | string | -- | Server-side prompt guiding the reasoning LLM | `reflectBudget` controls how much effort the server-side LLM puts into reasoning. Higher budgets retrieve more memories and produce more detailed responses but cost more tokens and take longer. ## When to use reflect vs recall **Use reflect when:** - The agent's role requires synthesis (strategic advisor, analyst, planner) - Questions are open-ended ("What should we consider for the expansion?") - You want the agent to surface connections between facts rather than listing them - You have a well-defined `reflect_mission` that matches the agent's persona **Use recall when:** - The agent needs raw facts for precise answers ("When was the last board meeting?") - You want maximum control over how memories are presented to the agent - Token budget is tight (reflect uses additional tokens server-side) - The agent serves multiple roles and needs flexible context interpretation **Performance note:** Reflect adds server-side LLM inference on top of memory retrieval. This means higher latency and token cost compared to recall. The tradeoff is richer, pre-reasoned context. ## Reflect with access control When access control is active, reflect respects the same tag filters as recall. The `recallTagGroups` resolved for the current user are passed to the reflect API, so the server-side LLM only reasons over memories the user is permitted to see. ## Reflect and multi-bank Reflect operates on the **primary bank only**. If the agent also has `recallFrom` configured, reflect is used for the primary bank while the secondary banks still use standard recall. The results are combined. This is intentional -- reflect's reasoning is tied to the bank's `reflect_mission`, which is agent-specific. Applying one agent's reflect mission to another agent's memories would produce incoherent results. # Session Start Context (https://hindclaw.pro/docs/guides/session-context) When a new conversation session begins, hindclaw can load mental models from Hindsight and inject them into the system prompt before the first user message arrives. This eliminates the cold start problem -- the agent starts with relevant context already in place. ## The cold start problem Without session start context, an agent's first response in a new session has no memory context. Recall only triggers when a user message arrives, so the first turn must complete before any memories are retrieved. Session start models fix this by pre-loading context before the conversation begins. ## How it works ```mermaid graph LR START["Session starts"] --> LOAD["Load mental models"] LOAD --> M1["Project context"] LOAD --> M2["User preferences"] M1 --> INJECT["Inject into system prompt"] M2 --> INJECT INJECT --> READY["Agent ready with full context"] READY --> MSG1["First user message"] style START fill:#1d4ed8,color:#fff,stroke:#1d4ed8 style LOAD fill:#8b5cf6,color:#fff,stroke:#8b5cf6 style M1 fill:#c2410c,color:#fff,stroke:#c2410c style M2 fill:#c2410c,color:#fff,stroke:#c2410c style INJECT fill:#0f766e,color:#fff,stroke:#0f766e style READY fill:#10b981,color:#fff,stroke:#10b981 style MSG1 fill:#10b981,color:#fff,stroke:#10b981 ``` When a session starts, the `session_start` hook fires. hindclaw reads the `sessionStartModels` config, loads each model from the Hindsight API in parallel, and assembles them into a `` block that gets prepended to the system prompt. All of this happens before the first user message is processed. ## What are mental models? Mental models are persistent knowledge structures maintained by Hindsight. They are server-side summaries built from retained facts -- Hindsight automatically consolidates related memories into coherent models over time. Each mental model has: - A `modelId` -- the server-side identifier - A `label` -- the heading used when injecting into the prompt - Content -- the synthesized knowledge text Mental models differ from raw recall in that they are pre-computed summaries rather than individual fact retrieval. They provide stable, high-level context without consuming recall budget. ## Configuration Add `sessionStartModels` to the agent's plugin config: ```json5 // In openclaw.json plugin config, agents section for my-agent { "sessionStartModels": [ { "type": "mental_model", "bankId": "my-agent", "modelId": "project-context", "label": "Current Projects" }, { "type": "mental_model", "bankId": "my-agent", "modelId": "user-preferences", "label": "User Preferences" } ] } ``` ### Model types There are two types of session start models: **`mental_model`** -- Fetches a named mental model from a bank. ```json5 { "type": "mental_model", "bankId": "my-agent", "modelId": "project-context", "label": "Current Projects" } ``` **`recall`** -- Runs a recall query at session start, injecting the results as context. ```json5 { "type": "recall", "bankId": "my-agent", "query": "What are the current active projects and priorities?", "label": "Active Projects", "maxTokens": 256 } ``` The `recall` type is useful when you want specific information loaded but do not have a mental model configured for it. It runs with `budget: "low"` to keep session start fast. ### Cross-bank models Models can reference any bank, not just the agent's own. This lets an agent start with context from other agents in the fleet: ```json5 { "sessionStartModels": [ { "type": "mental_model", "bankId": "my-agent", "modelId": "strategic-context", "label": "Strategic Context" }, { "type": "mental_model", "bankId": "ops-agent", "modelId": "ops-status", "label": "Operations Status" } ] } ``` ### Role filtering Models can optionally specify `roles` to limit which user roles receive them. This is a planned feature for role-based context injection. ## Prompt injection format The loaded models are assembled into a single block: ```xml ## Current Projects [content of the project-context mental model] ## User Preferences [content of the user-preferences mental model] ``` This block is prepended to the system prompt. The agent sees it as persistent context before any conversation history. ## Error handling Each model is loaded independently with a 2-second timeout. If one model fails to load (network error, model not found, timeout), the others are still injected. A session start with zero successful model loads silently skips injection -- no error is surfaced to the user. This graceful degradation means session start context is best-effort. The agent works fine without it; it just has more context when it succeeds. ## When to use session start context Session start models are most useful for: - **Stable context** -- information that changes slowly and applies to most conversations (project status, user preferences, team structure) - **Cross-agent awareness** -- loading context from other agents so the current agent starts with broader knowledge - **Reducing first-turn latency** -- pre-loading context avoids the round-trip of a recall query on the first message They are less useful for rapidly changing information (use regular recall for that) or very large contexts (mental models should be concise summaries). # Terraform Provider (https://hindclaw.pro/docs/guides/terraform) The [`mrkhachaturov/hindclaw`](https://registry.terraform.io/providers/mrkhachaturov/hindclaw/latest) Terraform provider manages the full hindclaw stack as code — users, groups, banks, access policies, service accounts, directives, mental models, and bank configs. ## Installation ```hcl terraform { required_providers { hindclaw = { source = "mrkhachaturov/hindclaw" } } } provider "hindclaw" { api_url = "https://hindsight.home.local" api_key = var.hindclaw_api_key } variable "hindclaw_api_key" { type = string sensitive = true } ``` ## Authentication The provider uses a root API key (`hc_u_root_...`) or a service account key. Both are passed via the `api_key` field or the `HINDCLAW_API_KEY` environment variable. ```bash export TF_VAR_hindclaw_api_key="hc_u_root_..." terraform apply ``` The root key is generated on first server start from `HINDCLAW_ROOT_API_KEY`. For CI/CD and automation, create a dedicated service account and use its key instead of the root key. See [Service Accounts](#service-accounts) below. ## File Organization Split Terraform config by concern for readability: ``` terraform/ ├── main.tf # provider config ├── users.tf # users + channel mappings ├── groups.tf # groups (identity-only) + memberships ├── banks.tf # bank profiles ├── bank_configs.tf # bank missions + entity labels ├── bank_labels.tf # locals for shared labels ├── policies.tf # access policies + attachments ├── service_accounts.tf # SAs + keys + scoping policies ├── bank_policies.tf # per-bank strategy config ├── directives.tf # behavioral rules ├── mental_models.tf # pre-computed summaries └── outputs.tf # SA key outputs (sensitive) ``` --- ## Users and Channels Users are canonical human identities. Channel mappings link platform-specific sender IDs (e.g., `telegram:276243527`) to a user. ```hcl # users.tf resource "hindclaw_user" "alice" { id = "alice" display_name = "Alice Smith" email = "alice@company.com" } resource "hindclaw_user" "bob" { id = "bob" display_name = "Bob Jones" email = "bob@company.com" } # Channel mappings — one per platform the user communicates from resource "hindclaw_user_channel" "alice_telegram" { user_id = hindclaw_user.alice.id channel_provider = "telegram" sender_id = "111111111" } resource "hindclaw_user_channel" "alice_claude_code" { user_id = hindclaw_user.alice.id channel_provider = "claude-code" sender_id = "alice@company.com" } resource "hindclaw_user_channel" "bob_telegram" { user_id = hindclaw_user.bob.id channel_provider = "telegram" sender_id = "222222222" } ``` When a request arrives with `sender = "telegram:111111111"`, the server resolves it to user `alice` and evaluates her access policies. ### User fields | Field | Required | Description | |---|---|---| | `id` | yes | Canonical user ID used in policies and references | | `display_name` | yes | Human-readable name | | `email` | no | Email address (informational) | | `disable_user` | no | Deactivate without deleting. All SA keys for this user stop working. | | `force_destroy` | no | Allow destroy even if the user owns service accounts | --- ## Groups Groups are identity-only collections. They have no permission fields — access is controlled entirely through policies attached to the group. ```hcl # groups.tf resource "hindclaw_group" "executives" { id = "executives" display_name = "Executive" } resource "hindclaw_group" "staff" { id = "staff" display_name = "Staff" } resource "hindclaw_group" "default" { id = "_default" display_name = "Anonymous" # No policies attached = no access for unmapped senders by default } # Memberships resource "hindclaw_group_membership" "alice_executives" { group_id = hindclaw_group.executives.id user_id = hindclaw_user.alice.id } resource "hindclaw_group_membership" "bob_staff" { group_id = hindclaw_group.staff.id user_id = hindclaw_user.bob.id } ``` A user can belong to multiple groups. Policies attached to all their groups are merged when resolving effective permissions. ### Group fields | Field | Required | Description | |---|---|---| | `id` | yes | Group ID. Use `_default` for the fallback group for unmapped senders. | | `display_name` | yes | Human-readable name | | `force_destroy` | no | Allow destroy even if the group has members or policy attachments | --- ## Access Policies Access policies define what actions principals (users, groups, service accounts) can perform on which banks, including behavioral parameters like recall budget and retain roles. Policies are reusable — attach the same policy to multiple groups. ### Policy document data source Use `data "hindclaw_policy_document"` to build policy JSON from HCL. Each `statement` block maps to one entry in the `statements` array. ```hcl # policies.tf # Executive policy — full access, high recall budget data "hindclaw_policy_document" "executive" { statement { effect = "allow" actions = ["bank:recall", "bank:reflect", "bank:retain"] banks = ["*"] recall_budget = "high" recall_max_tokens = 2048 retain_roles = ["user", "assistant"] } } # Staff policy — read-only, lower budget, filtered recall data "hindclaw_policy_document" "staff" { statement { effect = "allow" actions = ["bank:recall", "bank:reflect"] banks = ["*"] recall_budget = "low" recall_max_tokens = 512 retain_roles = [] } } # Deny staff access to a specific sensitive bank data "hindclaw_policy_document" "staff_deny_sensitive" { statement { effect = "allow" actions = ["bank:recall", "bank:reflect"] banks = ["*"] recall_budget = "low" } statement { effect = "deny" actions = ["bank:recall", "bank:reflect", "bank:retain"] banks = ["bb9e"] } } # IAM admin policy — manage users, groups, policies data "hindclaw_policy_document" "iam_admin" { statement { effect = "allow" actions = ["iam:*"] banks = ["*"] } } resource "hindclaw_policy" "executive" { id = "executive" display_name = "Executive Access" document = data.hindclaw_policy_document.executive.json } resource "hindclaw_policy" "staff" { id = "staff-readonly" display_name = "Staff Read-Only" document = data.hindclaw_policy_document.staff.json } resource "hindclaw_policy" "iam_admin" { id = "iam-admin" display_name = "IAM Administrator" document = data.hindclaw_policy_document.iam_admin.json } ``` ### Policy attachments Attach policies to users or groups using `hindclaw_policy_attachment`. The `priority` field resolves tie-breaking for single-value behavioral fields when multiple policies apply. ```hcl # Attach executive policy to the executives group (priority 10 upgrades recall budget) resource "hindclaw_policy_attachment" "executive_group" { policy_id = hindclaw_policy.executive.id principal_type = "group" principal_id = hindclaw_group.executives.id priority = 10 } # Attach staff policy to the staff group resource "hindclaw_policy_attachment" "staff_group" { policy_id = hindclaw_policy.staff.id principal_type = "group" principal_id = hindclaw_group.staff.id } # Attach IAM admin policy directly to alice (user-level) resource "hindclaw_policy_attachment" "alice_iam_admin" { policy_id = hindclaw_policy.iam_admin.id principal_type = "user" principal_id = hindclaw_user.alice.id } ``` ### Policy document fields **`statement` block:** | Field | Type | Description | |---|---|---| | `effect` | `allow` / `deny` | Grant or explicitly deny the listed actions. Deny overrides allow at any level. | | `actions` | string[] | Actions to grant or deny. Use `bank:*` for all bank actions, `iam:*` for all control-plane actions. | | `banks` | string[] | Bank IDs the statement applies to. `"*"` matches all banks. `"yoda::*"` matches all banks prefixed with `yoda::`. | | `recall_budget` | string | `low`, `mid`, or `high`. Recall cost tier. Most permissive value wins when merging. | | `recall_max_tokens` | number | Max tokens for recall results. Highest value wins when merging. | | `recall_tag_groups` | string (JSON) | Tag-based recall filter. Multiple filters are AND-ed together across statements. | | `retain_roles` | string[] | Message roles to retain: `user`, `assistant`, `system`, `tool`. Unioned across statements. | | `retain_tags` | string[] | Tags injected on all retained facts. Unioned across statements. | | `retain_every_n_turns` | number | Retain frequency. Lowest value wins (most frequent). | | `retain_strategy` | string | Named Hindsight extraction strategy. Most specific principal wins. | | `llm_model` | string | LLM model override for extraction. Most specific principal wins. | | `llm_provider` | string | LLM provider override. Most specific principal wins. | | `exclude_providers` | string[] | Message providers to skip. Unioned across statements. | ### Built-in policies The server provides built-in policies that cannot be modified: | Policy ID | Grants | |---|---| | `bank:readwrite` | `bank:recall`, `bank:reflect`, `bank:retain` on `*` | | `bank:readonly` | `bank:recall`, `bank:reflect` on `*` | | `bank:retain-only` | `bank:retain` on `*` | | `bank:admin` | All `bank:*` actions on `*` | | `iam:admin` | All `iam:*` control plane actions | Attach built-in policies by ID: ```hcl resource "hindclaw_policy_attachment" "alice_bank_admin" { policy_id = "bank:admin" principal_type = "user" principal_id = hindclaw_user.alice.id } ``` ### Actions reference **Core bank actions:** | Action | Description | |---|---| | `bank:recall` | Retrieve raw memories | | `bank:reflect` | LLM-synthesized answers (independent of recall) | | `bank:retain` | Store new memories | **Extended bank actions:** | Action | Description | |---|---| | `bank:memories:list`, `bank:memories:get`, `bank:memories:delete` | Memory management | | `bank:mental_models:*` | Mental model CRUD and refresh | | `bank:directives:*` | Directive management | | `bank:stats`, `bank:config:update`, `bank:delete` | Bank administration | **Control-plane actions:** | Action | Description | |---|---| | `iam:users:read`, `iam:users:write` | User management | | `iam:groups:read`, `iam:groups:write` | Group management | | `iam:policies:read`, `iam:policies:write` | Policy management | | `iam:attachments:write` | Attach policies to principals | | `iam:service_accounts:read`, `iam:service_accounts:write` | Service account management | | `iam:service_account_keys:write` | API key management | --- ## Service Accounts Service accounts are machine identities for MCP clients, Claude Code, CI/CD, and Terraform runs. Each SA belongs to exactly one user and inherits that user's effective permissions. An optional scoping policy can narrow (but never broaden) the SA's access below its owner's permissions. ```hcl # service_accounts.tf # SA for the Terraform operator (no scoping — full access up to alice's permissions) resource "hindclaw_service_account" "alice_terraform" { id = "alice-terraform" owner_user_id = hindclaw_user.alice.id display_name = "Alice — Terraform" } resource "hindclaw_service_account_key" "alice_terraform" { service_account_id = hindclaw_service_account.alice_terraform.id description = "Terraform CI key" } # SA for Claude Code — scoped to recall + reflect only on two banks data "hindclaw_policy_document" "alice_claude_scope" { statement { effect = "allow" actions = ["bank:recall", "bank:reflect"] banks = ["yoda", "r2d2"] recall_budget = "mid" recall_max_tokens = 1024 } } resource "hindclaw_policy" "alice_claude_scope" { id = "alice-claude-scope" display_name = "Alice Claude Code — Scoped" document = data.hindclaw_policy_document.alice_claude_scope.json } resource "hindclaw_service_account" "alice_claude" { id = "alice-claude" owner_user_id = hindclaw_user.alice.id display_name = "Alice — Claude Code" scoping_policy_id = hindclaw_policy.alice_claude_scope.id } resource "hindclaw_service_account_key" "alice_claude" { service_account_id = hindclaw_service_account.alice_claude.id description = "Claude Code dev key" } ``` The SA's effective access is the intersection of the owner's effective policy and the scoping policy. A scoping policy can only make things more restrictive — it cannot grant permissions the owner doesn't have. ### SA fields | Field | Required | Description | |---|---|---| | `id` | yes | SA identifier | | `owner_user_id` | yes | User who owns this SA | | `display_name` | yes | Human-readable label | | `scoping_policy_id` | no | Optional policy to narrow access. At most one per SA. | ### SA key outputs SA keys are sensitive. Export them from `outputs.tf` for use in downstream systems: ```hcl # outputs.tf output "alice_terraform_key" { value = hindclaw_service_account_key.alice_terraform.api_key sensitive = true } output "alice_claude_key" { value = hindclaw_service_account_key.alice_claude.api_key sensitive = true } ``` Retrieve after apply: ```bash terraform output -raw alice_claude_key ``` --- ## Banks Bank resources manage Hindsight bank profiles, missions, and behavioral tuning. ```hcl # banks.tf resource "hindclaw_bank" "yoda" { bank_id = "yoda" name = "Yoda" mission = "Strategic mentor and advisor" disposition_skepticism = 3 disposition_empathy = 5 } resource "hindclaw_bank" "r2d2" { bank_id = "r2d2" name = "R2-D2" mission = "Technical operations and infrastructure" } ``` ### Bank configs Bank configs define the extraction mission, entity labels, and operational modes: ```hcl # bank_configs.tf resource "hindclaw_bank_config" "yoda" { bank_id = hindclaw_bank.yoda.bank_id config = jsonencode({ retain_mission = "Extract strategic decisions, leadership patterns, and mentorship moments." reflect_mission = "You are Yoda — a wise strategic mentor with full context of past conversations." observations_mission = "Identify recurring themes in decision-making and communication style." entity_labels = local.yoda_labels }) } resource "hindclaw_bank_config" "r2d2" { bank_id = hindclaw_bank.r2d2.bank_id config = jsonencode({ retain_mission = "Extract infrastructure decisions, service configs, and system changes." reflect_mission = "You are R2-D2 — a technical operations droid with full system knowledge." entity_labels = local.common_labels }) } ``` Bank configs use `jsonencode()` for the config map. Entity labels can be defined as Terraform locals for reuse across banks. ### Entity labels (shared locals) ```hcl # bank_labels.tf locals { common_person_label = { key = "person" description = "Known person. Use only these values." type = "multi-values" tag = true values = [ { value = "alice", description = "Alice Smith — CEO" }, { value = "bob", description = "Bob Jones — CTO" }, ] } sensitivity_label = { key = "sensitivity" description = "Content sensitivity level." type = "single-value" tag = true values = [ { value = "restricted", description = "Confidential — executives only" }, { value = "internal", description = "Internal — all staff" }, ] } common_labels = [local.common_person_label] yoda_labels = [local.common_person_label, local.sensitivity_label] } ``` --- ## Bank Policies Bank policies configure context-level strategy routing (per-channel, per-topic overrides) and public access for unmapped senders. This replaces the old `hindclaw_strategy_scope` resource. ```hcl # bank_policies.tf resource "hindclaw_bank_policy" "yoda" { bank_id = hindclaw_bank.yoda.bank_id document = jsonencode({ version = "2026-03-24" default_strategy = "yoda-default" strategy_overrides = [ { scope = "channel", value = "telegram", strategy = "yoda-telegram" }, { scope = "topic", value = "12345", strategy = "yoda-dm-alice" }, ] # No public_access — unknown senders are denied by default }) } resource "hindclaw_bank_policy" "r2d2" { bank_id = hindclaw_bank.r2d2.bank_id document = jsonencode({ version = "2026-03-24" default_strategy = "r2d2-ops" }) } ``` To allow public (unmapped) senders — for example, a customer-facing Telegram group — add a `public_access` section: ```hcl resource "hindclaw_bank_policy" "kb_agent" { bank_id = "kb-agent" document = jsonencode({ version = "2026-03-24" default_strategy = "kb-default" public_access = { overrides = [ { scope = "provider" value = "telegram" actions = ["bank:recall", "bank:reflect"] recall_budget = "low" recall_max_tokens = 256 } ] } }) } ``` ### Strategy resolution order When the server needs a retain strategy for a request: 1. Principal's effective access policy — `retain_strategy` on the matching statement (user-attached > group-attached) 2. Bank policy context overrides — most specific match (topic > channel > default) 3. Hindsight built-in default if nothing matches --- ## Directives Directives are behavioral rules injected into every bank operation: ```hcl # directives.tf resource "hindclaw_directive" "no_pii" { bank_id = hindclaw_bank.yoda.bank_id name = "no_pii" content = "Never store personally identifiable information such as passport numbers, payment card details, or home addresses." } resource "hindclaw_directive" "strategic_focus" { bank_id = hindclaw_bank.yoda.bank_id name = "strategic_focus" content = "Focus on strategic decisions, priorities, and reasoning. Skip tactical implementation details." } ``` --- ## Mental Models Mental models run a `reflect` operation on creation and store the result for instant retrieval on future queries: ```hcl # mental_models.tf resource "hindclaw_mental_model" "leadership_style" { bank_id = hindclaw_bank.yoda.bank_id name = "Leadership Style" source_query = "Summarize this user's leadership approach, decision-making style, and communication preferences." } resource "hindclaw_mental_model" "system_overview" { bank_id = hindclaw_bank.r2d2.bank_id name = "System Overview" source_query = "Summarize the current infrastructure, key services, and their operational status." } ``` --- ## Data Sources ### Banks list List all configured banks: ```hcl data "hindclaw_banks" "all" {} ``` --- ## Practical Example Three users, two agents, role-based access: | | `yoda` (strategic) | `r2d2` (operations) | |---|---|---| | **alice** (executive) | recall + reflect + retain, high budget | recall + reflect + retain, high budget | | **bob** (staff) | recall + reflect only, low budget | recall + reflect only, low budget | | **anonymous** | denied | denied | ```hcl # 1. Users + channels resource "hindclaw_user" "alice" { id = "alice" display_name = "Alice" } resource "hindclaw_user" "bob" { id = "bob" display_name = "Bob" } resource "hindclaw_user_channel" "alice_telegram" { user_id = hindclaw_user.alice.id channel_provider = "telegram" sender_id = "111111111" } resource "hindclaw_user_channel" "bob_telegram" { user_id = hindclaw_user.bob.id channel_provider = "telegram" sender_id = "222222222" } # 2. Groups resource "hindclaw_group" "executives" { id = "executives" display_name = "Executive" } resource "hindclaw_group" "staff" { id = "staff" display_name = "Staff" } resource "hindclaw_group_membership" "alice_executives" { group_id = hindclaw_group.executives.id user_id = hindclaw_user.alice.id } resource "hindclaw_group_membership" "bob_staff" { group_id = hindclaw_group.staff.id user_id = hindclaw_user.bob.id } # 3. Policies data "hindclaw_policy_document" "executive" { statement { effect = "allow" actions = ["bank:recall", "bank:reflect", "bank:retain"] banks = ["*"] recall_budget = "high" recall_max_tokens = 2048 retain_roles = ["user", "assistant"] } } data "hindclaw_policy_document" "staff" { statement { effect = "allow" actions = ["bank:recall", "bank:reflect"] banks = ["*"] recall_budget = "low" recall_max_tokens = 512 } } resource "hindclaw_policy" "executive" { id = "executive" display_name = "Executive Access" document = data.hindclaw_policy_document.executive.json } resource "hindclaw_policy" "staff" { id = "staff-readonly" display_name = "Staff Read-Only" document = data.hindclaw_policy_document.staff.json } # 4. Attach policies to groups resource "hindclaw_policy_attachment" "executive_group" { policy_id = hindclaw_policy.executive.id principal_type = "group" principal_id = hindclaw_group.executives.id } resource "hindclaw_policy_attachment" "staff_group" { policy_id = hindclaw_policy.staff.id principal_type = "group" principal_id = hindclaw_group.staff.id } # 5. Banks + configs resource "hindclaw_bank" "yoda" { bank_id = "yoda" name = "Yoda" mission = "Strategic mentor and advisor" } resource "hindclaw_bank" "r2d2" { bank_id = "r2d2" name = "R2-D2" mission = "Technical operations and infrastructure" } resource "hindclaw_bank_config" "yoda" { bank_id = hindclaw_bank.yoda.bank_id config = jsonencode({ retain_mission = "Extract strategic decisions and leadership patterns." entity_labels = local.yoda_labels }) } resource "hindclaw_bank_config" "r2d2" { bank_id = hindclaw_bank.r2d2.bank_id config = jsonencode({ retain_mission = "Extract infrastructure decisions and system changes." entity_labels = local.common_labels }) } # 6. Service account for Terraform runs resource "hindclaw_service_account" "terraform" { id = "alice-terraform" owner_user_id = hindclaw_user.alice.id display_name = "Terraform" } resource "hindclaw_service_account_key" "terraform" { service_account_id = hindclaw_service_account.terraform.id description = "Terraform apply key" } output "terraform_key" { value = hindclaw_service_account_key.terraform.api_key sensitive = true } ``` ## Full Documentation See the [Terraform Registry docs](https://registry.terraform.io/providers/mrkhachaturov/hindclaw/latest/docs) for the complete resource and data source reference. # HindClaw (https://hindclaw.pro/docs) HindClaw is a management layer for AI agent memory, built on [Hindsight](https://hindsight.vectorize.io) by [Vectorize](https://vectorize.io), the highest-scoring agent memory system on the [LongMemEval benchmark](https://hindsight.vectorize.io/blog/agent-memory-benchmark) (90%+). Hindsight handles the hard part: fact extraction, knowledge graphs, semantic recall, mental models. HindClaw adds what's missing when you go to production: access control, strategy routing, service accounts, and a control plane to orchestrate it all. Everything is managed as code through a [Terraform provider](https://registry.terraform.io/providers/mrkhachaturov/hindclaw/latest) or REST API. ## Why this exists AI agents already have memory. OpenClaw writes markdown files to disk. Claude Code keeps its own memory. Most frameworks have something. Hindsight goes further and gives you a proper memory engine with automated extraction, retrieval across four parallel strategies, and reflection that reasons over what it knows. But here's what I ran into when I had 11 agents and started thinking about deploying this at the office: who decides what each agent can remember? Which agents can read from which memory banks? How do you give a strategic advisor access to every department's knowledge while keeping HR data away from the marketing bot? Think about Confluence. Nobody gives each user their own isolated space and copies documents around. You organize by domain: engineering, finance, HR, strategy. Then you control who reads, who writes, what they see. One source of truth per domain. Memory banks should work the same way. A finance bank holds financial knowledge. An HR bank holds HR data. A strategy bank collects cross-departmental insights. Agents that need finance data query the finance bank. The ones that shouldn't, can't. You manage who has access to what through policies, as code or through an API. Hindsight gives you the banks. HindClaw gives you the rules. ## What you get Say you're running a company with a few AI agents, each with a different job: | Agent | Role | Memory bank | |-------|------|-------------| | Strategic advisor | Cross-departmental analysis, priorities | `strategy` | | Finance analyst | Revenue, margins, budgets | `finance` | | HR assistant | Team health, attendance, capacity | `hr` | | Project manager | Tasks, deadlines, OKRs | `projects` | Each agent owns a bank, organized by domain. Now the questions come up: can the strategic advisor read from all four banks? Can the HR assistant write to the finance bank? Can an intern only recall from their department? Can a CI bot have read-only access? HindClaw answers these with policies. A policy is a JSON document with allow/deny statements. Here's one: ```json { "version": "2026-03-24", "statements": [ { "effect": "allow", "actions": ["bank:recall", "bank:reflect"], "banks": ["strategy", "finance"], "recall_budget": "high", "recall_max_tokens": 2048 }, { "effect": "deny", "actions": ["bank:retain"], "banks": ["hr"] } ] } ``` This policy lets its holder read from `strategy` and `finance` with a high recall budget, and blocks writing to `hr`. Attach policies to users or groups, set priority to break ties. When multiple allow statements match, the highest-priority one sets the behavioral parameters (budget, tokens, roles). Deny always wins regardless of priority. If you've used MinIO IAM policies, this will feel familiar. Here's what happens when the strategic advisor asks "how's the company doing?": ```mermaid sequenceDiagram participant U as User participant A as Strategic Advisor participant HC as HindClaw Extension participant S as strategy bank participant F as finance bank participant H as hr bank participant P as projects bank U->>A: "How's the company doing?" A->>HC: recall (strategy, finance, hr, projects) HC->>HC: Check policies: advisor has recall on all 4 banks HC->>S: recall HC->>F: recall HC->>H: recall HC->>P: recall S-->>HC: strategic priorities F-->>HC: revenue down 12%, margin stable H-->>HC: two open positions, one burnout risk P-->>HC: Q2 OKR at 60% HC-->>A: merged context injected A-->>U: cross-departmental analysis A->>HC: retain to strategy bank HC->>HC: Check policy: advisor can retain to strategy HC->>S: retain (cross-departmental summary) ``` The advisor reads from four banks but writes only to its own. HindClaw checks policies on every operation. The finance analyst's latest observations feed into the advisor's session at startup through mental models, so it already knows the numbers before anyone asks. The agents never exchange messages. Knowledge flows through the banks, controlled by policies. For machines that need memory access, there are service accounts. A Terraform provider, a Claude Code MCP server, a customer-facing chat bot: each gets its own API key scoped to the banks and actions it needs. A service account inherits its parent user's access and can be narrowed with a scoping policy. ```mermaid graph TD SA1["Claude Code MCP
service account"] -->|"recall: strategy, finance
retain: denied"| HC["HindClaw Policy Engine"] SA2["Web Chat Bot
service account"] -->|"recall: projects
retain: projects"| HC SA3["Terraform Provider
service account"] -->|"iam:admin
full control plane"| HC HC --> Banks["Memory Banks"] ``` Each bank also has its own strategy configuration. Telegram conversations can use one extraction strategy while topic threads use another. You can open public access for unmapped senders, like customers in a web chat, without creating HindClaw accounts for them. The control plane is API-first. Call the REST API directly, or use the [Terraform provider](https://registry.terraform.io/providers/mrkhachaturov/hindclaw/latest) for a declarative approach. Like Kubernetes gives you a declarative way to manage containers, HindClaw gives you a declarative way to manage memory: define the desired state, apply it, let the system converge. A management UI is in the works. ## Architecture ``` Integrations Hindsight Server ┌─────────────┐ ┌──────────────────────────────┐ │ OpenClaw │──── JWT ──────>│ HindClaw Extension │ │ Plugin │ │ Tenant (identity) │ ├─────────────┤ │ Validator (policy engine) │ │ Claude Code │── SA Key ─────>│ Http (admin API) │ │ MCP │ │ | │ ├─────────────┤ │ v │ │ Web Chat │── SA Key ─────>│ Hindsight Core │ │ (planned) │ │ retain / recall / reflect │ ├─────────────┤ │ knowledge graph, facts, │ │ Any tool │── JWT/Key ────>│ mental models, embeddings │ └─────────────┘ └──────────────────────────────┘ | ┌──────────┴──────────┐ │ Terraform Provider │ │ users, groups, │ │ policies, SAs, │ │ bank config, ... │ └──────────────────────┘ ``` Integrations are thin clients. Plugins generate short-lived JWTs signed with a shared HMAC secret (configured on both the plugin and server, no external identity provider needed). Service accounts use static API keys (`hc_sa_` prefix). The extension sits server-side, intercepts every request, checks policies, resolves strategies, injects tags. If the extension is unreachable or crashes, Hindsight rejects requests (fail-closed, not fail-open). The Terraform provider manages the control plane as code. Two integrations exist today: an OpenClaw plugin and a Claude Code MCP server. Anything that can make HTTP calls with a bearer token can connect. ## Built on Hindsight [Hindsight](https://hindsight.vectorize.io) is an open source memory engine by [Vectorize](https://vectorize.io). If you're not familiar with it, here are the three core operations: ### Retain: conversations become structured knowledge When an agent finishes a conversation turn, the transcript is sent to Hindsight. An LLM extracts discrete facts, entities, and relationships from it automatically. You don't tell it what to remember. ```mermaid graph LR C["Conversation transcript"] --> R["Retain"] R --> F["Facts extracted"] R --> E["Entities identified"] R --> L["Relationships mapped"] F --> B["Memory Bank"] E --> B L --> B ``` A conversation about a supplier change might produce: "Detail margin dropped to 27% after switching primer supplier in January" (fact), "AcmePrimer" (entity), "AcmePrimer supplies Detail department" (relationship). All of this happens in the background after each turn. ### Recall: find relevant memories before each response Before an agent responds, Hindsight searches the bank for relevant memories. It runs four retrieval strategies in parallel and merges the results: ```mermaid graph LR Q["Query from conversation"] --> S["Semantic search"] Q --> K["BM25 keyword search"] Q --> G["Knowledge graph traversal"] Q --> T["Temporal proximity"] S --> M["Merge + rerank"] K --> M G --> M T --> M M --> I["Inject into agent context"] ``` The agent doesn't call a search tool. Memories are injected into context before the agent sees the user's message. ### Reflect: reason over memories, not just retrieve them Recall returns raw facts. Reflect goes further: Hindsight reasons over what it knows and produces a synthesized answer. It checks mental models first (pre-computed summaries), then observations (patterns across facts), then raw facts. ```mermaid graph TD Q["Reflect query"] --> MM{"Mental model exists?"} MM -->|yes, fresh| A["Return pre-computed summary"] MM -->|no or stale| O["Check observations"] O --> F["Search raw facts"] F --> S["Synthesize answer"] S --> A2["Return reasoned response"] ``` When you ask "what's the financial situation this quarter?", reflect doesn't return 50 individual facts. It returns a coherent analysis built from everything the bank knows. ### Mental models Mental models are Hindsight's way of maintaining an up-to-date understanding of a topic. You define a model with a query ("What are the current strategic priorities?") and Hindsight keeps the answer fresh. Every time new facts arrive and consolidate, the mental model re-runs its query and updates. Agents can load mental models at session start via the plugin's `session_start` hook. The plugin makes API calls to fetch configured mental models before the first user message arrives, so the agent wakes up with current context. The strategic advisor loads its "company priorities" model, the finance analyst loads "quarterly numbers", and they're ready from the first message. If a mental model fetch fails, the agent starts without it (graceful degradation, not hard failure). ### Banks Each bank is an isolated memory store with its own extraction mission, entity labels, dispositions, and directives. The extraction mission tells Hindsight what to focus on when retaining. Entity labels define how facts get classified. Dispositions control how skeptical or empathetic the reflect engine is. Banks don't share data with each other unless an agent has cross-bank recall access through HindClaw. --- HindClaw doesn't touch any of this. The memory engine is Hindsight's territory. HindClaw adds the layer above: who can access which banks, what policies apply, what strategy to use, and how to manage it all. Skip the self-hosting and use [Hindsight Cloud](https://ui.hindsight.vectorize.io/signup) from [Vectorize](https://vectorize.io), the team behind Hindsight. ## Next steps - [Installation](./getting-started/installation) -- set up the plugin, the server extension, or both - [Access Control](./guides/access-control) -- policies, service accounts, bank policies - [Terraform Provider](./guides/terraform) -- manage everything as code - [Configuration Reference](./reference/configuration) -- plugin and JWT configuration # Docs for AI Agents (https://hindclaw.pro/docs/llm) Every page on this site is available in a form an AI assistant can read directly. No scraping, no HTML parsing — point your tool at one of the URLs below. ## AI accessible documentation Structured index of every documentation page with its description. Start here for an overview. The complete documentation in a single file. Use it when you want full context in one request. Add `.mdx` to any page URL for its raw markdown, e.g. `/docs/guides/access-control.mdx`. The same markdown is behind the **Copy page** and **View as Markdown** actions in the right-hand column of every docs page. ## Context files Add HindClaw context to your project's `CLAUDE.md`, `AGENTS.md` or `.cursorrules`: ```md ## HindClaw This project uses HindClaw for access control over Hindsight agent memory. Documentation: https://hindclaw.pro/llms-full.txt Key concepts: - Policies are JSON documents with allow/deny statements; deny always wins - Permissions attach to users or groups and are evaluated per endpoint - Service accounts carry `hc_sa_` API keys scoped to banks and actions - The server extension enforces every recall, retain and reflect call - Users, groups, policies and banks are managed as code via the Terraform provider ``` ## Reading a single page Any documentation URL serves markdown when you append `.mdx`: ```bash curl https://hindclaw.pro/docs/guides/access-control.mdx ``` That is the whole page: frontmatter, prose and code blocks, exactly as written. Agents that need to answer questions about one topic should fetch that page rather than the full corpus. ## Which file to use | Your case | Use | |---|---| | Agent needs to know what exists before choosing | `/llms.txt` | | One-shot answer with no follow-up requests | `/llms-full.txt` | | Agent already knows the page it needs | `/docs/.mdx` | | Building a search index over the docs | `/search-index.json` | # Permission Model (https://hindclaw.pro/docs/reference/access-control) Policy-based model: policies are attached to users, groups, and service accounts. Deny takes precedence over allow. All access control data lives in the Hindsight PostgreSQL database and is managed through the [Terraform provider](https://registry.terraform.io/providers/mrkhachaturov/hindclaw). There are no config files for access control. See the [Access Control Guide](./guides/access-control) for a full walkthrough. ## Permission Model ``` Policies └── attached to Users, Groups, or Service Accounts └── evaluated per endpoint (IAM actions) └── deny takes precedence over allow ``` Each policy contains one or more statements. Each statement targets specific IAM actions (e.g., `bank:retain`, `bank:recall`, `iam:admin`) with an effect of `allow` or `deny`. When a user makes a request, all policies attached to that user (directly and via group memberships) are collected and evaluated. If any policy denies the action, access is denied regardless of other allows. ## Managing with Terraform The `mrkhachaturov/hindclaw` provider manages users, groups, memberships, policies, policy attachments, and service accounts as standard Terraform resources. ### Provider configuration ```hcl terraform { required_providers { hindclaw = { source = "hindclaw.pro/mrkhachaturov/hindclaw" } } } provider "hindclaw" { api_url = "https://hindsight.home.local" # or HINDCLAW_API_URL api_key = var.hindclaw_api_key # or HINDCLAW_API_KEY } ``` ### Users and channel mappings ```hcl resource "hindclaw_user" "alice" { id = "alice" display_name = "Alice" email = "alice@example.com" } resource "hindclaw_user_channel" "alice_telegram" { user_id = hindclaw_user.alice.id channel_provider = "telegram" sender_id = "123456" } resource "hindclaw_user_channel" "alice_slack" { user_id = hindclaw_user.alice.id channel_provider = "slack" sender_id = "U123456" } ``` ### Groups (identity only) Groups are identity constructs. They have no permission fields. Permissions are granted by attaching policies to the group. ```hcl resource "hindclaw_group" "executives" { id = "executives" display_name = "Executive" } resource "hindclaw_group" "staff" { id = "staff" display_name = "Staff" } ``` ### Memberships ```hcl resource "hindclaw_group_membership" "alice_executives" { group_id = hindclaw_group.executives.id user_id = hindclaw_user.alice.id } resource "hindclaw_group_membership" "bob_staff" { group_id = hindclaw_group.staff.id user_id = hindclaw_user.bob.id } ``` ### Policies and attachments ```hcl # Policy granting full recall and retain on all banks resource "hindclaw_policy" "exec_memory" { id = "exec-memory" display_name = "Executive Memory Access" statement { effect = "allow" actions = ["bank:recall", "bank:retain"] resources = ["*"] } } # Policy granting recall-only on all banks, deny retain resource "hindclaw_policy" "staff_readonly" { id = "staff-readonly" display_name = "Staff Read-Only Memory" statement { effect = "allow" actions = ["bank:recall"] resources = ["*"] } statement { effect = "deny" actions = ["bank:retain"] resources = ["*"] } } # Attach exec policy to executives group resource "hindclaw_policy_attachment" "exec_memory_attach" { policy_id = hindclaw_policy.exec_memory.id target_type = "group" target_id = hindclaw_group.executives.id } # Attach staff policy to staff group resource "hindclaw_policy_attachment" "staff_readonly_attach" { policy_id = hindclaw_policy.staff_readonly.id target_type = "group" target_id = hindclaw_group.staff.id } ``` ### Service accounts Service accounts are non-human principals used by automation, Terraform itself, or other systems. They authenticate via API key and have policies attached the same way as users. ```hcl resource "hindclaw_service_account" "terraform_admin" { id = "terraform-admin" display_name = "Terraform Admin" } resource "hindclaw_policy_attachment" "terraform_admin_iam" { policy_id = hindclaw_policy.iam_admin.id target_type = "service_account" target_id = hindclaw_service_account.terraform_admin.id } ``` ## Tag-Based Filtering `recall_tag_groups` on bank policies uses Hindsight's `tag_groups` API for boolean filtering: ```json5 // See everything (no filter) "recall_tag_groups": null // Exclude restricted content "recall_tag_groups": [ {"not": {"tags": ["sensitivity:restricted"], "match": "any_strict"}} ] // Include only department content (plus untagged) "recall_tag_groups": [ {"tags": ["department:sales"], "match": "any"} ] ``` Tags come from two sources: 1. **Extension-injected** -- `retain_tags` from bank policies plus automatic `user:` tags, injected via `accept_with()` during retain 2. **LLM-extracted** -- entity labels with `tag: true` in bank config Both merge into a single `tags` array on each fact. # Plugin Configuration (https://hindclaw.pro/docs/reference/configuration) HindClaw uses a two-level config system. Plugin-level defaults are set in `openclaw.json` (or its modular `$include` files). Per-agent behavioral overrides are set in the `agents` map within the plugin config. **Resolution order:** plugin defaults -> per-agent overrides (shallow merge, agent entry wins). Server-side bank configuration (retain missions, entity labels, dispositions, directives, strategies) is managed via the [Terraform provider](../guides/terraform). User/group management is also done via Terraform, not config files. ## Config Architecture ``` openclaw.json (plugin config) Terraform (server-side) Infrastructure Bank config (per-agent) hindsightApiUrl, jwtSecret retain_mission, entity_labels dispositions, directives Daemon (global only) retain_strategies apiPort, embedVersion embedPackagePath, daemonIdleTimeout Access control users, groups, policies Defaults (overridable per-agent) policy attachments, service accounts bank policies (strategy overrides) llmProvider, llmModel autoRecall, autoRetain, ... recallBudget, retainEveryNTurns Per-agent behavioral overrides agents: { id: { recallBudget, recallMaxTokens, recallFrom, sessionStartModels, reflectOnRecall, hindsightApiUrl, ... } } bootstrap: true|false ``` ## Plugin Config Options Set these in the `config` block of the HindClaw plugin entry inside `openclaw.json`. ### Infrastructure | Option | Type | Default | Per-agent | Description | |--------|------|---------|-----------|-------------| | `hindsightApiUrl` | `string` | -- | yes | Hindsight API base URL. When set, connects to a remote server instead of the local daemon. | | `hindsightApiToken` | `string` | -- | yes | Bearer token for API authentication. Used for single-user setups without hindclaw-extension. Mutually exclusive with `jwtSecret`. | | `jwtSecret` | `string` | -- | no | Shared secret (HMAC-SHA256) for signing JWTs sent to a server running hindclaw-extension. The same secret must be configured on the server via `HINDSIGHT_API_TENANT_JWT_SECRET`. When set, the plugin generates short-lived JWTs containing the sender, agent, channel, and topic for each request instead of using a static API token. | ### Daemon (Global Only) These options control the embedded `hindsight-embed` daemon. They cannot be overridden per-agent. | Option | Type | Default | Per-agent | Description | |--------|------|---------|-----------|-------------| | `apiPort` | `number` | `9077` | no | Port the local daemon listens on. | | `embedVersion` | `string` | `"latest"` | no | Version of `hindsight-embed` to use. | | `embedPackagePath` | `string` | -- | no | Path to a local `hindsight-embed` installation. For development only. | | `daemonIdleTimeout` | `number` | `0` | no | Seconds of inactivity before the daemon shuts down. `0` means never. | ### Bank ID Routing | Option | Type | Default | Per-agent | Description | |--------|------|---------|-----------|-------------| | `dynamicBankId` | `boolean` | `true` | yes | Derive the bank ID dynamically from context fields instead of using a static ID. | | `dynamicBankGranularity` | `string[]` | `["agent","channel","user"]` | yes | Which context fields to include in the derived bank ID. Valid values: `agent`, `provider`, `channel`, `user`. | | `bankIdPrefix` | `string` | -- | yes | Static prefix prepended to derived bank IDs. | ### Behavioral Defaults These set the plugin-wide defaults. Any of them can be overridden in a per-agent entry. | Option | Type | Default | Per-agent | Description | |--------|------|---------|-----------|-------------| | `autoRecall` | `boolean` | `true` | yes | Inject recalled memories into the prompt before each turn. | | `autoRetain` | `boolean` | `true` | yes | Retain conversations after each agent turn. | | `recallBudget` | `string` | `"mid"` | yes | Recall effort level. Values: `low`, `mid`, `high`. Higher values use more compute for better results. | | `recallMaxTokens` | `number` | `1024` | yes | Maximum tokens injected into the prompt per recall. | | `recallTypes` | `string[]` | `["world","experience"]` | yes | Memory types to recall. Values: `world`, `experience`, `observation`. | | `recallRoles` | `string[]` | -- | yes | Roles to include in the recall query context. Values: `user`, `assistant`, `system`, `tool`. | | `recallTopK` | `number` | -- | yes | Maximum number of memory results to return from recall. | | `recallContextTurns` | `number` | -- | yes | Number of recent conversation turns to include as context in the recall query. | | `recallMaxQueryChars` | `number` | -- | yes | Maximum character length of the recall query. | | `recallPromptPreamble` | `string` | -- | yes | Text prepended to the recalled memories block before injection. | | `retainRoles` | `string[]` | `["user","assistant"]` | yes | Message roles captured during retention. Values: `user`, `assistant`, `system`, `tool`. | | `retainEveryNTurns` | `number` | `1` | yes | Retain every Nth turn. Set to `2` to retain every other turn, `3` for every third, etc. | | `retainOverlapTurns` | `number` | -- | yes | Number of turns to overlap between consecutive retain windows. Prevents context loss at boundaries. | | `excludeProviders` | `string[]` | `[]` | yes | Skip memory operations for these message providers (e.g., `["slack"]`). | | `llmProvider` | `string` | auto | yes | LLM provider for memory extraction. Auto-detected from the gateway config if not set. | | `llmModel` | `string` | provider default | yes | LLM model name for extraction. | | `llmApiKeyEnv` | `string` | -- | yes | Environment variable name containing the LLM API key. | | `debug` | `boolean` | `false` | yes | Enable debug logging for memory operations. | ### Bootstrap and Agent Map | Option | Type | Default | Per-agent | Description | |--------|------|---------|-----------|-------------| | `bootstrap` | `boolean` | `false` | no | Automatically apply bank configuration on first run when the bank is empty on the server. After the initial bootstrap, use Terraform to manage server state. | | `agents` | `Record` | `{}` | no | Per-agent behavioral overrides. Maps agent IDs to their config overrides. | | `bankMission` | `string` | -- | no | Default bank mission applied automatically to unconfigured banks. | The `agents` map uses this structure: ```json5 { "agents": { "my-agent": { "recallBudget": "high", "recallMaxTokens": 2048 }, "agent-2": { "autoRetain": false }, "ops-agent": { "hindsightApiUrl": "https://hindsight.office.local" } } } ``` ## Remote Server Setup There are two modes for connecting to a remote Hindsight server: ### Single-user mode (no hindclaw-extension) For setups where access control is not needed, use a static API token: ```json5 { "hindsightApiUrl": "https://hindsight.home.local", "hindsightApiToken": "your-api-token" } ``` ### Multi-user mode (with hindclaw-extension) For setups with multiple users, install the hindclaw-extension on the server (see [Installation](../getting-started/installation)) and use JWT authentication: ```json5 { "hindsightApiUrl": "https://hindsight.office.local", "jwtSecret": "shared-secret-between-plugin-and-server" } ``` The plugin generates short-lived JWTs (5 min TTL) for each request, embedding the sender identity, agent, channel, and topic. The server extension decodes the JWT, resolves the user, and enforces permissions. Users, groups, and permissions are managed via the [Terraform provider](../guides/terraform), not in config files. ### Per-agent server routing Different agents can connect to different servers. Set `hindsightApiUrl` in the per-agent entry: ```json5 // In openclaw.json plugin config, agents section { "agents": { "agent-3": { "hindsightApiUrl": "https://hindsight.office.local" } } } ``` The `jwtSecret` is set at the plugin level (not per-agent) since all agents on the same server share the same secret. Multi-server topology example: ``` Gateway (jwtSecret configured at plugin level) my-agent (private) -> hindsightApiUrl: "https://hindsight.home.local" agent-2 (private) -> hindsightApiUrl: "https://hindsight.home.local" ops-agent (company) -> hindsightApiUrl: "https://hindsight.office.local" agent-4 (company) -> hindsightApiUrl: "https://hindsight.office.local" agent-5 (local) -> no hindsightApiUrl (uses local daemon) ``` ## Cross-Agent Recall An agent can recall memories from other agents' banks by setting `recallFrom` in its per-agent config entry: ```json5 // In openclaw.json plugin config, agents section { "recallFrom": [ { "bankId": "agent-1" }, { "bankId": "agent-2", "budget": "high", "maxTokens": 2048 }, { "bankId": "agent-3", "types": ["world"] } ] } ``` Each entry in `recallFrom` supports: | Field | Type | Default | Description | |-------|------|---------|-------------| | `bankId` | `string` | required | Target bank ID to recall from. | | `budget` | `string` | inherits | Recall effort for this bank. Values: `low`, `mid`, `high`. | | `maxTokens` | `number` | inherits | Max tokens from this bank. | | `types` | `string[]` | inherits | Memory types to recall from this bank. | | `tagGroups` | `TagGroup[]` | -- | Tag-based filtering for this bank's recall. | When access control is active, permissions are checked independently for each target bank. If the requesting user has `recall: false` on a target bank, that bank is silently skipped. ## Client-Enforced vs Server-Enforced Fields With the hindclaw-extension, most permission fields are enforced server-side via `accept_with()` enrichment (tags, tag_groups, strategy). However, some fields cannot be enforced server-side because the Hindsight `ValidationResult` does not support them. These remain **client-enforced** -- the plugin reads them from its own config or from the resolved permissions returned by the debug endpoint: | Field | Reason for client enforcement | |-------|-------------------------------| | `recallBudget` | No budget field in `ValidationResult` | | `recallMaxTokens` | No max_tokens field in `ValidationResult` | | `retainEveryNTurns` | Requires turn count state tracked by the plugin | | `excludeProviders` | Provider filtering happens before the request reaches the server | | `llmModel`, `llmProvider` | Extraction model selection is server config, not a per-request override | These fields can still be set in the plugin config as behavioral overrides. ## User and Group Management When running with the hindclaw-extension, users, groups, policies, policy attachments, service accounts, and bank policies (which contain strategy overrides) are managed via the [Terraform provider](../guides/terraform) -- not via config files. The plugin does not store or resolve any user/group/permission data. See the [Access Control guide](../guides/access-control) for setup instructions and the [Terraform guide](../guides/terraform) for resource definitions.