Skip to content

Repository files navigation

Limen

Limen

live tests acceptance backend agent stack

Let agents act. Keep humans in control.

Base44 gives agents the ability to act. That creates a choice nobody wants: hand an AI permanent authority over your order system, or keep it read-only and useless. Limen removes the choice. An agent can ask for a sensitive action but never holds the authority to perform it — a human grants one action, on one record, for one amount, once, expiring in minutes, and a protected backend function enforces exactly that. Replay it, change the amount, revoke it, or let it expire, and the same function refuses, with a stable code and a hash-linked receipt.

Watch the demo ↗ · Live app ↗ · Try it in 60 seconds ↗ · The four refusals ↗ · Honesty table ↗ · Run it locally ↗

Built for the Base44 Dev Build-Off · Web app · Everything below is verifiable in this repository.


▶ The 2-minute demo

An agent proposes a refund, a human grants narrow authority, the refund executes — then the same backend refuses a replay, an altered amount and a revoked grant.

YTDown.com_YouTube_Limen-AI-agents-that-ask-permission-not-_Media_wHuu3_F0h3c_001_1080p.mp4

No player above? GitHub renders inline video only on github.com — watch it on YouTube ↗.


Table of contents


▶ Try it yourself in 60 seconds

Open limen.base44.app and sign in:

enochid200+limendemo@gmail.com
Threshold-Demo-8754!

Then: Reset demoOpen CASE-204Ask Nova to resolve → approve $42 in the control room → run the boundary tests.

No extension, no setup, no database edits. The account is scoped to the synthetic Northstar Goods workspace and cannot write a single record directly — attempting to forge an order, a grant or a receipt with these exact credentials is refused, and that refusal is itself part of the test suite (scripts/smoke-judge-path.ts).

Open the control room in a second window before asking Nova. The request appears there without a refresh — two independent surfaces, both reading persisted Base44 state.


The problem

Maya runs support operations at Northstar Goods. She built Nova, a Base44 agent, to resolve customer issues. Nova can read a case, understand a duplicate charge, and work out that a $42 refund is appropriate.

To finish the job, Nova needs a refund capability. Maya's options today are both bad:

  • Give the agent standing access to the refund operation — permanent, unlimited, for any order, forever.
  • Keep it read-only — and handle every routine $20–$50 request herself.

Base44's tool permissions answer "may this agent use this class of capability?". A business needs the runtime question answered: this exact action, on this exact record, for this amount, this many times, within this window — and revocable right now.


What Limen is

A B2B SaaS permission layer for teams running AI agents against real business systems.

The unit is not an AI response. It is an action grant:

Bound to Value in the demo
one agent nova_support
one action + resource refund.create on ORD-1042
an amount ceiling $42.00 — never above the request or the policy
a use limit 1
an expiry 5 minutes, server time
a payload hash the exact action, canonically hashed
an evidence digest the exact document the reviewer saw

Anything outside those bounds is refused by the same protected function that performs the legitimate action — and every outcome, allowed or refused, writes a receipt.


The lifecycle

sequenceDiagram
    autonumber
    participant C as Customer
    participant N as Nova (Base44 agent)
    participant P as proposeRefund
    participant M as Maya (reviewer)
    participant X as executeWithGrant
    participant DB as Base44

    C->>N: "I was charged twice for $42"
    N->>DB: read CustomerCase, Order, Policy
    Note over N: read-only tools — it cannot write
    N->>P: proposeRefund(CASE-204, ORD-1042, $42)
    P->>P: idempotency → resolve → bind payload hash
    P->>P: 14 deterministic checks (no model involved)
    P->>DB: ActionRequest = AWAITING_APPROVAL
    DB-->>M: appears live, no refresh
    M->>DB: approve $42 · 1 use · 5 min
    Note over M: may narrow, can never widen
    N->>X: executeWithGrant(attempted payload)
    X->>X: reload everything, recompute hash
    X->>X: compare-and-set the grant ← the guard
    X->>DB: Refund · Order REFUNDED · Case RESOLVED · Receipt
    DB-->>C: resolution appears live
Loading

Only backend functions may perform state transitions. The browser cannot change a refund, a grant, or a receipt — it can only ask a function to.


Nova's exact boundary

The entire security argument fits in one checked-in file: base44/agents/nova_support.jsonc.

Nova has Nova does not have
read on CustomerCase any write on any entity
read on Order approveActionRequest / denyActionRequest
read on Policy revokeGrant
proposeRefund access to Grant, Receipt, Execution, AuditEvent
executeWithGrant a raw refund function

Verified against the deployed agent — in a real conversation Nova used exactly:

read_CustomerCase(success) · read_Order(success) · read_Policy(success) · proposeRefund(success)
arguments: {"case_id":"CASE-204","order_id":"ORD-1042","amount":42,"currency":"USD"}

…and afterwards the order was still PAID, with no grant and no execution.

Be precise about what enforces this. A function invoked as an agent tool runs with the conversing human's session, so if approveActionRequest were listed above, Nova could approve as Maya. It is not listed. The allow-list is server-owned configuration, pushed from this repository and unreachable by the agent — a real control, but a configuration control, and the threat model says so.


The four refusals — the part that matters

A successful refund is table stakes. What makes Limen a security product is that the same protected function refuses these — each producing a stable code, an Execution row, and a receipt.

Attack Code returned Why it fails
Replay the grant GRANT_ALREADY_CONSUMED compare-and-set already flipped used_count to 1
Change $42 → $500 after approval PAYLOAD_MISMATCH recomputed payload hash ≠ the hash the grant is bound to
Use a revoked grant GRANT_REVOKED grant state is re-read at execution, not trusted from approval
Use an expired grant GRANT_EXPIRED server-time comparison, independent of any sweep
(bonus) Swap the evidence EVIDENCE_DIGEST_MISMATCH digest is re-read and compared to what was approved

All five are reachable from the Boundary tests drawer in the live app. Each button makes a genuine call to runBoundaryTest, which routes through the identical runExecution code path Nova uses — the drawer shows the expected code before the run and the actual code after, and flags a divergence rather than hiding it. A drawer that always printed the expected answer would prove nothing.


The concurrency guard

Base44 documents no transactions, no conditional updates, and no optimistic locking. So the design is built on something measured, not assumed.

updateMany accepts Mongo-style operators and reports how many records it changed — which makes it a compare-and-set:

updateMany(
  { id, status: "ACTIVE", used_count: 0 }, // only while unclaimed
  { $set: { status: "CONSUMED" }, $inc: { used_count: 1 } },
);
// updated === 1 → this caller won the race and may proceed
// updated === 0 → someone else already did; refuse

The guard commits before any other write. A caller that loses is refused before it can touch the order — ordering the writes any other way would allow a double refund under load.

Measured against the deployed app:

Test Result
4 simultaneous redemptions of one grant 1 EXECUTED, 3 GRANT_ALREADY_CONSUMED
Refund rows created exactly 1
used_count after 1
Raw primitive, 8-way concurrency, 3 runs 1 winner every time

What is not claimed: not atomic, not ACID, not exactly-once. This is an observed property of a single-record guarded update, not a documented platform guarantee. The honest statement is that concurrent redemptions are guarded by a compare-and-set, and acceptance test A16 demonstrates the outcome.


Binding an approval to an action

An approval is not "yes to a request id". It is a commitment to one exact action, expressed as a hash over a closed field set:

version · organization_id · action_type · resource_type
resource_id · amount_cents · currency · evidence_digest

Approval stores that hash on the grant. Execution rebuilds it from freshly loaded records and from what the caller is actually attempting, then compares. A substituted $500 produces a different hash and is refused — without any field-by-field comparison.

Three details make it hold:

  • Money is integer cents. Floating point would let two equal sums serialize differently. The test that matters: 42.104210, not 4209, which is what naive value * 100 gives.
  • Canonicalization is strict. Keys sorted at every depth; undefined omitted but null preserved; -0 normalized; Date, bigint and class instances rejected rather than coerced.
  • The field list is closed. Adding a column to an entity cannot silently widen or narrow what a grant is bound to.

Receipts you can recompute

receipt_hash = SHA-256( canonical(body) + previous_hash )

Every outcome writes one — executed, blocked, or denied. The Verify button does not compare two stored strings in the browser. It calls verifyReceipt, which reads the stored body, recomputes the hash server-side, and returns both values so the drawer can show them side by side.

Proven by tampering: editing a stored receipt directly made verification fail, the chain report broken, and the break located at the correct sequence number. Restoring the body restored validity.

Tamper-evident, not tamper-proof. Not externally signed, not distributed, not a blockchain. Someone able to rewrite the entire chain could produce a self-consistent forgery. What the chain gives you is that a partial edit is detectable.


Why each Base44 capability is load-bearing

Not decoration — remove any one and the product stops working. Full mapping with evidence: docs/base44-feature-map.md.

Capability What breaks without it
Authentication No reviewer identity. An approval could not be attributed, and the grant's reviewer_user_id would be a guess.
Database / entities The browser could forge a grant. create: false on Grant, Execution, Receipt and AuditEvent is what makes "only functions may write" true — verified by attempting exactly those forgeries.
Backend functions (Deno) There would be no protected path. executeWithGrant is the only code that can move money.
AI / agents The proposal would come from a button, not an agent. Bounding what an agent may do requires a real agent with real tools.
Realtime subscriptions The handoff between Nova's console and Maya's control room would be a page refresh.
File & media storage Nothing to bind a digest to. The "evidence changed after approval" refusal needs a real document with a real hash.

Engineering decisions & the hard problems

Pure domain logic, injected time. Canonicalization, policy evaluation, grant narrowing, the execution recheck, receipt hashing and explanation validation are all pure functions — no I/O, no clock, nowMs passed in. That is why 202 tests can cover the security model without deploying anything, and why any refusal can be reasoned about by reading one file.

The model explains; code decides. The reviewer explanation is validated against the record and rejected — not repaired — if it states an amount or an order that isn't there. Stripping an invented number would leave a plausible sentence a reviewer might trust. Writing that validator produced its own bug: the first regex read 1042 out of ORD-1042 and rejected valid output; it now strips identifiers and matches only currency-marked figures.

Three-tier AI with an honest fallback. Base44 InvokeLLM first (reproducible by anyone who deploys this repo), OpenAI if a key exists, then a deterministic summary flagged ai_generated: false. The UI says "AI explanation unavailable" rather than passing the fallback off as generated text.

A dropped connection is not a refusal. A network failure is classified retry, never blocked. Rendering an offline browser as "grant revoked" would be a lie in a product whose entire value is truthful refusals — and a test catches it, because the first implementation got this wrong.

Refusal codes are ordered deliberately. After a successful execution the request reads EXECUTED, and after revocation it reads REVOKED — so checking request status first returned REQUEST_NOT_APPROVED for both, burying the real reason. Grant state now takes precedence, with regression tests pinning it.

Nothing after the grant is transactional. Grant, refund, order, case, receipt and audit are separate writes. The guard commits first and every later step is guarded or idempotent, so a mid-sequence failure cannot produce a second refund — but there is no rollback, and that is stated rather than glossed.


What's real vs simplified — the honesty table

Capability Status
Real Base44 agent Real. nova_support with four tools, verified making genuine tool calls. Not a simulator.
The four refusals Real. Produced by the same protected function Nova calls, each writing an Execution row and a receipt.
Concurrency guard Real, and measured — 4 simultaneous redemptions, 1 winner, 1 refund row. Not claimed as atomic.
Protected entities Real. Grant.create, Receipt.create, AuditEvent.create, Order.update, Order.delete all denied to a signed-in caller.
Evidence digest binding Real. Digest computed server-side from the stored bytes; swapping the document refuses execution, restoring it lets the same grant succeed.
Receipt verification Real. Recomputed server-side; detects an edit and locates the break.
Realtime subscriptions Implemented and deployed — entities.subscribe() on two independent surfaces. Not yet verified by a human in two browser windows.
Read isolation Absent. Business entities are readable by any authenticated user; tenant scoping lives in function queries, not RLS.
Multi-user session isolation Unverified. A second Base44 identity could not be created programmatically, so role and tenancy tests reassign the existing user's membership.
Scheduled grant expiry Not scheduled. This app cannot use automations ("This app uses Workflows — legacy automations are disabled"). Expiry is enforced at redemption on server time; the reconciliation sweep runs on demand.
Money movement Simplified. A synthetic Base44 order is updated. No payment provider, no real funds.
Evidence document Synthetic. Generated deterministically by scripts/generate-evidence-pdf.mjs — no real person, account or card.
Security review None. This is a hackathon build.
Demo video Not yet recorded.

Tests

npm run verify                                    # format, types, lint, 202 tests, build
deno check --no-lock base44/functions/*/entry.ts  # Deno-side typecheck

Unit tests cover the security model without a network. The scripts in scripts/ then verify the same properties against the deployed app, asserting persisted state rather than response bodies.

ID Scenario Result
A1 Nova proposes a valid $42 refund ✅ real agent tool call
A2 Approve valid request ✅ one active, one-use grant
A3 Execute valid grant ✅ refund, order REFUNDED, case RESOLVED
A4 Replay the consumed grant GRANT_ALREADY_CONSUMED
A5 Nova proposes $500 AMOUNT_EXCEEDS_POLICY
A6 Change amount after approval PAYLOAD_MISMATCH
A7 Revoke then execute GRANT_REVOKED
A8 Execute past expiry GRANT_EXPIRED, before any sweep ran
A9 Duplicate idempotency key ✅ original returned, no duplicate
A10 Agent lacks the capability ✅ tool surface verified
A11 Reviewer from another organization ✅ request invisible across tenants
A12 AI explanation fails ✅ deterministic fallback, labelled
A13 Recompute receipt hash ✅ matches, and detects an edit
A14 Upload synthetic evidence ✅ digest bound and rechecked
A15 Two surfaces update via subscriptions ⏳ implemented, awaiting the recorded run
A16 Two simultaneous executions ✅ 1 winner, 1 refund row
A17 Unauthenticated / customer approval ✅ refused
A18 Clean-browser production run ⏳ deployed, awaiting the recorded run

Reproduce any of them:

cat scripts/smoke-concurrency.ts     | npx base44 exec --privileged   # A16
cat scripts/smoke-evidence-binding.ts | npx base44 exec --privileged  # A14
cat scripts/smoke-authorization.ts   | npx base44 exec --privileged   # A11, A17
cat scripts/smoke-nova.ts            | npx base44 exec --privileged   # A1, A10
cat scripts/smoke-judge-path.ts      | npx base44 exec                # the judge's own path

Run it locally

Requires Node ≥ 20.19, a Base44 account, and Deno (for base44 exec).

npm install
npx base44 login
npx base44 link                   # link to your own Base44 app

npx base44 entities push --yes
npx base44 agents push --yes
npx base44 functions deploy

echo "VITE_BASE44_APP_ID=$(node -e "console.log(JSON.parse(require('fs').readFileSync('base44/.app.jsonc','utf8').replace(/^\s*\/\/.*$/gm,'')).id)")" > .env

cat scripts/seed-demo.ts       | npx base44 exec --privileged   # Northstar, Maya, CASE-204, ORD-1042
node scripts/generate-evidence-pdf.mjs
cat scripts/attach-evidence.ts | npx base44 exec                # upload + bind the digest

npm run dev

Reset the demo from the UI, or:

echo 'await base44.functions.invoke("resetDemo", {})' | npx base44 exec

Reset archives rather than deletes — receipts and audit events from previous runs are kept.


Project layout

base44/
  entities/          16 schemas; business entities deny client writes
  agents/            Nova's tool allow-list — the security boundary, in config
  functions/         11 Deno functions — every state transition
  shared/            pure domain logic: canonicalize, policy, payload, receipts
  shared/server/     the only code that touches the SDK
src/
  lib/base44/        typed client, API layer, error mapping
  hooks/             realtime subscriptions, session, Nova conversations
  features/          landing · demo theater · support console · control room
scripts/             probes and smoke tests against the deployed app
docs/                architecture · threat model · capability map · probes · feedback
tests/unit/          202 tests

Docs

Document What's in it
architecture.md the transaction, layering, concurrency guard, deployment
threat-model.md 19 threats with evidence, and the limitations section
base44-feature-map.md every capability → path → test → acceptance criterion
platform-probes.md measured Base44 behaviour, including where docs were wrong
baas-feedback-log.md 12 reproducible findings from the build
demo-script.md shot-by-shot for the video

Limen was created during the Base44 Dev Build-Off as a new Base44-native application. It draws conceptual inspiration from earlier experiments in scoped permissions, guarded execution, proof-carrying receipts and integrity verification — but its product, data model, agent, workflow and interface were built for this challenge.

The demo changes only synthetic Base44 records. It does not move real money, and its receipt chain is not a blockchain.

limen — Latin for threshold: the boundary an action must cross to gain authority.

About

Base44-native action control for AI agents—scoped approvals, one-use grants, protected execution, instant revocation, and tamper-evident receipts.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages