@odla-ai/brand
v0.7.2
Published
Conversational brand-book builder for odla apps — document/image uploads onto odla-db files, agent-driven color exploration and palette proposals with human-gated approval, a zero-dep OKLCH/WCAG color engine, and a token compiler that turns an approved br
Maintainers
Readme
@odla-ai/brand
A conversational brand-book builder for odla apps — document and image uploads onto odla-db file storage, agent-driven color exploration with human-gated palette proposals, a zero-dependency OKLCH/WCAG color engine, and a token compiler that turns an approved brand book into @odla-ai/ui design tokens. The conversation itself rides @odla-ai/chat.
npm i @odla-ai/brandAgentic experiment. This package is built and maintained by AI agents working from bounded runbooks with human review. Review its documented guarantees before relying on it.
Ask the runbooks first. odla's operational procedures live in a database, not in this file:
npx @odla-ai/cli runbook ask "<question>"returns the current steps, and unlike anything written here it cannot be out of date. Use it before searching the web or working from memory. This README and the JSDoc in the shipped.d.tsare the version-matched API reference; a runbook is the procedure. Most tasks need an answer from both.
The shape
The package is a schema + rules + an agent skill + a color engine + a token compiler + a route factory — not a service you run.
Proposals are the human checkpoint. Brand direction, image/logo choices, typography, voice, and colors stay proposals in the conversation. Palette candidates are the executable path today: the agent explores them with deterministic OKLCH/WCAG math and writes an open
brand_proposal. A human approves, rejects, or requests a revision before any palette effect. The agent skill has no resolution tool: bothpropose_paletteandupdate_sectionpark open proposals. A current book member who is also a direct Clerk app owner/co-owner posts the exact reviewed snapshot to the guarded route. It consumes one centralhuman_exactbrand.approveuse and atomically writes the effect plus an immutablebrand_approval_receipt. The central receipt names the exact Registry app incarnation, so an approval from a purged project cannot resolve content in a same-id replacement. Review clients must retain one mutation id for each exact proposal digest + accepted/rejected decision across pending requests and retries. Proposal author names must come from a Registry-owned principal projection; mutable chat participants and raw ids are not identity presentation.Vision happens through a semantic bridge.
view_assetasks the trusted host for bytes by asset id and returns image/PDF blocks inside the tool result when the model supports them. The rules-scoped agent credential cannot read raw files, mint signed URLs, or writebrand_*rows. There is no raw or signed-URL fallback. Message attachment ids are revalidated against the exact live Brand book before they enter the prompt; every facet proposal may preserve the source asset's immutable byte snapshot.Brand child writes are worker-mediated. CEL permits members to read their sections, palettes, proposals, and approval receipts, but all writes are closed.
createBrandRoutesowns guarded human decisions and proxies private multipart uploads, short-lived signed downloads, and human effects. Agent tools call only the host-providedBrandAgentBridgeoperations inBRAND_AGENT_PROFILE.The upload surface is intentionally narrow. Raster PNG/JPEG/GIF/WebP and PDF inputs are supported. SVG active content and binary font files are not; typography remains an exact, reviewable family/style proposal.
Tokens compile to the @odla-ai/ui contract.
compileBrandTokensmaps swatch roles onto--ui-*names — always emitting every required token plus the accent-composing derived set, so a scoped preview island never keeps stale root composites — andrenderTokensCssemits theme-structured CSS (light, dark, invert). Dark is derived algorithmically and contrast re-tuned per token. A reviewed typography proposal may also carry boundedtokenOverrides; those--ui-*component and layout values are validated, stored with the approved section, and compiled into both published token maps.Zero runtime dependencies. The db client is injected structurally (a real
@odla-ai/dbAdminDbsatisfiesBrandDb);@odla-ai/aiis a types-only optional peer.Claude Designs are a first-class input. A design exported from Claude as standalone HTML is a self-contained bundle: a loader plus a base64 asset manifest plus the real document. Upload it as a
designasset and the worker parses it in the same write — the--ui-*tokens it declares (resolved throughvar()chains, so a vendored theme re-keyed by a brand block yields the brand's real colors), its typefaces, its configurable props, and its heading outline — and stores that digest on the row.designis the only kind whose content type may betext/html, andtext/htmlis refused for every other kind.
Quick start
import { createBrandRoutes, BRAND_SCHEMA, brandRules } from "@odla-ai/brand";
import { init } from "@odla-ai/db";
const db = init({ appId, adminToken, endpoint });
// One-time provisioning: POST BRAND_SCHEMA to /schema and brandRules() to
// /admin/rules (or use the brandIntegration descriptor with the odla CLI).
const routes = createBrandRoutes({
db,
appId,
authorize: verifyDirectCaller,
// Read-only: exact brand/brand.read/audience/app-incarnation assertion.
authorizeDiscussionReferences: verifyDiscussionReferenceAssertion,
// Optional test/edge fetch; it receives only a Brand-minted private URL.
fetchPrivateAsset: fetch,
authorizeCapability: verifyCurrentHumanAppOwner,
consumeHumanExact: async (input) =>
(await appsForBearer(input.req).consumeHumanExactAuthority(
input.appId,
{
capability: input.capability,
projectCapability: input.projectCapability,
effect: input.effect,
resource: input.resource,
actionDigest: input.actionDigest,
consumptionIdempotencyKey: input.consumptionIdempotencyKey,
},
)).receipt,
verifySourceAssetSnapshot: verifyPrivateObjectExactly,
});
export default {
async fetch(req: Request): Promise<Response> {
return (await routes(req)) ?? new Response("not found", { status: 404 });
},
};GET /api/brand/discussion-references?q=… searches only books on the
asserted principal's current roster and their live assets, palettes, proposals,
and receipts. Supplying kind + id performs exact lookup and returns no item
unless that row still exists in the named book. Returned links use stable
odla-ref=brand:kind/book-id[/resource-id] identities; the destination calls
parseBrandDiscussionReference to select the book and review surface. Each
projection includes a bounded product-authored summary, status, and
destination for people and read-only agent tools. Registry assertions bind
the exact brand product, brand.read, current canonical endpoint audience,
app incarnation, environment, and principal; an agent assertion also binds its
manager and live grant revision. The host must set its current
ODLA_APP_INCARNATION and fail closed on a mismatch. This assertion cannot
authorize proposal resolution or any other Brand mutation.
Palette projections additionally carry at most 24 validated semantic roles,
normalized #rrggbb colors, and bounded optional names so Discussion can use
the shared @odla-ai/ui PaletteStrip without reconstructing brand state.
An assertion-authorized exact lookup may request
inspect=asset-content&kind=brand:asset&id=…. Brand resolves the linked row,
mints its own short-lived private-object URL, and returns only bounded PNG,
JPEG, GIF, WebP, or PDF bytes—never the URL or a separate object path, storage
id, app id, or book id. Only the already-discovered canonical reference id
crosses the boundary. It validates type and magic, size, and SHA-256, then
reloads both
membership and the exact linked asset row after I/O. SVG, malformed, spoofed,
changed, unsupported, and over-budget assets fail closed. Returned bytes carry
untrusted_project_material taint for the hosted inference boundary; the
native link remains navigation only and human proposal approval is unchanged.
Resolve a proposal from a human review UI with one stable mutation id. Send
back exactly the proposal snapshot that UI rendered; a stale or concurrently
resolved snapshot answers 409, while an identical retry returns the original
receipt with duplicate: true.
await fetch(`/api/brand/books/${book.id}/proposals/${proposal.id}/resolve`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
mutationId: crypto.randomUUID(),
resolution: "accepted", // or "rejected"
reviewedProposal: proposal,
note: "Approved in palette review",
}),
});Wire the agent by attaching brandSkill to a persona (or let the chat-agent worker route a brandBotTrigger dispatch into dispatchBrandTurn):
import { createBrandPersona, supportsBrandVision } from "@odla-ai/brand";
const spec = inference.catalog[model];
const persona = createBrandPersona({
model,
brand: {
db: rulesScopedAgentDb,
bookId,
self: { selfId: botId, kind: "bot" },
agentDbBinding: { principalId: botId, credentialRef },
agentJobId: jobId,
agentBridge,
authorizeCapability: verifyLiveAgentGrant,
resolvePrincipals: resolveSafeDirectoryProjection,
visionInToolResults: supportsBrandVision(spec),
},
});
const run = await runAgent(inference, persona, { input: userMessage });Compile anywhere, including a host that wants only the zero-dependency token
engine. The ./tokens subpath does not load the agent, routes, or brand-book
schema:
import { compileBrandTokens, renderTokensCss } from "@odla-ai/brand/tokens";
const { light, dark, warnings } = compileBrandTokens({ swatches });
const css = renderTokensCss(light, { dark }); // :root / [data-theme="dark"] / media guardDesigns
Three resolutions of the same design, because none of them is right for every reader:
| surface | what it gives you |
|---|---|
| GET /books/:id/assets/:aid/preview | the design itself, proxied under Content-Security-Policy: sandbox allow-scripts. No allow-same-origin, so it renders in an opaque origin and cannot touch the host app's session, storage, or DOM. Proxied rather than redirected to storage, so the sandbox header is the app's to set. |
| GET /books/:id/assets/:aid/design | the stored digest as JSON — tokens, fonts, colors, props, outline, inventory. Kilobytes, not megabytes. |
| read_design, read_design_source, propose_palette_from_design | the agent's view: the digest as prose, a window of the real template for porting exact markup, and the design's own --ui-* declarations read back into a palette proposal. |
compileBrandTokens runs brand book → --ui-*. A design authored on the
@odla-ai/ui contract arrives with that
mapping's output already filled in, so swatchesFromDesignTokens inverts it —
built from the forward map's own role table, so the two cannot drift. Only
literal opaque colors convert; a token left as color-mix(…), a translucent
rgba(…), or an unresolved var(…) is reported, not guessed at, because a
fabricated hex would silently poison the contrast math. The result is an open
brand_proposal a human resolves through the same guarded route as any other.
For work outside the conversation, the CLI unpacks a bundle to disk with no account and no network:
npx odla-ai brand design unpack "Built Not Found Capital.html" --out site/design
# index.html the design, asset references rewritten to real files — runs offline
# assets/ every embedded font, image, and script, decoded (gzip inflated)
# pages/ nested page bundles, for multi-page designs
# digest.json tokens, props, outline, inventory
# tokens.css the design's tokens as an @odla-ai/ui theme sheet (light + dark)That directory is what you point a coding agent at: real files it can read and diff, instead of one megabyte of base64.
Authority and asset contract
- Install
BRAND_AGENT_PROFILE: agents receive onlybrand.readandbrand.edit, and may create proposals, record analysis, and read bounded asset content only through semantic host operations. Deny raw Brand writes, raw file reads, signing, and admin credentials. createBrandRoutesrequires all security seams.authorizederives the actor from a verified bearer;authorizeCapabilityrevalidates current app ownership;consumeHumanExactconsumes the exact reviewed action; andverifySourceAssetSnapshotrevalidates private path, ETag, size, media type, and SHA-256 immediately before approval. The returned authority receipt must carry the current 32-character app incarnation and is stored losslessly with the Brand decision.409means the review or authority conflicted,410means its authority or source is gone, and registry/storage/network outages remain opaque500s. Definite identity or object mismatches fail closed.
