{"_id":"@braedonsaunders/appkit-agent-tools","name":"@braedonsaunders/appkit-agent-tools","dist-tags":{"latest":"0.2.0"},"versions":{"0.2.0":{"name":"@braedonsaunders/appkit-agent-tools","version":"0.2.0","description":"Governed catalogue, install lifecycle, and sandboxed execution of managed command-line tools for agents, with separate install and execution approval gates.","license":"AGPL-3.0-or-later","type":"module","exports":{".":{"types":"./index.d.ts","import":"./index.js","default":"./index.js"},"./drizzle":{"types":"./drizzle.d.ts","import":"./drizzle.js","default":"./drizzle.js"},"./image-manifest":{"types":"./image-manifest.d.ts","import":"./image-manifest.js","default":"./image-manifest.js"},"./schema":{"types":"./schema.d.ts","import":"./schema.js","default":"./schema.js"},"./package.json":"./package.json"},"main":"./index.js","types":"./index.d.ts","dependencies":{"@braedonsaunders/appkit-process-sandbox":"^0.3.0","zod":"^4.4.3"},"peerDependencies":{"@braedonsaunders/appkit-db":"^0.2.0","drizzle-orm":"^0.45.2"},"peerDependenciesMeta":{"@braedonsaunders/appkit-db":{"optional":true},"drizzle-orm":{"optional":true}},"author":{"name":"Braedon Saunders"},"repository":{"type":"git","url":"git+https://github.com/braedonsaunders/appkit.git","directory":"packages/agent-tools"},"homepage":"https://github.com/braedonsaunders/appkit/tree/main/packages/agent-tools#readme","bugs":{"url":"https://github.com/braedonsaunders/appkit/issues"},"engines":{"node":">=22"},"keywords":["agents","appkit","application-framework","approvals","cli","sandbox","typescript"],"_id":"@braedonsaunders/appkit-agent-tools@0.2.0","_integrity":"sha512-nqRtKFbftLRBdarZV3iuV4ueAFpxibqKpYMaI0YhJLGLxjeTHhmZnJfBtaJtpZG1l7AkkzySds30085qFYrq8g==","_resolved":"/tmp/5dd0d273b596285f457632b8fded3ba4/braedonsaunders-appkit-agent-tools-0.2.0.tgz","_from":"file:braedonsaunders-appkit-agent-tools-0.2.0.tgz","_nodeVersion":"22.23.2","_npmVersion":"10.9.8","dist":{"integrity":"sha512-nqRtKFbftLRBdarZV3iuV4ueAFpxibqKpYMaI0YhJLGLxjeTHhmZnJfBtaJtpZG1l7AkkzySds30085qFYrq8g==","shasum":"a7c6b69a50d396d159f75077fca86d9939db2a58","tarball":"https://registry.npmjs.org/@braedonsaunders/appkit-agent-tools/-/appkit-agent-tools-0.2.0.tgz","fileCount":39,"unpackedSize":324599,"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@braedonsaunders%2fappkit-agent-tools@0.2.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQDL0r8P6++ZQDjho6zixVE0UEGW1M0EHkjaDmcvStovwwIgEyGTo0wPdJU72Akwoc8Rd34pmt7J5gvoHTEg2R/qsQg="}]},"_npmUser":{"name":"braedonsaunders","email":"bsaunders@rassaun.com"},"directories":{},"maintainers":[{"name":"braedonsaunders","email":"bsaunders@rassaun.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/appkit-agent-tools_0.2.0_1787007443275_0.42824647200836385"},"_hasShrinkwrap":false}},"time":{"created":"2026-08-17T22:57:23.129Z","0.2.0":"2026-08-17T22:57:23.434Z","modified":"2026-08-17T22:57:23.859Z"},"maintainers":[{"name":"braedonsaunders","email":"bsaunders@rassaun.com"}],"description":"Governed catalogue, install lifecycle, and sandboxed execution of managed command-line tools for agents, with separate install and execution approval gates.","homepage":"https://github.com/braedonsaunders/appkit/tree/main/packages/agent-tools#readme","keywords":["agents","appkit","application-framework","approvals","cli","sandbox","typescript"],"repository":{"type":"git","url":"git+https://github.com/braedonsaunders/appkit.git","directory":"packages/agent-tools"},"author":{"name":"Braedon Saunders"},"bugs":{"url":"https://github.com/braedonsaunders/appkit/issues"},"license":"AGPL-3.0-or-later","readme":"# @braedonsaunders/appkit-agent-tools\n\nA governed catalogue of command-line tools an agent may install and run.\n\nA shell ability lets an agent run whatever is already in the image. This package\nis the layer above that: named tools, declared in a manifest, each carrying its\nown risk, capabilities, health check, network requirement, and resource\nceilings. Adding `ripgrep` or `pandoc` to an agent's reach becomes a catalogue\nentry rather than a Dockerfile change, and every install and every execution\npasses a policy gate first.\n\nExecution goes through `@braedonsaunders/appkit-process-sandbox`. There is no unsandboxed path:\nthe runner throws on a host without bubblewrap rather than falling back.\n\n## The base-image manifest\n\nAgainst an agent that has a terminal, the install and execute gates enforce\nnothing — one line in a shell walks around them. But the manifest's other\nproperties never depended on the gate: exact pinned versions, health checks,\nan operator-visible shelf, and revocability all survive. So the manifest is\nalso the declaration of what a golden VM base image contains. A tool with\n`sourceKind: 'apt-package'` pins a Debian package (`aptPackage` +\n`aptVersion`, exact — never a range) that the image build installs; the\nruntime never installs it, and `install()` only verifies the declared\nexecutables exist on disk before marking the record installed. Consumers not\non a desk keep using the gated paths exactly as before.\n\n`@braedonsaunders/appkit-agent-tools/image-manifest` turns a shelf into build input:\n\n```ts\nimport { imageManifest, renderAptInstallFragment } from '@braedonsaunders/appkit-agent-tools/image-manifest'\n\nconst build = imageManifest(tools)\n// { aptPackages: [{ name, version, toolId }], npmPackages: [...], binaryPaths: [...] }\n// Deterministically sorted; throws when two tools pin one package differently.\n\nconst fragment = renderAptInstallFragment(tools)\n// apt-get install -y --no-install-recommends \\\n//   jq=1.7.1-3 \\\n//   ripgrep=14.1.0-1\n```\n\n## The two gates\n\nInstalling a tool and running one are separate questions with separate policies.\nApproving the first never implies the second.\n\n| Mode | Meaning |\n| --- | --- |\n| `deny` | Refused outright; no request is filed. |\n| `approval` | Always asks a person. |\n| `allow_safe` | Proceeds for low-risk, network-isolated tools; asks for the rest. |\n| `allow_all` | Proceeds. |\n\nA tool is \"safe\" only when it is low risk **and** cannot reach the network while\nit runs. A low-risk tool that phones out is still a tool that phones out.\n\n## Grants are bounded\n\nAn approval is not a permanent permission. Each one carries:\n\n- a **scope** — a digest of exactly what was asked. A grant for\n  `upload ./report.pdf` never covers `upload /etc/shadow`. Tools whose every\n  invocation is equally safe can opt into `approvalScope: 'command'`.\n- a **grant expiry** — how long the answer stands.\n- a **use count** — how many executions it permits, defaulting to one.\n\nA refusal is bounded the same way, so an agent that was told no cannot re-file\nthe same question in a loop, and a stale no does not last forever. Spending a\nuse goes through `store.consumeGrant`, which the Drizzle adapter implements as a\nsingle conditional `UPDATE` so two concurrent runs cannot both spend the last one.\n\n## Isolation\n\n| | Install | Health check | Execution |\n| --- | --- | --- | --- |\n| Network | host — the registry is the point | none | `none` unless the manifest says `requiresNetwork` |\n| Writable | the tool's install directory | nothing | only the caller's `workdir` |\n| Read-only | — | the tool's own bytes | the tool's own bytes, `/usr`, `/etc`, `/opt` |\n\nnpm installs run with `--ignore-scripts`. Lifecycle scripts are arbitrary code\nfrom the registry running with the installer's reach; a CLI that needs them is\nnot a managed tool. Versions must be exact — a range would let the installed\nbytes change under an approval granted against a specific version.\n\n## Usage\n\n```ts\nimport {\n  createAgentToolRuntime,\n  createProcessSandboxRunner,\n  defineAgentTool,\n} from '@braedonsaunders/appkit-agent-tools'\nimport { createDrizzleAgentToolStore } from '@braedonsaunders/appkit-agent-tools/drizzle'\n\nconst runtime = createAgentToolRuntime({\n  store: createDrizzleAgentToolStore(db),\n  runner: createProcessSandboxRunner({ launcherIdentity: { uid: 1000, gid: 1000 } }),\n  installRoot: '/data/agent-tools',\n  policy: async (tenantId) => loadToolPolicy(tenantId),\n  audit: (entry) => recordActivity(entry),\n})\n\nawait runtime.register(tenantId, defineAgentTool({\n  id: 'ripgrep',\n  name: 'ripgrep',\n  description: 'Fast recursive search across files.',\n  sourceKind: 'npm-package',\n  risk: 'low',\n  packageName: '@microsoft/ripgrep-prebuilt',\n  packageVersion: '0.1.2',\n  capabilities: ['search'],\n  bins: [{ name: 'rg', bin: 'rg', healthCheckArgs: ['--version'] }],\n  limits: { cpuSeconds: 30, processes: 32 },\n}))\n\nconst result = await runtime.execute({\n  tenantId,\n  toolId: 'ripgrep',\n  command: 'rg',\n  argv: ['--json', 'invoice'],\n  workdir: agentHomePath,\n  installIfMissing: true,\n  actor: agentId,\n})\n// 'ran' | 'blocked' (a person was asked) | 'denied' | 'unavailable'\n```\n\nThe catalogue itself is the application's, not this package's — which tools a\nbusiness trusts is a product decision, and pinned versions belong where they can\nbe reviewed and updated.\n\n## Persistence\n\nThe `AgentToolStore` port has two adapters: `createMemoryAgentToolStore()` for\ntests and single-process tooling, and `createDrizzleAgentToolStore(db)` over the\ntables in `@braedonsaunders/appkit-agent-tools/schema`. Every method is tenant-scoped; pass a\ntenant-bound `db` from `@braedonsaunders/appkit-db` so row-level security is the outer boundary.\n\nArguments reach the executable directly — there is no shell to quote for — so\nthe runtime rejects only what cannot survive an `execve` and leaves the sandbox\nto be the actual boundary.\n","readmeFilename":"README.md","_rev":"1-8fc361e748d53fcdcbfe71da092a1018"}