Skip to content

celesnity/Embinder

Repository files navigation

Embinder — the agent moves into your app.

Embinder

Drop a pointer on your components and the agent moves in — it sees the screen the user sees and operates it natively.
one-hook pointer SDK  ·  render-scoped agent context  ·  server-side approval gate  ·  no connector, no separate tool server

License MIT TypeScript context render-scoped gate server-side npx, no cloud e2e 36 passing status early preview

The idea  ·  Quick start  ·  The pointer  ·  The gate  ·  Compare  ·  FAQ

Note

Early preview (v0.1.0). npm run e2e runs 36 assertions green, covering the pointer lifecycle, the render-scoped context switch, bound state, grace semantics, and the full approval gate. APIs will still move. Issues and feedback very welcome.


💡 The idea

Every agent-on-app system drowns the model in "here are all 300 tools" — context bloat, wrong-tool selection, latency. And MCP-style integrations wire a separate agent to your app over a connector, blind to what the user is actually looking at.

Embinder makes the agent a resident of the app. The primitive is a pointer — the useEmbinder hook — attached to a UI element in your existing code. Attaching it does two things at once:

  • 🫧 Legibility — while that element is rendered, its existence, its function, and (optionally) the state it displays enter the agent's live context automatically. The agent sees what the user sees.
  • 🕹️ Actionability — the agent invokes the bound function directly. Real handlers, structured arguments — not pixel clicking.

And the part that matters most: context is scoped to the current display. Button on screen → the agent knows it. User navigates away → it drops out of context; whatever's on the new page loads in. The agent only ever holds the handful of actions in front of the user — smaller context, higher accuracy, lower cost, and it maps exactly to how a human uses software: you can only click what's in front of you.

⚡ Quick start

npm install

# tell the embedded runtime which model to use (key stays server-side too)
LLM_BASE_URL=http://127.0.0.1:1234/v1 LLM_MODEL=qwen2.5-7b-instruct npm run dev
#   app + resident agent bubble:  http://localhost:5173
#   approvals:                    http://127.0.0.1:7331/approve   ← keep on a second window

Open the app, click the ✦ bubble, and ask for something on the screen. Switch to the Archive page and watch the agent's world change with yours.

Tip

Prove the whole pipeline headlessly — no LLM, no browser needed:

npm run e2e      # ✅ E2E + GATE GREEN  (36 assertions)

🫧 The pointer

One call registers the capability while the component is mounted, anchors it to the element, and unregisters when it unmounts. That's the entire integration:

import { useEmbinder } from '@embinder/react';

const addBind = useEmbinder({
  name: 'add_task',
  description: 'Add a new task to the board',
  input: { text: z.string().describe('Task text') },
  handler: async ({ text }) => { dispatch({ type: 'ADD', text }); return { ok: true }; },
});

<button {...addBind} onClick={add}>Add</button>

A pointer can also expose what the element shows, so the agent reads the screen instead of calling list_* tools:

// context-only pointer: no handler → never a callable tool, purely legibility
const boardBind = useEmbinder({
  name: 'task_board',
  description: 'The task board the user is looking at',
  context: () => ({ openTasks }),   // live, read-only, debounced
});

<section {...boardBind}></section>

Each agent turn starts with exactly what's mounted right now — an "On-screen now" block with the capabilities and their bound state. Navigate, and the next turn sees the new page instead. Mid-task navigation is safe too: in-flight calls get a ~2 s grace window (quick tab flips survive), then fail with a clean capability left the screen error the agent can react to.

🧭 How it works

flowchart LR
    user["👤 User + resident agent<br/>✦ chat bubble in the app"]
    app["🫧 Your app<br/>@embinder/react · useEmbinder"]
    relay["⚙️ Embedded agent runtime<br/>LLM loop · policy gate · audit"]
    approve["✋ /approve<br/>out-of-band, off the app tab"]
    mcp["🤖 External MCP agents<br/>(optional path)"]

    user -->|"asks"| relay
    app <-->|"ws /app · register / context / call"| relay
    relay -->|"🔦 spotlight + lock"| app
    relay -.->|"destructive → pause"| approve
    mcp -.->|"http /mcp"| relay
Loading

Everything runs locally on a single port 127.0.0.1:7331. The relay is the app's embedded agent runtime: it hosts the LLM loop for the bubble (/chat, config from env via /chat-config), holds the live capability registry, and gates every call. No cloud, one npx.

📦 Packages

Package Role
@embinder/react App SDK. useEmbinder (the pointer), EmbinderProvider (ws shim + default-mounted resident bubble), driver.js action spotlight (feature-flagged, code-split).
@embinder/relay Embedded agent runtime: capability registry with render-scoped membership, /chat LLM loop, policy gate, approval surface, audit log, rate limit, token/origin hardening. Optional /mcp endpoint for external agents.
apps/todo Reference app — two pages (Board ⇄ Archive) whose point is watching the agent's context switch when you navigate.

Note

Package names shown as @embinder/* are the published target scope. This is an npx-first monorepo — clone it and run the commands; you don't need the packages off npm to try it.

⚙️ Configuration

Risk is authoritative in embinder.policy.json — it wins over the pointer's destructive flag, and unknown tools are deny-by-default.

{
  "unknownTool": "destructive",          // anything not listed → must be approved
  "tools": {
    "task_board":       "read",          // context-only pointer (never callable anyway)
    "add_task":         "write",         // pass through
    "restore_task":     "write",
    "delete_task":      "destructive",   // ⏸ pause at the gate
    "purge_archive":    "destructive"    // ⏸ pause at the gate
  },
  "rateLimit": { "perToolPerMin": 30 }
}

The bubble's model config lives in the relay's environment, next to the key: LLM_BASE_URL, LLM_MODEL, LLM_KEY. App code carries zero LLM config; an unconfigured relay shows a "connect a model" hint in the bubble.

🚦 The gate, seen

A resident agent operating real handlers makes governance more necessary, not less. When any agent — bubble or external — calls a destructive capability, the owning element is spotlit and locked, a popover shows the canonical args, and the call hangs until a human decides on /approve — a page the agent's tab can't reach.

sequenceDiagram
    participant Ag as ✦ Agent (bubble or MCP)
    participant Re as ⚙️ Runtime · gate
    participant App as 🫧 Your app
    participant You as 👤 You · /approve
    Ag->>Re: call purge_archive
    Re->>App: intent → spotlight + lock the button
    Re->>You: awaiting approval (canonical args)
    You-->>Re: Approve ✅ / Deny ⛔
    alt Approved
      Re->>App: execute
      App-->>Ag: { ok: true }
    else Denied
      Re-->>Ag: error — blocked by policy
    end
Loading

Every call is written to audit.jsonl with approver and latency. Bound state flowing into the model's prompt is delimited and labeled as display data — but the security boundary is the gate, not the prompt.

Important

Embinder is a governance layer, not a prompt-injection defense. The gate enforces least-privilege, out-of-band destructive-action approval, rate limits, and a tamper-evident audit trail — it reduces blast radius and gives you control and visibility. It does not stop tool poisoning or indirect prompt injection; those manipulate the agent's own reasoning, upstream of the runtime. Anyone selling "agent security" that stops injection is overselling — we won't.

🆚 How it compares

Traditional MCP CopilotKit Embinder
The agent lives outside, over a connector in your app in your app
Tools come from hand-written server in-app actions your live UI (one hook)
Agent's context holds the whole catalog registered actions only what's on screen
Agent reads app state extra read tools partial bound state, automatic
Human approval hints only client-side (skippable) server-side, out-of-band
Runtime your servers their runtime npx, local, no cloud

The agent that only knows what's on the screen — like a user, not like an API.

❓ FAQ

Is this MCP?

No — the product is the resident agent. The app and runtime speak their own minimal protocol (register / context / call / result over a local ws). An MCP endpoint (/mcp) is kept as an optional path if you deliberately want a separated agent (LM Studio, Claude, Cursor, Inspector) to drive the same capabilities through the same gate.

Why scope context to the screen?

Because flat tool catalogs don't scale: selection accuracy drops and cost grows with tool count. Render-scoping solves it structurally — the context is small by construction, and the pruning heuristic (what's on screen) is the one your app's designers already encoded when they decided what to show the user.

What happens if the user navigates mid-task?

In-flight calls get a ~2 s grace window — a quick remount re-delivers them; otherwise they fail fast with capability left the screen, pending approvals for the vanished capability are cancelled (audited as unmounted), and the agent's next turn sees the new page.

Do I need the cloud or an API key?

No. The runtime is local (127.0.0.1), and the bubble works with any OpenAI-compatible endpoint — including a fully local one like LM Studio. Keys and model config stay in the runtime's environment, never in app code or the browser.

Does the gate stop prompt injection / tool poisoning?

No — and we say so loudly (see the note above). Those attacks live in the agent's reasoning, upstream of the runtime. Embinder limits what an action can do and whether it runs at all, with an audit trail.

🛠️ Status

Pointer primitive, render-scoped context, bound state, grace semantics, resident bubble, and the full gate are implemented and verified — npm run e2e runs 36 assertions green. See BUILD_STATUS.md for the per-task map and docs/DEMO.md for the walkthrough.

🤝 Contributing

Early preview — issues, ideas, and PRs are welcome.

git clone https://github.com/celesnity/Embinder.git
cd Embinder && npm install
npm run e2e        # make sure the gate is green before you start
npm run dev        # runtime :7331 + todo :5173

Please keep npm run e2e green and add an assertion when you change pointer or gate behavior. Open an issue first for anything that touches the policy model or the approval surface.

🧱 Built with

@modelcontextprotocol/sdk · Vercel AI SDK · assistant-ui · driver.js · Vite · React · zod · express · ws

📄 License

MIT (intended). Reference sources under .references/ are third-party and not distributed.

Built in the open. The agent should live where the user lives — and you should always hold the pen. 🫧

About

Embinder makes the agent a resident of the app. Turn every traditional platform to AI-native platform

Resources

Stars

19 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors