Agentifys // IDENTITY & VAULT

It acts as the right person, with their own secrets.

// a signed per-user identity the browser cannot forge, plus an encrypted vault that hands tools each user’s credentials without ever showing the model the raw value.

The assistant acts against a third-party system using the signed-in user own stored credential, and refuses a record outside their permissions.

// her token, her permissions. the same question from someone else gets a different answer.

TWO MODES

01OPEN MODE

The agent trusts the identity you pass as-is. Good for personalization: it greets people by name and reads their role and data. Simple to start.

02SIGNED MODE

You sign the identity server-side with HMAC-SHA256 over canonical JSON plus a fresh timestamp, valid for 300 seconds. Now it is tamper-proof, and no browser can impersonate a user.

WHAT SIGNING UNLOCKS

PRIVATE UPLOADS

Only a signed user can add and retrieve their own knowledge documents.

THE VAULT

Reading or writing per-user credentials requires a signed identity.

PER-USER MCP OAUTH

Connecting an MCP server as an individual user is gated behind a signature.

MY TASKS PANEL

Scheduled tasks are tied to a signed user, so no one sees another’s queue.

SIGN IT SERVER-SIDE

A few lines on your backend.

The signing secret lives in Settings, Identity. Keep it on the server, never in a VITE_ or NEXT_PUBLIC_ variable. Sign the identity, pass it to the widget, done.

import { createHmac } from "crypto";

// Canonical JSON, byte-for-byte what Agentifys verifies against:
//   json.dumps(ctx, sort_keys=True, separators=(",", ":"))   # ensure_ascii=True by default
// Three things diverge if you hand-roll this with JSON.stringify:
//   1. every non-ASCII char must become \uXXXX (an Arabic name signs wrong otherwise),
//   2. keys sort by Unicode CODE POINT, not UTF-16 code unit (differs above the BMP),
//   3. only integers are safe (Python renders 1e-7 as 1e-07).
function enc(v) {
  return JSON.stringify(v).replace(/[^ -~]/g,
    c => "\\u" + c.charCodeAt(0).toString(16).padStart(4, "0"));
}
const codePoints = (s) => Array.from(s, (c) => c.codePointAt(0));
function byCodePoint(a, b) {
  const A = codePoints(a), B = codePoints(b);
  for (let i = 0; i < Math.min(A.length, B.length); i++) if (A[i] !== B[i]) return A[i] - B[i];
  return A.length - B.length;
}
function canonicalJson(v) {
  if (Array.isArray(v)) return "[" + v.map(canonicalJson).join(",") + "]";
  if (v && typeof v === "object") {
    return "{" + Object.keys(v).sort(byCodePoint)
      .map((k) => enc(k) + ":" + canonicalJson(v[k])).join(",") + "}";
  }
  if (typeof v === "number" && !Number.isInteger(v)) {
    throw new Error("Sign integers only: " + v + " formats differently in Python. Send it as a string.");
  }
  return enc(v);
}

// Add _ts (unix seconds), sign the context WITHOUT _sig, then send { ...identity, _ts, _sig }.
// Valid for 5 minutes, and re-checked on every /v1/chat call.
function signUserContext(identity, secret) {
  const ctx = { ...identity, _ts: Math.floor(Date.now() / 1000) };
  ctx._sig = createHmac("sha256", secret).update(canonicalJson(ctx)).digest("hex");
  return ctx;
}

THE CREDENTIAL VAULT

ENCRYPTED AT REST

Every secret is Fernet-encrypted in the database. Caps of 50 credentials per user and 8 KB per value.

INJECTED, NOT EXPOSED

Reference a secret in tool auth as {{user.creds.name}}. It is substituted server-side. The raw value never reaches the model or the browser.

LIVE JWT PASSTHROUGH

Use {{user.auth_token}} to forward the user’s own token to your API, so the tool acts with exactly their permissions.

PROVISIONED SERVER-TO-SERVER

Your backend sets secrets with a signed PUT /v1/credentials. Names up to 64 chars, values up to 8 KB.

GO DEEPER// identify users →// per-user credentials →

Make it act as each user.

// signed identity is on Pro. the per-user vault is on Business.