Quickstart
Two working programs: one that embeds the kernel in your own process, and one that talks to it over HTTP. Both run against the published packages — nothing in this page requires a clone of this repository.
npm install @aicoo/sharedos
Node.js 20.11 or newer. The packages are ESM-only. Every SharedOS package is a
0.x prerelease, so pin an exact version if you need reproducibility.
What you have to bring
SharedOS decides whether an action is allowed. It never stores your data, calls your model, or holds your credentials. A first integration therefore supplies three things, and only the first is required to see anything work:
| You supply | Interface | Needed for |
|---|---|---|
| Somewhere to put files | ResourceProvider | Any files operation at all |
| A model, or a script | AgentTurnDriver | Running a turn rather than single calls |
| Durable stores | five host ports | Production. Not this page |
The examples below use an in-memory provider so they run immediately.
examples/reference-host
is the same shape backed by a real filesystem and SQLite.
Embedded: authorize one file read
The whole model in one file. Alice owns some files. Bob's agent may search one subtree, for one purpose, and nothing else.
import {
CapabilityAuthorizer,
SharedOSKernel,
registerStandardOsTools,
type AccessContext,
type CapabilityGrant,
type ResourceProvider,
} from "@aicoo/sharedos";
const alice = { kind: "human", userId: "alice" } as const;
const bobAgent = { kind: "agent", agentId: "bob-assistant" } as const;
// 1. Your storage, behind one interface. Real hosts hit a database here.
const files: ResourceProvider = {
namespace: "files",
async invoke(operation, signal) {
signal.throwIfAborted();
return {
operationId: operation.operationId,
completedAt: new Date().toISOString(),
status: "succeeded",
output: { hits: [{ text: "Atlas ships 2026-09-30." }] },
};
},
};
// 2. A kernel. Registering a provider exposes it; it grants nothing.
const kernel = new SharedOSKernel({ authorizer: new CapabilityAuthorizer() });
kernel.registerResourceProvider(files);
registerStandardOsTools(kernel, { files });
// 3. Authority, issued explicitly and scoped on every axis that matters.
const grant: CapabilityGrant = {
id: "grant-1",
namespaceId: "acme",
subject: bobAgent, // who may use it
issuer: alice, // whose authority it is
capabilities: [
{
resource: { namespace: "files", path: ["Work", "Projects", "atlas"], owner: alice },
actions: ["search"],
scope: "descendants",
},
],
constraints: {
purposes: ["atlas-status"],
expiresAt: new Date(Date.now() + 3_600_000).toISOString(),
maxUses: 3,
},
issuedAt: new Date().toISOString(),
};
// 4. The trusted context. Build it from server-side state, never from a
// request body, a message, or model output.
const context: AccessContext = {
namespaceId: "acme",
actor: bobAgent, // who is acting
authority: alice, // whose grants are being exercised
owner: alice, // whose resources are in scope
purpose: "atlas-status",
traceId: crypto.randomUUID(),
enabledToolNamespaces: ["files"],
grants: [grant],
now: new Date().toISOString(),
};
// The catalog is already filtered: `files.search` is visible, the other
// eleven file tools are not, because no grant covers them.
const visible = await kernel.listTools(context);
console.log(visible.map(({ name }) => name)); // [ 'files.search' ]
const allowed = await kernel.invokeTool(context, {
id: crypto.randomUUID(),
tool: "files.search",
arguments: { path: ["Work", "Projects", "atlas"], query: "ship date" },
traceId: context.traceId,
requestedAt: new Date().toISOString(),
});
console.log(allowed.status); // 'succeeded'
const denied = await kernel.invokeTool(context, {
id: crypto.randomUUID(),
tool: "files.search",
arguments: { path: ["Work", "Finance"], query: "salary" },
traceId: context.traceId,
requestedAt: new Date().toISOString(),
});
console.log(denied.status); // 'denied'
Note what the second call proves. Nothing about the request changed except the path, and the path came from the model's own arguments. The tool was visible, the namespace was enabled, and it was still refused — because discovery is not permission, and the exact resource is authorized again at invocation.
maxUses is denied unless you give the kernel somewhere to count
maxUses: 3 above will refuse on the first call with
usage_store_unavailable — a bounded grant with nowhere to record usage fails
closed rather than silently becoming unbounded. For a local experiment:
import { CapabilityAuthorizer, InMemoryGrantUsageStore } from "@aicoo/sharedos";
const authorizer = new CapabilityAuthorizer({ usageStore: new InMemoryGrantUsageStore() });
In production this must be a durable store whose tryConsume is one atomic
statement. Two concurrent turns must not both be allowed to spend the last use.
Embedded: run one agent turn
Single calls are the floor. The reason to use SharedOS is to hand a whole turn
to a model and still know what it can reach. You supply an AgentTurnDriver;
SharedOS gives it the filtered catalog and re-authorizes every call it makes.
import {
SharedOSExecutor,
StandardRuntime,
agentExecutionCapability,
type AgentTurnDriver,
} from "@aicoo/sharedos";
const aliceAgent = { kind: "agent", agentId: "alice-assistant" } as const;
// Annotate the driver. Without it TypeScript widens `type: "tool_call"` to
// `string` and the object stops matching AgentTurnDecision.
const driver: AgentTurnDriver = {
async open(request) {
let asked = false;
return {
async next(input) {
if (!asked) {
asked = true;
// request.tools is the effective catalog. Hand it to your model as
// its tool definitions; `inputSchema` is already JSON Schema.
return {
type: "tool_call",
call: {
id: crypto.randomUUID(),
tool: "files.search",
arguments: { path: ["Work", "Projects", "atlas"], query: "ship date" },
traceId: request.context.traceId,
requestedAt: new Date().toISOString(),
},
};
}
return { type: "complete", output: { saw: input.type } };
},
};
},
};
// Running someone's agent needs its own grant. A message addressed to it is
// never sufficient.
const invokeAlice: CapabilityGrant = {
...grant,
id: "grant-invoke",
capabilities: [agentExecutionCapability(aliceAgent, alice)],
constraints: { purposes: ["atlas-status"] },
};
const turnContext = { ...context, grants: [grant, invokeAlice] };
const tools = await kernel.listTools(turnContext);
const result = await new SharedOSExecutor(kernel, new StandardRuntime(driver), {
defaultMaxSteps: 8,
defaultMaxToolCalls: 8,
defaultTimeoutMs: 30_000,
}).execute({
version: "1",
executionId: crypto.randomUUID(),
agent: aliceAgent,
context: turnContext,
message: {
version: "1",
id: crypto.randomUUID(),
sender: bobAgent,
receiver: aliceAgent,
intent: "answer-question",
purpose: turnContext.purpose,
payload: { text: "When does Atlas ship?" },
traceId: turnContext.traceId,
createdAt: new Date().toISOString(),
},
tools: [...tools],
});
console.log(
result.status,
result.events.map(({ type }) => type),
);
// succeeded [ 'turn.started', 'tool.requested', 'tool.completed', 'turn.completed' ]
The driver received request.context without grants and without the issuing
authority. It cannot widen its own access, and it cannot reach a tool outside
the catalog it was given — RuntimeHost.invokeTool checks the catalog and
re-authorizes before anything runs.
Remote: the same kernel over HTTP
Expose the kernel when the caller cannot embed it. The contracts are identical; only the transport changes.
import { createKernelSharedOSApi, createSharedOSHandler } from "@aicoo/sharedos";
const handler = createSharedOSHandler({
api: createKernelSharedOSApi({ kernel, turns }),
// The security boundary. Derive identity from your own authenticated
// server-side state. The request body carries no grants, no tools, and no
// access context, so a caller cannot describe its own authority.
async resolveContext(request) {
const session = await authenticate(request.headers.get("authorization"));
return {
namespaceId: session.tenantId,
actor: session.agent,
authority: session.authority,
owner: session.owner,
purpose: request.headers.get("x-sharedos-purpose") ?? "default",
traceId: crypto.randomUUID(),
enabledToolNamespaces: await settings.enabledFor(session),
grants: await grantStore.resolveEffectiveGrants(session),
now: new Date().toISOString(),
};
},
});
createSharedOSHandler returns a plain (Request) => Promise<Response>, so it
mounts anywhere the Fetch API is available:
// Node 20+, Bun, Deno
Deno.serve(handler);
// Next.js app router: app/api/sharedos/[...path]/route.ts
export const GET = handler;
export const POST = handler;
export const PUT = handler;
On the caller side:
import { SharedOSClient } from "@aicoo/sharedos";
const sharedos = new SharedOSClient({
baseUrl: "https://sharedos.internal.example",
headers: async () => ({ authorization: `Bearer ${await serviceIdentityToken()}` }),
});
const tools = await sharedos.listTools();
const result = await sharedos.invokeTool({
id: crypto.randomUUID(),
tool: "files.search",
arguments: { path: ["Work", "Projects", "atlas"], query: "ship date" },
traceId: crypto.randomUUID(),
requestedAt: new Date().toISOString(),
});
headers may be a value or an async function, so a short-lived token can be
minted per call. Every route, request shape, and status code is listed in the
HTTP API reference.
Three things that will bite a first integration
authority is not the data owner. It is the issuer whose grants are being
exercised. For a grant Alice issued, that is Alice. For a grant Bob derived
from it, that is Bob. Get it wrong and the grant is invisible — reported as
no_matching_grant, identical to having no grant at all.
Bounded and derived grants fail closed. maxUses needs a usageStore;
deriveGrant output needs a chainResolver. Without them the kernel denies
rather than assuming, and the reason code tells you which one is missing.
A filesystem provider must reject links, not just escapes. Authorization is decided on the logical path; your provider serves the physical target. Staying under your root is not sufficient — a symlink that stays inside the tenant and points at another subtree voids the grant without SharedOS seeing it. See the reference host.
Next
- HTTP API reference — every route, body, and status code
- Tool catalog — the twelve
filestools, the three availability gates, and how to register your own - Reason and error codes — what a denial means and what to do
- Host integration guide — the production ports
- Permission model — the normative invariants