@invokehq/cli 0.2.2 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,887 @@
1
+ # Invoke
2
+
3
+ Real-world execution for AI agents.
4
+
5
+ Invoke sits between AI agents and production tools so agent actions do not become wrong CRM updates, duplicate charges, stale approvals, or impossible debugging sessions.
6
+
7
+ Agents can reason. Production breaks when they execute.
8
+
9
+ Invoke turns every tool call into a controlled execution:
10
+
11
+ - validate schema, scope, and policy
12
+ - block wrong-entity actions before they touch the tool
13
+ - retry safe failures without custom glue
14
+ - reconcile unknown outcomes before retrying
15
+ - prevent duplicate side effects with idempotency
16
+ - freeze risky work for approval, then revalidate before execution
17
+ - trace what happened end to end
18
+
19
+ ## Quick Start
20
+
21
+ ```bash
22
+ # 1. Install the CLI
23
+ npm install -g @invokehq/cli
24
+
25
+ # 2. Authenticate to your Invoke runtime
26
+ invoke login --base-url https://agentgate-ai.onrender.com --api-key inv_live_...
27
+
28
+ # 3. Scaffold an execution project
29
+ invoke init support-agent --template crm-guardrail
30
+ cd support-agent
31
+
32
+ # 4. Run its local MCP server
33
+ invoke dev
34
+
35
+ # 5. Register its tools with Invoke
36
+ invoke deploy
37
+
38
+ # 6. Call a tool through the execution layer
39
+ invoke call crm_update_customer '{"customer_id":"cust_123","account_status":"review"}'
40
+ ```
41
+
42
+ Then call tools through one execution layer:
43
+
44
+ ```ts
45
+ import { Invoke } from "./sdk";
46
+
47
+ const invoke = Invoke.fromEnv();
48
+
49
+ await invoke.call({
50
+ tool: "linear.create_issue",
51
+ params: {
52
+ team_id: "team_123",
53
+ title: "Investigate webhook drift",
54
+ },
55
+ agentId: "prod-support-agent",
56
+ idempotencyKey: "linear:Investigate webhook drift:team_123",
57
+ });
58
+ ```
59
+
60
+ That's it. Your agent still chooses the action. Invoke makes the execution reliable: scoped, checked, retried, reconciled, approved, and traced.
61
+
62
+ ## What You Can Build
63
+
64
+ Invoke ships with wrapper generation for common production tools:
65
+
66
+ | Command | What it creates |
67
+ | --- | --- |
68
+ | `invoke wrap github` | GitHub MCP wrapper with approval-ready issue creation metadata |
69
+ | `invoke wrap linear` | Linear issue workflow wrapper with idempotency hints |
70
+ | `invoke wrap notion` | Notion page/document wrapper with Invoke capability metadata |
71
+ | `invoke wrap postgresql --query "SELECT ..."` | Scoped PostgreSQL query tool with inferred input schema |
72
+ | `invoke wrap billing-api --openapi openapi.json --base-url https://billing.example.com` | OpenAPI-backed wrapper for an internal service |
73
+
74
+ Or start from an existing MCP server and register its capability card with Invoke:
75
+
76
+ ```bash
77
+ invoke init billing-agent --template default
78
+ cd billing-agent
79
+ # run a local MCP endpoint, or edit invoke.json to point at your hosted MCP URL
80
+ invoke dev
81
+ invoke deploy
82
+ ```
83
+
84
+ ## See The Aha Moment
85
+
86
+ Run the local failure demo:
87
+
88
+ ```bash
89
+ ./demo/run_demo.sh
90
+ ```
91
+
92
+ It starts mock tools and shows six production failure modes:
93
+
94
+ - tool timeout recovered with bounded retry
95
+ - payment timeout reconciled before a duplicate charge
96
+ - wrong CRM update blocked before the record is touched
97
+ - duplicate retry replayed instead of creating a second issue
98
+ - stale approval requeued after live state changed
99
+ - webhook inconsistency returned as `replan_required`
100
+
101
+ The buyer takeaway is simple:
102
+
103
+ ```text
104
+ Invoke does not make agents smarter.
105
+ It makes their execution bounded when production gets messy.
106
+ ```
107
+
108
+ ## How It Works
109
+
110
+ ```text
111
+ Agents call tools Invoke controls execution Production systems
112
+ +----------------+ +------------------------+ +------------------+
113
+ | SDK / HTTP | | Scope + schema check | | Linear |
114
+ | MCP clients |----->| Retry + reconciliation |----->| Slack |
115
+ | workflows | | Approval + revalidate | | CRM / billing |
116
+ | background jobs|<-----| Trace + outcome |<-----| Internal APIs |
117
+ +----------------+ +------------------------+ +------------------+
118
+ ```
119
+
120
+ Define - Wrap a service with `invoke wrap` or register an existing MCP tool with a capability card, schema, risk level, and retry/idempotency hints.
121
+
122
+ Execute - Agents call `/call` through the SDK or HTTP. Invoke validates the request, checks scope, classifies the action, and routes it to the right tool.
123
+
124
+ Control - If the tool times out, partially succeeds, or returns an unknown outcome, Invoke reconciles current state before retrying. If the action is risky, Invoke freezes it for approval and revalidates live state before execution.
125
+
126
+ Trace - Every call gets a structured execution record your team can inspect, export, and debug.
127
+
128
+ ## SDK And API
129
+
130
+ ### 1. Get an API key
131
+
132
+ Ask for an Invoke API key, then export it in your shell:
133
+
134
+ ```bash
135
+ export INVOKE_API_KEY="inv_or_ag_live_..."
136
+ export INVOKE_BASE_URL="https://agentgate-ai.onrender.com"
137
+ ```
138
+
139
+ Every API request uses:
140
+
141
+ ```text
142
+ X-API-Key: $INVOKE_API_KEY
143
+ ```
144
+
145
+ ### 2. Check available tools
146
+
147
+ This confirms your key works and shows the tools Invoke can route to.
148
+
149
+ ```bash
150
+ curl "$INVOKE_BASE_URL/v1/tools" \
151
+ -H "X-API-Key: $INVOKE_API_KEY"
152
+ ```
153
+
154
+ ### 3. Retrieve live context with Exa
155
+
156
+ This is what we mean by `curl /v1/search`: your agent asks Invoke for fresh docs or web context before acting. Invoke calls Exa server-side and returns normalized sources plus a trace.
157
+
158
+ ```bash
159
+ curl -X POST "$INVOKE_BASE_URL/v1/search" \
160
+ -H "Content-Type: application/json" \
161
+ -H "X-API-Key: $INVOKE_API_KEY" \
162
+ -d '{
163
+ "query": "latest MCP agent failures",
164
+ "limit": 3
165
+ }'
166
+ ```
167
+
168
+ ### 4. Create a safe execution
169
+
170
+ This is what we mean by `curl /v1/executions`: your agent asks Invoke to run a workflow with an idempotency key. If the request is repeated after a timeout, Invoke replays the completed execution instead of creating duplicate side effects.
171
+
172
+ ```bash
173
+ curl -X POST "$INVOKE_BASE_URL/v1/executions" \
174
+ -H "Content-Type: application/json" \
175
+ -H "X-API-Key: $INVOKE_API_KEY" \
176
+ -H "Idempotency-Key: demo-charge-001" \
177
+ -d '{
178
+ "workflow": "safe-tool-execution",
179
+ "agent_id": "revops_agent",
180
+ "input": {
181
+ "params": {
182
+ "customer_id": "cust_acme",
183
+ "amount": 2400,
184
+ "currency": "usd"
185
+ }
186
+ }
187
+ }'
188
+ ```
189
+
190
+ The response includes a durable execution object:
191
+
192
+ ```json
193
+ {
194
+ "success": true,
195
+ "execution": {
196
+ "execution_id": "exec_142",
197
+ "workflow_id": "safe-tool-execution",
198
+ "status": "completed",
199
+ "idempotency_key": "demo-charge-001",
200
+ "final_outcome": "completed",
201
+ "trace": [
202
+ {"step": "request_received", "status": "completed"},
203
+ {"step": "unknown_outcome_reconciled", "status": "completed"},
204
+ {"step": "duplicate_retry_blocked", "status": "completed"}
205
+ ]
206
+ }
207
+ }
208
+ ```
209
+
210
+ ### 5. Use the SDK
211
+
212
+ Python:
213
+
214
+ ```python
215
+ from sdk import Invoke
216
+
217
+ invoke = Invoke.from_env()
218
+
219
+ context = invoke.search("latest MCP agent failures", limit=3)
220
+ print(context["results"][0]["url"])
221
+
222
+ execution = invoke.execute(
223
+ workflow="safe-tool-execution",
224
+ agent_id="revops_agent",
225
+ idempotency_key="demo-charge-001",
226
+ input={
227
+ "params": {
228
+ "customer_id": "cust_acme",
229
+ "amount": 2400,
230
+ "currency": "usd",
231
+ }
232
+ },
233
+ )
234
+
235
+ print(execution["execution"]["execution_id"])
236
+ print(execution["execution"]["final_outcome"])
237
+ ```
238
+
239
+ TypeScript:
240
+
241
+ ```ts
242
+ import { Invoke } from "./sdk";
243
+
244
+ const invoke = Invoke.fromEnv();
245
+
246
+ const context = await invoke.search("latest MCP agent failures", { limit: 3 });
247
+ console.log(context.results);
248
+
249
+ const execution = await invoke.execute({
250
+ workflow: "safe-tool-execution",
251
+ agentId: "revops_agent",
252
+ idempotencyKey: "demo-charge-001",
253
+ input: {
254
+ params: {
255
+ customer_id: "cust_acme",
256
+ amount: 2400,
257
+ currency: "usd",
258
+ },
259
+ },
260
+ });
261
+
262
+ console.log(execution.execution);
263
+ ```
264
+
265
+ ### 6. Run packaged workflows
266
+
267
+ The CLI wraps the same API:
268
+
269
+ ```bash
270
+ invoke search "latest MCP agent failures"
271
+ invoke workflow safe-tool-execution
272
+ invoke workflow live-context-retrieval --query "latest OpenAI MCP auth changes before deploying"
273
+ invoke workflow failure-trace-visualization
274
+ ```
275
+
276
+ The workflow response includes a buyer-readable `visual_flow` and a structured `trace`, for example:
277
+
278
+ ```text
279
+ request_received -> context_retrieved -> risk_scanned -> tool_authorized -> execution_completed
280
+ ```
281
+
282
+ ### 7. Call a tool directly
283
+
284
+ ```bash
285
+ curl -X POST "$INVOKE_BASE_URL/v1/call" \
286
+ -H "Content-Type: application/json" \
287
+ -H "X-API-Key: $INVOKE_API_KEY" \
288
+ -d '{
289
+ "tool": "fetch.url",
290
+ "params": {"url": "https://github.com"},
291
+ "agent_id": "research_agent_v1"
292
+ }'
293
+ ```
294
+
295
+ ## Deploy on Render
296
+
297
+ This repo is ready to deploy to Render as a Docker web service.
298
+
299
+ ### 1. Push this repo to GitHub
300
+
301
+ Render deploys from your Git branch, so make sure the latest backend code is pushed first.
302
+
303
+ ### 2. Create the service from `render.yaml`
304
+
305
+ In Render, create a new Blueprint and point it at this repo. The checked-in [render.yaml](render.yaml) provisions:
306
+
307
+ - a Docker web service named `invoke-api`
308
+ - a health check at `/health`
309
+ - env vars for Invoke plus Supabase-backed persistence
310
+
311
+ ### 3. Fill the required secrets
312
+
313
+ Render will prompt for:
314
+
315
+ - `INVOKE_API_KEYS`: comma-separated API keys allowed to call Invoke
316
+ - `INVOKE_PUBLIC_URL`: your deployed URL, such as `https://invoke-api.onrender.com`
317
+ - `INVOKE_ALLOWED_ORIGINS`: comma-separated frontend origins allowed to call the API, such as `https://invoke.vercel.app`
318
+ - `INVOKE_ALLOWED_ORIGIN_REGEX`: optional regex for preview deployments, such as `https://.*\\.vercel\\.app`
319
+ - `SUPABASE_URL`: your Supabase project URL, for example `https://xyzcompany.supabase.co` (the backend also tolerates a full `/rest/v1` URL if you already copied that)
320
+ - `SUPABASE_SERVICE_ROLE_KEY`: your Supabase service role key
321
+ - `LINEAR_API_KEY`: Linear API key for real issue creation
322
+ - `LINEAR_TEAM_ID`: optional default Linear team UUID for hosted demos
323
+ - `SLACK_BOT_TOKEN`: Slack bot token for real channel listing and message posting
324
+ - `SLACK_DEFAULT_CHANNEL`: optional default Slack channel ID for hosted demos
325
+ - `EXA_API_KEY`: Exa API key for `/search` and `invoke search`
326
+
327
+ ### 4. Create the Supabase tables
328
+
329
+ Open the Supabase SQL editor and run [supabase/schema.sql](supabase/schema.sql).
330
+
331
+ That creates the tables Invoke uses for:
332
+
333
+ - connected providers
334
+ - provider keys
335
+ - dynamic tools
336
+ - pending approvals
337
+ - tool-call traces
338
+ - execution records
339
+
340
+ If Render logs `supabase_schema_missing` or a PostgREST `404` for
341
+ `invoke_providers`, the API is alive but persistence is not installed yet. Run
342
+ the schema file above in the same Supabase project used by `SUPABASE_URL`.
343
+
344
+ ### 5. Verify the deployment
345
+
346
+ Health check:
347
+
348
+ ```bash
349
+ curl https://YOUR-SERVICE.onrender.com/health
350
+ ```
351
+
352
+ Tool registry:
353
+
354
+ ```bash
355
+ curl https://YOUR-SERVICE.onrender.com/tools \
356
+ -H "X-API-Key: YOUR_API_KEY"
357
+ ```
358
+
359
+ ### Notes
360
+
361
+ - If `SUPABASE_URL` and `SUPABASE_SERVICE_ROLE_KEY` are set, Invoke stores runtime state in Supabase instead of local SQLite files.
362
+ - `TRACE_DB`, `TRACE_LOG_FILE`, and `TRACE_EVENTS_FILE` still exist as local fallbacks for development and emergency startup.
363
+ - Keep the Supabase service role key server-side only. Do not expose it in browser code or client SDKs.
364
+ - If a Vercel frontend calls the Render backend directly from the browser, set `TRACE_ALLOWED_ORIGINS` and, if you use preview deploys, `TRACE_ALLOWED_ORIGIN_REGEX`.
365
+ - For Slack, the bot token needs `chat:write` to send messages and `channels:read` / `groups:read` if you want the frontend to list channels.
366
+
367
+ ### Vercel Frontend -> Render Backend
368
+
369
+ This repo does not include a Next.js or Vercel frontend yet, but the backend is ready for one.
370
+
371
+ Frontend env vars:
372
+
373
+ ```bash
374
+ NEXT_PUBLIC_INVOKE_BASE_URL=https://invoke-ai.onrender.com
375
+ NEXT_PUBLIC_INVOKE_API_KEY=YOUR_PUBLIC_DEMO_KEY
376
+ ```
377
+
378
+ Backend env vars on Render:
379
+
380
+ ```bash
381
+ INVOKE_PUBLIC_URL=https://invoke-ai.onrender.com
382
+ INVOKE_ALLOWED_ORIGINS=https://your-frontend.vercel.app
383
+ INVOKE_ALLOWED_ORIGIN_REGEX=https://.*\\.vercel\\.app
384
+ ```
385
+
386
+ For server-side calls from a Vercel route handler or server action, use the existing SDK env names instead:
387
+
388
+ ```bash
389
+ INVOKE_BASE_URL=https://invoke-ai.onrender.com
390
+ INVOKE_API_KEY=YOUR_SERVER_SIDE_KEY
391
+ ```
392
+
393
+ ## Build A Tool Wrapper
394
+
395
+ Create launch connector wrappers:
396
+
397
+ ```bash
398
+ invoke wrap github
399
+ invoke wrap notion
400
+ invoke wrap linear
401
+ ```
402
+
403
+ Wrap a PostgreSQL query:
404
+
405
+ ```bash
406
+ invoke wrap postgresql \
407
+ --query "SELECT * FROM invoices WHERE id = :invoice_id" \
408
+ --name "invoice lookup"
409
+ ```
410
+
411
+ Wrap an OpenAPI service:
412
+
413
+ ```bash
414
+ invoke wrap billing-api \
415
+ --openapi openapi.json \
416
+ --base-url https://billing.example.com
417
+ ```
418
+
419
+ This creates a runnable MCP wrapper under `wrapped_tools/` with:
420
+
421
+ - capability metadata
422
+ - JSON schema validation
423
+ - structured JSON-RPC errors
424
+ - idempotency hints
425
+ - retry hints
426
+ - `invoke.register.json` for provider onboarding
427
+
428
+ The npm command is the recommended path. The underlying Python entrypoint remains available for local development:
429
+
430
+ ```bash
431
+ python agentify.py wrap github
432
+ ```
433
+
434
+ ## Runtime API
435
+
436
+ ### Connect Hosted MCP Gateway
437
+
438
+ ```python
439
+ from sdk import invoke
440
+
441
+ connected = invoke.connect(
442
+ "github",
443
+ owner_email="dev@acme.example",
444
+ approval_email="ops@acme.example",
445
+ )
446
+
447
+ print(connected["gateway_url"])
448
+ print(connected["tools"][0]["key"])
449
+ ```
450
+
451
+ The gateway returns a hosted endpoint such as `https://github.invoke.dev` plus an MCP URL and preloaded launch-tool metadata.
452
+
453
+ ### TypeScript SDK
454
+
455
+ ```ts
456
+ import { Invoke } from "./sdk";
457
+
458
+ const invoke = new Invoke({ apiKey: process.env.INVOKE_API_KEY! });
459
+
460
+ const result = await invoke.call({
461
+ tool: "fetch.url",
462
+ params: { url: "https://github.com" },
463
+ agentId: "research_agent_v1",
464
+ });
465
+
466
+ const connected = await invoke.connect({
467
+ saas: "linear",
468
+ ownerEmail: "dev@acme.example",
469
+ });
470
+ ```
471
+
472
+ ### HTTP
473
+
474
+ All runtime endpoints require `X-API-Key`.
475
+
476
+ Create and inspect a v1 execution:
477
+
478
+ ```bash
479
+ curl -X POST http://localhost:8000/v1/executions \
480
+ -H "Content-Type: application/json" \
481
+ -H "X-API-Key: $INVOKE_API_KEY" \
482
+ -H "Idempotency-Key: checkout-retry-001" \
483
+ -d '{"workflow":"safe-tool-execution","input":{"params":{"invoice_id":"inv_123"}}}'
484
+
485
+ curl http://localhost:8000/v1/executions \
486
+ -H "X-API-Key: $INVOKE_API_KEY"
487
+ ```
488
+
489
+ List registered tools:
490
+
491
+ ```bash
492
+ curl http://localhost:8000/tools \
493
+ -H "X-API-Key: $INVOKE_API_KEY"
494
+ ```
495
+
496
+ Call a tool:
497
+
498
+ ```bash
499
+ curl http://localhost:8000/v1/call \
500
+ -H "Content-Type: application/json" \
501
+ -H "X-API-Key: $INVOKE_API_KEY" \
502
+ -d '{
503
+ "tool": "fetch.url",
504
+ "params": {"url": "https://github.com"},
505
+ "agent_id": "research_agent_v1"
506
+ }'
507
+ ```
508
+
509
+ Connect a launch SaaS and get its hosted gateway:
510
+
511
+ ```bash
512
+ curl -X POST http://localhost:8000/connect/github \
513
+ -H "Content-Type: application/json" \
514
+ -H "X-API-Key: $INVOKE_API_KEY" \
515
+ -d '{"owner_email": "dev@acme.example"}'
516
+ ```
517
+
518
+ ## State Revalidation Engine
519
+
520
+ Agents should act on current truth, not stale assumptions. Use `invoke.verify_state(...)` before execution when an action depends on critical business state:
521
+
522
+ ```python
523
+ from sdk import invoke
524
+
525
+ state = invoke.verify_state(
526
+ intent="send_invoice_reminder",
527
+ required_fields=["invoice_status", "balance"],
528
+ assumed_state={"invoice_status": "unpaid", "balance": 125},
529
+ state_refetch={
530
+ "tool": "fetch.url",
531
+ "params": {"url": "https://billing.example.com/invoices/inv_123"},
532
+ },
533
+ conditions={
534
+ "invoice_status": "unpaid",
535
+ "balance": "> 0",
536
+ },
537
+ )
538
+
539
+ if state["decision"] != "execute":
540
+ return "Invoice is no longer unpaid. Abort reminder."
541
+ ```
542
+
543
+ Invoke re-fetches current state, compares it with the decision-time assumptions, computes field-level drift, then returns:
544
+
545
+ - `verified` / `execute`: required fields still match and conditions pass.
546
+ - `blocked` / `abort`: state is missing, changed, or no longer satisfies conditions.
547
+ - `replan_required` / `replan`: same mismatch path when `on_mismatch="replan"`.
548
+
549
+ ## Entity Resolution Tracking
550
+
551
+ Agents should act on the correct entity, not a guessed ID. Attach `entity_resolution` to a tool call when the agent resolved a customer, account, invoice, or user before acting:
552
+
553
+ ```python
554
+ from sdk import invoke
555
+
556
+ result = invoke.call(
557
+ tool="billing.send_reminder",
558
+ params={"customer_id": "cust_123", "invoice_id": "inv_456"},
559
+ agent_id="billing_agent",
560
+ entity_resolution={
561
+ "entity_id": "cust_123",
562
+ "source": "crm_lookup",
563
+ "resolved_at": "2026-05-01T12:00:00Z",
564
+ },
565
+ )
566
+ ```
567
+
568
+ Invoke logs the resolved `entity_id`, source, and timestamp into the tool-call trace. Before execution, it compares that entity against IDs in the action params and execution state. If the action points at a different entity, Invoke blocks the call with `409` and never touches the tool.
569
+
570
+ The same check runs again when a pending approval is approved. If the thawed state now points at a different customer or account, the approval is blocked instead of executing stale work.
571
+
572
+ ## Failure Policy Engine
573
+
574
+ Agents should not guess what to do when tools fail. Add a failure policy to a tool call to make retry, fallback, and escalation behavior explicit and bounded:
575
+
576
+ ```json
577
+ {
578
+ "retry": 2,
579
+ "fallback": "secondary_api",
580
+ "on_failure": "escalate"
581
+ }
582
+ ```
583
+
584
+ ```python
585
+ from sdk import invoke
586
+
587
+ result = invoke.call(
588
+ tool="billing.primary_lookup",
589
+ params={"invoice_id": "inv_123"},
590
+ agent_id="billing_agent",
591
+ failure_policy={
592
+ "retry": 2,
593
+ "fallback": "billing.secondary_lookup",
594
+ "on_failure": "escalate",
595
+ },
596
+ )
597
+ ```
598
+
599
+ Invoke enforces a hard retry cap. `retry: 2` means one initial attempt plus two bounded retries, never an infinite loop. If the primary tool still fails, Invoke can call the fallback tool once. If everything fails and `on_failure` is `escalate`, Invoke creates a pending approval with the failure context for a human to review.
600
+
601
+ ## Outcome Reconciliation
602
+
603
+ Never retry blindly when the outcome is unknown. If a timeout or partial failure happens after a side effect may have occurred, reconcile the action first:
604
+
605
+ ```python
606
+ from sdk import invoke
607
+
608
+ outcome = invoke.reconcile({
609
+ "action": {
610
+ "intent": "charge_customer",
611
+ "params": {"payment_id": "pay_123"},
612
+ },
613
+ "outcome": "UNKNOWN",
614
+ "state_refetch": {
615
+ "tool": "payments.lookup",
616
+ "params": {"payment_id": "pay_123"},
617
+ },
618
+ "conditions": {"charged": True},
619
+ })
620
+
621
+ if outcome["decision"] == "do_not_retry":
622
+ return "Payment already succeeded. Do not charge again."
623
+ ```
624
+
625
+ You can also attach reconciliation to a failure policy:
626
+
627
+ ```json
628
+ {
629
+ "retry": 2,
630
+ "on_failure": "escalate",
631
+ "reconcile": {
632
+ "action": "charge_customer",
633
+ "state_refetch": {
634
+ "tool": "payments.lookup",
635
+ "params": {"payment_id": "pay_123"}
636
+ },
637
+ "conditions": {"charged": true}
638
+ }
639
+ }
640
+ ```
641
+
642
+ When a failure has an `UNKNOWN` outcome, Invoke runs reconciliation before retrying. If reconciliation shows the action already succeeded, Invoke returns `outcome_reconciled` and blocks duplicate retries. If reconciliation shows it did not succeed, bounded retry can continue. If the outcome is still unknown, Invoke escalates or errors according to policy.
643
+
644
+ ## Approval Gates
645
+
646
+ Approval checkpoints carry an execution snapshot: params, variables, tool outputs, action, policy contract, and timestamps. Approval revalidates policy against fresh state before execution and returns `executed`, `cancelled`, `replan_required`, or `requeued`.
647
+
648
+ ### Conditional Approval Contracts
649
+
650
+ Use a conditional approval contract when an approval is only valid if live business state still matches what the human approved. A contract with `intent` and `conditions` creates a pending approval even if the underlying tool is normally low-risk:
651
+
652
+ ```json
653
+ {
654
+ "intent": "send_invoice_reminder",
655
+ "conditions": {
656
+ "invoice_status": "overdue",
657
+ "customer_balance": "> 0"
658
+ },
659
+ "threshold": "strict",
660
+ "expires_at": "2026-04-30T15:00:00Z"
661
+ }
662
+ ```
663
+
664
+ When approval happens, Invoke fetches or accepts current state, compares it with the frozen approval-time state, and computes drift for each condition key.
665
+
666
+ - Valid: execute the original action.
667
+ - Changed: mark the old approval `invalidated` and create a fresh pending approval with the live state.
668
+ - Expired: cancel before execution.
669
+
670
+ `threshold: "strict"` means condition values must still match the approval-time snapshot exactly and the current values must still satisfy every condition. `threshold: "conditions"` allows value changes as long as the current values still satisfy the conditions.
671
+
672
+ ```python
673
+ from sdk import invoke
674
+
675
+ policy = {
676
+ "rules": [
677
+ {
678
+ "when": "action == git_push and branch == main",
679
+ "effect": "require_approval",
680
+ "intent": "push_to_main",
681
+ "allowed_action": "git_push",
682
+ "reason": "Human approval required for pushes to main",
683
+ }
684
+ ]
685
+ }
686
+
687
+ pending = invoke.call(
688
+ tool="fetch.url",
689
+ params={"url": "https://example.com/repo", "branch": "main"},
690
+ agent_id="dev_agent_v1",
691
+ action="git_push",
692
+ policy=policy,
693
+ execution_state={
694
+ "variables": {"branch": "main"},
695
+ "tool_outputs": {"diff_summary": {"files_changed": 3}},
696
+ },
697
+ )
698
+ ```
699
+
700
+ A domain policy can freeze intent-specific work:
701
+
702
+ ```json
703
+ {
704
+ "intent": "send_invoice_reminder",
705
+ "condition": "invoice_status == overdue",
706
+ "allowed_action": "send_email",
707
+ "expires_at": "2026-04-30T15:00:00Z"
708
+ }
709
+ ```
710
+
711
+ When the human approves, Invoke can accept fresh state from the approver or run a configured `state_refetch` read tool. It then thaws the checkpoint, validates `condition`, checks `allowed_action` and `expires_at`, and decides whether to execute, cancel, or ask the agent to re-plan.
712
+
713
+ Open the approval dashboard:
714
+
715
+ ```bash
716
+ open "http://localhost:8000/dashboard/approvals?api_key=$INVOKE_API_KEY"
717
+ ```
718
+
719
+ For approval notifications, set:
720
+
721
+ ```bash
722
+ export APPROVAL_SLACK_WEBHOOK_URL="https://hooks.slack.com/services/..."
723
+ export APPROVAL_EMAIL_WEBHOOK_URL="https://email-webhook.example/send"
724
+ ```
725
+
726
+ ## CLI Commands
727
+
728
+ The CLI gives you the first Invoke project lifecycle:
729
+
730
+ ```bash
731
+ invoke login --base-url https://agentgate-ai.onrender.com --api-key inv_live_...
732
+ invoke init support-agent --template crm-guardrail
733
+ invoke dev
734
+ invoke deploy
735
+ invoke call <tool> '{"json":"params"}'
736
+ invoke agents list
737
+ ```
738
+
739
+ Wrapper generation is still available for production tools:
740
+
741
+ ```bash
742
+ invoke wrap github
743
+ invoke wrap notion
744
+ invoke wrap linear
745
+ invoke wrap postgresql --query "SELECT * FROM users WHERE id = :user_id"
746
+ invoke wrap billing-api --openapi openapi.json --base-url http://localhost:8000
747
+ ```
748
+
749
+ Credentials from `invoke login` are stored in `~/.invoke/credentials.json`. `invoke dev` writes the local MCP URL to `.invoke/dev.json`, and `invoke deploy` uses it automatically when `invoke.json` still has the placeholder MCP URL. Set `INVOKE_HOME` to override the global credentials location in CI.
750
+
751
+ ## Configuration
752
+
753
+ Local server environment variables:
754
+
755
+ | Variable | Description |
756
+ | --- | --- |
757
+ | `INVOKE_API_KEYS` | Comma-separated full-access server API keys |
758
+ | `TRACE_API_KEYS` | Existing Trace-compatible server API key env var |
759
+ | `INVOKE_API_KEY_SCOPES` | JSON scoped-token config for tool allowlists and scopes |
760
+ | `TRACE_API_KEY_SCOPES` | Existing Trace-compatible scoped-token env var |
761
+ | `INVOKE_LOG_FILE` | Override JSON audit log filename |
762
+ | `TRACE_LOG_FILE` | Existing Trace-compatible audit log filename |
763
+ | `INVOKE_ALLOWED_ORIGINS` | Comma-separated browser origins allowed by CORS |
764
+ | `TRACE_ALLOWED_ORIGINS` | Existing Trace-compatible CORS allowlist env var |
765
+ | `INVOKE_ALLOWED_ORIGIN_REGEX` | Optional regex for preview frontend origins such as Vercel previews |
766
+ | `TRACE_ALLOWED_ORIGIN_REGEX` | Existing Trace-compatible preview-origin regex env var |
767
+ | `SUPABASE_URL` | Optional Supabase project URL for hosted persistence; `/rest/v1` suffix is tolerated |
768
+ | `SUPABASE_SERVICE_ROLE_KEY` | Optional Supabase service role key for hosted persistence |
769
+ | `SUPABASE_TABLE_PREFIX` | Optional Supabase table prefix, default `invoke_` |
770
+ | `LINEAR_API_KEY` | Optional Linear API key for direct issue creation |
771
+ | `LINEAR_TEAM_ID` | Optional default Linear team UUID for hosted demos |
772
+ | `SLACK_BOT_TOKEN` | Optional Slack bot token for direct channel listing and posting |
773
+ | `SLACK_DEFAULT_CHANNEL` | Optional default Slack channel ID for hosted demos |
774
+ | `FAILURE_POLICY_MAX_RETRIES` | Hard cap for per-call failure-policy retries, default `5` |
775
+ | `HOST` | Server bind host, default `0.0.0.0` |
776
+ | `PORT` | Server bind port, default `8000` |
777
+
778
+ Client environment variables:
779
+
780
+ | Variable | Description |
781
+ | --- | --- |
782
+ | `INVOKE_API_KEY` | API key used by the Python and TypeScript SDKs |
783
+ | `TRACE_API_KEY` | Existing Trace-compatible client API key env var |
784
+ | `INVOKE_BASE_URL` | SDK base URL override |
785
+ | `TRACE_BASE_URL` | Existing Trace-compatible SDK base URL override |
786
+
787
+ Scoped token example:
788
+
789
+ ```bash
790
+ export INVOKE_API_KEY_SCOPES='{
791
+ "inv_scoped_fetch_read": {
792
+ "scopes": ["tools:read", "tools:call", "traces:read"],
793
+ "allowed_tools": ["fetch.url"],
794
+ "read_only": true,
795
+ "agent_id": "research_agent_v1",
796
+ "envs": ["dev", "prod"],
797
+ "agents": ["research_agent_v1"],
798
+ "workflows": ["market_research"],
799
+ "resources": ["cust_123", "repo:acme/app"]
800
+ }
801
+ }'
802
+ ```
803
+
804
+ Supported scopes today: `tools:read`, `tools:call`, `state:verify`, `outcomes:reconcile`, `approvals:read`, `approvals:write`, `logs:read`, `traces:read`, and `providers:admin`.
805
+
806
+ `allowed_tools` and `read_only` gate tool access. `envs`, `agents`, `workflows`, `allowed_actions`, and `resources` gate execution context, so a token can mean "this agent may call this tool in prod for this workflow and resource" instead of only re-exposing provider OAuth scopes.
807
+
808
+ ## Observability
809
+
810
+ List recent tool-call traces:
811
+
812
+ ```bash
813
+ curl http://localhost:8000/traces \
814
+ -H "X-API-Key: $INVOKE_API_KEY"
815
+ ```
816
+
817
+ Export traces:
818
+
819
+ ```bash
820
+ curl "http://localhost:8000/traces/export?format=langsmith" \
821
+ -H "X-API-Key: $INVOKE_API_KEY"
822
+ ```
823
+
824
+ Local logs:
825
+
826
+ - JSON audit logs: `logs/trace.log`
827
+ - tool-call traces: `logs/tool_calls.jsonl`
828
+ - trace export formats: JSON, JSONL, LangSmith-shaped, and Helicone-shaped records
829
+
830
+ When Supabase persistence is enabled, `/traces` and `/traces/export` read from Supabase instead of the local JSONL file.
831
+
832
+ ## Blast-Radius Demo
833
+
834
+ There is a demo harness that starts mock tools and runs a production-failure story. It shows how Invoke contains the blast radius when agent execution gets messy:
835
+
836
+ - tool timeout / transient upstream failure
837
+ - partial success with unknown outcome
838
+ - wrong CRM update blocked by entity resolution
839
+ - duplicated retry
840
+ - stale approval
841
+ - webhook inconsistency
842
+
843
+ ```bash
844
+ FLAKY_FAIL_FIRST_N=2 ./demo/run_demo.sh
845
+ ```
846
+
847
+ The demo starts the flaky MCP simulator, a mock tool/CRM MCP server, the Invoke gateway, then runs `demo_comparison.py` and cleans up processes. The buyer takeaway is simple: Invoke does not make agents smarter; it makes their execution bounded when production gets messy.
848
+
849
+ ## What Exists Today
850
+
851
+ - API-key authentication
852
+ - scoped API tokens with tool allowlists and read-only checks
853
+ - npm/npx CLI package with `invoke login`, `invoke init`, `invoke deploy`, `invoke call`, `invoke agents list`, and `invoke wrap`
854
+ - `invoke wrap` generator for OpenAPI, GitHub, Notion, Linear, and PostgreSQL MCP wrappers
855
+ - hosted gateway URL metadata for connected SaaS tools
856
+ - agent-readable tool registry
857
+ - `/tools` capability cards
858
+ - `/discover` capability search
859
+ - provider onboarding with registered tools available in discovery and calls
860
+ - `/call` reliable tool invocation
861
+ - `/state/verify` state revalidation before execution
862
+ - entity resolution tracking with pre-execution mismatch blocking
863
+ - failure policy engine for bounded retry, fallback, and escalation
864
+ - outcome reconciliation to prevent duplicate retries after unknown results
865
+ - policy-as-code `pending_approval` responses
866
+ - conditional approval contracts with drift-based requeue
867
+ - frozen execution checkpoints with variables and tool outputs
868
+ - `/approvals` approval queue
869
+ - `/dashboard/approvals` web dashboard for human review
870
+ - Slack and email-webhook approval notifications
871
+ - approve/reject plus thaw-time execute, cancel, or re-plan decisions
872
+ - MCP Streamable HTTP support
873
+ - direct HTTP fallback for `fetch.url`
874
+ - JSON audit logs in `logs/trace.log`
875
+ - tool-call traces in `logs/tool_calls.jsonl`
876
+ - `/traces/export` for JSON, JSONL, LangSmith-shaped, and Helicone-shaped records
877
+
878
+ ## Direction
879
+
880
+ Invoke is moving toward the runtime layer for real-world agent capabilities:
881
+
882
+ - scoped agent identities
883
+ - approval gates for risky actions
884
+ - provider onboarding and wrapper templates
885
+ - richer capability search
886
+ - usage metering and policy controls
887
+ - dashboard-grade observability