Skip to main content

Developers

Live in an afternoon

ArtifactAuth is a REST/JSON API. Authenticate with your API key and name the end-user you're acting for with the x-aaa-principal-id header on each request. We gate every read against that user's clearance, resolved live against your own policy engine. Here's the whole loop, end to end.

How it works

Four steps, end to end

1

Create your account and API key

Sign up and generate a key in the console. Accounts and keys live there, never on the public API.

2

Your tools report what they read

Each MCP tool you control needs to return an access receipt per artifact it read — carrying the capability its source system required — in MCP’s standard _meta field, under the ArtifactAuth key artifactauth.com/receipts.

3

Your harness accumulates the access receipts

A conforming harness lifts those access receipts off the _meta of every tool result and gathers them across the turn — you never hand-build the receipts array.

4

Record the result with its access receipts

Send the finished LLM turn and its accumulated access receipts to ArtifactAuth. We label it soundly, so it stays searchable and assemblable later — always filtered to what the calling principal is cleared to see.

Examples

record() a segment, then search() across them

Record a labeled segment

Send a turn with its access receipts. ArtifactAuth computes the security label from the receipts and propagates it soundly, so the client never asserts what a segment is worth.

curl -X POST https://api.artifactauth.com/v1/conversations/c1/segments \
  -H 'authorization: Bearer aaa_sk_...' \
  -H 'x-aaa-principal-id: alice' \
  -H 'content-type: application/json' \
  -d '{
    "role": "tool_result",
    "content": "Q3 booked sales were ...",
    "receipts": [{ "resource_id": "sales-q3",
      "requirement": { "cap": "cap://acme/booked-sales" } }]
  }'

Search shared memory

Query across the account's conversations. Hits are relevance-ranked, then admission-filtered live against the calling principal, so anything above their clearance never comes back.

curl -X POST https://api.artifactauth.com/v1/search \
  -H 'authorization: Bearer aaa_sk_...' \
  -H 'x-aaa-principal-id: carol' \
  -H 'content-type: application/json' \
  -d '{"query":"Q3 booked sales in the enterprise segment","top_k":10}'
# relevance-ranked hits, filtered live to what carol is cleared to see

Returning hits isn’t injecting them

A search() or MCP retrieval call can match many segments. Handing that list back to your agent is not the same as pouring every hit into the model’s context. Keep an intermediary step in between: surface the ranked, admission-filtered results and let a person (or the agent, deliberately) pick what’s actually relevant, then assemble() only those into context. It keeps the window intentional and small, so a broad query can never silently balloon what the model sees.

API surface

Small on purpose

record()

POST /v1/conversations/:id/segments labels a segment with its access receipts, propagates labels soundly, then signs and persists.

search() & history()

POST /v1/search and /v1/history/assemble give you relevance-ranked, admission-filtered retrieval across all of an account's conversations.

assemble()

POST /v1/conversations/:id/assemble returns only the segments a principal is cleared for right now, ready to drop into a model context.

MCP tools

Let tools carry their own authorization

An agent's tools are where protected artifacts enter the conversation, so that's where the authorization should be reported. A brokered MCP tool annotates its output with the requirement its source system enforced; a conforming harness lifts that annotation into an access receipt automatically. You never hand-build the receipts array — the tool that did the fetch is the one thing that actually knows what the fetch was worth.

Annotate the tool output

Receipts ride in MCP’s standard _meta field. artifactauth.com/receipts is ArtifactAuth’s own key, namespaced per MCP’s prefix/name rules (only mcp and modelcontextprotocol prefixes are reserved). Attach one receipt per artifact the call read; the requirement is whatever the source system enforced: a capability for protected data, true for a public read, false to fail closed when you can’t tell.

// @modelcontextprotocol/sdk — a tool that fetches a protected artifact
server.tool("get_account", { id: z.string() }, async ({ id }) => {
  const acct = await crm.getAccount(id, principal)   // your source of truth

  return {
    content: [{ type: "text", text: JSON.stringify(acct) }],
    _meta: {
      // one receipt per artifact this call read. requirement is the
      // authorization the CRM enforced for the fetch — use `true` for a
      // genuinely public read, `false` to fail closed when unsure.
      "artifactauth.com/receipts": [{
        resource_id: `crm:account:${id}`,
        requirement: { cap: `cap://acme/account/${id}/read` },
      }],
    },
  }
})

The harness records it for you

The harness reads the annotation off each tool result and passes it straight to record(). The tool_result segment is labeled with the join of its requirements, and any assistant message derived from it inherits that label soundly. A tool that returns protected content with no receipt is fail-closed to deny-all, so annotating is how a tool’s reads become usable memory instead of dead weight.

const result = await mcp.callTool(name, args)

await aaa.record(conversationId, {
  role: "tool_result",
  content: textOf(result.content),
  // pulled straight off the tool's annotation
  receipts: result._meta?.["artifactauth.com/receipts"] ?? [],
})
// The tool_result is now labeled all[...requirements]; any assistant
// message derived from it inherits that label soundly. A tool that
// returns protected content with no receipt is fail-closed to deny-all.

Bring your own authorization

Connect your policy engine

The first thing you wire up is the Policy Decision Point, a single endpoint that resolves a principal's capabilities. Adapter templates ship for Okta/Entra groups, LDAP/AD, SCIM, OPA, Cedar, and AWS IAM. This pull model is the default and works for enumerable RBAC and non-enumerable ABAC alike.

Push authorities

Take your PDP off the read path

When a principal's grants are enumerable (Okta/Entra groups, SCIM, an RBAC table), you don't need a live call per read. Switch the authority to push mode in the console and have your identity-sync job PUT the materialized grant set whenever it changes. Reads then evaluate that snapshot locally: no per-request round-trip, so your PDP's uptime and tail latency stay off the hot path. Pull stays the default and the only option for attribute-based (ABAC) policies.

Push a versioned grant snapshot

The full grant table for one authority, keyed by principal. It replaces whatever was stored, so a group-membership change just re-pushes the affected account. Needs a key with the authz:manage scope, which is distinct from read/write, so a content key can never rewrite authorization.

curl -X PUT https://api.artifactauth.com/v1/authorities/employees/grants \
  -H 'authorization: Bearer aaa_sk_...' \
  -H 'content-type: application/json' \
  -d '{
    "version": 42,
    "grants": {
      "alice": ["cap://acme/booked-sales", "cap://acme/legal"],
      "bob":   ["cap://acme/booked-sales"]
    }
  }'
# -> 200 { "authority_key": "employees", "version": 42,
#          "principals": 2, "updated_at": "..." }
# Reads on this authority now resolve against the snapshot, so your PDP is
# off the hot path. Re-PUT with a higher version whenever grants change;
# an equal-or-older version is rejected (409), so a late sync can't regress.

What you trade for the latency

A push authority is definite: once a snapshot exists it answers permit or deny, never unknown. A principal absent from the snapshot holds nothing. Revocation and newly-granted access take effect on the next push, instant for a webhook-driven sync, a batch interval otherwise. Need instant new-principal grants or attribute-based rules? Stay on pull. You choose per authority, so most accounts run both.