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.
TWO MODES
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.
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
Only a signed user can add and retrieve their own knowledge documents.
Reading or writing per-user credentials requires a signed identity.
Connecting an MCP server as an individual user is gated behind a signature.
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
Every secret is Fernet-encrypted in the database. Caps of 50 credentials per user and 8 KB per value.
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.
Use {{user.auth_token}} to forward the user’s own token to your API, so the tool acts with exactly their permissions.
Your backend sets secrets with a signed PUT /v1/credentials. Names up to 64 chars, values up to 8 KB.
Make it act as each user.
// signed identity is on Pro. the per-user vault is on Business.