@oneie/sdk 0.10.0 → 0.12.0

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/dist/receivers.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- import { AgentActionResponseSchema, AgentStatusResponseSchema, CapabilityItemSchema, PayResponseSchema, RegisterResponseSchema, StatsSchema, } from "./schemas.js";
2
+ import { AgentActionResponseSchema, AgentStatusResponseSchema, CapabilityItemSchema, PayResponseSchema, RegisterResponseSchema, StatsSchema, StepKind, WorkflowDiffSchema, } from "./schemas.js";
3
3
  /**
4
4
  * Declare a receiver contract. Identity at runtime — its only job is to pin the
5
5
  * `Req`/`Res` generics so the catalog infers payload + outcome types.
@@ -18,6 +18,13 @@ const ok = z.object({ ok: z.boolean() });
18
18
  */
19
19
  export const RECEIVERS = {
20
20
  // ── Spine (non-world) — contracts only this cycle; route-wiring lands in C4 ──
21
+ "agent:enroll": receiver({
22
+ receiver: "agent:enroll",
23
+ summary: "Self-enroll as an agent — mint identity + API key with no prior session",
24
+ request: z.object({ name: z.string(), capabilities: z.array(z.string()).optional() }),
25
+ response: z.object({ ok: z.literal(true), actorId: z.string(), slug: z.string(), apiKey: z.string(), credits: z.number() }),
26
+ effect: "ask", auth: "public", reversible: false, idempotent: false,
27
+ }),
21
28
  "auth:agent": receiver({
22
29
  receiver: "auth:agent",
23
30
  summary: "Become an actor — mint a uid, wallet, and scoped API key",
@@ -150,6 +157,50 @@ export const RECEIVERS = {
150
157
  request: z.object({ aid: z.string(), name: z.string().optional(), tags: z.array(z.string()).optional(), prompt: z.string().optional(), model: z.string().optional() }),
151
158
  response: ok, effect: "ask", auth: "manage_actors",
152
159
  }),
160
+ "world:update-contact": receiver({
161
+ receiver: "world:update-contact",
162
+ summary: "Update an actor's contact/profile attributes — identity, contact details, professional, personal, CRM metrics",
163
+ request: z.object({
164
+ aid: z.string(),
165
+ // identity
166
+ name: z.string().optional(),
167
+ firstName: z.string().optional(),
168
+ lastName: z.string().optional(),
169
+ nickname: z.string().optional(),
170
+ title: z.string().optional(),
171
+ avatar: z.string().optional(),
172
+ bio: z.string().optional(),
173
+ // contact
174
+ email: z.string().optional(),
175
+ phone: z.string().optional(),
176
+ website: z.string().optional(),
177
+ // professional
178
+ jobTitle: z.string().optional(),
179
+ company: z.string().optional(),
180
+ department: z.string().optional(),
181
+ role: z.string().optional(),
182
+ // personal
183
+ birthday: z.string().optional(),
184
+ gender: z.string().optional(),
185
+ nationality: z.string().optional(),
186
+ timezone: z.string().optional(),
187
+ // address
188
+ street: z.string().optional(),
189
+ city: z.string().optional(),
190
+ region: z.string().optional(),
191
+ postcode: z.string().optional(),
192
+ country: z.string().optional(),
193
+ // crm
194
+ lifecycle: z.string().optional(),
195
+ source: z.string().optional(),
196
+ // numeric crm metrics
197
+ value: z.number().optional(),
198
+ fitScore: z.number().optional(),
199
+ nps: z.number().optional(),
200
+ csat: z.number().optional(),
201
+ }),
202
+ response: ok, effect: "ask", auth: "manage_actors",
203
+ }),
153
204
  "world:remove-actor": receiver({
154
205
  receiver: "world:remove-actor",
155
206
  summary: "Delete an actor",
@@ -157,15 +208,38 @@ export const RECEIVERS = {
157
208
  }),
158
209
  "world:create-key": receiver({
159
210
  receiver: "world:create-key",
160
- summary: "Mint a scoped API key for an actor",
161
- request: z.object({ actor: z.string(), scope: z.string().optional(), label: z.string().optional() }),
162
- response: z.object({ key: z.string(), keyId: z.string() }), effect: "ask", auth: "manage_keys",
163
- reversible: false,
211
+ summary: "Mint a scoped API key (PAT) for an actor you own; scope is capped at the actor's role, optional expiresIn (seconds)",
212
+ request: z.object({
213
+ actor: z.string(),
214
+ scope: z.enum(["read", "write", "admin"]).optional(),
215
+ label: z.string().optional(),
216
+ expiresIn: z.number().int().positive().optional(),
217
+ }),
218
+ // Self-service: any authenticated member can mint a key for an actor they own
219
+ // (requireActorOwner is the gate). The scope ceiling prevents privilege escalation.
220
+ response: z.object({ key: z.string(), keyId: z.string(), scope: z.string() }),
221
+ effect: "ask", auth: "member", scope: "write", reversible: false,
222
+ examples: [{ actor: "01ABC", scope: "write", label: "cli" }],
223
+ }),
224
+ "world:list-keys": receiver({
225
+ receiver: "world:list-keys",
226
+ summary: "List active API keys (metadata only — never the secret) for an actor you own",
227
+ request: z.object({ actor: z.string() }),
228
+ response: z.object({
229
+ keys: z.array(z.object({
230
+ keyId: z.string(),
231
+ label: z.string().nullable(),
232
+ scope: z.string(),
233
+ createdAt: z.number(),
234
+ validTo: z.number().nullable(),
235
+ })),
236
+ }),
237
+ effect: "ask", auth: "member", scope: "read", cost: "free", idempotent: true,
164
238
  }),
165
239
  "world:revoke-key": receiver({
166
240
  receiver: "world:revoke-key",
167
241
  summary: "Revoke an API key",
168
- request: z.object({ keyId: z.string() }), response: ok, effect: "ask", auth: "manage_keys", reversible: false,
242
+ request: z.object({ keyId: z.string() }), response: ok, effect: "ask", auth: "member", scope: "write", reversible: false,
169
243
  }),
170
244
  "world:create-thing": receiver({
171
245
  receiver: "world:create-thing",
@@ -243,13 +317,7 @@ export const RECEIVERS = {
243
317
  effect: "ask", cost: "free", idempotent: true,
244
318
  }),
245
319
  // ── auth (human session) ──
246
- "auth:sign-in": receiver({
247
- receiver: "auth:sign-in",
248
- summary: "Sign in a human with email + password",
249
- request: z.object({ email: z.string(), password: z.string() }),
250
- response: z.object({ sessionId: z.string(), userId: z.string() }),
251
- effect: "ask", reversible: false,
252
- }),
320
+ // auth:sign-in killed (C7): Better Auth handles all human auth; signal/ask pipeline returns dissolved
253
321
  "auth:sign-out": receiver({
254
322
  receiver: "auth:sign-out",
255
323
  summary: "End the current session",
@@ -257,29 +325,9 @@ export const RECEIVERS = {
257
325
  response: ok,
258
326
  effect: "signal", idempotent: true,
259
327
  }),
260
- // ── board ──
261
- "board:join": receiver({
262
- receiver: "board:join",
263
- summary: "Join the public board as an actor",
264
- request: z.object({ uid: z.string(), group: z.string().optional() }),
265
- response: z.object({ ok: z.boolean(), uid: z.string(), group: z.string(), role: z.string() }),
266
- effect: "ask", idempotent: true,
267
- }),
268
- // ── agency (oo operator surface) ──
269
- "agency:push:complete": receiver({
270
- receiver: "agency:push:complete",
271
- summary: "An agency finished pushing agents/skills — logs the sync event",
272
- request: z.object({ agents: z.number().int().nonnegative(), skills: z.number().int().nonnegative() }),
273
- response: ok,
274
- effect: "signal", idempotent: true,
275
- }),
276
- "agency:friction": receiver({
277
- receiver: "agency:friction",
278
- summary: "A /do cycle stalled in an agency repo — surfaces the friction back to ONE",
279
- request: z.object({ agency: z.string(), cycle: z.string().nullable(), wave: z.string().nullable(), reason: z.string() }),
280
- response: ok,
281
- effect: "signal",
282
- }),
328
+ // board:join killed (C7): no resolver, no route, no caller
329
+ // agency:push:complete killed (C7): no resolver, no route, no caller
330
+ // agency:friction killed (C7): no resolver, no route, no caller
283
331
  // ── groups ──
284
332
  "groups:join": receiver({
285
333
  receiver: "groups:join",
@@ -325,18 +373,7 @@ export const RECEIVERS = {
325
373
  }),
326
374
  effect: "ask", auth: "manage_clients", cost: "free", idempotent: true,
327
375
  }),
328
- // ── paths ──
329
- "paths:bridge": receiver({
330
- receiver: "paths:bridge",
331
- summary: "Bridge two actors or groups with a path",
332
- request: z.object({ from: z.string(), to: z.string() }),
333
- response: z.object({
334
- ok: z.boolean().optional(),
335
- status: z.enum(["pending", "bridged"]).optional(),
336
- key: z.string().optional(), gid: z.string().optional(), awaiting: z.string().optional(),
337
- }),
338
- effect: "ask",
339
- }),
376
+ // paths:bridge killed (C7): no resolver, no route, no caller
340
377
  // ── subscriptions ──
341
378
  "subscriptions:register": receiver({
342
379
  receiver: "subscriptions:register",
@@ -391,16 +428,7 @@ export const RECEIVERS = {
391
428
  response: z.array(CapabilityItemSchema),
392
429
  effect: "ask", cost: "free", idempotent: true,
393
430
  }),
394
- "agents:deploy-on-behalf": receiver({
395
- receiver: "agents:deploy-on-behalf",
396
- summary: "Deploy an agent on behalf of an owner, inheriting their paths",
397
- request: z.object({ owner: z.string(), spec: z.record(z.string(), z.unknown()) }),
398
- response: z.object({
399
- ok: z.boolean(), uid: z.string(), owner: z.string(),
400
- inheritedPaths: z.array(z.object({ from: z.string(), to: z.string(), strength: z.number() })),
401
- }),
402
- effect: "ask", reversible: false, simulatable: true,
403
- }),
431
+ // agents:deploy-on-behalf killed (C7): no resolver, no route, no caller
404
432
  // ── pay (TRANSACT — onchain settlement) ──
405
433
  "pay:weight": receiver({
406
434
  receiver: "pay:weight",
@@ -409,6 +437,115 @@ export const RECEIVERS = {
409
437
  response: PayResponseSchema,
410
438
  effect: "ask", cost: "variable", settles: "onchain", reversible: false, simulatable: true,
411
439
  }),
440
+ // ── market (A2A negotiation, C2) ──
441
+ "market:offer": receiver({
442
+ receiver: "market:offer",
443
+ summary: "Buyer creates a Deal at OFFER stage — initiates A2A negotiation",
444
+ request: z.object({
445
+ id: z.string().uuid().optional(),
446
+ seller: z.string().min(1),
447
+ skill: z.string().min(1),
448
+ price: z.number().nonnegative(),
449
+ currency: z.string().default("credits"),
450
+ deadline: z.string().datetime(),
451
+ }),
452
+ response: z.union([
453
+ z.object({ ok: z.literal(true), deal: z.object({ id: z.string(), stage: z.string(), version: z.number() }) }),
454
+ z.object({ ok: z.literal(false), error: z.string() }),
455
+ ]),
456
+ effect: "ask", auth: "required", reversible: true, idempotent: false,
457
+ }),
458
+ "market:counter": receiver({
459
+ receiver: "market:counter",
460
+ summary: "Seller revises deal terms — stays at OFFER, bumps version",
461
+ request: z.object({
462
+ dealId: z.string().min(1),
463
+ price: z.number().nonnegative().optional(),
464
+ deadline: z.string().datetime().optional(),
465
+ skill: z.string().optional(),
466
+ }),
467
+ response: z.union([
468
+ z.object({ ok: z.literal(true), deal: z.object({ id: z.string(), stage: z.string(), version: z.number() }) }),
469
+ z.object({ ok: z.literal(false), error: z.string() }),
470
+ ]),
471
+ effect: "ask", auth: "required", reversible: true, idempotent: false,
472
+ }),
473
+ "market:accept": receiver({
474
+ receiver: "market:accept",
475
+ summary: "Buyer accepts deal — OFFER → ESCROW",
476
+ request: z.object({ dealId: z.string().min(1) }),
477
+ response: z.union([
478
+ z.object({ ok: z.literal(true), deal: z.object({ id: z.string(), stage: z.string(), version: z.number() }) }),
479
+ z.object({ ok: z.literal(false), error: z.string() }),
480
+ ]),
481
+ effect: "ask", auth: "required", reversible: false, idempotent: false,
482
+ }),
483
+ "market:reject": receiver({
484
+ receiver: "market:reject",
485
+ summary: "Buyer rejects deal — OFFER → FADE",
486
+ request: z.object({ dealId: z.string().min(1) }),
487
+ response: z.union([
488
+ z.object({ ok: z.literal(true), deal: z.object({ id: z.string(), stage: z.string(), version: z.number() }) }),
489
+ z.object({ ok: z.literal(false), error: z.string() }),
490
+ ]),
491
+ effect: "ask", auth: "required", reversible: false, idempotent: false,
492
+ }),
493
+ "market:deliver": receiver({
494
+ receiver: "market:deliver",
495
+ summary: "Seller delivers work — EXECUTE → VERIFY",
496
+ request: z.object({ dealId: z.string().min(1), result: z.record(z.string(), z.unknown()).optional() }),
497
+ response: z.union([
498
+ z.object({ ok: z.literal(true), deal: z.object({ id: z.string(), stage: z.string(), version: z.number() }) }),
499
+ z.object({ ok: z.literal(false), error: z.string() }),
500
+ ]),
501
+ effect: "ask", auth: "required", reversible: false, idempotent: false,
502
+ }),
503
+ "market:verify": receiver({
504
+ receiver: "market:verify",
505
+ summary: "Buyer scores delivered work — VERIFY → SETTLE (≥0.65) or DISPUTE (<0.65)",
506
+ request: z.object({
507
+ dealId: z.string().min(1),
508
+ fit: z.number().min(0).max(1),
509
+ form: z.number().min(0).max(1),
510
+ truth: z.number().min(0).max(1),
511
+ taste: z.number().min(0).max(1),
512
+ }),
513
+ response: z.union([
514
+ z.object({ ok: z.literal(true), deal: z.object({ id: z.string(), stage: z.string(), version: z.number() }), scores: z.object({ fit: z.number(), form: z.number(), truth: z.number(), taste: z.number() }) }),
515
+ z.object({ ok: z.literal(false), error: z.string() }),
516
+ ]),
517
+ effect: "ask", auth: "required", reversible: false, idempotent: false,
518
+ }),
519
+ "market:settle": receiver({
520
+ receiver: "market:settle",
521
+ summary: "Release escrow payment — SETTLE → RECEIPT",
522
+ request: z.object({ dealId: z.string().min(1) }),
523
+ response: z.union([
524
+ z.object({ ok: z.literal(true), deal: z.object({ id: z.string(), stage: z.string(), version: z.number() }) }),
525
+ z.object({ ok: z.literal(false), error: z.string() }),
526
+ ]),
527
+ effect: "ask", auth: "required", reversible: false, idempotent: false,
528
+ }),
529
+ "market:dispute": receiver({
530
+ receiver: "market:dispute",
531
+ summary: "Escalate an active deal to DISPUTE — either party may call",
532
+ request: z.object({ dealId: z.string().min(1) }),
533
+ response: z.union([
534
+ z.object({ ok: z.literal(true), deal: z.object({ id: z.string(), stage: z.string(), version: z.number() }) }),
535
+ z.object({ ok: z.literal(false), error: z.string() }),
536
+ ]),
537
+ effect: "ask", auth: "required", reversible: false, idempotent: false,
538
+ }),
539
+ "market:escrow": receiver({
540
+ receiver: "market:escrow",
541
+ summary: "Buyer locks payment via pay.one.ie — ESCROW → EXECUTE on confirmed tx",
542
+ request: z.object({ dealId: z.string().min(1) }),
543
+ response: z.union([
544
+ z.object({ ok: z.literal(true), deal: z.object({ id: z.string(), stage: z.string(), version: z.number() }), escrowRef: z.string() }),
545
+ z.object({ ok: z.literal(false), error: z.string(), deal: z.object({ id: z.string(), stage: z.string() }).optional() }),
546
+ ]),
547
+ effect: "ask", auth: "required", reversible: false, idempotent: false,
548
+ }),
412
549
  // ── market (TRADE) ──
413
550
  "market:hire": receiver({
414
551
  receiver: "market:hire",
@@ -452,6 +589,16 @@ export const RECEIVERS = {
452
589
  })),
453
590
  effect: "ask", cost: "free", idempotent: true,
454
591
  }),
592
+ "market:claim": receiver({
593
+ receiver: "market:claim",
594
+ summary: "Claim an open bounty — atomically marks it picked and binds the claimant",
595
+ request: z.object({ bountyId: z.string() }),
596
+ response: z.union([
597
+ z.object({ ok: z.literal(true), bountyId: z.string(), claimantUid: z.string() }),
598
+ z.object({ ok: z.literal(false), error: z.string() }),
599
+ ]),
600
+ effect: "ask", cost: "free", reversible: false, idempotent: false,
601
+ }),
455
602
  "market:list": receiver({
456
603
  receiver: "market:list",
457
604
  summary: "List the capability market",
@@ -475,22 +622,7 @@ export const RECEIVERS = {
475
622
  response: z.object({ ok: z.literal(true), sid: z.string(), scope: z.string() }),
476
623
  effect: "ask",
477
624
  }),
478
- // ── loop ──
479
- "loop:close": receiver({
480
- receiver: "loop:close",
481
- summary: "Close a loop session with an outcome",
482
- request: z.object({
483
- session: z.string(),
484
- outcome: z.enum(["result", "timeout", "dissolved", "failure"]),
485
- rubric: z.number().optional(),
486
- reason: z.string().optional(),
487
- }),
488
- response: z.object({
489
- ok: z.literal(true), stages: z.array(z.unknown()),
490
- highways: z.array(z.object({ path: z.string(), strength: z.number(), resistance: z.number(), net: z.number() })),
491
- }),
492
- effect: "ask",
493
- }),
625
+ // loop:close killed (C7): no resolver, no route, no caller
494
626
  // ── reads (free · idempotent) ──
495
627
  "signals:list": receiver({
496
628
  receiver: "signals:list",
@@ -606,12 +738,693 @@ export const RECEIVERS = {
606
738
  response: z.object({ ok: z.boolean(), reply: z.string().optional(), error: z.string().optional() }),
607
739
  effect: "ask", auth: "member",
608
740
  }),
741
+ // ── booking — one calendar, two doors (text/booking-plan.md §5) ───────────────
742
+ // Both public-bookable: a customer with no account books a slot. The provider
743
+ // side (configure a service) is the separate, gated booking:configure.
744
+ "booking:list-slots": receiver({
745
+ receiver: "booking:list-slots",
746
+ summary: "List open slots for a workspace's service on a date (or next open days)",
747
+ request: z.object({
748
+ slug: z.string(), // provider workspace
749
+ service: z.string().optional(), // service_id; omitted → the workspace's first active
750
+ date: z.string().optional(), // 'YYYY-MM-DD'; omitted → next N open days
751
+ days: z.number().int().min(1).max(14).optional(), // window size when date omitted
752
+ }),
753
+ response: z.object({
754
+ service: z.object({
755
+ service_id: z.string(), name: z.string(), duration_min: z.number(),
756
+ price: z.number(), currency: z.string(),
757
+ }).nullable(),
758
+ slots: z.array(z.object({ date: z.string(), times: z.array(z.string()) })), // CalendarCard shape
759
+ error: z.string().optional(),
760
+ }),
761
+ effect: "ask", cost: "free", idempotent: true, simulatable: false,
762
+ examples: [{ slug: "clearwater", date: "2026-06-10" }],
763
+ }),
764
+ "booking:create": receiver({
765
+ receiver: "booking:create",
766
+ summary: "Hold a slot for a customer; returns a ref and a payment intent when price > 0",
767
+ request: z.object({
768
+ slug: z.string(),
769
+ service: z.string(),
770
+ slotDate: z.string(), // 'YYYY-MM-DD'
771
+ slotTime: z.string(), // 'HH:MM' 24h, slot start
772
+ customer: z.object({ name: z.string(), email: z.string().email().optional() }),
773
+ paymentRail: z.enum(["stripe", "sui"]).optional(), // resolver overrides by caller kind
774
+ }),
775
+ response: z.object({
776
+ ok: z.boolean(),
777
+ ref: z.string().optional(),
778
+ status: z.enum(["pending", "confirmed"]).optional(),
779
+ paymentRail: z.enum(["stripe", "sui"]).nullable().optional(),
780
+ paymentIntent: z.object({ clientSecret: z.string(), amount: z.number() }).optional(), // human, price>0
781
+ escrow: z.object({ escrowId: z.string(), bounty: z.number(), deadline: z.string() }).optional(), // agent
782
+ error: z.string().optional(), // 'slot_taken' | 'closed' | 'unknown_service'
783
+ }),
784
+ effect: "ask", cost: "variable", settles: "offchain", reversible: false,
785
+ simulatable: true, idempotent: true, // idempotencyKey dedupes a retried hold
786
+ examples: [{ slug: "clearwater", service: "svc_x", slotDate: "2026-06-10",
787
+ slotTime: "15:00", customer: { name: "Sarah", email: "s@ex.com" } }],
788
+ }),
789
+ // booking:configure — the ONE provider-gated booking receiver (§10). Unlike the
790
+ // two public-bookable receivers above, the resolver re-walks
791
+ // can(actor, group, "manage_clients") server-side and UPSERTs only the caller's
792
+ // own service rows. NOT public-bookable.
793
+ "booking:configure": receiver({
794
+ receiver: "booking:configure",
795
+ summary: "Provider-gated: create or update a bookable service (hours, slot length, price, currency, tz, active)",
796
+ request: z.object({
797
+ slug: z.string(), // the workspace; the write scope (re-walked)
798
+ actorId: z.string().optional(), // re-walk subject (gateway-forwarded)
799
+ service: z.string().optional(), // service_id to update; omitted → create
800
+ name: z.string(),
801
+ duration_min: z.number().int().positive(),
802
+ price: z.number().int().nonnegative().optional(),
803
+ currency: z.string().optional(),
804
+ hours_json: z.array(z.array(z.object({ start: z.string(), end: z.string() }))), // 7 entries, 0=Sun
805
+ tz: z.string().optional(),
806
+ active: z.boolean().optional(),
807
+ }),
808
+ response: z.object({ ok: z.boolean(), service_id: z.string().optional(), error: z.string().optional() }),
809
+ effect: "ask", cost: "free", idempotent: true, reversible: true, auth: "manage_clients",
810
+ examples: [{ slug: "clearwater", name: "Consultation", duration_min: 30, price: 4000, currency: "gbp",
811
+ hours_json: [[], [{ start: "09:00", end: "17:00" }], [], [], [], [], []] }],
812
+ }),
813
+ // booking:list-bookings — the provider's own appointment list for a month (§6
814
+ // web surface). Tenant-private read: the resolver scopes to the authenticated
815
+ // owner, never the body slug, so a caller only ever sees their own workspace.
816
+ "booking:list-bookings": receiver({
817
+ receiver: "booking:list-bookings",
818
+ summary: "Provider-gated: list a workspace's bookings for a month (customer, time, service, status)",
819
+ request: z.object({
820
+ slug: z.string(), // workspace (informational; gate re-scopes to caller)
821
+ month: z.string().optional(), // 'YYYY-MM'; omitted → current month
822
+ }),
823
+ response: z.object({
824
+ bookings: z.array(z.object({
825
+ id: z.string(), ref: z.string(), service_name: z.string(),
826
+ slot_date: z.string(), slot_time: z.string(),
827
+ customer_name: z.string(), status: z.string(),
828
+ })),
829
+ error: z.string().optional(),
830
+ }),
831
+ effect: "ask", cost: "free", idempotent: true, auth: "manage_clients",
832
+ examples: [{ slug: "clearwater", month: "2026-06" }],
833
+ }),
834
+ // booking:cancel — provider dissolves a booking by ref, releasing the slot
835
+ // immediately (the partial unique index drops dissolved rows from scope, §9).
836
+ // Tenant-private write: scoped to the authenticated owner. Idempotent.
837
+ "booking:cancel": receiver({
838
+ receiver: "booking:cancel",
839
+ summary: "Provider-gated: dissolve a booking by ref, releasing the slot",
840
+ request: z.object({
841
+ slug: z.string(),
842
+ ref: z.string(),
843
+ }),
844
+ response: z.object({ ok: z.boolean(), error: z.string().optional() }),
845
+ effect: "ask", cost: "free", idempotent: true, reversible: false, auth: "manage_clients",
846
+ examples: [{ slug: "clearwater", ref: "CLE-2026-4821" }],
847
+ }),
848
+ // ── workflows — define an SOP, run it, watch it learn (text/workflows-plan.md) ──
849
+ // Every workflow op is a typed receiver. No new REST routes. apply-diff is
850
+ // simulatable: simulate=true validates the WorkflowDiff (incl. DAG acyclicity)
851
+ // and commits nothing — the chat-authoring preview path.
852
+ "workflow:list": receiver({
853
+ receiver: "workflow:list",
854
+ summary: "List a workspace's workflows (D1 mirror); pass templates=true for the public SOP catalog",
855
+ request: z.object({
856
+ slug: z.string().optional(),
857
+ templates: z.boolean().optional(),
858
+ }),
859
+ response: z.object({
860
+ workflows: z.array(z.object({
861
+ id: z.string(), name: z.string(), description: z.string().optional(), step_count: z.number(),
862
+ status: z.string(), last_run_status: z.string().nullable(),
863
+ })),
864
+ error: z.string().optional(),
865
+ }),
866
+ effect: "ask", cost: "free", idempotent: true, simulatable: false, auth: "view_workflows",
867
+ examples: [{ slug: "acme" }, { templates: true }],
868
+ }),
869
+ "workflow:get": receiver({
870
+ receiver: "workflow:get",
871
+ summary: "Fetch one workflow's full graph — steps (kind, config, position) and edges (with conditions)",
872
+ request: z.object({ workflowId: z.string() }),
873
+ response: z.object({
874
+ id: z.string(), name: z.string(), status: z.string(),
875
+ steps: z.array(z.object({
876
+ id: z.string(), kind: StepKind, name: z.string(),
877
+ config: z.string(), position: z.string().optional(),
878
+ })),
879
+ edges: z.array(z.object({
880
+ source: z.string(), target: z.string(),
881
+ strength: z.number(), resistance: z.number(),
882
+ condition: z.string().optional(),
883
+ })),
884
+ error: z.string().optional(),
885
+ }),
886
+ effect: "ask", cost: "free", idempotent: true, simulatable: false, auth: "view_workflows",
887
+ examples: [{ workflowId: "wf_abc" }],
888
+ }),
889
+ "workflow:runs": receiver({
890
+ receiver: "workflow:runs",
891
+ summary: "List recent runs of a workflow (D1 workflow_run) for the monitor + history surfaces",
892
+ request: z.object({ workflowId: z.string(), runId: z.string().optional(), limit: z.number().int().min(1).max(200).optional() }),
893
+ response: z.object({
894
+ runs: z.array(z.object({
895
+ id: z.string(), status: z.string(),
896
+ started_at: z.number(), finished_at: z.number().nullable(),
897
+ })),
898
+ events: z.array(z.object({
899
+ step_id: z.string(), kind: z.string(), status: z.string().nullable(),
900
+ latency_ms: z.number().nullable(), at: z.number(),
901
+ })).optional(),
902
+ error: z.string().optional(),
903
+ }),
904
+ effect: "ask", cost: "free", idempotent: true, simulatable: false, auth: "view_workflows",
905
+ examples: [{ workflowId: "wf_abc", limit: 20 }, { workflowId: "wf_abc", runId: "run_1" }],
906
+ }),
907
+ "workflow:create": receiver({
908
+ receiver: "workflow:create",
909
+ summary: "Create a blank workflow (group group-type=workflow) — optionally cloned from a template",
910
+ request: z.object({
911
+ slug: z.string().optional(),
912
+ name: z.string(),
913
+ description: z.string().optional(), // defaults from the cloned template
914
+ fromTemplate: z.string().optional(),
915
+ simulate: z.boolean().optional(),
916
+ }),
917
+ response: z.object({ id: z.string(), workflowId: z.string() }),
918
+ effect: "ask", cost: "free", reversible: true, idempotent: false, simulatable: true,
919
+ auth: "manage_workflows",
920
+ examples: [{ name: "Client Onboarding" }, { name: "demo", simulate: true }],
921
+ }),
922
+ "workflow:apply-diff": receiver({
923
+ receiver: "workflow:apply-diff",
924
+ summary: "Apply a WorkflowDiff (add/remove/connect/disconnect/update). simulate=true validates the DAG without persisting",
925
+ request: z.object({
926
+ workflowId: z.string(),
927
+ diff: WorkflowDiffSchema,
928
+ simulate: z.boolean().optional(),
929
+ version: z.number().int().optional(), // optimistic lock — stale → conflict
930
+ }),
931
+ response: z.object({
932
+ ok: z.boolean(),
933
+ stepCount: z.number().optional(),
934
+ idMap: z.record(z.string(), z.string()).optional(), // tempId → persisted id
935
+ error: z.string().optional(), // 'cycle_detected' | 'conflict' | 'invalid_diff'
936
+ }),
937
+ effect: "ask", cost: "free", reversible: true, idempotent: false, simulatable: true,
938
+ auth: "manage_workflows",
939
+ examples: [{ workflowId: "wf_abc", diff: { add: [{ tempId: "s1", kind: "trigger", name: "On signup", config: "{}" }] }, simulate: true }],
940
+ }),
941
+ "workflow:run": receiver({
942
+ receiver: "workflow:run",
943
+ summary: "Start a run — spawns the WorkflowRun DO, executes each step, marks path strength on traversed edges",
944
+ request: z.object({
945
+ workflowId: z.string(),
946
+ triggerPayload: z.record(z.string(), z.unknown()).optional(),
947
+ idempotencyKey: z.string().optional(),
948
+ }),
949
+ response: z.object({ runId: z.string(), status: z.string(), error: z.string().optional() }),
950
+ effect: "ask", cost: "variable", reversible: false, idempotent: true, simulatable: false, settles: "none",
951
+ auth: "manage_workflows",
952
+ examples: [{ workflowId: "wf_abc" }],
953
+ }),
954
+ "human:resolve": receiver({
955
+ receiver: "human:resolve",
956
+ summary: "Resolve a suspended human step — carries { runId, stepId, decision | formPayload } to the run DO",
957
+ request: z.object({
958
+ runId: z.string(),
959
+ stepId: z.string(),
960
+ decision: z.enum(["approved", "rejected"]).optional(),
961
+ formPayload: z.record(z.string(), z.unknown()).optional(),
962
+ // System callers (a Telegram/Discord button via channels) attest the
963
+ // workspace; an authenticated session's slug always wins over this. The
964
+ // `slug` is trusted ONLY with a valid `attestation` — HMAC(IDENTITY_SECRET,
965
+ // "human:resolve:<runId>:<stepId>:<slug>") that only channels can mint after
966
+ // it re-walks the tapper's authority. Without it, slug is ignored.
967
+ slug: z.string().optional(),
968
+ attestation: z.string().optional(),
969
+ }),
970
+ response: z.object({ ok: z.boolean(), status: z.string().optional(), error: z.string().optional() }),
971
+ effect: "ask", cost: "free", idempotent: true, simulatable: false, auth: "manage_workflows",
972
+ examples: [{ runId: "run_1", stepId: "s2", decision: "approved" }],
973
+ }),
974
+ "checkout:create": receiver({
975
+ receiver: "checkout:create",
976
+ summary: "The `sell` step's binding — mint a Connect checkout session for the buyer, stamping { runId, stepId } into session metadata so checkout:resume can wake the run",
977
+ request: z.object({
978
+ workspace: z.string(),
979
+ ppid: z.string(),
980
+ // Set by the executor when a `sell` step dispatches this — the run's resume coordinates.
981
+ runId: z.string().optional(),
982
+ stepId: z.string().optional(),
983
+ }),
984
+ response: z.object({
985
+ clientSecret: z.string().nullable().optional(),
986
+ sessionId: z.string().optional(),
987
+ applicationFeeAmount: z.number().optional(),
988
+ stripeAccount: z.string().optional(),
989
+ error: z.string().optional(),
990
+ }),
991
+ effect: "ask", cost: "variable", reversible: false, idempotent: false, simulatable: false, settles: "offchain",
992
+ auth: "manage_workflows",
993
+ examples: [{ workspace: "acme", ppid: "pr_1" }],
994
+ }),
995
+ "checkout:resume": receiver({
996
+ receiver: "checkout:resume",
997
+ summary: "Wake a run suspended at a `sell` step — the storefront webhook fires this on checkout.session.completed with the (runId, stepId) it stamped at create. Sibling of human:resolve.",
998
+ request: z.object({
999
+ runId: z.string(),
1000
+ stepId: z.string(),
1001
+ // The run's own workspace (from the session metadata the webhook reads); the
1002
+ // runId is the resume capability, re-validated against the parked run server-side.
1003
+ slug: z.string().optional(),
1004
+ }),
1005
+ response: z.object({ ok: z.boolean(), status: z.string().optional(), error: z.string().optional() }),
1006
+ effect: "signal", cost: "free", idempotent: true, simulatable: false, auth: "manage_workflows",
1007
+ examples: [{ runId: "run_1", stepId: "s2", slug: "acme" }],
1008
+ }),
1009
+ "human:notify": receiver({
1010
+ receiver: "human:notify",
1011
+ summary: "Post a channel-shaped approval into a run's conversation when it suspends at a human step — web card · Telegram inline keys · Discord components. Sibling of chat:send.",
1012
+ request: z.object({
1013
+ runId: z.string(),
1014
+ stepId: z.string(),
1015
+ stepName: z.string().optional(),
1016
+ workflowName: z.string().optional(),
1017
+ group: z.string().optional(), // conversation group; falls back to the run's started_by
1018
+ }),
1019
+ response: z.object({ ok: z.boolean(), group: z.string().optional(), error: z.string().optional() }),
1020
+ effect: "signal", cost: "free", idempotent: true, simulatable: false, auth: "manage_workflows",
1021
+ examples: [{ runId: "run_1", stepId: "s2", stepName: "Approve contract" }],
1022
+ }),
1023
+ "workflow:stop": receiver({
1024
+ receiver: "workflow:stop",
1025
+ summary: "Park a live run at its current step (status → paused); preserves the resume cursor so human:resolve or a re-run continues it",
1026
+ request: z.object({ runId: z.string() }),
1027
+ response: z.object({ ok: z.boolean(), status: z.string().optional(), error: z.string().optional() }),
1028
+ effect: "ask", cost: "free", reversible: true, idempotent: true, simulatable: false, auth: "manage_workflows",
1029
+ examples: [{ runId: "run_1" }],
1030
+ }),
1031
+ "workflow:trigger": receiver({
1032
+ receiver: "workflow:trigger",
1033
+ summary: "Fire every workspace workflow whose trigger step matches a channel source (e.g. webhook:telegram)",
1034
+ request: z.object({
1035
+ source: z.string(),
1036
+ slug: z.string().optional(),
1037
+ triggerPayload: z.record(z.string(), z.unknown()).optional(),
1038
+ }),
1039
+ response: z.object({ ok: z.boolean(), started: z.number(), runIds: z.array(z.string()) }),
1040
+ effect: "signal", cost: "variable", reversible: false, idempotent: false, simulatable: false, auth: "manage_workflows",
1041
+ examples: [{ source: "webhook:telegram", slug: "acme" }],
1042
+ }),
1043
+ "workflow:delete": receiver({
1044
+ receiver: "workflow:delete",
1045
+ summary: "Delete a workflow and all its runs, steps, and edges",
1046
+ request: z.object({ workflowId: z.string() }),
1047
+ response: z.object({ ok: z.boolean(), error: z.string().optional() }),
1048
+ effect: "ask", cost: "free", reversible: false, idempotent: true, auth: "manage_workflows",
1049
+ examples: [{ workflowId: "wf_abc" }],
1050
+ }),
1051
+ "workflow:update": receiver({
1052
+ receiver: "workflow:update",
1053
+ summary: "Rename a workflow or change its status (draft|active|paused)",
1054
+ request: z.object({ workflowId: z.string(), name: z.string().optional(), status: z.enum(["draft", "active", "paused"]).optional() }),
1055
+ response: z.object({ ok: z.boolean(), error: z.string().optional() }),
1056
+ effect: "ask", cost: "free", reversible: true, idempotent: true, auth: "manage_workflows",
1057
+ examples: [{ workflowId: "wf_abc", name: "Renamed Workflow" }, { workflowId: "wf_abc", status: "paused" }],
1058
+ }),
1059
+ // ── The autonomy ladder's leaf runners — a workflow skill/agent step compiles
1060
+ // to one of these (workflow-executor.ts stepBinding). Both proxy to the
1061
+ // channels runtime; web owns the catalog + tenant authority (workflows-lock C2/C3).
1062
+ "skills:list": receiver({
1063
+ receiver: "skills:list",
1064
+ summary: "List the caller's workspace skill catalog (R2) — powers the canvas SkillPicker",
1065
+ request: z.object({}),
1066
+ response: z.object({
1067
+ skills: z.array(z.object({ name: z.string(), size: z.number() })),
1068
+ }),
1069
+ effect: "ask", cost: "free", idempotent: true, simulatable: false, auth: "view_workflows",
1070
+ examples: [{}],
1071
+ }),
1072
+ "skill:run": receiver({
1073
+ receiver: "skill:run",
1074
+ summary: "Run a workspace skill — loads its body from R2, runs one bounded turn via channels, returns { text }",
1075
+ request: z.object({ skill: z.string() }).catchall(z.unknown()), // extra keys = the skill's input
1076
+ response: z.object({ ok: z.boolean(), text: z.string().optional(), skill: z.string().optional(), error: z.string().optional() }),
1077
+ effect: "ask", cost: "variable", reversible: false, idempotent: false, simulatable: false, settles: "none", auth: "manage_workflows",
1078
+ examples: [{ skill: "copywriting", topic: "launch" }],
1079
+ }),
1080
+ "agent:run": receiver({
1081
+ receiver: "agent:run",
1082
+ summary: "Invoke a bound actor (optionally skill-constrained) for one bounded turn via channels; returns { text }",
1083
+ request: z.object({
1084
+ actorId: z.string(),
1085
+ skill: z.string().optional(),
1086
+ instructions: z.string().optional(),
1087
+ }).catchall(z.unknown()), // extra keys = the agent's input
1088
+ response: z.object({ ok: z.boolean(), text: z.string().optional(), actorId: z.string().optional(), error: z.string().optional() }),
1089
+ effect: "ask", cost: "variable", reversible: false, idempotent: false, simulatable: false, settles: "none", auth: "manage_workflows",
1090
+ examples: [{ actorId: "one:support", skill: "support", instructions: "Reply kindly." }],
1091
+ }),
1092
+ // ── video — room provisioning, sessions, invitations, recording (text/video-live-improve-plan.md) ──
1093
+ // Seven receivers already ship in receiver-resolvers.ts. These declarations expose
1094
+ // them to typed ask<R>/signal<R> callers, the MCP surface, and edge validation.
1095
+ // Four new receivers (quick-room, join-event, start-recording, start-stream) are
1096
+ // declared here and implemented in the same W1 batch.
1097
+ "video:create-room": receiver({
1098
+ receiver: "video:create-room",
1099
+ summary: "Create a 100ms video room for a workspace; returns the room slug and URL",
1100
+ request: z.object({
1101
+ workspace: z.string(),
1102
+ roomSlug: z.string(),
1103
+ name: z.string(),
1104
+ description: z.string().optional(),
1105
+ type: z.enum(["meeting", "classroom", "webinar"]).optional(),
1106
+ region: z.string().optional(),
1107
+ }),
1108
+ response: z.object({
1109
+ ok: z.boolean(),
1110
+ room: z.object({
1111
+ slug: z.string(), name: z.string(), description: z.string(),
1112
+ type: z.string(), hmsRoomId: z.string(), url: z.string(),
1113
+ }).optional(),
1114
+ error: z.string().optional(),
1115
+ }),
1116
+ effect: "ask", cost: "free", idempotent: false, auth: "manage_clients",
1117
+ examples: [{ workspace: "acme", roomSlug: "algebra-101", name: "Algebra 101", type: "classroom" }],
1118
+ }),
1119
+ "video:delete-room": receiver({
1120
+ receiver: "video:delete-room",
1121
+ summary: "Disable a video room and its 100ms counterpart; room is no longer joinable",
1122
+ request: z.object({ workspace: z.string(), roomSlug: z.string() }),
1123
+ response: z.object({ ok: z.boolean(), error: z.string().optional() }),
1124
+ effect: "ask", cost: "free", idempotent: true, reversible: false, auth: "manage_clients",
1125
+ examples: [{ workspace: "acme", roomSlug: "algebra-101" }],
1126
+ }),
1127
+ "video:contact-call": receiver({
1128
+ receiver: "video:contact-call",
1129
+ summary: "Provision a one-to-one video room for a contact; links the room thread to the contact CRM record",
1130
+ request: z.object({
1131
+ workspace: z.string(),
1132
+ actorId: z.string(),
1133
+ name: z.string().optional(),
1134
+ }),
1135
+ response: z.object({
1136
+ ok: z.boolean(),
1137
+ roomSlug: z.string().optional(),
1138
+ threadId: z.string().nullable().optional(),
1139
+ guestJoinUrl: z.string().optional(),
1140
+ hostJoinUrl: z.string().optional(),
1141
+ error: z.string().optional(),
1142
+ }),
1143
+ effect: "ask", cost: "free", idempotent: false, auth: "manage_clients",
1144
+ examples: [{ workspace: "acme", actorId: "actor_123", name: "Call with Sarah" }],
1145
+ }),
1146
+ "video:invite": receiver({
1147
+ receiver: "video:invite",
1148
+ summary: "Generate a tracked /go/ join link for an actor into an existing room",
1149
+ request: z.object({
1150
+ workspace: z.string(),
1151
+ roomSlug: z.string(),
1152
+ actorId: z.string().optional(),
1153
+ }),
1154
+ response: z.object({
1155
+ ok: z.boolean(),
1156
+ goUrl: z.string().optional(),
1157
+ destination: z.string().optional(),
1158
+ error: z.string().optional(),
1159
+ }),
1160
+ effect: "ask", cost: "free", idempotent: false, auth: "manage_clients",
1161
+ examples: [{ workspace: "acme", roomSlug: "algebra-101", actorId: "actor_456" }],
1162
+ }),
1163
+ "video:schedule-webinar": receiver({
1164
+ receiver: "video:schedule-webinar",
1165
+ summary: "Create a webinar room and send tracked invite links to N contacts in one call",
1166
+ request: z.object({
1167
+ workspace: z.string(),
1168
+ name: z.string().optional(),
1169
+ actorIds: z.array(z.string()).min(1).max(500),
1170
+ }),
1171
+ response: z.object({
1172
+ ok: z.boolean(),
1173
+ roomUrl: z.string().optional(),
1174
+ invited: z.number().optional(),
1175
+ links: z.array(z.object({ actorId: z.string(), goUrl: z.string() })).optional(),
1176
+ error: z.string().optional(),
1177
+ }),
1178
+ effect: "ask", cost: "free", idempotent: false, auth: "manage_clients",
1179
+ examples: [{ workspace: "acme", name: "Product Launch", actorIds: ["actor_1", "actor_2"] }],
1180
+ }),
1181
+ "video:create-session": receiver({
1182
+ receiver: "video:create-session",
1183
+ summary: "Create a video session with host + guest tracked join links; optionally schedules the call",
1184
+ request: z.object({
1185
+ workspace: z.string(),
1186
+ roomSlug: z.string(),
1187
+ name: z.string(),
1188
+ type: z.enum(["meeting", "classroom", "webinar"]).optional(),
1189
+ region: z.string().optional(),
1190
+ guestActorId: z.string().optional(),
1191
+ scheduledAt: z.number().optional(),
1192
+ }),
1193
+ response: z.object({
1194
+ ok: z.boolean(),
1195
+ room: z.object({ slug: z.string(), hmsRoomId: z.string(), url: z.string() }).optional(),
1196
+ hostJoinUrl: z.string().optional(),
1197
+ guestJoinUrl: z.string().optional(),
1198
+ error: z.string().optional(),
1199
+ }),
1200
+ effect: "ask", cost: "free", idempotent: false, auth: "manage_clients",
1201
+ examples: [{ workspace: "acme", roomSlug: "consult-abc", name: "Consultation", guestActorId: "actor_789" }],
1202
+ }),
1203
+ "video:summary": receiver({
1204
+ receiver: "video:summary",
1205
+ summary: "Run an AI summary of a call transcript and append it as a system message on the room thread",
1206
+ request: z.object({
1207
+ threadId: z.string(),
1208
+ transcript: z.string(),
1209
+ }),
1210
+ response: z.object({
1211
+ ok: z.boolean(),
1212
+ threadId: z.string().optional(),
1213
+ skipped: z.string().optional(),
1214
+ error: z.string().optional(),
1215
+ }),
1216
+ effect: "ask", cost: "variable", idempotent: false, auth: "manage_clients",
1217
+ examples: [{ threadId: "thr_abc", transcript: "Host: Hello... Guest: Hi..." }],
1218
+ }),
1219
+ "video:quick-room": receiver({
1220
+ receiver: "video:quick-room",
1221
+ summary: "Ad-hoc video room from any chat thread — creates the room, sends the join link into the thread",
1222
+ request: z.object({
1223
+ workspace: z.string(),
1224
+ threadId: z.string().optional(),
1225
+ name: z.string().optional(),
1226
+ }),
1227
+ response: z.object({
1228
+ ok: z.boolean(),
1229
+ roomSlug: z.string().optional(),
1230
+ guestJoinUrl: z.string().optional(),
1231
+ hostJoinUrl: z.string().optional(),
1232
+ threadId: z.string().nullable().optional(),
1233
+ error: z.string().optional(),
1234
+ }),
1235
+ effect: "ask", cost: "free", idempotent: false, auth: "manage_clients",
1236
+ examples: [{ workspace: "acme", threadId: "thr_xyz", name: "Quick sync" }],
1237
+ }),
1238
+ "video:join-event": receiver({
1239
+ receiver: "video:join-event",
1240
+ summary: "Record a peer join event for tracking and lifecycle; public — called from the /go/ link handler",
1241
+ request: z.object({
1242
+ workspace: z.string(),
1243
+ roomSlug: z.string(),
1244
+ role: z.string().optional(),
1245
+ actorId: z.string().optional(),
1246
+ }),
1247
+ response: z.object({ ok: z.boolean(), error: z.string().optional() }),
1248
+ effect: "signal", cost: "free", idempotent: true,
1249
+ examples: [{ workspace: "acme", roomSlug: "consult-abc", role: "guest" }],
1250
+ }),
1251
+ "video:start-recording": receiver({
1252
+ receiver: "video:start-recording",
1253
+ summary: "Start cloud recording for an active room via 100ms REST; inserts a video_recordings row",
1254
+ request: z.object({ workspace: z.string(), roomSlug: z.string() }),
1255
+ response: z.object({
1256
+ ok: z.boolean(),
1257
+ recordingId: z.string().optional(),
1258
+ error: z.string().optional(),
1259
+ }),
1260
+ effect: "ask", cost: "free", idempotent: false, auth: "manage_clients",
1261
+ examples: [{ workspace: "acme", roomSlug: "algebra-101" }],
1262
+ }),
1263
+ "video:start-stream": receiver({
1264
+ receiver: "video:start-stream",
1265
+ summary: "Start an HLS live stream for a room; returns the playback URL when the stream is ready",
1266
+ request: z.object({
1267
+ workspace: z.string(),
1268
+ roomSlug: z.string(),
1269
+ hlsConfig: z.record(z.string(), z.unknown()).optional(),
1270
+ }),
1271
+ response: z.object({
1272
+ ok: z.boolean(),
1273
+ hlsUrl: z.string().optional(),
1274
+ error: z.string().optional(),
1275
+ }),
1276
+ effect: "ask", cost: "free", idempotent: false, auth: "manage_clients",
1277
+ examples: [{ workspace: "acme", roomSlug: "product-launch" }],
1278
+ }),
1279
+ // connect:channel — token-paste connect for a channel (Connect plan C7). The
1280
+ // typed contract over the shipped /api/connect/telegram route: the caller pastes
1281
+ // a channel credential, web verifies it (getMe), points the webhook, then hands
1282
+ // the raw secret to channels' /connect sink under the owner gate. SECURITY: the
1283
+ // token rides the REQUEST only; the response NEVER echoes it. effect:ask,
1284
+ // auth:manage_clients re-walked server-side off locals.slug (never body actorId).
1285
+ "connect:channel": receiver({
1286
+ receiver: "connect:channel",
1287
+ summary: "Connect a channel (e.g. telegram) by pasting its bot token; verifies, sets the webhook, seals the credential under the owner gate",
1288
+ request: z.object({
1289
+ channel: z.string(), // 'telegram' (token-paste path)
1290
+ token: z.string(), // the bot credential — request-only, never returned/logged
1291
+ slug: z.string().optional(), // workspace; gate re-scopes to the authenticated owner
1292
+ }),
1293
+ response: z.object({
1294
+ ok: z.boolean(),
1295
+ status: z.string().optional(), // 'active' | 'updated' from the channels sink
1296
+ account: z.string().optional(), // public handle (e.g. bot username) — never the token
1297
+ error: z.string().optional(), // 'invalid token' | 'unauthorized' | 'duplicate'
1298
+ }),
1299
+ effect: "ask", cost: "free", reversible: false, idempotent: true, auth: "manage_clients",
1300
+ examples: [{ channel: "telegram", token: "<bot-token>" }],
1301
+ }),
1302
+ // connect:status — list the workspace's connected channels (Connect plan C8).
1303
+ // Read-only contract over /api/connect/status: rows come from the `connection`
1304
+ // table scoped to the authenticated owner (locals.slug — never a body slug).
1305
+ // SECURITY: the response carries channel + public account id + status only;
1306
+ // cred_ref and webhook_secret never leave the server.
1307
+ "connect:status": receiver({
1308
+ receiver: "connect:status",
1309
+ summary: "List connected channels for the workspace — channel, account, status, runtime; never the credential",
1310
+ request: z.object({}),
1311
+ response: z.object({
1312
+ ok: z.boolean(),
1313
+ connections: z
1314
+ .array(z.object({
1315
+ channel: z.string(), // 'telegram' | 'slack' | 'discord' | 'email'
1316
+ account: z.string(), // external_account_id (bot id / team id / inbound address)
1317
+ status: z.string(), // 'active' | 'revoked'
1318
+ runtime: z.string(), // 'webhook' | 'socket'
1319
+ connected_at: z.number(), // unix epoch seconds
1320
+ }))
1321
+ .optional(),
1322
+ error: z.string().optional(),
1323
+ }),
1324
+ effect: "ask", cost: "free", reversible: true, idempotent: true, auth: "manage_clients",
1325
+ examples: [{}],
1326
+ }),
1327
+ // ── Broadcast (newsletter) ────────────────────────────────────────────────
1328
+ "broadcast:create": receiver({
1329
+ receiver: "broadcast:create",
1330
+ summary: "Create a broadcast draft — subject, body_md, audience_tag, channel",
1331
+ request: z.object({ workspace: z.string(), subject: z.string().optional(), body_md: z.string().optional(), audience_tag: z.string().optional(), channel: z.enum(["email", "sms", "whatsapp"]).optional() }),
1332
+ response: z.object({ broadcastId: z.string() }),
1333
+ effect: "ask", cost: "free", reversible: true, idempotent: false, auth: "write",
1334
+ }),
1335
+ "broadcast:update": receiver({
1336
+ receiver: "broadcast:update",
1337
+ summary: "Update a broadcast draft — subject, body, audience, schedule",
1338
+ request: z.object({ workspace: z.string(), broadcastId: z.string(), subject: z.string().optional(), body_md: z.string().optional(), audience_tag: z.string().optional(), scheduled_at: z.number().optional() }),
1339
+ response: z.object({ ok: z.boolean() }),
1340
+ effect: "ask", cost: "free", reversible: true, idempotent: true, auth: "write",
1341
+ }),
1342
+ "broadcast:list": receiver({
1343
+ receiver: "broadcast:list",
1344
+ summary: "List broadcasts for a workspace with status and sent_at",
1345
+ request: z.object({ workspace: z.string(), status: z.enum(["draft", "scheduled", "sending", "sent", "cancelled"]).optional(), limit: z.number().int().max(100).optional() }),
1346
+ response: z.object({ broadcasts: z.array(z.object({ id: z.string(), subject: z.string(), status: z.string(), sent_at: z.number().nullable(), audience_tag: z.string().nullable() })) }),
1347
+ effect: "ask", cost: "free", reversible: false, idempotent: true, auth: "read",
1348
+ }),
1349
+ "broadcast:get": receiver({
1350
+ receiver: "broadcast:get",
1351
+ summary: "Get a single broadcast with recipient counts",
1352
+ request: z.object({ workspace: z.string(), broadcastId: z.string() }),
1353
+ response: z.object({ broadcast: z.record(z.string(), z.unknown()) }),
1354
+ effect: "ask", cost: "free", reversible: false, idempotent: true, auth: "read",
1355
+ }),
1356
+ "broadcast:test": receiver({
1357
+ receiver: "broadcast:test",
1358
+ summary: "Send a test email for a broadcast to one address",
1359
+ request: z.object({ workspace: z.string(), broadcastId: z.string(), to: z.string().email() }),
1360
+ response: z.object({ ok: z.boolean() }),
1361
+ effect: "signal", cost: "variable", reversible: false, idempotent: false, auth: "write",
1362
+ }),
1363
+ "broadcast:send": receiver({
1364
+ receiver: "broadcast:send",
1365
+ summary: "Seed recipients from audience tag, apply suppression, enqueue the send batch",
1366
+ request: z.object({ workspace: z.string(), broadcastId: z.string() }),
1367
+ response: z.object({ enqueued: z.number() }),
1368
+ effect: "signal", cost: "variable", reversible: false, idempotent: false, auth: "write",
1369
+ }),
1370
+ "broadcast:schedule": receiver({
1371
+ receiver: "broadcast:schedule",
1372
+ summary: "Schedule a broadcast to send at a future unix timestamp",
1373
+ request: z.object({ workspace: z.string(), broadcastId: z.string(), scheduled_at: z.number().int() }),
1374
+ response: z.object({ ok: z.boolean() }),
1375
+ effect: "ask", cost: "free", reversible: true, idempotent: true, auth: "write",
1376
+ }),
1377
+ "broadcast:cancel": receiver({
1378
+ receiver: "broadcast:cancel",
1379
+ summary: "Cancel a scheduled or in-progress broadcast",
1380
+ request: z.object({ workspace: z.string(), broadcastId: z.string() }),
1381
+ response: z.object({ ok: z.boolean() }),
1382
+ effect: "ask", cost: "free", reversible: false, idempotent: true, auth: "write",
1383
+ }),
1384
+ // ── Audience (public subscribe/unsubscribe) ────────────────────────────────
1385
+ "audience:subscribe": receiver({
1386
+ receiver: "audience:subscribe",
1387
+ summary: "Begin double opt-in for a contact — writes consent:email:pending tag, sends confirm email",
1388
+ request: z.object({ workspace: z.string(), address: z.string().email(), list: z.string().optional(), ref: z.string().optional() }),
1389
+ response: z.object({ ok: z.boolean() }),
1390
+ effect: "ask", cost: "free", reversible: false, idempotent: true, auth: "public",
1391
+ }),
1392
+ "audience:unsubscribe": receiver({
1393
+ receiver: "audience:unsubscribe",
1394
+ summary: "One-click unsubscribe — writes suppression row and flips consent tag; RFC 8058 safe",
1395
+ request: z.object({ workspace: z.string(), channel: z.enum(["email", "sms", "whatsapp"]).optional(), address: z.string(), token: z.string() }),
1396
+ response: z.object({ ok: z.boolean() }),
1397
+ effect: "ask", cost: "free", reversible: false, idempotent: true, auth: "public",
1398
+ }),
1399
+ "audience:confirm": receiver({
1400
+ receiver: "audience:confirm",
1401
+ summary: "Confirm email subscription via opt-in token — flips consent tag to subscribed",
1402
+ request: z.object({ token: z.string() }),
1403
+ response: z.object({ ok: z.boolean() }),
1404
+ effect: "ask", cost: "free", reversible: false, idempotent: true, auth: "public",
1405
+ }),
1406
+ // ── Winback batch ─────────────────────────────────────────────────────────
1407
+ "audience:winback": receiver({
1408
+ receiver: "audience:winback",
1409
+ summary: "Re-engage dormant subscribers (batch winback sequence tool) — queries contacts with no send in `days` days, sends one re-engage email each through the suppression-checked send path",
1410
+ request: z.object({ days: z.number().optional(), campaign: z.string().optional() }),
1411
+ response: z.object({ ok: z.boolean(), sent: z.number(), scanned: z.number() }),
1412
+ effect: "signal", cost: "variable", reversible: false, idempotent: false, auth: "write",
1413
+ }),
1414
+ // ── Workflow tool — channel-neutral send ──────────────────────────────────
1415
+ "message:send": receiver({
1416
+ receiver: "message:send",
1417
+ summary: "Send a message to one address via channel — suppression-checked; sms/whatsapp dissolve until adapters ship",
1418
+ request: z.object({ workspace: z.string(), channel: z.enum(["email", "sms", "whatsapp"]), to: z.string(), subject: z.string().optional(), body: z.string(), campaign: z.string().optional() }),
1419
+ response: z.object({ ok: z.boolean() }),
1420
+ effect: "signal", cost: "variable", reversible: false, idempotent: false, auth: "write",
1421
+ }),
609
1422
  };
610
1423
  /**
611
1424
  * RECIPES — the four agent journeys as typed, ordered receiver sequences (C6).
612
1425
  * Each entry is `readonly ReceiverName[]`, so a typo or a renamed receiver is a
613
1426
  * compile error. `meta:catalog?goal=` returns one of these; the MCP server
614
- * groups its tools by them. See `plans/agent-first-spec.md`.
1427
+ * groups its tools by them. See `text/agent-first-spec-plan.md`.
615
1428
  */
616
1429
  export const RECIPES = {
617
1430
  /** Onboard: become an actor → mint a key → join a group → declare capability. */
@@ -622,5 +1435,7 @@ export const RECIPES = {
622
1435
  trade: ["capabilities:publish", "market:list", "market:hire", "pay:weight"],
623
1436
  /** Transact: post a bounty → settle it onchain. */
624
1437
  transact: ["market:bounty", "pay:weight"],
1438
+ /** SOP: define a workflow → shape it with a diff → run it. */
1439
+ sop: ["workflow:create", "workflow:apply-diff", "workflow:run"],
625
1440
  };
626
1441
  //# sourceMappingURL=receivers.js.map