Skip to main content
Version: 1.0.0

Import a tool from another local module

Reuse a typed tool contract without acquiring permissions through an import. The implementation performs a real HTTP read of the Données Québec catalogue; it does not invent dataset titles. Prerequisites: Touba, Node.js 24, npm and Internet. No paid model or API key is needed.

Create the project and modules​

Terminal
mkdir -p imported-tool-demo
cd imported-tool-demo
mkdir -p src/examples handlers/examples/data_tools 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
src/examples/data_tools.tb
module examples.data_tools

type DatasetSearchRequest
query is Text
rows is Int

type DatasetSearchResult
count is Int
first_title is Text

tool SearchDatasets
description "Search the live Données Québec catalogue and return its match count and first title."
takes request as DatasetSearchRequest
gives DatasetSearchResult
requires network
src/examples/research_app.tb
module examples.research_app

import type DatasetSearchRequest from examples.data_tools
import type DatasetSearchResult from examples.data_tools
import tool SearchDatasets from examples.data_tools

skill BuildDatasetBrief
description "Read the public catalogue through the imported tool contract."
takes request as DatasetSearchRequest
gives DatasetSearchResult
uses SearchDatasets
do
SearchDatasets
with request as request
into result
give result

strategy RunDatasetSearch
takes request as DatasetSearchRequest
gives DatasetSearchResult
uses BuildDatasetBrief
do
BuildDatasetBrief
with request as request
into result
give result

Implement and authorize the declaring tool​

Both the handler and manifest use the declaring reference, tool:examples.data_tools/SearchDatasets, not the importing module's name. The host explicitly authorizes network access and selects a read-only effect contract. The denied manifest removes this grant without changing the source.

handlers/examples/data_tools/search_datasets.handler.mjs
import { defineTool, ToubaHandlerError } from "@touba/sdk";
await defineTool({ toolRef: "tool:examples.data_tools/SearchDatasets",
inputSignature: [{ name: "request", type_ref: "type:examples.data_tools/DatasetSearchRequest" }],
outputType: "type:examples.data_tools/DatasetSearchResult", requiredGrants: ["network"],
run: async ({ request }) => {
const url = new URL("https://www.donneesquebec.ca/recherche/api/3/action/package_search");
url.searchParams.set("q", request.query); url.searchParams.set("rows", String(request.rows));
const response = await fetch(url, { signal: AbortSignal.timeout(15000) });
if (!response.ok) throw new ToubaHandlerError("CatalogueHttpError", "The public catalogue request failed.");
const data = await response.json();
if (data.success !== true || !Number.isSafeInteger(data.result?.count) || !Array.isArray(data.result?.results))
throw new ToubaHandlerError("CatalogueResponseInvalid", "The public catalogue returned an invalid response.");
const first = data.result.results[0];
const title = first?.title ?? first?.name ?? "No matching dataset";
if (typeof title !== "string") throw new ToubaHandlerError("CatalogueTitleInvalid", "The dataset title must be text.");
return { count: data.result.count, first_title: title };
},
});
configure.mjs
import { writeFileSync } from "node:fs";
const manifest = { manifest_version: "touba-manifest-v1", bindings: {
"tool:examples.data_tools/SearchDatasets": {
handler: { kind: "process", command: process.execPath,
args: ["handlers/examples/data_tools/search_datasets.handler.mjs"], cwd: "." },
grants: ["network"], effect_contract: { kind: "none" }, timeout_ms: 20000, max_payload_bytes: 65536,
payload_constraints: { input: { "request.query": { non_empty: true, max_length: 120 }, "request.rows": { min: 1, max: 20 } } },
},
} };
writeFileSync("touba.manifest.json", JSON.stringify(manifest, null, 2));
manifest.bindings["tool:examples.data_tools/SearchDatasets"].grants = [];
writeFileSync("denied.manifest.json", JSON.stringify(manifest, null, 2));

Prepare, run and verify​

Preparation scans the project sources, resolves both modules, and preserves the completed handler. The script checks its bytes, the canonical tool identity, a live HTTP result and refusal before handler authorization when the network grant is absent. The response may change with the catalogue; a network failure is an error, not a substitute result.

exercise.mjs
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { spawnSync } from "node:child_process";
const path = "handlers/examples/data_tools/search_datasets.handler.mjs";
const before = readFileSync(path, "utf8");
const prepared = spawnSync("touba", ["prepare", "--profile", "dev", "--manifest", "touba.manifest.json",
"--entry", "strategy:examples.research_app/RunDatasetSearch", "--mode", "write", "--json"], { encoding: "utf8" });
assert.equal(prepared.status, 0, prepared.stderr || prepared.stdout);
assert.equal(readFileSync(path, "utf8"), before);
const air = ".touba/build/dev/air/examples/research_app.air.json";
const document = JSON.parse(readFileSync(air, "utf8"));
assert.ok(document.definitions.tools.some(tool => tool.id === "tool:examples.data_tools/SearchDatasets"));
for (const [manifest, expected] of [["touba.manifest.json", 0], ["denied.manifest.json", 1]]) {
const child = spawnSync("touba", ["run", air, "--entry", "strategy:examples.research_app/RunDatasetSearch",
"--manifest", manifest, "--inputs-json", JSON.stringify({ request: { query: "pompier", rows: 1 } }), "--output", "json"], { encoding: "utf8" });
assert.equal(child.status, expected, child.stderr || child.stdout);
const result = JSON.parse(child.stdout);
if (expected === 0) {
assert.equal(result.status, "success");
assert.ok(Number.isSafeInteger(result.result.count) && result.result.count >= 0);
assert.equal(typeof result.result.first_title, "string");
console.log(JSON.stringify(result.result));
} else {
assert.equal(result.reason, "ToolCapabilityNotGranted");
assert.ok(!result.trace.events.some(event => event.kind === "tool_binding_authorized"));
}
}
console.log("IMPORTED_TOOL_LIVE_READ_AND_AUTHORITY_REFUSAL_OK");
Terminal
node configure.mjs
node exercise.mjs
touba inspect .touba/build/dev/air/examples/research_app.air.json

Expected marker: IMPORTED_TOOL_LIVE_READ_AND_AUTHORITY_REFUSAL_OK. A grant is not an OS sandbox. Use the supply-chain tutorial to bind handler identity and declared dependencies before a strict run.