Skip to content

About

Persistent and versioned filestorage fully compatible with Corbits

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

@corbits/artifacts

Versioned documents and files for Interchange agents and their users: a Corbits hub module that mounts grant-gated Hono routes on @intx/hub-api, keeps versions in Postgres and bytes in a pluggable ContentStore, and ships agent tools for the sidecar.

Why @corbits/artifacts?

  1. Every version is kept. Each revision of a document or uploaded file is a new version with its own content, bytes and SHA-256 digest. expectedVersion turns a revise into a compare-and-set.
  2. Mounts like any hub route. createArtifactRoutes returns a Hono<TenantEnv> sub-app. Reads are confined to the caller's tenant, and writes run through the hub's requireGrant.
  3. Agents write through the same store. A run-scoped route set and a sidecar tool pack let a workflow run create, read and revise the artifacts its users see.
  4. Migrations that replay safely. Every SQL file is idempotent and runs on every boot under an advisory lock, so several replicas can start at once.

It ships no UI and no object store: the host renders artifacts and brings its own ContentStore for large files.

Install

bun add @corbits/artifacts \
  @intx/agent @intx/authz @intx/db @intx/hub-api @intx/types \
  drizzle-orm hono hono-openapi postgres

Add @intx/agent only if an agent uses the sidecar tools. Runs on Node >= 24.

Quickstart

With DATABASE_URL pointing at a hub database that has run runArtifactMigrations (see Using with Interchange):

import {
  createArtifact,
  createArtifactDb,
  getArtifact,
} from "@corbits/artifacts";

const { db, close } = createArtifactDb(process.env.DATABASE_URL!);

const created = await db.transaction((tx) =>
  createArtifact(tx, {
    scope: { tenantId: "acme", principalId: "alice" },
    ownerPrincipalId: "alice",
    kind: "document",
    title: "Notes",
    content: "Hello",
    source: { origin: "manual" },
  }),
);
console.log(await getArtifact(db, created.id));
await close();

acme and alice must be a tenant and principal in the hub. It prints the artifact at version 1.

Where it fits

Interchange runs AI agents as principals: accounts with their own identity, permissions and credentials. Its hub is the multi-tenant control plane that holds tenants, principals and grants (permissions a principal holds on a resource); its sidecar is the agent runtime.

Reference

createArtifactRoutes(deps)

deps Type What the host provides
db ArtifactDb The hub's drizzle handle, for example from createDB.
contentStore ContentStore Where file bytes go. InlineContentStore keeps them in Postgres; implement the port for object storage.
requireGrant RequireGrant From @intx/hub-api's createRequireGrant.
onArtifactCreated (tx, row, scope) => Promise<void> Optional. Runs in the creating transaction for any other creation side effect. The creator's write and archive grants are minted for you.
decorate (tenantId, rows) => Promise<void> Optional. Adds display-only fields to serialized rows. It must not change which rows are returned.
uploadPolicy UploadPolicy Optional. MIME types POST /artifacts/upload accepts. Defaults to ARTIFACT_UPLOAD_POLICY.
countSegments ArtifactCountSegments Optional. Named predicates for GET /artifacts/counts.
Route Grant Does
GET /artifacts none (tenant-scoped) Lists artifacts, paginated. Empty 200 with no principal.
GET /artifacts/counts none (tenant-scoped) Counts per countSegments bucket, plus all.
POST /artifacts create on artifact:* Creates a text or URL artifact at version 1.
POST /artifacts/upload create on artifact:* Uploads one or more files (multipart/form-data), one artifact each.
GET /artifacts/:id none (tenant-scoped) The latest version.
GET /artifacts/:id/versions none (tenant-scoped) Version history.
GET /artifacts/:id/versions/:version none (tenant-scoped) One version, with its content.
POST /artifacts/:id/versions write on artifact:<id> Adds a version from JSON, or from a new file for an uploaded file.
POST /artifacts/:id/archive, unarchive archive on artifact:<id> Archives or restores.
GET /artifacts/:id/download?version=N none (tenant-scoped) The bytes of a version, latest by default.
GET /artifacts/:id/preview none (tenant-scoped) A text/html artifact under a sandboxing CSP; 415 otherwise.

With no principal, every route except the list and counts answers 403. Another tenant's artifact, or an unknown id, answers 404. Set a request-body limit on the host for the upload and file-revise routes; they buffer the body before checking MAX_UPLOAD_BYTES.

createWorkflowArtifactRoutes(deps)

Run-scoped routes for a workflow run, which authenticates with a bearer token and an x-workflow-run-address header instead of a session.

deps Type What the host provides
db ArtifactDb Same as above.
contentStore ContentStore Same as above.
resolveRunScope WorkflowRunResolver (bearerToken, runAddress) => ResolvedWorkflowRunScope | null. null answers 401.
agentToken AgentTokenAuth Optional. Accepts an agent's own hub token as a second way in.
uploadPolicy UploadPolicy Optional. MIME types POST /artifacts/binary accepts. Defaults to ARTIFACT_UPLOAD_POLICY.
maxBinaryBytes number Optional. Byte ceiling for POST /artifacts/binary. Defaults to MAX_UPLOAD_BYTES.
maxContentChars number Optional. Character ceiling on the resulting content — full text or edited text — for POST /artifacts and PATCH /artifacts/:id/versions. Defaults to 64,000.

runArtifactMigrations(config, { schema })

Takes the same DBConfig and schema as Interchange's runMigrations. schema holds the host's tenant and principal tables; this package's tables always go in artifacts.

@corbits/artifacts/sidecar-bundle

artifacts is an @intx/agent tool pack of three tools. They call the run-scoped routes through the agent's hub credential.

  • artifact_write creates an artifact when no id is given, from content or a workspace file path. With an id it revises, either with the full text or with edits: exact passages, each of which must appear once, or nothing is written. expectedVersion refuses the write if a newer version exists.
  • artifact_read returns the content, a past version with version, or a range with offset and limit.
  • artifact_search lists the newest artifacts, filtered by kind and by a query matched against title and content.

Using with Interchange

Run the migrations after Interchange's, mount both route sets, grant create on artifact:* to principals that create artifacts, and give agents the tool pack.

import { timeWindowEvaluator } from "@intx/authz";
import { createDB, createGrantStore, runMigrations } from "@intx/db";
import { createRequireGrant } from "@intx/hub-api";
import {
  createArtifactRoutes,
  InlineContentStore,
  runArtifactMigrations,
} from "@corbits/artifacts";

const dbConfig = {
  host: "localhost",
  port: 5432,
  user: "postgres",
  password: "postgres",
  database: "interchange",
};

await runMigrations(dbConfig, { schema: "public" });
await runArtifactMigrations(dbConfig, { schema: "public" });

const { db } = createDB(dbConfig);
const requireGrant = createRequireGrant({
  grantStore: createGrantStore(db),
  conditionRegistry: { time_window: timeWindowEvaluator },
});

export const artifactRoutes = createArtifactRoutes({
  db,
  contentStore: InlineContentStore,
  requireGrant,
});

Mount artifactRoutes on the hub app at /api, behind the hub's auth and tenant middleware.

For agents, mount createWorkflowArtifactRoutes({ db, contentStore: InlineContentStore, resolveRunScope }) at /api/workflow-artifacts. resolveRunScope is the host's (bearerToken, runAddress) lookup that returns the run's tenant and principal from the hub's workflow runs, or null. The sidecar tools call /api/workflow-artifacts. Add them to an agent and bind its hub credential to the agent's hub token when you deploy it:

import { defineAgent, type InferencePreference } from "@intx/agent";
import { artifacts } from "@corbits/artifacts/sidecar-bundle";

export function buildAssistant(sources: readonly InferencePreference[]) {
  return defineAgent({
    id: "assistant",
    systemPrompt: "You help the team read and write shared artifacts.",
    capabilities: [],
    inference: { sources },
    tools: [artifacts],
  });
}

Upgrading from 0.1

  • mountArtifacts(app, opts) is now createArtifactRoutes(deps), and mountWorkflowArtifacts(app, opts) is now createWorkflowArtifactRoutes(deps). Mount both with app.route.
  • POST /artifacts and POST /artifacts/upload need create on artifact:*. The upgrade grants it to every principal that has already created an artifact in its tenant; grant it to new principals yourself.
  • runArtifactMigrations(db) is now runArtifactMigrations(dbConfig, { schema }). Existing 0.1.0 data upgrades in place on the first boot.
  • That boot drops the 0.1.0 migration ledger. You cannot roll back to 0.1.0, and 0.1.0 and 0.2.0 replicas must not share a database.
  • The drizzle tables, the web_site helpers, SKILL_DRAFT_KIND, windowContent and the mail-attachment routes and helpers are removed. The mail_attachment_ref table is dropped.
  • Node 24 or newer is required.

Upgrading from 0.2

Separated by surface: what changed for AGENTS (the sidecar tool pack) and what changed for HOSTS (the routes and exports).

Agent tools

The sidecar tool pack collapsed its five finer-grained tools into three. artifact_write now covers both create and link (revise via edits/content is unchanged), and its { artifactId, version } result is the same.

0.2 tool 0.3 How
artifact_create artifact_write Call it with no artifactId, passing title, kind, content.
artifact_link_file artifact_write Call it with no artifactId, passing a workspace path.
artifact_read_chunk artifact_read Pass the range as offset/limit instead of start/end.
artifact_list artifact_search Results are ordered most-recently-updated first.
artifact_find_by_title artifact_search Not a drop-in — see the caveat below.

artifact_find_by_title is not a drop-in. In 0.2 it matched the title exactly, matched the title only, and returned at most one { artifactId, version } (or null). In 0.3 artifact_search does a case-insensitive substring (ILIKE) match on both the title and the content, and returns a list of matches. A caller that needs the old exact-title, single-result semantics must post-filter the results (compare title case-insensitively and take the first exact match).

Search results are capped at 50 artifacts per call. artifact_search accepts a cursor property (plus limit, capped at 50) to page through all matches; pass back the previous call's nextCursor (keeping the same query and kind) until none is returned.

Hosts

The roles are unchanged, but two routes and one export were removed:

  • GET /artifacts/find → GET /artifacts?query=…, where query is the same case-insensitive substring (ILIKE) match on title and content.
  • GET /artifacts/:id/chunk → GET /artifacts/:id/read?offset&limit.
  • readArtifactChunk is removed. findArtifactByTitle and linkFileArtifact remain exported. README.md's Reference section describes the current routes and tools.

Two error classes are now exported from the package entry: VersionConflictError answers 409 when an expectedVersion is stale, and ArtifactEditError answers 400 when a revise is refused. Catch them to surface precise failures to your users.

License

LGPL-2.1-only

About

Persistent and versioned filestorage fully compatible with Corbits

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages