Tutorial - Scoped memory with an agent
Read a real project knowledge file through a typed tool. Distinguish the logical memory, source scopes, host grants and actual storage. Prerequisites: Touba, Node.js 24 and npm. No model service or external database is required.
1. Prepare the isolated project
In a macOS/Linux terminal, install the distributed SDK:
mkdir -p scoped-memory-demo
cd scoped-memory-demo
mkdir -p handlers providers vendor
npm init -y
node --input-type=module -e 'import {execFileSync} from "node:child_process"; import {copyFileSync} from "node:fs"; const v=JSON.parse(execFileSync("touba",["version","--json"],{encoding:"utf8"})); copyFileSync(v.distribution.sdk_archive,"vendor/touba-sdk.tgz");'
npm install --offline --ignore-scripts --save-exact ./vendor/touba-sdk.tgz
{
"architecture": "Project Atlas separates the governed runtime from its storage adapters.",
"deployment": "The staging service is deployed separately from production."
}
2. Declare memory and read authority
module examples.memory_agent_read
memory ProjectKnowledge
type KnowledgeQuery
text is Text
type KnowledgeResult
summary is Text
tool ReadProjectKnowledge
description "Read the project knowledge file for an exact topic."
uses memory ProjectKnowledge read
takes query as KnowledgeQuery
gives KnowledgeResult
requires memory_read, filesystem_read
skill UseProjectKnowledge
description "Read an authorized topic from project knowledge."
takes query as KnowledgeQuery
gives KnowledgeResult
uses ReadProjectKnowledge
memory ProjectKnowledge read
do
ReadProjectKnowledge
with query as query
into result
give result
agent Researcher
description "Answer the requested topic using project knowledge."
responsibility "Use the authorized knowledge skill and return its observed KnowledgeResult."
takes query as KnowledgeQuery
gives KnowledgeResult
memory ProjectKnowledge read
uses UseProjectKnowledge
callable_by strategy RunKnowledgeReview
strategy RunKnowledgeReview
takes query as KnowledgeQuery
gives KnowledgeResult
uses Researcher
do
Researcher
with query as query
into result
give result
The agent and skill declare read scopes. The tool declares its resource access
and required grants. memory itself does not store or retrieve any data.
3. Connect the file handler
import { readFile } from "node:fs/promises";
import { defineTool, ToubaHandlerError } from "@touba/sdk";
await defineTool({
toolRef: "tool:examples.memory_agent_read/ReadProjectKnowledge",
inputSignature: [{ name: "query", type_ref: "type:examples.memory_agent_read/KnowledgeQuery" }],
outputType: "type:examples.memory_agent_read/KnowledgeResult",
requiredGrants: ["memory_read", "filesystem_read"],
run: async ({ request: query }) => {
const knowledge = JSON.parse(await readFile("knowledge.json", "utf8"));
if (!Object.hasOwn(knowledge, query.text) || typeof knowledge[query.text] !== "string") {
throw new ToubaHandlerError("UnknownTopic", "The requested topic is absent from project knowledge.");
}
return { summary: knowledge[query.text] };
},
});
import { writeFileSync } from "node:fs";
const manifest = { manifest_version: "touba-manifest-v1", bindings: {
"tool:examples.memory_agent_read/ReadProjectKnowledge": {
handler: { kind: "process", command: process.execPath, args: ["handlers/read.mjs"], cwd: "." },
grants: ["memory_read", "filesystem_read"],
memories: [{ ref: "memory:examples.memory_agent_read/ProjectKnowledge", access: "read" }],
effect_contract: { kind: "none" }, timeout_ms: 10000, max_payload_bytes: 65536,
} },
};
writeFileSync("touba.manifest.json", JSON.stringify(manifest, null, 2) + "\n");
manifest.bindings["tool:examples.memory_agent_read/ReadProjectKnowledge"].memories = [];
writeFileSync("denied.manifest.json", JSON.stringify(manifest, null, 2) + "\n");
memories is an array of { ref, access } entries, not an object indexed by
resource name. The second manifest deliberately omits the memory authorization.
The file path belongs to this adapter; host resource bindings
show how to move backend configuration outside it.
4. Execute and verify both paths
The deterministic provider correlates each observation with its arguments. It does not ban reuse of a capability or act as the source of authority.
import { isDeepStrictEqual } from "node:util";
export function decideAgentCapability(request) {
const target = "skill:examples.memory_agent_read/UseProjectKnowledge";
const args = { query: request.inputs.query };
const observed = request.observations?.findLast(item => item.target_ref === target
&& isDeepStrictEqual(item.arguments, args) && typeof item.value?.summary === "string");
if (observed) return { action: "answer_directly", source: "custom_provider",
reason: "The requested result is available in the matching observation.", result: observed.value };
if (request.mode === "finalization") return { action: "cannot_complete", source: "custom_provider",
reason: "No matching result is available within the execution budget." };
if (request.mode === "summary") return { action: "request_capabilities", source: "custom_provider",
reason: "The requested result requires the authorized capability." };
if (!request.capability_refs.includes(target)) throw new Error("Required capability unavailable");
return { target_ref: target, arguments: args, source: "custom_provider",
reason: "The authorized capability produces the requested result." };
}
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { spawnSync } from "node:child_process";
const args = ["run", "memory.air.json", "--entry", "strategy:examples.memory_agent_read/RunKnowledgeReview",
"--agent-decision-provider", "providers/researcher.mjs", "--inputs-json", '{"query":{"text":"architecture"}}',
"--output", "json"];
for (const [manifest, expectedExit] of [["touba.manifest.json", 0], ["denied.manifest.json", 1]]) {
const child = spawnSync("touba", [...args, "--manifest", manifest], { encoding: "utf8" });
assert.equal(child.status, expectedExit, child.stderr);
const run = JSON.parse(child.stdout);
const authorized = run.trace.events.filter(event => event.kind === "tool_binding_authorized");
if (expectedExit === 0) {
assert.equal(run.status, "success");
assert.equal(run.result.summary, JSON.parse(readFileSync("knowledge.json", "utf8")).architecture);
assert.ok(run.trace.events.some(event => event.kind === "memory_access_authorized"));
assert.ok(!JSON.stringify(run.trace).includes(run.result.summary));
assert.equal(authorized.length, 1);
console.log(run.result.summary);
} else {
assert.equal(run.status, "failure");
assert.match(run.reason, /Memory/);
assert.equal(authorized.length, 0);
}
}
console.log("SCOPED_MEMORY_READ_AND_REFUSAL_OK");
node configure.mjs
touba compile memory.tb --out memory.air.json
node verify.mjs
Expected: SCOPED_MEMORY_READ_AND_REFUSAL_OK. The successful result equals the
file's actual content. The denied run exits with 1 before handler dispatch.
The trace records authorized access, not the raw stored text.
A grant is not a filesystem sandbox. The host must still confine trusted handler code to the intended store. PostgreSQL, files and vector stores remain host-side storage choices, not built-in Touba memory engines.