A shared memory bank gives an agent access to investigations completed by other agents. In my Hindsight plugin fork, the first useful security test was therefore a denied request: could a disabled agent reach an old recall result without making a new network call?
A cache hit is still a read of someone else's work. Checking access only around the outbound request leaves that path open. Here is how the fork orders the checks and how I test the failure case.
The gate belongs before every memory path
View the access path at full size.
The fork builds on the Hindsight Paperclip integration. It adds team-oriented recall, provenance tags, optional run outcomes and filtering of noisy comments. Its SDK pin and host compatibility need their own qualification; a working GitLab bridge does not establish compatibility for this separate plugin.
The enabled-agent helper is deliberately small:
export function isAgentEnabled(config, agentId) {
const allowlist = config.enabledAgentIds;
if (!allowlist || allowlist.length === 0) return true;
return !!agentId && allowlist.includes(agentId);
}
An unset or empty list permits everyone at this plugin layer. It does not mean “deny all.” A restricted installation must populate the list and configure the host's independent tool permissions.
The caller ID comes from Paperclip's trusted run context. An agentId supplied inside tool arguments is ordinary input and cannot select whose authority to use.
Make the ordering executable
This dependency-free model preserves the ordering of the recall path. It is a small test fixture for the contract, not a replacement for the SDK adapter:
export async function recall(args, run, config, ports) {
if (!isAgentEnabled(config, run.agentId)) throw new Error('Agent is not enabled');
const cached = await ports.readState(run.runId);
if (cached !== undefined) return cached;
const credential = await ports.resolveSecret();
return ports.request({query: args.query, agentId: run.agentId, credential});
}
Download the model. Save the following beside it as memory-gate.test.mjs and run node --test memory-gate.test.mjs with Node.js 22 or later:
import assert from 'node:assert/strict';
import {test} from 'node:test';
import {recall} from './memory-gate.mjs';
test('disabled caller cannot read cached memory using a spoofed argument ID', async () => {
const calls = [];
const ports = {
readState: async () => {
calls.push('cache');
return 'private cached memory';
},
resolveSecret: async () => calls.push('secret'),
request: async () => calls.push('network'),
};
await assert.rejects(
recall(
{query: 'deployment decisions', agentId: 'allowed-agent'},
{agentId: 'disabled-agent', runId: 'run-1'},
{enabledAgentIds: ['allowed-agent']},
ports,
),
/not enabled/,
);
assert.deepEqual(calls, []);
});
The important assertion is the empty call list. A rejection after reading the cached memory would still be the wrong ordering. The real plugin regression tests also check that the denied path performs no state writes. Companion cases use an allowed trusted caller with a misleading argument ID, and an allowed caller with an existing cached result.
Configure a shared bank as an intentional sharing boundary
A shortened configuration for two explicitly admitted agents might look like this. The IDs and endpoint are invented; the secret value stays in the existing secret store.
{
"hindsightApiUrl": "https://memory.example.com",
"hindsightApiKeyRef": {
"type": "secret_ref",
"secretId": "22222222-2222-4222-8222-222222222222",
"version": "latest"
},
"dynamicBankId": false,
"bankId": "engineering-team",
"recallScope": "project",
"enabledAgentIds": [
"33333333-3333-4333-8333-333333333333",
"44444444-4444-4444-8444-444444444444"
],
"retainRunOutcomes": true,
"minCommentChars": 40,
"scrubSecrets": true
}
A static bank makes knowledge available across the admitted team. recallScope: "project" narrows the retrieval query. It is not a tenant-isolation mechanism: an authorized client with broader bank access may still be able to request a different scope. Separate credentials, host permissions and backend authorization are needed when information must remain inaccessible across a boundary.
The fork attaches company, agent, issue, project, source and authorship tags. Those tags help answer “where did this come from?” and select relevant material. They cannot turn a reported conclusion into a verified fact.
Retain observations with their provenance
The automatic retention path can discard short comments and configured boilerplate patterns. minCommentChars: 40 reduces “started work” noise, but it can also discard a short, decisive finding. I would inspect a sample of rejected comments before raising the threshold.
Run outcomes can be retained as experiences. A failed run's diagnosis should remain attributable to that run, including its outcome, rather than becoming an unqualified team rule. Credential-shaped text is scrubbed before retention; pattern matching cannot recognize every secret, so source selection remains necessary.
The three checks I want before evaluating retrieval quality are concrete: a denied caller touches no cache or secret; an admitted caller receives only the intended shared scope; and the returned memory carries enough origin information to distinguish a user decision from an assistant's hypothesis. The knowledge compiler applies the same provenance concern to notes outside the runtime.