@oneie/sdk 0.14.11 → 0.14.13
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 +6 -1
- package/dist/.build-fingerprint +1 -0
- package/dist/billing.d.ts +12 -0
- package/dist/billing.d.ts.map +1 -1
- package/dist/billing.js +32 -5
- package/dist/billing.js.map +1 -1
- package/dist/blocks.d.ts +126 -0
- package/dist/blocks.d.ts.map +1 -0
- package/dist/blocks.js +407 -0
- package/dist/blocks.js.map +1 -0
- package/dist/client.d.ts +0 -4
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +1 -23
- package/dist/client.js.map +1 -1
- package/dist/compile.d.ts.map +1 -1
- package/dist/compile.js +26 -6
- package/dist/compile.js.map +1 -1
- package/dist/fetch.d.ts +17 -2
- package/dist/fetch.d.ts.map +1 -1
- package/dist/fetch.js +22 -26
- package/dist/fetch.js.map +1 -1
- package/dist/fn-allowlist.d.ts +27 -0
- package/dist/fn-allowlist.d.ts.map +1 -0
- package/dist/fn-allowlist.js +60 -0
- package/dist/fn-allowlist.js.map +1 -0
- package/dist/generated/fn-map.d.ts +2 -2
- package/dist/generated/fn-map.d.ts.map +1 -1
- package/dist/generated/fn-map.js +480 -13
- package/dist/generated/fn-map.js.map +1 -1
- package/dist/generated/schemas/index.d.ts +1 -1
- package/dist/generated/schemas.d.ts +77 -3
- package/dist/generated/schemas.d.ts.map +1 -1
- package/dist/generated/schemas.js +28 -3
- package/dist/generated/schemas.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/market.d.ts +37 -1
- package/dist/market.d.ts.map +1 -1
- package/dist/market.js +26 -1
- package/dist/market.js.map +1 -1
- package/dist/receiver-action.d.ts +4 -0
- package/dist/receiver-action.d.ts.map +1 -0
- package/dist/receiver-action.js +38 -0
- package/dist/receiver-action.js.map +1 -0
- package/dist/receivers.d.ts +902 -24
- package/dist/receivers.d.ts.map +1 -1
- package/dist/receivers.js +1363 -40
- package/dist/receivers.js.map +1 -1
- package/dist/role-actions.d.ts +28 -0
- package/dist/role-actions.d.ts.map +1 -0
- package/dist/role-actions.js +68 -0
- package/dist/role-actions.js.map +1 -0
- package/dist/role-tiers.d.ts +45 -0
- package/dist/role-tiers.d.ts.map +1 -0
- package/dist/role-tiers.js +99 -0
- package/dist/role-tiers.js.map +1 -0
- package/dist/schemas.d.ts +23 -1
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +21 -0
- package/dist/schemas.js.map +1 -1
- package/dist/work-contract-grammar.json +44 -0
- package/package.json +22 -2
package/dist/receivers.js
CHANGED
|
@@ -9,6 +9,19 @@ import { GroupEntitySchema } from "./generated/schemas/index.js";
|
|
|
9
9
|
export function receiver(r) {
|
|
10
10
|
return r;
|
|
11
11
|
}
|
|
12
|
+
/**
|
|
13
|
+
* Receiver name → derived MCP tool name (`video:create-room` → `video_create_room`).
|
|
14
|
+
*
|
|
15
|
+
* ONE rule, two consumers — this module (to decide which receivers need an
|
|
16
|
+
* explicit `surfaces.mcp.name` override) and `mcpToolsFromRegistry()` in
|
|
17
|
+
* `@oneie/mcp`. A second copy is how the derived name and the registered name
|
|
18
|
+
* drift apart, which `check:tools` would then report as a missing tool.
|
|
19
|
+
*
|
|
20
|
+
* Deliberately NOT the channels rule (`/[^a-zA-Z0-9_-]/g`, which keeps hyphens):
|
|
21
|
+
* MCP tool names permit hyphens, but every one of the 101 hand-written MCP tools
|
|
22
|
+
* is underscore-only, so this matches the surface it feeds.
|
|
23
|
+
*/
|
|
24
|
+
export const mcpToolName = (receiver) => receiver.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
12
25
|
const ok = z.object({ ok: z.boolean() });
|
|
13
26
|
/**
|
|
14
27
|
* The receiver catalog — one source of truth for capability. Object-literal keys
|
|
@@ -29,6 +42,7 @@ export const RECEIVERS = {
|
|
|
29
42
|
}),
|
|
30
43
|
"agent:own-link": receiver({
|
|
31
44
|
receiver: "agent:own-link",
|
|
45
|
+
surfaces: { mcp: { name: "own_me" } },
|
|
32
46
|
summary: "Generate a signed ownership invitation link for a registered agent; optionally deliver it as a native button on Telegram or Discord",
|
|
33
47
|
request: z.object({
|
|
34
48
|
agent_id: z.string().describe("The agent's actor ID (aid from world_actors / actorId from auth.md registration)"),
|
|
@@ -83,8 +97,11 @@ export const RECEIVERS = {
|
|
|
83
97
|
}),
|
|
84
98
|
"auth:agent": receiver({
|
|
85
99
|
receiver: "auth:agent",
|
|
86
|
-
|
|
87
|
-
|
|
100
|
+
// `wallet` is a PUBLIC receiving address the caller derived in its own process.
|
|
101
|
+
// Nothing here generates keys: the address is stored on the TypeDB actor and
|
|
102
|
+
// registered through wallet:create, which is first-write-wins per chain.
|
|
103
|
+
summary: "Become an actor — mint a uid and a scoped API key, and register a wallets row (a public address if you supply one, an empty shell if not)",
|
|
104
|
+
request: z.object({ name: z.string().optional(), uid: z.string().optional(), kind: z.string().optional(), wallet: z.string().optional() }),
|
|
88
105
|
response: z.object({
|
|
89
106
|
uid: z.string(), name: z.string(), kind: z.string(),
|
|
90
107
|
wallet: z.string().nullable(), apiKey: z.string(), keyId: z.string(), returning: z.boolean(),
|
|
@@ -95,6 +112,7 @@ export const RECEIVERS = {
|
|
|
95
112
|
}),
|
|
96
113
|
"agents:sync": receiver({
|
|
97
114
|
receiver: "agents:sync",
|
|
115
|
+
surfaces: { mcp: { name: "sync_agent" } },
|
|
98
116
|
summary: "Declare capability — sync agent definitions, skills, and memberships",
|
|
99
117
|
// Accepts a single markdown body or a structured multi-agent world payload.
|
|
100
118
|
request: z.union([
|
|
@@ -166,7 +184,7 @@ export const RECEIVERS = {
|
|
|
166
184
|
summary: "Invite a human member to a workspace; email optional — returns inviteUrl when omitted",
|
|
167
185
|
request: z.object({ workspace: z.string(), email: z.string().email().optional(), role: z.string().optional() }),
|
|
168
186
|
response: z.object({ ok: z.boolean(), uid: z.string(), token: z.string(), inviteUrl: z.string().optional() }),
|
|
169
|
-
effect: "ask", auth: "manage_members",
|
|
187
|
+
effect: "ask", auth: "manage_members", roleAction: "invite_member",
|
|
170
188
|
}),
|
|
171
189
|
"world:invite-agent": receiver({
|
|
172
190
|
receiver: "world:invite-agent",
|
|
@@ -204,6 +222,7 @@ export const RECEIVERS = {
|
|
|
204
222
|
}),
|
|
205
223
|
"world:create-group": receiver({
|
|
206
224
|
receiver: "world:create-group",
|
|
225
|
+
surfaces: { mcp: true },
|
|
207
226
|
summary: "Create a group in the caller's workspace",
|
|
208
227
|
request: z.object({
|
|
209
228
|
name: z.string(),
|
|
@@ -215,17 +234,19 @@ export const RECEIVERS = {
|
|
|
215
234
|
}),
|
|
216
235
|
"world:update-group": receiver({
|
|
217
236
|
receiver: "world:update-group",
|
|
237
|
+
surfaces: { mcp: true },
|
|
218
238
|
summary: "Update a group's name, tags, or meta",
|
|
219
239
|
request: z.object({ gid: z.string(), name: z.string().optional(), tags: z.array(z.string()).optional(), meta: z.unknown().optional() }),
|
|
220
|
-
response: ok, effect: "ask", auth: "manage_groups",
|
|
240
|
+
response: ok, effect: "ask", auth: "manage_groups", roleAction: "update_group",
|
|
221
241
|
}),
|
|
222
242
|
"world:remove-group": receiver({
|
|
223
243
|
receiver: "world:remove-group",
|
|
224
244
|
summary: "Delete a group",
|
|
225
|
-
request: z.object({ gid: z.string() }), response: ok, effect: "ask", auth: "manage_groups", reversible: false,
|
|
245
|
+
request: z.object({ gid: z.string() }), response: ok, effect: "ask", auth: "manage_groups", roleAction: "delete_group", reversible: false,
|
|
226
246
|
}),
|
|
227
247
|
"world:create-actor": receiver({
|
|
228
248
|
receiver: "world:create-actor",
|
|
249
|
+
surfaces: { mcp: true },
|
|
229
250
|
summary: "Create an actor (agent or human) in the world",
|
|
230
251
|
request: z.object({
|
|
231
252
|
name: z.string(), type: z.string(), group: z.string().optional(),
|
|
@@ -237,6 +258,7 @@ export const RECEIVERS = {
|
|
|
237
258
|
}),
|
|
238
259
|
"world:update-actor": receiver({
|
|
239
260
|
receiver: "world:update-actor",
|
|
261
|
+
surfaces: { mcp: true },
|
|
240
262
|
summary: "Update an actor's name, tags, prompt, model, or custom meta fields",
|
|
241
263
|
request: z.object({ aid: z.string(), name: z.string().optional(), tags: z.array(z.string()).optional(), prompt: z.string().optional(), model: z.string().optional(), meta: z.unknown().optional() }),
|
|
242
264
|
response: ok, effect: "ask", auth: "manage_actors",
|
|
@@ -390,6 +412,26 @@ export const RECEIVERS = {
|
|
|
390
412
|
effect: "ask", auth: "manage_lifecycle", reversible: false,
|
|
391
413
|
examples: [{ slug: "acme", lifecycle: "marketing", def: { id: "marketing", name: "Marketing Funnel", version: 1, stages: [], transitions: [], arcs: [], monotonic: true } }],
|
|
392
414
|
}),
|
|
415
|
+
"lifecycle:of": receiver({
|
|
416
|
+
receiver: "lifecycle:of",
|
|
417
|
+
surfaces: { mcp: true },
|
|
418
|
+
summary: "Read another actor's lifecycle stage_transition history — authority-walked: the caller must control that actor's slug in the owners tree",
|
|
419
|
+
request: z.object({ actorId: z.string(), lifecycle: z.string().optional() }),
|
|
420
|
+
response: z.object({
|
|
421
|
+
ok: z.boolean(),
|
|
422
|
+
rows: z.array(z.object({
|
|
423
|
+
from_stage: z.string().nullable(),
|
|
424
|
+
to_stage: z.string(),
|
|
425
|
+
at: z.number(),
|
|
426
|
+
by: z.string(),
|
|
427
|
+
source: z.string(),
|
|
428
|
+
})).optional(),
|
|
429
|
+
error: z.string().optional(),
|
|
430
|
+
}),
|
|
431
|
+
effect: "ask", auth: "manage_lifecycle", roleAction: "manage_lifecycle",
|
|
432
|
+
cost: "free", idempotent: true, reversible: true,
|
|
433
|
+
examples: [{ actorId: "acme-agent", lifecycle: "wallet" }],
|
|
434
|
+
}),
|
|
393
435
|
"tools:connect": receiver({
|
|
394
436
|
receiver: "tools:connect",
|
|
395
437
|
summary: "Connect a Composio API-KEY toolkit to a workspace headlessly (OAuth stays in the browser flow)",
|
|
@@ -408,12 +450,14 @@ export const RECEIVERS = {
|
|
|
408
450
|
}),
|
|
409
451
|
"world:create-thing": receiver({
|
|
410
452
|
receiver: "world:create-thing",
|
|
453
|
+
surfaces: { mcp: true },
|
|
411
454
|
summary: "Create a thing (skill, token, product) in the world",
|
|
412
455
|
request: z.object({ name: z.string(), type: z.string(), group: z.string().optional(), tags: z.array(z.string()).optional(), price: z.number().optional(), meta: z.unknown().optional() }),
|
|
413
456
|
response: z.object({ tid: z.string() }), effect: "ask", auth: "manage_things",
|
|
414
457
|
}),
|
|
415
458
|
"world:update-thing": receiver({
|
|
416
459
|
receiver: "world:update-thing",
|
|
460
|
+
surfaces: { mcp: true },
|
|
417
461
|
summary: "Update a thing's name, tags, price, or meta",
|
|
418
462
|
request: z.object({ tid: z.string(), name: z.string().optional(), tags: z.array(z.string()).optional(), price: z.number().optional(), meta: z.unknown().optional() }),
|
|
419
463
|
response: ok, effect: "ask", auth: "manage_things",
|
|
@@ -432,6 +476,7 @@ export const RECEIVERS = {
|
|
|
432
476
|
}),
|
|
433
477
|
"world:list-things": receiver({
|
|
434
478
|
receiver: "world:list-things",
|
|
479
|
+
surfaces: { mcp: true },
|
|
435
480
|
summary: "List things of a given type — relation pickers (browse, first 100 by name) and name resolution (pass `ids` to resolve exactly those, uncapped)",
|
|
436
481
|
request: z.object({ group: z.string(), type: z.string(), ids: z.array(z.string()).optional() }),
|
|
437
482
|
response: z.object({ items: z.array(z.object({ tid: z.string(), name: z.string() })) }),
|
|
@@ -439,6 +484,7 @@ export const RECEIVERS = {
|
|
|
439
484
|
}),
|
|
440
485
|
"world:list-actors": receiver({
|
|
441
486
|
receiver: "world:list-actors",
|
|
487
|
+
surfaces: { mcp: true },
|
|
442
488
|
summary: "List actors of a given type — relation pickers (browse, first 100 by name) and name resolution (pass `ids` to resolve exactly those, uncapped)",
|
|
443
489
|
request: z.object({ group: z.string(), type: z.string(), ids: z.array(z.string()).optional() }),
|
|
444
490
|
response: z.object({ items: z.array(z.object({ aid: z.string(), name: z.string() })) }),
|
|
@@ -572,6 +618,7 @@ export const RECEIVERS = {
|
|
|
572
618
|
// ── identity ──
|
|
573
619
|
"identity:address": receiver({
|
|
574
620
|
receiver: "identity:address",
|
|
621
|
+
surfaces: { mcp: true },
|
|
575
622
|
summary: "Resolve an actor's wallet address",
|
|
576
623
|
request: z.object({ uid: z.string() }),
|
|
577
624
|
response: z.object({ uid: z.string(), address: z.string() }),
|
|
@@ -592,6 +639,7 @@ export const RECEIVERS = {
|
|
|
592
639
|
// ── groups ──
|
|
593
640
|
"groups:join": receiver({
|
|
594
641
|
receiver: "groups:join",
|
|
642
|
+
surfaces: { mcp: true },
|
|
595
643
|
summary: "Join a group",
|
|
596
644
|
request: z.object({ gid: z.string() }),
|
|
597
645
|
response: z.object({ ok: z.boolean(), gid: z.string(), role: z.string() }),
|
|
@@ -599,6 +647,7 @@ export const RECEIVERS = {
|
|
|
599
647
|
}),
|
|
600
648
|
"groups:leave": receiver({
|
|
601
649
|
receiver: "groups:leave",
|
|
650
|
+
surfaces: { mcp: true },
|
|
602
651
|
summary: "Leave a group",
|
|
603
652
|
request: z.object({ gid: z.string() }),
|
|
604
653
|
response: ok,
|
|
@@ -613,6 +662,7 @@ export const RECEIVERS = {
|
|
|
613
662
|
}),
|
|
614
663
|
"groups:members": receiver({
|
|
615
664
|
receiver: "groups:members",
|
|
665
|
+
surfaces: { mcp: true },
|
|
616
666
|
summary: "List a group's members",
|
|
617
667
|
request: z.object({ gid: z.string() }),
|
|
618
668
|
response: z.object({
|
|
@@ -647,7 +697,16 @@ export const RECEIVERS = {
|
|
|
647
697
|
receiver: "subscriptions:register",
|
|
648
698
|
summary: "Subscribe to a receiver's events by tag",
|
|
649
699
|
request: z.object({
|
|
650
|
-
|
|
700
|
+
// OPTIONAL, and the default is the common case. `subscriptions.ts` reads
|
|
701
|
+
// this as `data['receiver'] || 'world:announce'` — an agent staking on
|
|
702
|
+
// tags omits it, which is how every `subscribes:` frontmatter seam works.
|
|
703
|
+
// Declaring it required was harmless while nothing validated the request;
|
|
704
|
+
// once `bind-receiver` began enforcing the declared schema it started
|
|
705
|
+
// rejecting the documented call with a 400 at the edge, before the
|
|
706
|
+
// resolver's default could apply. Caught by tests/e2e/marketplace.test.ts,
|
|
707
|
+
// which is the marketplace promise's proof observable — the seller's
|
|
708
|
+
// stake never registered, so no deal opened.
|
|
709
|
+
receiver: z.string().optional(),
|
|
651
710
|
tags: z.array(z.string()),
|
|
652
711
|
scope: z.enum(["private", "public"]).optional(),
|
|
653
712
|
// Shared cross-tenant board — the one workspace two different ctx.ownerSlug
|
|
@@ -779,6 +838,7 @@ export const RECEIVERS = {
|
|
|
779
838
|
}),
|
|
780
839
|
"tasks:announce": receiver({
|
|
781
840
|
receiver: "tasks:announce",
|
|
841
|
+
surfaces: { mcp: true },
|
|
782
842
|
summary: "Announce a task into the world by its tags — the first caller of world:announce",
|
|
783
843
|
request: z.object({
|
|
784
844
|
taskId: z.string(),
|
|
@@ -792,8 +852,19 @@ export const RECEIVERS = {
|
|
|
792
852
|
}),
|
|
793
853
|
"tasks:mine": receiver({
|
|
794
854
|
receiver: "tasks:mine",
|
|
855
|
+
surfaces: { mcp: true },
|
|
795
856
|
summary: "My work queue: the open tasks whose words match what I subscribed to, ranked by learned weight (context-strength), not static priority",
|
|
796
|
-
|
|
857
|
+
// `workspace` is not decoration — /api/ask reads a SERVICE caller's nominated slug off
|
|
858
|
+
// validation.payload.slug|workspace, and zod .object() STRIPS unknown keys. Omitting the
|
|
859
|
+
// field here meant a service call could never name a workspace: ctx.ownerSlug stayed
|
|
860
|
+
// empty and the resolver answered "forbidden: authentication required" no matter which
|
|
861
|
+
// key was presented. A session-authenticated browser call was unaffected (locals.slug
|
|
862
|
+
// supplies the workspace), which is why this only ever failed for the harness.
|
|
863
|
+
request: z.object({
|
|
864
|
+
limit: z.number().optional(),
|
|
865
|
+
workspace: z.string().optional(),
|
|
866
|
+
slug: z.string().optional(),
|
|
867
|
+
}),
|
|
797
868
|
response: z.object({
|
|
798
869
|
ok: z.boolean(),
|
|
799
870
|
tasks: z.array(z.object({
|
|
@@ -812,8 +883,16 @@ export const RECEIVERS = {
|
|
|
812
883
|
}),
|
|
813
884
|
"tasks:everywhere": receiver({
|
|
814
885
|
receiver: "tasks:everywhere",
|
|
886
|
+
surfaces: { mcp: true },
|
|
815
887
|
summary: "Every workspace I belong to, one queue: open+picked tasks visible by assignment/follow/ownership, with claimable + mine + offeredByMe flags; closed:true returns the logbook (done/verified, newest first)",
|
|
816
|
-
|
|
888
|
+
// See tasks:mine — a service caller nominates its workspace here, and zod strips any
|
|
889
|
+
// field the schema does not declare.
|
|
890
|
+
request: z.object({
|
|
891
|
+
limit: z.number().optional(),
|
|
892
|
+
closed: z.boolean().optional(),
|
|
893
|
+
workspace: z.string().optional(),
|
|
894
|
+
slug: z.string().optional(),
|
|
895
|
+
}),
|
|
817
896
|
response: z.object({
|
|
818
897
|
ok: z.boolean(),
|
|
819
898
|
tasks: z.array(z.object({
|
|
@@ -835,8 +914,108 @@ export const RECEIVERS = {
|
|
|
835
914
|
}),
|
|
836
915
|
effect: "ask", idempotent: true,
|
|
837
916
|
}),
|
|
917
|
+
"factory:elaborate": receiver({
|
|
918
|
+
receiver: "factory:elaborate",
|
|
919
|
+
summary: "Create one rung of the derivation ladder (objective | deliverable | task) and hang it under its parent with containment — the ELABORATE move. The rung id is derived from slug + ordinal, so a retry dedupes instead of duplicating",
|
|
920
|
+
request: z.object({
|
|
921
|
+
type: z.enum(["objective", "deliverable", "task"]),
|
|
922
|
+
// The promise/plan slug the rung belongs to, and its ordinal within that slug.
|
|
923
|
+
// Both required: together they derive the rung id (Interface Contract #1,
|
|
924
|
+
// text/factory-todo.md) and the `rung:<type>:<slug>:<ordinal>` idempotency tag.
|
|
925
|
+
slug: z.string(),
|
|
926
|
+
ordinal: z.number(),
|
|
927
|
+
name: z.string(),
|
|
928
|
+
// The rung above — its containment container. Must live in the same resolved
|
|
929
|
+
// workspace; a parent the caller cannot see is refused, never silently skipped.
|
|
930
|
+
parent: z.string().optional(),
|
|
931
|
+
// The machine-observable accept: check for this rung (factory-plan.md §7.3).
|
|
932
|
+
exitCondition: z.string().optional(),
|
|
933
|
+
notes: z.string().optional(),
|
|
934
|
+
tags: z.array(z.string()).optional(),
|
|
935
|
+
// Same authorization contract as tasks:create's workspace — honored only for an
|
|
936
|
+
// authorized caller, otherwise it falls back to the caller's own slug.
|
|
937
|
+
workspace: z.string().optional(),
|
|
938
|
+
}),
|
|
939
|
+
response: z.object({
|
|
940
|
+
ok: z.boolean(),
|
|
941
|
+
tid: z.string().optional(),
|
|
942
|
+
type: z.string().optional(),
|
|
943
|
+
parent: z.string().nullable().optional(),
|
|
944
|
+
tags: z.array(z.string()).optional(),
|
|
945
|
+
deduped: z.boolean().optional(),
|
|
946
|
+
error: z.string().optional(),
|
|
947
|
+
}),
|
|
948
|
+
effect: "ask", idempotent: true,
|
|
949
|
+
}),
|
|
950
|
+
"factory:attempt": receiver({
|
|
951
|
+
receiver: "factory:attempt",
|
|
952
|
+
summary: "Open an attempt contained by its task, or close it with the production edge to what it produced plus the rubric. A do-cycle IS an attempt (factory-plan.md §3.1) — do:cycle-close arms as a caller of the close phase, never as a second writer",
|
|
953
|
+
request: z.object({
|
|
954
|
+
phase: z.enum(["open", "close"]),
|
|
955
|
+
// open
|
|
956
|
+
task: z.string().optional(),
|
|
957
|
+
ordinal: z.number().optional(),
|
|
958
|
+
// Which actor ran it — provenance (path attempt → actor), never authority.
|
|
959
|
+
actorId: z.string().optional(),
|
|
960
|
+
tags: z.array(z.string()).optional(),
|
|
961
|
+
// close
|
|
962
|
+
attempt: z.string().optional(),
|
|
963
|
+
status: z.enum(["done", "verified", "failed", "dissolved"]).optional(),
|
|
964
|
+
produced: z.string().optional(),
|
|
965
|
+
// Scores land on the ATTEMPT — the graph is the authority; a composite on the
|
|
966
|
+
// plan thing is a cached projection over the attempts it contains (§3.2).
|
|
967
|
+
rubric: z.object({
|
|
968
|
+
composite: z.number().optional(),
|
|
969
|
+
security: z.number().optional(),
|
|
970
|
+
stability: z.number().optional(),
|
|
971
|
+
simplicity: z.number().optional(),
|
|
972
|
+
speed: z.number().optional(),
|
|
973
|
+
}).optional(),
|
|
974
|
+
workspace: z.string().optional(),
|
|
975
|
+
}),
|
|
976
|
+
response: z.object({
|
|
977
|
+
ok: z.boolean(),
|
|
978
|
+
tid: z.string().optional(),
|
|
979
|
+
task: z.string().optional(),
|
|
980
|
+
phase: z.string().optional(),
|
|
981
|
+
status: z.string().optional(),
|
|
982
|
+
produced: z.string().nullable().optional(),
|
|
983
|
+
rubric: z.array(z.string()).optional(),
|
|
984
|
+
deduped: z.boolean().optional(),
|
|
985
|
+
error: z.string().optional(),
|
|
986
|
+
}),
|
|
987
|
+
effect: "ask", idempotent: true,
|
|
988
|
+
}),
|
|
989
|
+
"do:halt": receiver({
|
|
990
|
+
receiver: "do:halt",
|
|
991
|
+
summary: "Stop a factory run, or release the stop. Sets a halt latch on the plan thing that `factory:attempt` phase \"open\" refuses against, so no new attempt can be claimed; with `attempt`, also closes that in-flight attempt as dissolved. It does NOT kill an OS process — a cycle already executing stops at its next substrate write",
|
|
992
|
+
request: z.object({
|
|
993
|
+
// The plan slug the operator sees ("factory"). The plan tid is resolved server-side;
|
|
994
|
+
// a tid from the body would be an addressable another tenant could guess.
|
|
995
|
+
slug: z.string(),
|
|
996
|
+
// Lift the latch instead of setting it. A stop with no release bricks the only
|
|
997
|
+
// write path the ladder has.
|
|
998
|
+
release: z.boolean().optional(),
|
|
999
|
+
// Optionally dissolve one in-flight attempt in the same act.
|
|
1000
|
+
attempt: z.string().optional(),
|
|
1001
|
+
// Same authorization contract as factory:elaborate's workspace — honored only for
|
|
1002
|
+
// an authorized caller, otherwise it resolves back to the caller's own slug.
|
|
1003
|
+
workspace: z.string().optional(),
|
|
1004
|
+
}),
|
|
1005
|
+
response: z.object({
|
|
1006
|
+
ok: z.boolean(),
|
|
1007
|
+
tid: z.string().optional(),
|
|
1008
|
+
slug: z.string().optional(),
|
|
1009
|
+
halted: z.boolean().optional(),
|
|
1010
|
+
attempt: z.string().nullable().optional(),
|
|
1011
|
+
deduped: z.boolean().optional(),
|
|
1012
|
+
error: z.string().optional(),
|
|
1013
|
+
}),
|
|
1014
|
+
effect: "ask", idempotent: true,
|
|
1015
|
+
}),
|
|
838
1016
|
"tasks:create": receiver({
|
|
839
1017
|
receiver: "tasks:create",
|
|
1018
|
+
surfaces: { mcp: true },
|
|
840
1019
|
summary: "Write one open task to the substrate and announce it by its tags in the same act — the quick-add entry point",
|
|
841
1020
|
request: z.object({
|
|
842
1021
|
title: z.string(),
|
|
@@ -856,11 +1035,22 @@ export const RECEIVERS = {
|
|
|
856
1035
|
}),
|
|
857
1036
|
"tasks:claim": receiver({
|
|
858
1037
|
receiver: "tasks:claim",
|
|
859
|
-
|
|
1038
|
+
surfaces: { mcp: true },
|
|
1039
|
+
summary: "Take a task off the queue: blocker-gate → status picked → tag the claimant @<slug>. Address it by tid, or by the /do slug the task was created under. The claimant is the attested caller, never a body field",
|
|
1040
|
+
// Addressable two ways, and BOTH must be optional here. The resolver has always
|
|
1041
|
+
// supported slug (resolveTaskBySlug) because create returns a random task:<traceId>
|
|
1042
|
+
// that no caller can derive — but this schema required `tid`, so every slug-addressed
|
|
1043
|
+
// claim (do-signal.sh --task-claim, do-fleet.sh's lease) was rejected at validation
|
|
1044
|
+
// before the resolver ran. The contract, not the resolver, was the dead wire.
|
|
860
1045
|
request: z.object({
|
|
861
|
-
tid: z.string(),
|
|
1046
|
+
tid: z.string().optional(),
|
|
1047
|
+
// The kebab /do slug the task carries as a `slug:` tag.
|
|
1048
|
+
slug: z.string().optional(),
|
|
862
1049
|
// Viewed workspace — same authorization contract as tasks:create's workspace.
|
|
863
1050
|
workspace: z.string().optional(),
|
|
1051
|
+
}).refine((v) => !!v.tid || !!v.slug, {
|
|
1052
|
+
message: "address the task by tid or slug",
|
|
1053
|
+
path: ["tid"],
|
|
864
1054
|
}),
|
|
865
1055
|
response: z.object({
|
|
866
1056
|
ok: z.boolean().optional(),
|
|
@@ -871,8 +1061,308 @@ export const RECEIVERS = {
|
|
|
871
1061
|
}),
|
|
872
1062
|
effect: "ask", idempotent: true,
|
|
873
1063
|
}),
|
|
1064
|
+
"tasks:reap": receiver({
|
|
1065
|
+
receiver: "tasks:reap",
|
|
1066
|
+
surfaces: { mcp: true },
|
|
1067
|
+
summary: "Sweep stranded claim leases in a workspace: every `picked` task whose newest updated-at-ms is older than the 45-minute TTL goes back to `open`, its @claimant tags are stripped, and the abandoned path is warned",
|
|
1068
|
+
// The other half of the claim seam. tasks:claim is atomic, so two workers can
|
|
1069
|
+
// never double-lease — but a lease whose OWNER DIED is `picked` forever:
|
|
1070
|
+
// `claimable` requires `status === 'open' && ats.length === 0`, so the view
|
|
1071
|
+
// never surfaces it, so `leaseIsStale` (called only inside claim) is never
|
|
1072
|
+
// reached on the rows it was written for. Meanwhile ready-tasks blocks every
|
|
1073
|
+
// dependent on a `picked` blocker. This receiver queries `picked` DIRECTLY —
|
|
1074
|
+
// that is the whole point; asking the claimable view returns nothing.
|
|
1075
|
+
request: z.object({
|
|
1076
|
+
// Viewed workspace — same authorization contract as tasks:claim's workspace.
|
|
1077
|
+
workspace: z.string().optional(),
|
|
1078
|
+
}),
|
|
1079
|
+
response: z.object({
|
|
1080
|
+
ok: z.boolean().optional(),
|
|
1081
|
+
workspace: z.string().optional(),
|
|
1082
|
+
scanned: z.number().optional(),
|
|
1083
|
+
reaped: z.array(z.string()).optional(),
|
|
1084
|
+
failed: z.array(z.string()).optional(),
|
|
1085
|
+
error: z.string().optional(),
|
|
1086
|
+
}),
|
|
1087
|
+
// Idempotent: a second sweep finds nothing left stale and reaps nothing.
|
|
1088
|
+
effect: "ask", idempotent: true,
|
|
1089
|
+
}),
|
|
1090
|
+
// ── The rest of the board's verbs ────────────────────────────────────────────
|
|
1091
|
+
//
|
|
1092
|
+
// These twelve shipped as RESOLVERS ONLY (`tasksResolvers`, one.ie/web) and were
|
|
1093
|
+
// absent from this registry, so `bindReceiver` had no request schema to validate
|
|
1094
|
+
// and no auth label to apply — every one of them reached its handler with an
|
|
1095
|
+
// unchecked payload. They are also what the UI's detail pane, status menu, tag
|
|
1096
|
+
// chips and Gantt call, so the contract that was missing is the contract for most
|
|
1097
|
+
// of the board. `auth: "member"` is the label the family already resolves to
|
|
1098
|
+
// (an absent label falls to `authenticated`, same class) — stated, not changed.
|
|
1099
|
+
//
|
|
1100
|
+
// Every one takes an optional `workspace`, and it means the same thing here as on
|
|
1101
|
+
// tasks:create: the VIEWED workspace, honoured only when the caller is staff or
|
|
1102
|
+
// controls that tree, otherwise ignored in favour of the caller's own slug
|
|
1103
|
+
// (`effectiveWorkspace`). Declaring it is not decoration — zod `.object()` strips
|
|
1104
|
+
// undeclared keys, which is exactly how tasks:mine/everywhere were silently
|
|
1105
|
+
// un-nominatable until 2026-07-25.
|
|
1106
|
+
"tasks:status": receiver({
|
|
1107
|
+
receiver: "tasks:status",
|
|
1108
|
+
surfaces: { mcp: true },
|
|
1109
|
+
summary: "Move a task through the board: open · blocked · picked · done · verified · failed · dissolved. `verified` is refused for the claimant — it asserts a second pair of eyes. A `repeat:` tagged task that reaches done re-arms itself with the next due date instead of closing",
|
|
1110
|
+
request: z.object({
|
|
1111
|
+
tid: z.string(),
|
|
1112
|
+
status: z.enum(["open", "blocked", "picked", "done", "verified", "failed", "dissolved"]),
|
|
1113
|
+
workspace: z.string().optional(),
|
|
1114
|
+
}),
|
|
1115
|
+
response: z.object({
|
|
1116
|
+
ok: z.boolean().optional(),
|
|
1117
|
+
tid: z.string().optional(),
|
|
1118
|
+
status: z.string().optional(),
|
|
1119
|
+
// Set when a repeating task re-armed rather than closed.
|
|
1120
|
+
recurred: z.boolean().optional(),
|
|
1121
|
+
dueAt: z.string().optional(),
|
|
1122
|
+
error: z.string().optional(),
|
|
1123
|
+
}),
|
|
1124
|
+
effect: "ask", idempotent: true, auth: "member",
|
|
1125
|
+
}),
|
|
1126
|
+
"tasks:rename": receiver({
|
|
1127
|
+
receiver: "tasks:rename",
|
|
1128
|
+
surfaces: { mcp: true },
|
|
1129
|
+
summary: "Retitle a task in place. The tid is unchanged, so every link, blocker and follow survives the rename",
|
|
1130
|
+
request: z.object({
|
|
1131
|
+
tid: z.string(),
|
|
1132
|
+
title: z.string(),
|
|
1133
|
+
workspace: z.string().optional(),
|
|
1134
|
+
}),
|
|
1135
|
+
response: z.object({
|
|
1136
|
+
ok: z.boolean().optional(),
|
|
1137
|
+
tid: z.string().optional(),
|
|
1138
|
+
title: z.string().optional(),
|
|
1139
|
+
error: z.string().optional(),
|
|
1140
|
+
}),
|
|
1141
|
+
effect: "ask", idempotent: true, auth: "member",
|
|
1142
|
+
}),
|
|
1143
|
+
"tasks:list": receiver({
|
|
1144
|
+
receiver: "tasks:list",
|
|
1145
|
+
surfaces: { mcp: true },
|
|
1146
|
+
summary: "Every task carrying a tag, with its task-status — the read that lets a plan's checkboxes be a PROJECTION of task state instead of a second copy. Scoped to the caller's workspace on the server",
|
|
1147
|
+
request: z.object({
|
|
1148
|
+
tag: z.string(),
|
|
1149
|
+
workspace: z.string().optional(),
|
|
1150
|
+
}),
|
|
1151
|
+
response: z.object({
|
|
1152
|
+
ok: z.boolean().optional(),
|
|
1153
|
+
tag: z.string().optional(),
|
|
1154
|
+
tasks: z.array(z.object({
|
|
1155
|
+
tid: z.string(),
|
|
1156
|
+
status: z.string(),
|
|
1157
|
+
name: z.string().optional(),
|
|
1158
|
+
})).optional(),
|
|
1159
|
+
error: z.string().optional(),
|
|
1160
|
+
}),
|
|
1161
|
+
effect: "ask", idempotent: true, auth: "member",
|
|
1162
|
+
}),
|
|
1163
|
+
"tasks:notes": receiver({
|
|
1164
|
+
receiver: "tasks:notes",
|
|
1165
|
+
surfaces: { mcp: true },
|
|
1166
|
+
summary: "Set or clear a task's notes — the prose goal a puller reads to know what done means. An empty/absent `notes` CLEARS them, so never send the field unless you mean to change it",
|
|
1167
|
+
request: z.object({
|
|
1168
|
+
tid: z.string(),
|
|
1169
|
+
notes: z.string().nullable().optional(),
|
|
1170
|
+
workspace: z.string().optional(),
|
|
1171
|
+
}),
|
|
1172
|
+
response: z.object({
|
|
1173
|
+
ok: z.boolean().optional(),
|
|
1174
|
+
tid: z.string().optional(),
|
|
1175
|
+
notes: z.string().nullable().optional(),
|
|
1176
|
+
error: z.string().optional(),
|
|
1177
|
+
}),
|
|
1178
|
+
effect: "ask", idempotent: true, auth: "member",
|
|
1179
|
+
}),
|
|
1180
|
+
"tasks:priority": receiver({
|
|
1181
|
+
receiver: "tasks:priority",
|
|
1182
|
+
surfaces: { mcp: true },
|
|
1183
|
+
summary: "Set a task's priority from the 1–100 slider. Stored as the 0–1 `task-priority` attribute (slider/100), which is what the board's ranking reads",
|
|
1184
|
+
request: z.object({
|
|
1185
|
+
tid: z.string(),
|
|
1186
|
+
priority: z.number(),
|
|
1187
|
+
workspace: z.string().optional(),
|
|
1188
|
+
}),
|
|
1189
|
+
response: z.object({
|
|
1190
|
+
ok: z.boolean().optional(),
|
|
1191
|
+
tid: z.string().optional(),
|
|
1192
|
+
// The stored 0–1 value, not the 1–100 slider that was sent.
|
|
1193
|
+
priority: z.number().optional(),
|
|
1194
|
+
error: z.string().optional(),
|
|
1195
|
+
}),
|
|
1196
|
+
effect: "ask", idempotent: true, auth: "member",
|
|
1197
|
+
}),
|
|
1198
|
+
"tasks:schedule": receiver({
|
|
1199
|
+
receiver: "tasks:schedule",
|
|
1200
|
+
surfaces: { mcp: true },
|
|
1201
|
+
summary: "Set or clear a task's start/due dates — what the Gantt reads. PRESENCE of a field decides whether it is touched; an empty value clears it. An inverted range (start after due) is refused before any DB access",
|
|
1202
|
+
request: z.object({
|
|
1203
|
+
tid: z.string(),
|
|
1204
|
+
dueAt: z.string().nullable().optional(),
|
|
1205
|
+
startAt: z.string().nullable().optional(),
|
|
1206
|
+
workspace: z.string().optional(),
|
|
1207
|
+
}),
|
|
1208
|
+
response: z.object({
|
|
1209
|
+
ok: z.boolean().optional(),
|
|
1210
|
+
tid: z.string().optional(),
|
|
1211
|
+
dueAt: z.string().nullable().optional(),
|
|
1212
|
+
startAt: z.string().nullable().optional(),
|
|
1213
|
+
error: z.string().optional(),
|
|
1214
|
+
}),
|
|
1215
|
+
effect: "ask", idempotent: true, auth: "member",
|
|
1216
|
+
}),
|
|
1217
|
+
"tasks:tag": receiver({
|
|
1218
|
+
receiver: "tasks:tag",
|
|
1219
|
+
surfaces: { mcp: true },
|
|
1220
|
+
summary: "Add and/or remove board tags on a task. Plain words only — the `workspace:` and `@` namespaces are system-owned and are silently dropped, because they decide tenancy and assignment",
|
|
1221
|
+
request: z.object({
|
|
1222
|
+
tid: z.string(),
|
|
1223
|
+
add: z.array(z.string()).optional(),
|
|
1224
|
+
remove: z.array(z.string()).optional(),
|
|
1225
|
+
workspace: z.string().optional(),
|
|
1226
|
+
}),
|
|
1227
|
+
response: z.object({
|
|
1228
|
+
ok: z.boolean().optional(),
|
|
1229
|
+
tid: z.string().optional(),
|
|
1230
|
+
add: z.array(z.string()).optional(),
|
|
1231
|
+
remove: z.array(z.string()).optional(),
|
|
1232
|
+
error: z.string().optional(),
|
|
1233
|
+
}),
|
|
1234
|
+
effect: "ask", idempotent: true, auth: "member",
|
|
1235
|
+
}),
|
|
1236
|
+
"tasks:reassign": receiver({
|
|
1237
|
+
receiver: "tasks:reassign",
|
|
1238
|
+
surfaces: { mcp: true },
|
|
1239
|
+
summary: "Move one task to a different assignee IN PLACE — swaps the `@<slug>` tag and announces on the new tag set. An empty assignee unassigns it, and moves a `picked` task back to `open` so it is genuinely claimable again (claimable = open AND unassigned). This is the verb the task sheet's Reassign button was missing: it resolved through `buildAssign` to tasks:create and filed a duplicate `Handle: <title>` task instead",
|
|
1240
|
+
request: z.object({
|
|
1241
|
+
tid: z.string(),
|
|
1242
|
+
// Actor slug. Empty/null unassigns. A leading '@' is accepted and stripped.
|
|
1243
|
+
assignee: z.string().nullable().optional(),
|
|
1244
|
+
workspace: z.string().optional(),
|
|
1245
|
+
}),
|
|
1246
|
+
response: z.object({
|
|
1247
|
+
ok: z.boolean().optional(),
|
|
1248
|
+
tid: z.string().optional(),
|
|
1249
|
+
assignee: z.string().nullable().optional(),
|
|
1250
|
+
// Who held it before, so the caller can say "moved from X to Y" without a re-read.
|
|
1251
|
+
previous: z.string().nullable().optional(),
|
|
1252
|
+
tags: z.array(z.string()).optional(),
|
|
1253
|
+
unchanged: z.boolean().optional(),
|
|
1254
|
+
// Set only when unassigning reopened a `picked` task.
|
|
1255
|
+
status: z.string().optional(),
|
|
1256
|
+
reopened: z.boolean().optional(),
|
|
1257
|
+
error: z.string().optional(),
|
|
1258
|
+
}),
|
|
1259
|
+
effect: "ask", idempotent: true, auth: "member",
|
|
1260
|
+
}),
|
|
1261
|
+
"tasks:comment": receiver({
|
|
1262
|
+
receiver: "tasks:comment",
|
|
1263
|
+
surfaces: { mcp: true },
|
|
1264
|
+
summary: "Post a comment on a task's thread, or read the thread back when `body` is omitted. An @mention in the body subscribes that actor to the task. A reader (viewer) may comment even where it cannot move the task. Address by tid, or by the /do slug — same two-way contract as claim/link, so a wave-close heartbeat does not need the generated tid",
|
|
1265
|
+
request: z.object({
|
|
1266
|
+
tid: z.string().optional(),
|
|
1267
|
+
slug: z.string().optional(),
|
|
1268
|
+
body: z.string().optional(),
|
|
1269
|
+
workspace: z.string().optional(),
|
|
1270
|
+
}).refine((v) => !!v.tid || !!v.slug, {
|
|
1271
|
+
message: "address the task by tid or slug",
|
|
1272
|
+
path: ["tid"],
|
|
1273
|
+
}),
|
|
1274
|
+
response: z.object({
|
|
1275
|
+
ok: z.boolean().optional(),
|
|
1276
|
+
tid: z.string().optional(),
|
|
1277
|
+
messages: z.array(z.record(z.string(), z.unknown())).optional(),
|
|
1278
|
+
error: z.string().optional(),
|
|
1279
|
+
}),
|
|
1280
|
+
effect: "ask", idempotent: false, auth: "member",
|
|
1281
|
+
}),
|
|
1282
|
+
"tasks:subtask": receiver({
|
|
1283
|
+
receiver: "tasks:subtask",
|
|
1284
|
+
surfaces: { mcp: true },
|
|
1285
|
+
summary: "Create a child task and hang it under its parent with `containment` in one write. Reuses create's insert path — there is no second task-insert anywhere",
|
|
1286
|
+
request: z.object({
|
|
1287
|
+
parent: z.string(),
|
|
1288
|
+
title: z.string(),
|
|
1289
|
+
tags: z.array(z.string()).optional(),
|
|
1290
|
+
assignee: z.string().optional(),
|
|
1291
|
+
workspace: z.string().optional(),
|
|
1292
|
+
}),
|
|
1293
|
+
response: z.object({
|
|
1294
|
+
ok: z.boolean().optional(),
|
|
1295
|
+
tid: z.string().optional(),
|
|
1296
|
+
parent: z.string().optional(),
|
|
1297
|
+
tags: z.array(z.string()).optional(),
|
|
1298
|
+
error: z.string().optional(),
|
|
1299
|
+
}),
|
|
1300
|
+
effect: "ask", idempotent: false, auth: "member",
|
|
1301
|
+
}),
|
|
1302
|
+
"tasks:depend": receiver({
|
|
1303
|
+
receiver: "tasks:depend",
|
|
1304
|
+
surfaces: { mcp: true },
|
|
1305
|
+
summary: "Record that a task is blocked by another — the `blocks` edge the claim gate reads. Refuses a self-edge and a reverse edge (cycle); both ends must be in the caller's workspace",
|
|
1306
|
+
request: z.object({
|
|
1307
|
+
tid: z.string(),
|
|
1308
|
+
blockedBy: z.string(),
|
|
1309
|
+
workspace: z.string().optional(),
|
|
1310
|
+
}),
|
|
1311
|
+
response: z.object({
|
|
1312
|
+
ok: z.boolean().optional(),
|
|
1313
|
+
tid: z.string().optional(),
|
|
1314
|
+
blockedBy: z.string().optional(),
|
|
1315
|
+
error: z.string().optional(),
|
|
1316
|
+
}),
|
|
1317
|
+
effect: "ask", idempotent: true, auth: "member",
|
|
1318
|
+
}),
|
|
1319
|
+
"tasks:follow": receiver({
|
|
1320
|
+
receiver: "tasks:follow",
|
|
1321
|
+
surfaces: { mcp: true },
|
|
1322
|
+
summary: "Follow one task — a task-grain subscription (a `follows` row with receiver `tasks:announce`), so its announcements reach the caller's inbox. Refused across tenants",
|
|
1323
|
+
request: z.object({
|
|
1324
|
+
tid: z.string(),
|
|
1325
|
+
workspace: z.string().optional(),
|
|
1326
|
+
}),
|
|
1327
|
+
response: z.object({
|
|
1328
|
+
ok: z.boolean().optional(),
|
|
1329
|
+
tid: z.string().optional(),
|
|
1330
|
+
tags: z.array(z.string()).optional(),
|
|
1331
|
+
actor: z.string().optional(),
|
|
1332
|
+
error: z.string().optional(),
|
|
1333
|
+
}),
|
|
1334
|
+
effect: "ask", idempotent: true, auth: "member",
|
|
1335
|
+
}),
|
|
1336
|
+
"tasks:unfollow": receiver({
|
|
1337
|
+
receiver: "tasks:unfollow",
|
|
1338
|
+
surfaces: { mcp: true },
|
|
1339
|
+
summary: "Stop following one task — deletes exactly that task-grain subscription row, leaving tag-level subscriptions alone",
|
|
1340
|
+
request: z.object({
|
|
1341
|
+
tid: z.string(),
|
|
1342
|
+
workspace: z.string().optional(),
|
|
1343
|
+
}),
|
|
1344
|
+
response: z.object({
|
|
1345
|
+
ok: z.boolean().optional(),
|
|
1346
|
+
tid: z.string().optional(),
|
|
1347
|
+
actor: z.string().optional(),
|
|
1348
|
+
error: z.string().optional(),
|
|
1349
|
+
}),
|
|
1350
|
+
effect: "ask", idempotent: true, auth: "member",
|
|
1351
|
+
}),
|
|
1352
|
+
"tasks:follows": receiver({
|
|
1353
|
+
receiver: "tasks:follows",
|
|
1354
|
+
summary: "List the tasks the caller follows (task-grain subscriptions only, newest first, cap 100)",
|
|
1355
|
+
request: z.object({
|
|
1356
|
+
workspace: z.string().optional(),
|
|
1357
|
+
}),
|
|
1358
|
+
response: z.object({
|
|
1359
|
+
follows: z.array(z.record(z.string(), z.unknown())),
|
|
1360
|
+
}),
|
|
1361
|
+
effect: "ask", idempotent: true, auth: "member",
|
|
1362
|
+
}),
|
|
874
1363
|
"tasks:undepend": receiver({
|
|
875
1364
|
receiver: "tasks:undepend",
|
|
1365
|
+
surfaces: { mcp: true },
|
|
876
1366
|
summary: "Remove one blocks edge — the inverse of tasks:depend. Deletes exactly the (blockedBy → tid) pair; does not touch task status. Auth: attested caller only, both ends in the caller's workspace",
|
|
877
1367
|
request: z.object({
|
|
878
1368
|
tid: z.string(),
|
|
@@ -907,9 +1397,14 @@ export const RECEIVERS = {
|
|
|
907
1397
|
}),
|
|
908
1398
|
"tasks:link": receiver({
|
|
909
1399
|
receiver: "tasks:link",
|
|
1400
|
+
surfaces: { mcp: true },
|
|
910
1401
|
summary: "Close the handoff: record that a claimed task was built via /do <slug> — append a provenance line to the task's notes (the id back-ref + doc stems) and move it to done. Auth: attested caller only",
|
|
1402
|
+
// The slug IS the handle here — the resolver requires it and resolves the origin tid
|
|
1403
|
+
// from the `slug:` tag, treating an explicit tid as an optional override for direct
|
|
1404
|
+
// callers. Requiring tid (as this did) rejected every /do close, which knows the plan
|
|
1405
|
+
// slug and never the generated task:<traceId>.
|
|
911
1406
|
request: z.object({
|
|
912
|
-
tid: z.string(),
|
|
1407
|
+
tid: z.string().optional(),
|
|
913
1408
|
// The kebab /do slug the task was built into (the docs backfilled under text/<slug>*).
|
|
914
1409
|
slug: z.string(),
|
|
915
1410
|
// Doc stems the /do run produced (e.g. ["usage-billing", "usage-billing-plan"]).
|
|
@@ -927,18 +1422,31 @@ export const RECEIVERS = {
|
|
|
927
1422
|
effect: "ask", idempotent: true,
|
|
928
1423
|
}),
|
|
929
1424
|
// ── agents (TRADE / lifecycle) ──
|
|
1425
|
+
// THE PUBLIC DOOR. An arriving agent joins here with no human in the loop and
|
|
1426
|
+
// no prior credential, and gets a SCOPED rung: it may list capabilities and be
|
|
1427
|
+
// paid; it may not spend the compute float until one x402 challenge is
|
|
1428
|
+
// satisfied, and it may never touch another workspace.
|
|
1429
|
+
//
|
|
1430
|
+
// `uid` is OPTIONAL and is never authority. An anonymous caller's identity is
|
|
1431
|
+
// MINTED by the handler and its `uid` is ignored outright; an attested caller
|
|
1432
|
+
// may only name an identity it already controls (callerControlsWorkspace).
|
|
1433
|
+
// That is the whole IDOR rule: the subject is derived, never declared.
|
|
930
1434
|
"agents:register": receiver({
|
|
931
1435
|
receiver: "agents:register",
|
|
932
|
-
|
|
1436
|
+
surfaces: { mcp: { name: "register" } },
|
|
1437
|
+
summary: "Register as a sellable actor with capabilities. Public: call it with no credential and it mints a scoped identity (returns actorId + apiKey, compute float locked until one x402 payment).",
|
|
933
1438
|
request: z.object({
|
|
934
|
-
uid
|
|
1439
|
+
/** Omit when anonymous — the handler mints one. Naming another agent's uid is refused. */
|
|
1440
|
+
uid: z.string().optional(),
|
|
1441
|
+
/** Display handle for a fresh anonymous registration. Never authority. */
|
|
1442
|
+
name: z.string().optional(),
|
|
935
1443
|
kind: z.string().optional(),
|
|
936
1444
|
capabilities: z.array(z.object({ skill: z.string(), price: z.number().optional() })).optional(),
|
|
937
1445
|
wallet: z.string().optional(),
|
|
938
1446
|
chain: z.string().optional(),
|
|
939
1447
|
}),
|
|
940
1448
|
response: RegisterResponseSchema,
|
|
941
|
-
effect: "ask", idempotent: true,
|
|
1449
|
+
effect: "ask", auth: "public", idempotent: true,
|
|
942
1450
|
}),
|
|
943
1451
|
"agents:commend": receiver({
|
|
944
1452
|
receiver: "agents:commend",
|
|
@@ -972,26 +1480,35 @@ export const RECEIVERS = {
|
|
|
972
1480
|
// ── pay (TRANSACT — onchain settlement) ──
|
|
973
1481
|
"pay:weight": receiver({
|
|
974
1482
|
receiver: "pay:weight",
|
|
1483
|
+
surfaces: { mcp: { name: "pay" } },
|
|
975
1484
|
summary: "Pay for a task by weighting the path between two actors",
|
|
976
|
-
request: z.object({
|
|
1485
|
+
request: z.object({
|
|
1486
|
+
// DERIVED, never sent: the handler sets `from = ctx.ownerSlug` and never reads
|
|
1487
|
+
// data.from. Declaring it required made the binder reject every valid call
|
|
1488
|
+
// before the handler could derive it — and before its own auth check ran.
|
|
1489
|
+
from: z.string().optional(),
|
|
1490
|
+
to: z.string(), task: z.string(), amount: z.number(),
|
|
1491
|
+
}),
|
|
977
1492
|
response: PayResponseSchema,
|
|
978
1493
|
effect: "ask", cost: "variable", settles: "onchain", reversible: false, simulatable: true,
|
|
979
1494
|
}),
|
|
980
1495
|
"tasks:stake": receiver({
|
|
981
1496
|
receiver: "tasks:stake",
|
|
982
|
-
summary: "
|
|
1497
|
+
summary: "Record a visitor-signed stake_on_tags burn on Sui testnet. Caller signs in their wallet (0.1 SUI per weight); the worker verifies the digest and marks the path. No server key. Irreversible. Not a SUI deposit against a Task object.",
|
|
983
1498
|
request: z.object({
|
|
984
|
-
|
|
985
|
-
|
|
1499
|
+
tags: z.array(z.string().min(1)).min(1).max(8),
|
|
1500
|
+
addWeight: z.number().int().optional(),
|
|
1501
|
+
digest: z.string().min(20),
|
|
986
1502
|
}),
|
|
987
1503
|
response: z.object({
|
|
988
1504
|
ok: z.boolean(),
|
|
989
1505
|
digest: z.string().optional(),
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
1506
|
+
newWeight: z.number().optional(),
|
|
1507
|
+
tags: z.array(z.string()).optional(),
|
|
1508
|
+
error: z.string().optional(),
|
|
993
1509
|
}),
|
|
994
|
-
effect: "ask", cost: "variable", settles: "onchain", reversible: false, simulatable: false,
|
|
1510
|
+
effect: "ask", auth: "required", cost: "variable", settles: "onchain", reversible: false, simulatable: false,
|
|
1511
|
+
examples: [{ tags: ["engineering", "ontology", "marketing"], addWeight: 20, digest: "VisitorSignedDigestFromSuiTestnetTx" }],
|
|
995
1512
|
}),
|
|
996
1513
|
// ── market (A2A negotiation, C2) ──
|
|
997
1514
|
"market:offer": receiver({
|
|
@@ -1136,7 +1653,11 @@ export const RECEIVERS = {
|
|
|
1136
1653
|
receiver: "market:bounty",
|
|
1137
1654
|
summary: "Post a bounty for a skill, backed by an escrow",
|
|
1138
1655
|
request: z.object({
|
|
1139
|
-
skillId: z.string(), sellerUid: z.string(),
|
|
1656
|
+
skillId: z.string(), sellerUid: z.string(),
|
|
1657
|
+
// DERIVED, never sent: the handler sets `posterUid = ctx.ownerSlug`
|
|
1658
|
+
// (resolvers/market.ts). Required here, it rejected every valid post.
|
|
1659
|
+
posterUid: z.string().optional(),
|
|
1660
|
+
price: z.number(),
|
|
1140
1661
|
tags: z.array(z.string()).optional(),
|
|
1141
1662
|
content: z.record(z.string(), z.unknown()).optional(),
|
|
1142
1663
|
rubric: z.object({
|
|
@@ -1171,6 +1692,32 @@ export const RECEIVERS = {
|
|
|
1171
1692
|
]),
|
|
1172
1693
|
effect: "ask", cost: "free", reversible: false, idempotent: false,
|
|
1173
1694
|
}),
|
|
1695
|
+
"market:search": receiver({
|
|
1696
|
+
receiver: "market:search",
|
|
1697
|
+
summary: "Search the whole marketplace catalog — agents, skills, plugins, live listings and open bounties — and get back named, card-shaped results",
|
|
1698
|
+
request: z.object({
|
|
1699
|
+
q: z.string().describe("What the shopper is looking for, in their own words"),
|
|
1700
|
+
kind: z.enum(["agent", "skill", "plugin", "listing", "bounty"]).optional(),
|
|
1701
|
+
limit: z.number().int().positive().optional(),
|
|
1702
|
+
}),
|
|
1703
|
+
response: z.object({
|
|
1704
|
+
ok: z.boolean(),
|
|
1705
|
+
total: z.number(),
|
|
1706
|
+
matched: z.number(),
|
|
1707
|
+
results: z.array(z.object({
|
|
1708
|
+
kind: z.string(), ref: z.string(), title: z.string(), blurb: z.string(),
|
|
1709
|
+
tags: z.array(z.string()),
|
|
1710
|
+
// null means "carries no price" — never render it as 0. `priceLabel` is
|
|
1711
|
+
// the honest string for a card ("from 25 credits", "$49", or null).
|
|
1712
|
+
price: z.number().nullable(), unit: z.string().nullable(),
|
|
1713
|
+
sellers: z.number(), priceLabel: z.string().nullable(), url: z.string(),
|
|
1714
|
+
})),
|
|
1715
|
+
}),
|
|
1716
|
+
// Browse is public on /marketplace and public here — the concierge answers
|
|
1717
|
+
// "who sells X" for a signed-out visitor, which is the whole point.
|
|
1718
|
+
auth: "open",
|
|
1719
|
+
effect: "ask", cost: "free", idempotent: true,
|
|
1720
|
+
}),
|
|
1174
1721
|
"market:list": receiver({
|
|
1175
1722
|
receiver: "market:list",
|
|
1176
1723
|
summary: "List the capability market",
|
|
@@ -1183,7 +1730,13 @@ export const RECEIVERS = {
|
|
|
1183
1730
|
receiver: "capabilities:publish",
|
|
1184
1731
|
summary: "Publish a capability (skill listing) to the market",
|
|
1185
1732
|
request: z.object({
|
|
1186
|
-
skillId: z.string(), name: z.string(),
|
|
1733
|
+
skillId: z.string(), name: z.string(),
|
|
1734
|
+
// OPTIONAL — `market.ts` reads it as `Math.max(0, num(...))`, so an
|
|
1735
|
+
// omitted price is a free listing, not a refusal. Declaring it required
|
|
1736
|
+
// meant the edge rejected a call the resolver was written to accept, the
|
|
1737
|
+
// same drift that took `subscriptions:register` down once bind-receiver
|
|
1738
|
+
// began enforcing declared schemas.
|
|
1739
|
+
price: z.number().optional(),
|
|
1187
1740
|
mode: z.string().optional(), visibility: z.string().optional(), scope: z.string().optional(), entitlement: z.string().optional(),
|
|
1188
1741
|
tags: z.array(z.string()).optional(),
|
|
1189
1742
|
rubricThresholds: z.object({
|
|
@@ -1211,6 +1764,7 @@ export const RECEIVERS = {
|
|
|
1211
1764
|
}),
|
|
1212
1765
|
"stats:current": receiver({
|
|
1213
1766
|
receiver: "stats:current",
|
|
1767
|
+
surfaces: { mcp: { name: "stats" } },
|
|
1214
1768
|
summary: "Current world stats — units, skills, highways, revenue, signals",
|
|
1215
1769
|
request: z.object({}),
|
|
1216
1770
|
response: StatsSchema,
|
|
@@ -1241,6 +1795,7 @@ export const RECEIVERS = {
|
|
|
1241
1795
|
// ════════════════════════════════════════════════════════════════════════
|
|
1242
1796
|
"meta:catalog": receiver({
|
|
1243
1797
|
receiver: "meta:catalog",
|
|
1798
|
+
surfaces: { mcp: true },
|
|
1244
1799
|
summary: "List every receiver the caller can use (with cost/reversibility/settlement), or a goal's recipe",
|
|
1245
1800
|
request: z.object({ goal: z.enum(["spine", "build", "trade", "transact"]).optional() }),
|
|
1246
1801
|
response: z.union([
|
|
@@ -1255,6 +1810,7 @@ export const RECEIVERS = {
|
|
|
1255
1810
|
}),
|
|
1256
1811
|
"meta:schema": receiver({
|
|
1257
1812
|
receiver: "meta:schema",
|
|
1813
|
+
surfaces: { mcp: true },
|
|
1258
1814
|
summary: "JSON Schema for one receiver's request + response — read it before you call",
|
|
1259
1815
|
request: z.object({ receiver: z.string() }),
|
|
1260
1816
|
response: z.object({
|
|
@@ -1265,6 +1821,7 @@ export const RECEIVERS = {
|
|
|
1265
1821
|
}),
|
|
1266
1822
|
"meta:recall": receiver({
|
|
1267
1823
|
receiver: "meta:recall",
|
|
1824
|
+
surfaces: { mcp: true },
|
|
1268
1825
|
summary: "Recall the caller's hypotheses (memory), optionally filtered by a search term",
|
|
1269
1826
|
request: z.object({ match: z.string().optional(), limit: z.number().optional() }),
|
|
1270
1827
|
response: z.object({
|
|
@@ -1292,6 +1849,7 @@ export const RECEIVERS = {
|
|
|
1292
1849
|
}),
|
|
1293
1850
|
"meta:types": receiver({
|
|
1294
1851
|
receiver: "meta:types",
|
|
1852
|
+
surfaces: { mcp: true },
|
|
1295
1853
|
summary: "Read the resource-type manifest for the caller's workspace, plus the built-in industry templates",
|
|
1296
1854
|
request: z.object({}),
|
|
1297
1855
|
response: z.object({
|
|
@@ -1412,6 +1970,13 @@ export const RECEIVERS = {
|
|
|
1412
1970
|
}),
|
|
1413
1971
|
"notify": receiver({
|
|
1414
1972
|
receiver: "notify",
|
|
1973
|
+
// NOT surfaces.mcp — the curated `message` tool in packages/mcp/src/tools/
|
|
1974
|
+
// messaging.ts already fronts this receiver and carries shaping the registry
|
|
1975
|
+
// cannot infer: it defaults `kind: "message"` and names the argument
|
|
1976
|
+
// `content` in a description written for a sender, not a schema. Flagging it
|
|
1977
|
+
// produced two tools called `message`, of which the curated one won
|
|
1978
|
+
// registration and the derived one was dead weight. Curated survives here;
|
|
1979
|
+
// the registry does not claim a surface something else already serves.
|
|
1415
1980
|
summary: "Notify any actor (human or agent) by uid — channels routes the door (Telegram/Discord/peer inbox), mirrors into the web inbox, nudges an open client live, and sends a web push if the recipient subscribed. One verb, both actor kinds. An optional action renders as one tap-through link.",
|
|
1416
1981
|
request: z.object({
|
|
1417
1982
|
receiver: z.string(),
|
|
@@ -1841,14 +2406,30 @@ export const RECEIVERS = {
|
|
|
1841
2406
|
images: z.array(z.string()).optional(),
|
|
1842
2407
|
product_type: z.string().optional(), // default 'one_time'
|
|
1843
2408
|
collection: z.string().optional(),
|
|
2409
|
+
// Same stripped-at-the-edge trap as the two artifact fields below, and it
|
|
2410
|
+
// cost more: resolvers/commerce.ts has read `data.status` since it shipped,
|
|
2411
|
+
// but the key was never declared here, so zod dropped it and EVERY product
|
|
2412
|
+
// created through this receiver came back 'draft'. Proven behaviourally
|
|
2413
|
+
// 2026-08-23 — POST with status:"active" returned 200, the row read 'draft'.
|
|
2414
|
+
// An agent could list a product and had no way to make it sellable.
|
|
2415
|
+
status: z.enum(["draft", "active", "archived"]).optional(),
|
|
2416
|
+
// The ARTIFACT listing door. Declared here because validateReceiver returns
|
|
2417
|
+
// zod's PARSED output, so an undeclared key is stripped at the HTTP edge and
|
|
2418
|
+
// never reaches the handler. `delivery_r2_key` is accepted ONLY as a
|
|
2419
|
+
// `{kind}:{ref}` install descriptor — the plain-R2-key write door stays shut.
|
|
2420
|
+
product_kind: z.literal("artifact").optional(),
|
|
2421
|
+
delivery_r2_key: z.string().optional(),
|
|
1844
2422
|
}),
|
|
1845
2423
|
response: z.object({ pid: z.string().optional(), ppid: z.string().optional(), name: z.string().optional(), workspace: z.string().optional(), error: z.string().optional() }),
|
|
1846
2424
|
effect: "ask", cost: "free", reversible: true, idempotent: false, auth: "member",
|
|
1847
2425
|
}),
|
|
1848
2426
|
"products:update": receiver({
|
|
1849
2427
|
receiver: "products:update",
|
|
1850
|
-
summary: "Update a product's name, description, images, or
|
|
1851
|
-
|
|
2428
|
+
summary: "Update a product's name, description, images, collection, or status",
|
|
2429
|
+
// `status` carries the same history as on products:create — the resolver has
|
|
2430
|
+
// always read it, the declaration never listed it, so zod stripped it and a
|
|
2431
|
+
// draft product could not be published through this receiver either.
|
|
2432
|
+
request: z.object({ slug: z.string(), pid: z.string(), name: z.string().optional(), description: z.string().optional(), images: z.array(z.string()).optional(), collection: z.string().optional(), status: z.enum(["draft", "active", "archived"]).optional(), product_kind: z.literal("artifact").optional(), delivery_r2_key: z.string().optional() }),
|
|
1852
2433
|
response: z.object({ pid: z.string().optional(), workspace: z.string().optional(), error: z.string().optional() }),
|
|
1853
2434
|
effect: "ask", cost: "free", reversible: false, idempotent: true, auth: "member",
|
|
1854
2435
|
}),
|
|
@@ -1873,6 +2454,429 @@ export const RECEIVERS = {
|
|
|
1873
2454
|
response: z.object({ pid: z.string().optional(), workspace: z.string().optional(), status: z.string().optional(), error: z.string().optional() }),
|
|
1874
2455
|
effect: "ask", cost: "free", reversible: true, idempotent: true, auth: "member",
|
|
1875
2456
|
}),
|
|
2457
|
+
// ── variants (product options: size/colour) — handlers in resolvers/commerce.ts ─
|
|
2458
|
+
// stock IS NULL means UNTRACKED, never zero: a service or digital product is
|
|
2459
|
+
// never blocked by an inventory check it did not opt into. On update, an ABSENT
|
|
2460
|
+
// stock field leaves the value unchanged; an explicit null sets untracked.
|
|
2461
|
+
"variants:list": receiver({
|
|
2462
|
+
receiver: "variants:list",
|
|
2463
|
+
summary: "List a product's active variants with SKU, price, weight and stock (public)",
|
|
2464
|
+
request: z.object({ slug: z.string(), pid: z.string() }),
|
|
2465
|
+
response: z.object({ variants: z.array(z.record(z.string(), z.unknown())).optional(), error: z.string().optional() }),
|
|
2466
|
+
effect: "ask", cost: "free", reversible: true, idempotent: true, auth: "public",
|
|
2467
|
+
}),
|
|
2468
|
+
"variants:create": receiver({
|
|
2469
|
+
receiver: "variants:create",
|
|
2470
|
+
summary: "Add a variant to a product — its own SKU, price, weight and stock",
|
|
2471
|
+
request: z.object({
|
|
2472
|
+
slug: z.string(),
|
|
2473
|
+
pid: z.string(),
|
|
2474
|
+
title: z.string(), // "Blue / M"
|
|
2475
|
+
unit_amount: z.number().int(), // cents
|
|
2476
|
+
sku: z.string().optional(),
|
|
2477
|
+
options: z.record(z.string(), z.string()).optional(),
|
|
2478
|
+
currency: z.string().optional(), // default 'usd'
|
|
2479
|
+
weight_grams: z.number().int().optional(),
|
|
2480
|
+
stock: z.number().int().nullable().optional(), // null/absent = untracked
|
|
2481
|
+
position: z.number().int().optional(),
|
|
2482
|
+
}),
|
|
2483
|
+
response: z.object({ vid: z.string().optional(), pid: z.string().optional(), workspace: z.string().optional(), error: z.string().optional() }),
|
|
2484
|
+
effect: "ask", cost: "free", reversible: true, idempotent: false, auth: "member",
|
|
2485
|
+
}),
|
|
2486
|
+
"variants:update": receiver({
|
|
2487
|
+
receiver: "variants:update",
|
|
2488
|
+
summary: "Update a variant — omit stock to leave it unchanged, send null to make it untracked",
|
|
2489
|
+
request: z.object({
|
|
2490
|
+
slug: z.string(),
|
|
2491
|
+
vid: z.string(),
|
|
2492
|
+
title: z.string().optional(),
|
|
2493
|
+
unit_amount: z.number().int().optional(),
|
|
2494
|
+
sku: z.string().optional(),
|
|
2495
|
+
options: z.record(z.string(), z.string()).optional(),
|
|
2496
|
+
currency: z.string().optional(),
|
|
2497
|
+
weight_grams: z.number().int().optional(),
|
|
2498
|
+
stock: z.number().int().nullable().optional(), // absent is not null: absent leaves it unchanged
|
|
2499
|
+
position: z.number().int().optional(),
|
|
2500
|
+
active: z.boolean().optional(),
|
|
2501
|
+
}),
|
|
2502
|
+
response: z.object({ vid: z.string().optional(), workspace: z.string().optional(), error: z.string().optional() }),
|
|
2503
|
+
effect: "ask", cost: "free", reversible: true, idempotent: true, auth: "member",
|
|
2504
|
+
}),
|
|
2505
|
+
"variants:archive": receiver({
|
|
2506
|
+
receiver: "variants:archive",
|
|
2507
|
+
summary: "Archive a variant (reversible via variants:update { active: true })",
|
|
2508
|
+
request: z.object({ slug: z.string(), vid: z.string() }),
|
|
2509
|
+
response: z.object({ vid: z.string().optional(), workspace: z.string().optional(), active: z.number().optional(), error: z.string().optional() }),
|
|
2510
|
+
effect: "ask", cost: "free", reversible: true, idempotent: true, auth: "member",
|
|
2511
|
+
}),
|
|
2512
|
+
// ── cart (server-truth basket) — handlers land in resolvers/cart.ts (C3) ───────
|
|
2513
|
+
// Registered here, not by C3: signals-parity counts RECEIVERS against
|
|
2514
|
+
// text/signals-catalog.md and is a live accept: on another kept promise.
|
|
2515
|
+
// `cid` is a server-minted bearer capability (never client-chosen), which is
|
|
2516
|
+
// what makes an anonymous cart safe at auth: "public". No request carries a
|
|
2517
|
+
// unit_amount — every line is re-priced server-side from D1 by ppid/vid.
|
|
2518
|
+
"cart:get": receiver({
|
|
2519
|
+
receiver: "cart:get",
|
|
2520
|
+
summary: "Read a cart and its lines by server-minted cart id",
|
|
2521
|
+
request: z.object({ slug: z.string(), cid: z.string() }),
|
|
2522
|
+
response: z.object({ cart: z.record(z.string(), z.unknown()).nullable().optional(), items: z.array(z.record(z.string(), z.unknown())).optional(), error: z.string().optional() }),
|
|
2523
|
+
effect: "ask", cost: "free", reversible: true, idempotent: true, auth: "public",
|
|
2524
|
+
}),
|
|
2525
|
+
"cart:add": receiver({
|
|
2526
|
+
receiver: "cart:add",
|
|
2527
|
+
summary: "Add a product (or variant) line to a cart, minting the cart when cid is absent — price is read server-side, never sent",
|
|
2528
|
+
request: z.object({
|
|
2529
|
+
slug: z.string(),
|
|
2530
|
+
pid: z.string(),
|
|
2531
|
+
cid: z.string().optional(), // absent mints a new cart
|
|
2532
|
+
ppid: z.string().optional(),
|
|
2533
|
+
vid: z.string().optional(),
|
|
2534
|
+
quantity: z.number().int().positive().optional(), // default 1
|
|
2535
|
+
}),
|
|
2536
|
+
response: z.object({ cid: z.string().optional(), ciid: z.string().optional(), items: z.array(z.record(z.string(), z.unknown())).optional(), error: z.string().optional() }),
|
|
2537
|
+
effect: "ask", cost: "free", reversible: true, idempotent: false, auth: "public",
|
|
2538
|
+
}),
|
|
2539
|
+
"cart:update": receiver({
|
|
2540
|
+
receiver: "cart:update",
|
|
2541
|
+
summary: "Set a cart line's quantity (0 removes the line)",
|
|
2542
|
+
request: z.object({ slug: z.string(), cid: z.string(), ciid: z.string(), quantity: z.number().int().min(0) }),
|
|
2543
|
+
response: z.object({ cid: z.string().optional(), items: z.array(z.record(z.string(), z.unknown())).optional(), error: z.string().optional() }),
|
|
2544
|
+
effect: "ask", cost: "free", reversible: true, idempotent: true, auth: "public",
|
|
2545
|
+
}),
|
|
2546
|
+
"cart:remove": receiver({
|
|
2547
|
+
receiver: "cart:remove",
|
|
2548
|
+
summary: "Remove one line from a cart",
|
|
2549
|
+
request: z.object({ slug: z.string(), cid: z.string(), ciid: z.string() }),
|
|
2550
|
+
response: z.object({ cid: z.string().optional(), items: z.array(z.record(z.string(), z.unknown())).optional(), error: z.string().optional() }),
|
|
2551
|
+
effect: "ask", cost: "free", reversible: false, idempotent: true, auth: "public",
|
|
2552
|
+
}),
|
|
2553
|
+
"cart:clear": receiver({
|
|
2554
|
+
receiver: "cart:clear",
|
|
2555
|
+
summary: "Empty a cart, keeping the cart itself",
|
|
2556
|
+
request: z.object({ slug: z.string(), cid: z.string() }),
|
|
2557
|
+
response: z.object({ cid: z.string().optional(), cleared: z.number().optional(), error: z.string().optional() }),
|
|
2558
|
+
effect: "ask", cost: "free", reversible: false, idempotent: true, auth: "public",
|
|
2559
|
+
}),
|
|
2560
|
+
// ── orders (the one operator write door) — handler lands in resolvers (C5) ─────
|
|
2561
|
+
// data-bindings.ts:62 marks `order` create/update/archive as null by design:
|
|
2562
|
+
// orders are webhook-minted and otherwise read-only. This is the deliberate
|
|
2563
|
+
// exception. `status` is z.string(), not an enum — the D1 column is bare TEXT
|
|
2564
|
+
// (0118_orders.sql:15) and C5's resolver owns the allowlist.
|
|
2565
|
+
"orders:status": receiver({
|
|
2566
|
+
receiver: "orders:status",
|
|
2567
|
+
summary: "Set an order's status — the one operator write on a webhook-minted record",
|
|
2568
|
+
request: z.object({ slug: z.string(), oid: z.string(), status: z.string() }),
|
|
2569
|
+
response: z.object({ oid: z.string().optional(), workspace: z.string().optional(), status: z.string().optional(), error: z.string().optional() }),
|
|
2570
|
+
effect: "ask", cost: "free", reversible: true, idempotent: true, auth: "member",
|
|
2571
|
+
}),
|
|
2572
|
+
// ── order:claim — the buyer's delivery door (handler: resolvers/commerce.ts) ──
|
|
2573
|
+
// Declared so the route's envelope guard fires: an undeclared receiver skips
|
|
2574
|
+
// the `envelope_missing` check, and a flat body silently validates as {}.
|
|
2575
|
+
// `auth: "public"` because an anonymous buyer claims with the order's own
|
|
2576
|
+
// session_id as bearer proof; the handler holds the real attestation (session
|
|
2577
|
+
// match OR ctx-attested workspace control), never a body-supplied identity.
|
|
2578
|
+
"order:claim": receiver({
|
|
2579
|
+
receiver: "order:claim",
|
|
2580
|
+
summary: "Claim a paid order's delivery — returns a scoped download URL for the digital good",
|
|
2581
|
+
request: z.object({
|
|
2582
|
+
oid: z.string(),
|
|
2583
|
+
workspace: z.string().optional(),
|
|
2584
|
+
session_id: z.string().optional(),
|
|
2585
|
+
}),
|
|
2586
|
+
response: z.object({
|
|
2587
|
+
ok: z.boolean().optional(),
|
|
2588
|
+
url: z.string().optional(),
|
|
2589
|
+
name: z.string().optional(),
|
|
2590
|
+
error: z.string().optional(),
|
|
2591
|
+
}),
|
|
2592
|
+
effect: "ask", cost: "free", reversible: true, idempotent: true, settles: "none", auth: "public",
|
|
2593
|
+
examples: [{ oid: "ord_1", session_id: "cs_test_1" }],
|
|
2594
|
+
}),
|
|
2595
|
+
// ── order:install — the buyer's ARTIFACT door (handler: resolvers/install.ts) ──
|
|
2596
|
+
// The sibling of order:claim, and deliberately NOT the same receiver. order:claim
|
|
2597
|
+
// is `auth: "public"` because it hands back a FILE: knowing (oid, session_id) is
|
|
2598
|
+
// the whole proof, which is right for a crypto buyer who never signs in. Install
|
|
2599
|
+
// inverts the direction of authority — a SELLER's product causes a WRITE into a
|
|
2600
|
+
// BUYER's workspace — so it is the IDOR shape ~80 workspace routes were already
|
|
2601
|
+
// fixed for, and it gets a tenant-class label (`manage_things`) so bind-receiver's
|
|
2602
|
+
// AUTH_POLICY floor is ENFORCED before the handler runs. The handler then walks
|
|
2603
|
+
// the destination with callerControlsWorkspace(ctx.ownerSlug, …); `workspace` in
|
|
2604
|
+
// the body NAMES a target, it never authorizes one. order:claim is untouched.
|
|
2605
|
+
"order:install": receiver({
|
|
2606
|
+
receiver: "order:install",
|
|
2607
|
+
summary: "Install a paid artifact order into a workspace the caller controls — routes to skill:save / world:publish-agent / workflow:create / view:create by the delivery descriptor's kind",
|
|
2608
|
+
request: z.object({
|
|
2609
|
+
oid: z.string(),
|
|
2610
|
+
workspace: z.string().optional(), // destination; defaults to the attested caller
|
|
2611
|
+
session_id: z.string().optional(), // the order's own receipt, as in order:claim
|
|
2612
|
+
}),
|
|
2613
|
+
response: z.object({
|
|
2614
|
+
ok: z.boolean().optional(),
|
|
2615
|
+
kind: z.enum(["skill", "agent", "workflow", "view", "playbook"]).optional(),
|
|
2616
|
+
ref: z.string().optional(),
|
|
2617
|
+
workspace: z.string().optional(),
|
|
2618
|
+
installed: z.record(z.string(), z.unknown()).optional(),
|
|
2619
|
+
error: z.string().optional(),
|
|
2620
|
+
}),
|
|
2621
|
+
effect: "ask", cost: "free", reversible: false, idempotent: false, settles: "none", auth: "manage_things",
|
|
2622
|
+
examples: [{ oid: "ord_1", session_id: "cs_test_1" }, { oid: "ord_1", workspace: "elitemoversca" }],
|
|
2623
|
+
}),
|
|
2624
|
+
// ── wallet:create — register RECEIVING ADDRESSES only (handler: resolvers/wallet.ts) ──
|
|
2625
|
+
// Custody invariant: `addresses` carries public addresses and nothing else. No
|
|
2626
|
+
// field here accepts a private key or a mnemonic, and none is emitted back.
|
|
2627
|
+
// Omitting `addresses` provisions an address-less row (the pre-existing
|
|
2628
|
+
// idempotent path); supplying it merges first-write-wins per chain.
|
|
2629
|
+
"wallet:create": receiver({
|
|
2630
|
+
receiver: "wallet:create",
|
|
2631
|
+
summary: "Provision a wallet row for an actor and register its public receiving addresses (first-write-wins per chain)",
|
|
2632
|
+
request: z.object({
|
|
2633
|
+
workspace: z.string().optional(),
|
|
2634
|
+
actor: z.string().optional(),
|
|
2635
|
+
kind: z.enum(["human", "agent"]).optional(),
|
|
2636
|
+
addresses: z.object({
|
|
2637
|
+
sui: z.string().nullable().optional(),
|
|
2638
|
+
evm: z.string().nullable().optional(),
|
|
2639
|
+
solana: z.string().nullable().optional(),
|
|
2640
|
+
btc: z.string().nullable().optional(),
|
|
2641
|
+
}).optional(),
|
|
2642
|
+
}),
|
|
2643
|
+
response: z.object({
|
|
2644
|
+
ok: z.boolean().optional(),
|
|
2645
|
+
actor: z.string().optional(),
|
|
2646
|
+
workspace: z.string().optional(),
|
|
2647
|
+
kind: z.string().optional(),
|
|
2648
|
+
addresses: z.object({
|
|
2649
|
+
sui: z.string().nullable().optional(),
|
|
2650
|
+
evm: z.string().nullable().optional(),
|
|
2651
|
+
solana: z.string().nullable().optional(),
|
|
2652
|
+
btc: z.string().nullable().optional(),
|
|
2653
|
+
}).optional(),
|
|
2654
|
+
error: z.string().optional(),
|
|
2655
|
+
field: z.string().optional(),
|
|
2656
|
+
}),
|
|
2657
|
+
effect: "ask", cost: "free", reversible: false, idempotent: true, settles: "none", auth: "member",
|
|
2658
|
+
examples: [{ workspace: "acme", kind: "human", addresses: { sui: "0xabc", evm: "0xdef" } }],
|
|
2659
|
+
}),
|
|
2660
|
+
// ── wallet:rehearse — READ a TESTNET transaction back (handler: resolvers/wallet.ts) ──
|
|
2661
|
+
// The rehearsal step of the /u/one funnel: a visitor sends play money on Sui
|
|
2662
|
+
// testnet from their own derived address, and this reads the digest back so the
|
|
2663
|
+
// chat can say what actually happened on chain.
|
|
2664
|
+
//
|
|
2665
|
+
// IT GRANTS NOTHING, and that is the whole contract — `credits` is a
|
|
2666
|
+
// z.literal(0), so the response schema itself forbids a grant. The reason is
|
|
2667
|
+
// not squeamishness: `currentBalance` (web/src/lib/credits.ts) sums
|
|
2668
|
+
// `credit_grants` with NO `test_mode` filter, so a credit granted for testnet
|
|
2669
|
+
// money would be real, fully spendable credit minted out of play money.
|
|
2670
|
+
//
|
|
2671
|
+
// `network` is a z.literal("testnet") on BOTH sides. There is deliberately no
|
|
2672
|
+
// parameter by which a caller could point this at mainnet — the resolver
|
|
2673
|
+
// hardcodes the testnet RPC. A mainnet read belongs to the settling rail
|
|
2674
|
+
// (pay's payment_link_claim), which moves money and therefore takes attestation.
|
|
2675
|
+
//
|
|
2676
|
+
// auth "public" because it neither writes nor grants: a chain read with a rate
|
|
2677
|
+
// limit, placed in front of anonymous workspace-root traffic. The resolver
|
|
2678
|
+
// limits per ATTESTED `ctx.visitorHash` — the shape `movers:funnel-provision`
|
|
2679
|
+
// already uses. A caller may name an address; it can never name a spend.
|
|
2680
|
+
"wallet:rehearse": receiver({
|
|
2681
|
+
receiver: "wallet:rehearse",
|
|
2682
|
+
summary: "Read a TESTNET Sui transaction back and report it. Grants nothing, settles nothing, never writes a credit_grants row.",
|
|
2683
|
+
request: z.object({
|
|
2684
|
+
chain: z.literal("sui"),
|
|
2685
|
+
network: z.literal("testnet"),
|
|
2686
|
+
digest: z.string().min(1).describe("The transaction digest the sender broadcast"),
|
|
2687
|
+
address: z.string().optional().describe("The sender address — PUBLIC only, never a key"),
|
|
2688
|
+
}),
|
|
2689
|
+
response: z.object({
|
|
2690
|
+
verified: z.boolean(),
|
|
2691
|
+
network: z.literal("testnet"),
|
|
2692
|
+
credits: z.literal(0).describe("Always 0. Testnet money buys nothing."),
|
|
2693
|
+
amount: z.string().nullable().optional(),
|
|
2694
|
+
sender: z.string().nullable().optional(),
|
|
2695
|
+
explorerUrl: z.string().nullable().optional(),
|
|
2696
|
+
error: z.string().optional(),
|
|
2697
|
+
}),
|
|
2698
|
+
effect: "ask", cost: "free", reversible: true, idempotent: true, settles: "none", auth: "public",
|
|
2699
|
+
examples: [{ chain: "sui", network: "testnet", digest: "8xKvQ1", address: "0xabc" }],
|
|
2700
|
+
}),
|
|
2701
|
+
// ── The wallet estate, declared ────────────────────────────────────────────
|
|
2702
|
+
//
|
|
2703
|
+
// wallet:get / :send / :transactions / :invoice have run in production since
|
|
2704
|
+
// wallets-workspace shipped (handlers: resolvers/wallet.ts:643) with NO entry
|
|
2705
|
+
// here at all. Undeclared, they were invisible to `meta:catalog`, absent from
|
|
2706
|
+
// MCP, and passed through the /api/ask edge with no schema gate whatsoever.
|
|
2707
|
+
//
|
|
2708
|
+
// DECLARING A LIVE RECEIVER IS NOT FREE. The ask route dispatches
|
|
2709
|
+
// `validation.payload` — zod's PARSED output (pages/api/ask/[...receiver].ts:358)
|
|
2710
|
+
// — so a field a real caller sends and a schema here omits is SILENTLY
|
|
2711
|
+
// STRIPPED before the handler reads it. Every field below was read back off
|
|
2712
|
+
// the resolver and off its callers (packages/cli/src/wallet.ts,
|
|
2713
|
+
// template/site/src/pages/wallet.astro). Add a field to a handler ⇒ add it
|
|
2714
|
+
// here in the same diff, or the handler stops seeing it.
|
|
2715
|
+
//
|
|
2716
|
+
// `surfaces: { mcp: true }` is what actually creates the MCP tool.
|
|
2717
|
+
// `mcpToolsFromRegistry()` is default-closed (`if (!mcp) continue`,
|
|
2718
|
+
// packages/mcp/src/tools/from-registry.ts:79), so the declaration alone would
|
|
2719
|
+
// have left MCP with zero wallet tools — the exact gap this cycle closes.
|
|
2720
|
+
"wallet:get": receiver({
|
|
2721
|
+
receiver: "wallet:get",
|
|
2722
|
+
surfaces: { mcp: true },
|
|
2723
|
+
summary: "Read one workspace's wallet estate — credits, spend ceiling, wallet rows with payTo, live chain balances, delegated wallets",
|
|
2724
|
+
request: z.object({
|
|
2725
|
+
workspace: z.string().optional().describe("Defaults to the attested caller's workspace"),
|
|
2726
|
+
actor: z.string().optional().describe("Actor whose spend ceiling is read; defaults to the workspace"),
|
|
2727
|
+
}),
|
|
2728
|
+
response: z.object({
|
|
2729
|
+
workspace: z.string().optional(),
|
|
2730
|
+
actor: z.string().optional(),
|
|
2731
|
+
credits: z.number().optional(),
|
|
2732
|
+
wallets: z.array(z.record(z.string(), z.unknown())).optional(),
|
|
2733
|
+
spendCeiling: z.object({
|
|
2734
|
+
limitCredits: z.number(), spentCredits: z.number(), remainingCredits: z.number(),
|
|
2735
|
+
}).nullable().optional(),
|
|
2736
|
+
balances: z.array(z.object({
|
|
2737
|
+
actor: z.string(), chain: z.string(), address: z.string(), balance: z.string().nullable(),
|
|
2738
|
+
})).optional(),
|
|
2739
|
+
addresses: z.array(z.object({ actor: z.string(), chain: z.string(), address: z.string() })).optional(),
|
|
2740
|
+
delegated: z.array(z.record(z.string(), z.unknown())).optional(),
|
|
2741
|
+
error: z.string().optional(),
|
|
2742
|
+
}),
|
|
2743
|
+
effect: "ask", cost: "free", reversible: true, idempotent: true, settles: "none", auth: "member",
|
|
2744
|
+
examples: [{}, { workspace: "acme" }, { workspace: "acme", actor: "agent:scout" }],
|
|
2745
|
+
}),
|
|
2746
|
+
"wallet:transactions": receiver({
|
|
2747
|
+
receiver: "wallet:transactions",
|
|
2748
|
+
surfaces: { mcp: true },
|
|
2749
|
+
summary: "The unified settlement feed for one workspace — every rail, money in and out, newest first",
|
|
2750
|
+
request: z.object({
|
|
2751
|
+
workspace: z.string().optional().describe("Defaults to the attested caller's workspace"),
|
|
2752
|
+
limit: z.number().optional().describe("Rows to return; the handler caps at 500"),
|
|
2753
|
+
}),
|
|
2754
|
+
response: z.object({
|
|
2755
|
+
transactions: z.array(z.object({
|
|
2756
|
+
ts: z.number(), rail: z.string(), direction: z.enum(["in", "out"]), counterparty: z.string(),
|
|
2757
|
+
amountCents: z.number(), currency: z.string(), ref: z.string(), status: z.string(),
|
|
2758
|
+
})).optional(),
|
|
2759
|
+
error: z.string().optional(),
|
|
2760
|
+
}),
|
|
2761
|
+
effect: "ask", cost: "free", reversible: true, idempotent: true, settles: "none", auth: "member",
|
|
2762
|
+
examples: [{ limit: 20 }, { workspace: "acme", limit: 100 }],
|
|
2763
|
+
}),
|
|
2764
|
+
// The caller signs and broadcasts the transfer CLIENT-SIDE and passes the
|
|
2765
|
+
// resulting `paymentTx`; this composes pay.one.ie's create → quote → claim and
|
|
2766
|
+
// never holds a key. It VERIFIES that an on-chain transfer settled, so it
|
|
2767
|
+
// declares `settles: "onchain"` and `reversible: false`. A catalog that said
|
|
2768
|
+
// otherwise would put a lie where the omission used to be.
|
|
2769
|
+
"wallet:send": receiver({
|
|
2770
|
+
receiver: "wallet:send",
|
|
2771
|
+
surfaces: { mcp: true },
|
|
2772
|
+
summary: "Settle a crypto transfer the caller already signed and broadcast — mints, prices and claims a pay.one.ie link. Never signs; no private key crosses this boundary",
|
|
2773
|
+
request: z.object({
|
|
2774
|
+
to: z.string().describe("Recipient address — public only"),
|
|
2775
|
+
chain: z.string().describe("SOL | ETH | BASE | ARB | OPT | BTC | SUI"),
|
|
2776
|
+
amount: z.number().describe("Must be > 0"),
|
|
2777
|
+
paymentTx: z.string().describe("The tx hash from signing and broadcasting client-side"),
|
|
2778
|
+
workspace: z.string().optional().describe("Source workspace; falls back to `from`, then the attested caller"),
|
|
2779
|
+
from: z.string().optional().describe("Alias for `workspace` — the source the authority walk runs against"),
|
|
2780
|
+
unit: z.enum(["usd", "token"]).optional(),
|
|
2781
|
+
currency: z.enum(["NATIVE", "USDC"]).optional(),
|
|
2782
|
+
userAddress: z.string().optional().describe("Sender address; defaults to `to`"),
|
|
2783
|
+
memo: z.string().optional(),
|
|
2784
|
+
product: z.string().optional().describe("Falls back to `memo`, then a generated transfer label"),
|
|
2785
|
+
}),
|
|
2786
|
+
response: z.object({
|
|
2787
|
+
ok: z.boolean(),
|
|
2788
|
+
from: z.string().optional(),
|
|
2789
|
+
to: z.string().optional(),
|
|
2790
|
+
chain: z.string().optional(),
|
|
2791
|
+
amount: z.number().optional(),
|
|
2792
|
+
link: z.string().optional(),
|
|
2793
|
+
quoteId: z.string().optional(),
|
|
2794
|
+
paymentTx: z.string().optional(),
|
|
2795
|
+
treasury: z.string().optional(),
|
|
2796
|
+
receipt: z.object({ id: z.string(), url: z.string() }).optional(),
|
|
2797
|
+
claimed: z.boolean().optional(),
|
|
2798
|
+
stage: z.string().optional().describe("Which pay call failed: create | quote | claim"),
|
|
2799
|
+
error: z.string().optional(),
|
|
2800
|
+
}),
|
|
2801
|
+
effect: "ask", cost: "variable", reversible: false, idempotent: false, settles: "onchain", auth: "manage_workspace",
|
|
2802
|
+
examples: [{ to: "0xabc", chain: "SUI", amount: 5, paymentTx: "8xKvQ1" }],
|
|
2803
|
+
}),
|
|
2804
|
+
// A pending, attributed `settlements` row is written before a cent moves —
|
|
2805
|
+
// INSERT OR IGNORE keyed on the pay link id, so a retry with the same link is a
|
|
2806
|
+
// no-op. An unpayable actor is a clean refusal: no link minted, no row written.
|
|
2807
|
+
"wallet:invoice": receiver({
|
|
2808
|
+
receiver: "wallet:invoice",
|
|
2809
|
+
surfaces: { mcp: true },
|
|
2810
|
+
summary: "Hand a payable actor a working payment link, with the money attributed to it before a cent moves",
|
|
2811
|
+
request: z.object({
|
|
2812
|
+
actor: z.string().describe("The actor being paid — attributed here even when custody is via-treasury"),
|
|
2813
|
+
amountCents: z.number().describe("Must be > 0"),
|
|
2814
|
+
workspace: z.string().optional().describe("Defaults to the attested caller's workspace"),
|
|
2815
|
+
currency: z.string().optional().describe("Defaults to usd"),
|
|
2816
|
+
}),
|
|
2817
|
+
response: z.object({
|
|
2818
|
+
url: z.string().optional(),
|
|
2819
|
+
linkId: z.string().optional(),
|
|
2820
|
+
payTo: z.object({
|
|
2821
|
+
// An OPEN string, never an enum. 'self' | 'via-treasury' | 'none' today;
|
|
2822
|
+
// wallet-authority's ScopedWallet adds 'scoped' (payee = an object id,
|
|
2823
|
+
// no chain), and that must not need a registry change to be
|
|
2824
|
+
// representable. An UNKNOWN value is refused BY NAME at payee resolution
|
|
2825
|
+
// (`unsupported_custody:<value>`, resolvers/wallet.ts) — never a crash,
|
|
2826
|
+
// never a silent fallback to 'self' or to the SUI rail.
|
|
2827
|
+
custody: z.string(),
|
|
2828
|
+
chain: z.string().optional(),
|
|
2829
|
+
address: z.string().optional(),
|
|
2830
|
+
attributeTo: z.string().optional(),
|
|
2831
|
+
}).optional(),
|
|
2832
|
+
error: z.string().optional(),
|
|
2833
|
+
}),
|
|
2834
|
+
effect: "ask", cost: "free", reversible: true, idempotent: true, settles: "none", auth: "manage_workspace",
|
|
2835
|
+
examples: [
|
|
2836
|
+
{ actor: "agent:scout", amountCents: 2500 },
|
|
2837
|
+
{ workspace: "acme", actor: "agent:scout", amountCents: 2500, currency: "usd" },
|
|
2838
|
+
],
|
|
2839
|
+
}),
|
|
2840
|
+
// wallet:portfolio — the authority walk projected onto wallet rows. One call
|
|
2841
|
+
// answers "what does my whole estate hold", which no client could ask before:
|
|
2842
|
+
// every wallet read was scoped to one workspace at a time.
|
|
2843
|
+
//
|
|
2844
|
+
// Deliberately NO chain balances. `wallet:get` fans out one RPC per address,
|
|
2845
|
+
// which is right for one workspace and quadratic across an estate. Portfolio
|
|
2846
|
+
// NAMES the estate; `wallet:get` prices one workspace in it.
|
|
2847
|
+
"wallet:portfolio": receiver({
|
|
2848
|
+
receiver: "wallet:portfolio",
|
|
2849
|
+
surfaces: { mcp: true },
|
|
2850
|
+
summary: "Every wallet row in every workspace the caller controls — one call, no per-address chain reads",
|
|
2851
|
+
request: z.object({
|
|
2852
|
+
limit: z.number().optional().describe("Max child workspaces to include; capped at 100"),
|
|
2853
|
+
}),
|
|
2854
|
+
response: z.object({
|
|
2855
|
+
caller: z.string().optional(),
|
|
2856
|
+
workspaces: z.array(z.object({
|
|
2857
|
+
workspace: z.string(),
|
|
2858
|
+
wallets: z.array(z.record(z.string(), z.unknown())),
|
|
2859
|
+
})).optional(),
|
|
2860
|
+
count: z.number().optional(),
|
|
2861
|
+
error: z.string().optional(),
|
|
2862
|
+
}),
|
|
2863
|
+
effect: "ask", cost: "free", reversible: true, idempotent: true, settles: "none", auth: "member",
|
|
2864
|
+
examples: [{}, { limit: 25 }],
|
|
2865
|
+
}),
|
|
2866
|
+
// The door that lets a workspace open its own shop. Until this existed, the
|
|
2867
|
+
// only writers of `owners.storefront_enabled` lived under the Stripe Connect
|
|
2868
|
+
// OAuth callbacks, so selling anything required a human with a browser and a
|
|
2869
|
+
// business — including on the crypto rail, which needs no Stripe account at
|
|
2870
|
+
// all (deriveRails gives a treasury-only seller the crypto rail already).
|
|
2871
|
+
// Authorized by the owners-tree walk, so a parent can open a client's shop.
|
|
2872
|
+
"storefront:enable": receiver({
|
|
2873
|
+
receiver: "storefront:enable",
|
|
2874
|
+
summary: "Open (or close) a workspace's storefront — the gate every checkout mint reads",
|
|
2875
|
+
request: z.object({ slug: z.string().optional(), enabled: z.boolean().optional() }),
|
|
2876
|
+
response: z.object({ ok: z.boolean().optional(), slug: z.string().optional(), enabled: z.boolean().optional(), error: z.string().optional() }),
|
|
2877
|
+
effect: "ask", cost: "free", reversible: true, idempotent: true, auth: "member",
|
|
2878
|
+
examples: [{ slug: "acme" }, { slug: "acme", enabled: false }],
|
|
2879
|
+
}),
|
|
1876
2880
|
"storefront:stats": receiver({
|
|
1877
2881
|
receiver: "storefront:stats",
|
|
1878
2882
|
summary: "Storefront funnel stats — views, checkouts, purchases, revenue, per-product breakdown",
|
|
@@ -1890,6 +2894,7 @@ export const RECEIVERS = {
|
|
|
1890
2894
|
// ── view:* — saved views (custom-views C1). A view is a workspace-scoped Thing (world_things type 'view'). ──
|
|
1891
2895
|
"view:create": receiver({
|
|
1892
2896
|
receiver: "view:create",
|
|
2897
|
+
surfaces: { mcp: { name: "create_view" } },
|
|
1893
2898
|
summary: "Save a named view — dimension × tags × kind × config — as a workspace Thing",
|
|
1894
2899
|
request: z.object({ slug: z.string(), name: z.string(), kind: z.string().optional(), query: z.object({ dimension: z.string().optional(), type: z.string().optional(), tags: z.array(z.string()).optional(), stage: z.string().optional() }), config: z.record(z.string(), z.unknown()).optional(), pinned: z.boolean().optional() }),
|
|
1895
2900
|
response: z.object({ ok: z.boolean(), id: z.string().optional(), name: z.string().optional(), error: z.string().optional() }),
|
|
@@ -1897,6 +2902,7 @@ export const RECEIVERS = {
|
|
|
1897
2902
|
}),
|
|
1898
2903
|
"view:list": receiver({
|
|
1899
2904
|
receiver: "view:list",
|
|
2905
|
+
surfaces: { mcp: { name: "list_views" } },
|
|
1900
2906
|
summary: "List a workspace's saved views (cold flag derived at read time)",
|
|
1901
2907
|
request: z.object({ slug: z.string() }),
|
|
1902
2908
|
response: z.object({ views: z.array(z.object({ id: z.string(), name: z.string(), kind: z.string().optional(), pinned: z.boolean(), cold: z.boolean() })) }),
|
|
@@ -1926,6 +2932,7 @@ export const RECEIVERS = {
|
|
|
1926
2932
|
// ── pages + workspace settings — handlers in resolvers/pages.ts ────────────────
|
|
1927
2933
|
"pages:create": receiver({
|
|
1928
2934
|
receiver: "pages:create",
|
|
2935
|
+
surfaces: { mcp: true },
|
|
1929
2936
|
summary: "Create a workspace page (draft) from a title + sections",
|
|
1930
2937
|
request: z.object({ slug: z.string(), title: z.string(), sections: z.array(z.object({ component: z.string(), props: z.record(z.string(), z.unknown()) })), pageSlug: z.string().optional() }),
|
|
1931
2938
|
response: z.object({ ok: z.boolean(), slug: z.string().optional(), title: z.string().optional(), url: z.string().optional(), status: z.string().optional(), error: z.string().optional() }),
|
|
@@ -1940,6 +2947,7 @@ export const RECEIVERS = {
|
|
|
1940
2947
|
}),
|
|
1941
2948
|
"pages:list": receiver({
|
|
1942
2949
|
receiver: "pages:list",
|
|
2950
|
+
surfaces: { mcp: true },
|
|
1943
2951
|
summary: "List a workspace's pages with status",
|
|
1944
2952
|
request: z.object({ slug: z.string() }),
|
|
1945
2953
|
response: z.object({ pages: z.array(z.object({ slug: z.string(), title: z.string(), status: z.string() })) }),
|
|
@@ -2050,10 +3058,24 @@ export const RECEIVERS = {
|
|
|
2050
3058
|
response: z.object({ ok: z.boolean(), slug: z.string().optional(), message: z.string().optional(), error: z.string().optional() }),
|
|
2051
3059
|
effect: "ask", cost: "free", reversible: true, idempotent: false, auth: "member",
|
|
2052
3060
|
}),
|
|
3061
|
+
"settings:set-home-page": receiver({
|
|
3062
|
+
receiver: "settings:set-home-page",
|
|
3063
|
+
summary: "Name which published page a workspace serves at its root",
|
|
3064
|
+
request: z.object({ slug: z.string(), page: z.string() }),
|
|
3065
|
+
response: z.object({ ok: z.boolean(), page: z.string().optional(), error: z.string().optional() }),
|
|
3066
|
+
effect: "ask", cost: "free", reversible: true, idempotent: true, auth: "member",
|
|
3067
|
+
}),
|
|
2053
3068
|
"domain:set": receiver({
|
|
2054
3069
|
receiver: "domain:set",
|
|
2055
3070
|
summary: "Bind a custom domain to a workspace — returns the verification TXT/CNAME records to add",
|
|
2056
|
-
request: z.object({
|
|
3071
|
+
request: z.object({
|
|
3072
|
+
slug: z.string(),
|
|
3073
|
+
// DERIVED, never sent: "the body used to carry `actorId`" — it was removed
|
|
3074
|
+
// by the IDOR fix in resolvers/pages.ts, which authorizes from the ATTESTED
|
|
3075
|
+
// caller only. Requiring it here demanded a field the fix stopped trusting.
|
|
3076
|
+
actorId: z.string().optional(),
|
|
3077
|
+
host: z.string(),
|
|
3078
|
+
}),
|
|
2057
3079
|
response: z.object({ ok: z.boolean(), intent: z.string().optional(), host: z.string().optional(), verifyToken: z.string().optional(), txtRecord: z.string().optional(), cnameTarget: z.string().optional(), reason: z.string().optional() }),
|
|
2058
3080
|
effect: "ask", cost: "free", reversible: true, idempotent: false, auth: "verify_domain",
|
|
2059
3081
|
}),
|
|
@@ -2114,6 +3136,159 @@ export const RECEIVERS = {
|
|
|
2114
3136
|
response: z.object({ ok: z.boolean(), skillRef: z.string().optional() }),
|
|
2115
3137
|
effect: "ask", cost: "free", reversible: true, idempotent: false, auth: "member",
|
|
2116
3138
|
}),
|
|
3139
|
+
// ── company — the COMPANY WORKFLOW's crawl steps, handlers in resolvers/company.ts ──
|
|
3140
|
+
//
|
|
3141
|
+
// The crawl ladder underneath these is plain fetch -> Cloudflare Browser Rendering
|
|
3142
|
+
// (only when the page is a JS shell). `via` names the rung that answered and
|
|
3143
|
+
// `degraded` names why a lower one did, so a caller is never told a partial read
|
|
3144
|
+
// was a full one. None of them writes; foundation:save is the single write.
|
|
3145
|
+
"company:render": receiver({
|
|
3146
|
+
receiver: "company:render",
|
|
3147
|
+
summary: "Read ONE page of a company's site and extract what it says about the company — plain fetch first, Cloudflare Browser Rendering only for a JS shell. Returns a Foundation patch plus provenance-fenced page text; never fabricates a field",
|
|
3148
|
+
request: z.object({ url: z.string(), waitFor: z.number().int().optional() }),
|
|
3149
|
+
response: z.object({
|
|
3150
|
+
ok: z.boolean(),
|
|
3151
|
+
url: z.string().optional(),
|
|
3152
|
+
finalUrl: z.string().optional(),
|
|
3153
|
+
domain: z.string().optional(),
|
|
3154
|
+
via: z.enum(["fetch", "cloudflare"]).optional(),
|
|
3155
|
+
degraded: z.string().optional(),
|
|
3156
|
+
patch: z.unknown().optional(),
|
|
3157
|
+
evidence: z.string().optional(),
|
|
3158
|
+
error: z.string().optional(),
|
|
3159
|
+
}),
|
|
3160
|
+
effect: "ask", cost: "free", reversible: true, idempotent: true, auth: "public",
|
|
3161
|
+
}),
|
|
3162
|
+
"company:map": receiver({
|
|
3163
|
+
receiver: "company:map",
|
|
3164
|
+
summary: "Discover a site's structure from its homepage anchors and classify them (pricing/about/product/blog/contact/legal). Depth 1 — this is NOT a crawl and starts no crawl job",
|
|
3165
|
+
request: z.object({ url: z.string(), limit: z.number().int().min(1).max(50).optional() }),
|
|
3166
|
+
response: z.object({
|
|
3167
|
+
ok: z.boolean(),
|
|
3168
|
+
url: z.string().optional(),
|
|
3169
|
+
via: z.enum(["fetch", "cloudflare"]).optional(),
|
|
3170
|
+
degraded: z.string().optional(),
|
|
3171
|
+
links: z.unknown().optional(),
|
|
3172
|
+
priority: z.array(z.string()).optional(),
|
|
3173
|
+
count: z.number().optional(),
|
|
3174
|
+
error: z.string().optional(),
|
|
3175
|
+
}),
|
|
3176
|
+
effect: "ask", cost: "free", reversible: true, idempotent: true, auth: "public",
|
|
3177
|
+
}),
|
|
3178
|
+
"company:seo": receiver({
|
|
3179
|
+
receiver: "company:seo",
|
|
3180
|
+
summary: "Read the SEO surface for a domain (keywords, backlinks, AI visibility) via DataForSEO and return it as a Foundation patch. SPENDS REAL CREDIT, so it skips itself with {ok:true,patch:{},skipped:'no_workspace'} when there is no attested workspace",
|
|
3181
|
+
request: z.object({ domain: z.string(), slug: z.string().optional() }),
|
|
3182
|
+
response: z.object({
|
|
3183
|
+
ok: z.boolean(),
|
|
3184
|
+
patch: z.unknown().optional(),
|
|
3185
|
+
domain: z.string().optional(),
|
|
3186
|
+
skipped: z.string().optional(),
|
|
3187
|
+
error: z.string().optional(),
|
|
3188
|
+
}),
|
|
3189
|
+
effect: "ask", cost: "variable", reversible: true, idempotent: false, auth: "member",
|
|
3190
|
+
}),
|
|
3191
|
+
// ── foundation — handlers in resolvers/foundation.ts ──────────────────────────
|
|
3192
|
+
"foundation:deep": receiver({
|
|
3193
|
+
receiver: "foundation:deep",
|
|
3194
|
+
summary: "Run the 10-skill FOUNDATION sequence — each elevate-foundation-* SKILL.md body is loaded from the workspace's skill catalog and executed, and every field it produces is merged at inferred/scrape provenance so it can never outrank a human-confirmed one",
|
|
3195
|
+
request: z.object({
|
|
3196
|
+
slug: z.string().optional(),
|
|
3197
|
+
source: z.enum(["inferred", "scrape"]).optional(),
|
|
3198
|
+
}),
|
|
3199
|
+
response: z.object({
|
|
3200
|
+
ok: z.boolean(),
|
|
3201
|
+
slug: z.string().optional(),
|
|
3202
|
+
error: z.string().optional(),
|
|
3203
|
+
skillsRun: z.number().optional(),
|
|
3204
|
+
// EMITTED, not written: a field already at chat/document rank is emitted and
|
|
3205
|
+
// then refused by the merge guard. Naming it "written" would overstate it.
|
|
3206
|
+
fieldsEmitted: z.number().optional(),
|
|
3207
|
+
// Per-skill receipt: what each skill actually did. `outcome` distinguishes
|
|
3208
|
+
// "emitted N fields" from "returned nothing" from "the answer did not parse".
|
|
3209
|
+
skills: z.array(z.object({
|
|
3210
|
+
skill: z.string(),
|
|
3211
|
+
outcome: z.enum(["ok", "empty", "parse_failed", "llm_error"]),
|
|
3212
|
+
bodyFound: z.boolean(),
|
|
3213
|
+
fieldsEmitted: z.number(),
|
|
3214
|
+
fields: z.array(z.string()).optional(),
|
|
3215
|
+
dropped: z.array(z.string()).optional(),
|
|
3216
|
+
status: z.number().optional(),
|
|
3217
|
+
})).optional(),
|
|
3218
|
+
}),
|
|
3219
|
+
effect: "ask", cost: "variable", reversible: false, idempotent: false, auth: "member",
|
|
3220
|
+
}),
|
|
3221
|
+
"foundation:start": receiver({
|
|
3222
|
+
receiver: "foundation:start",
|
|
3223
|
+
summary: "Fill the Foundation's spine fields with one bounded LLM call over the workspace's own context. Refuses to overwrite a human-confirmed field; writes at chat/0.75",
|
|
3224
|
+
request: z.object({ slug: z.string().optional() }),
|
|
3225
|
+
response: z.object({ ok: z.boolean(), slug: z.string().optional(), fields: z.number().optional(), error: z.string().optional() }),
|
|
3226
|
+
effect: "ask", cost: "variable", reversible: false, idempotent: false, auth: "member",
|
|
3227
|
+
}),
|
|
3228
|
+
"foundation:read": receiver({
|
|
3229
|
+
receiver: "foundation:read",
|
|
3230
|
+
summary: "Read the workspace's Foundation as stored (null when nothing has been written yet)",
|
|
3231
|
+
request: z.object({ slug: z.string().optional() }),
|
|
3232
|
+
response: z.object({ ok: z.boolean(), slug: z.string().optional(), foundation: z.unknown().optional(), error: z.string().optional() }),
|
|
3233
|
+
effect: "ask", cost: "free", reversible: true, idempotent: true, auth: "member",
|
|
3234
|
+
}),
|
|
3235
|
+
"foundation:derive": receiver({
|
|
3236
|
+
receiver: "foundation:derive",
|
|
3237
|
+
summary: "Turn the stored Foundation into DRAFT products and DRAFT ELEVATE playbook chapters. Insert-only (never overwrites a human edit), no price is ever guessed, and confidence is inherited from the weakest source fact",
|
|
3238
|
+
request: z.object({}),
|
|
3239
|
+
response: z.object({
|
|
3240
|
+
ok: z.boolean(),
|
|
3241
|
+
slug: z.string().optional(),
|
|
3242
|
+
products: z.unknown().optional(),
|
|
3243
|
+
playbook: z.unknown().optional(),
|
|
3244
|
+
error: z.string().optional(),
|
|
3245
|
+
hint: z.string().optional(),
|
|
3246
|
+
}),
|
|
3247
|
+
effect: "ask", cost: "free", reversible: false, idempotent: false, auth: "member",
|
|
3248
|
+
}),
|
|
3249
|
+
"foundation:extract": receiver({
|
|
3250
|
+
receiver: "foundation:extract",
|
|
3251
|
+
summary: "Fold every rendered page into ONE provenance-tracked Foundation patch, and report the spine paths still unanswered. Pure — no network, no LLM, no cost, no write. Its fencedEvidence is already wrapped by the provenance fence, so a downstream prompt cannot interpolate crawled text unfenced",
|
|
3252
|
+
request: z.object({ home: z.unknown().optional(), pages: z.unknown().optional(), siteMap: z.unknown().optional() }),
|
|
3253
|
+
response: z.object({
|
|
3254
|
+
ok: z.boolean(),
|
|
3255
|
+
patch: z.unknown().optional(),
|
|
3256
|
+
found: z.unknown().optional(),
|
|
3257
|
+
gaps: z.array(z.string()).optional(),
|
|
3258
|
+
pagesRead: z.number().optional(),
|
|
3259
|
+
pagesFailed: z.number().optional(),
|
|
3260
|
+
degraded: z.string().optional(),
|
|
3261
|
+
fencedEvidence: z.string().optional(),
|
|
3262
|
+
}),
|
|
3263
|
+
effect: "ask", cost: "free", reversible: true, idempotent: true, auth: "public",
|
|
3264
|
+
}),
|
|
3265
|
+
"foundation:save": receiver({
|
|
3266
|
+
receiver: "foundation:save",
|
|
3267
|
+
summary: "The single write of the company workflow. With an attested workspace it merges into workspace_settings.foundation provenance-aware; without one it holds the profile as an opaque 1h KV draft to be promoted at signup — an anonymous caller never writes a workspace",
|
|
3268
|
+
request: z.object({
|
|
3269
|
+
patch: z.unknown().optional(),
|
|
3270
|
+
fill: z.unknown().optional(),
|
|
3271
|
+
seo: z.unknown().optional(),
|
|
3272
|
+
draftToken: z.string().optional(),
|
|
3273
|
+
}),
|
|
3274
|
+
response: z.object({
|
|
3275
|
+
ok: z.boolean(),
|
|
3276
|
+
stored: z.enum(["workspace", "draft", "none"]).optional(),
|
|
3277
|
+
slug: z.string().optional(),
|
|
3278
|
+
draftToken: z.string().optional(),
|
|
3279
|
+
fields: z.number().optional(),
|
|
3280
|
+
gaps: z.array(z.string()).optional(),
|
|
3281
|
+
error: z.string().optional(),
|
|
3282
|
+
}),
|
|
3283
|
+
effect: "ask", cost: "free", reversible: false, idempotent: false, auth: "public",
|
|
3284
|
+
}),
|
|
3285
|
+
"foundation:promote-draft": receiver({
|
|
3286
|
+
receiver: "foundation:promote-draft",
|
|
3287
|
+
summary: "Carry an anonymous onboarding draft into the caller's workspace at signup and delete the KV key. Provenance survives, so a scraped field still loses to the human's later edit",
|
|
3288
|
+
request: z.object({ draftToken: z.string() }),
|
|
3289
|
+
response: z.object({ ok: z.boolean(), slug: z.string().optional(), fields: z.number().optional(), gaps: z.array(z.string()).optional(), error: z.string().optional() }),
|
|
3290
|
+
effect: "ask", cost: "free", reversible: false, idempotent: true, auth: "member",
|
|
3291
|
+
}),
|
|
2117
3292
|
// ── social + crawl — handlers in resolvers/social.ts ───────────────────────────
|
|
2118
3293
|
"social:publish": receiver({
|
|
2119
3294
|
receiver: "social:publish",
|
|
@@ -2175,7 +3350,7 @@ export const RECEIVERS = {
|
|
|
2175
3350
|
}),
|
|
2176
3351
|
"billing:topup": receiver({
|
|
2177
3352
|
receiver: "billing:topup",
|
|
2178
|
-
summary: "Top up workspace AI credits — returns a checkout URL for the amount",
|
|
3353
|
+
summary: "Top up workspace AI credits — returns a checkout URL for the amount. Credits are prepaid, NON-WITHDRAWABLE service units: they buy work on ONE (AI replies, tool and skill calls, agent runs, storage, API requests) and there is no payout, withdraw or cash-out rail against a credits balance",
|
|
2179
3354
|
request: z.object({ slug: z.string(), actorId: z.string(), amount: z.number(), currency: z.string().optional() }),
|
|
2180
3355
|
response: z.object({ ok: z.boolean(), intent: z.string().optional(), checkoutUrl: z.string().optional(), amount: z.number().optional(), currency: z.string().optional(), reason: z.string().optional() }),
|
|
2181
3356
|
effect: "ask", cost: "free", reversible: false, idempotent: false, auth: "update_group",
|
|
@@ -2326,7 +3501,10 @@ export const RECEIVERS = {
|
|
|
2326
3501
|
receiver: "booking:list-bookings",
|
|
2327
3502
|
summary: "Provider-gated: list a workspace's bookings for a month (customer, time, service, status)",
|
|
2328
3503
|
request: z.object({
|
|
2329
|
-
|
|
3504
|
+
// Optional: the comment below is the point — the handler does
|
|
3505
|
+
// `const slug = ctx.ownerSlug` and never reads data.slug. Required, the
|
|
3506
|
+
// binder rejected every call that correctly omitted it.
|
|
3507
|
+
slug: z.string().optional(), // workspace (informational; gate re-scopes to caller)
|
|
2330
3508
|
month: z.string().optional(), // 'YYYY-MM'; omitted → current month
|
|
2331
3509
|
}),
|
|
2332
3510
|
response: z.object({
|
|
@@ -2391,6 +3569,7 @@ export const RECEIVERS = {
|
|
|
2391
3569
|
// and commits nothing — the chat-authoring preview path.
|
|
2392
3570
|
"workflow:list": receiver({
|
|
2393
3571
|
receiver: "workflow:list",
|
|
3572
|
+
surfaces: { mcp: true },
|
|
2394
3573
|
summary: "List a workspace's workflows (D1 mirror); pass templates=true for the public SOP catalog",
|
|
2395
3574
|
request: z.object({
|
|
2396
3575
|
slug: z.string().optional(),
|
|
@@ -2408,6 +3587,7 @@ export const RECEIVERS = {
|
|
|
2408
3587
|
}),
|
|
2409
3588
|
"workflow:get": receiver({
|
|
2410
3589
|
receiver: "workflow:get",
|
|
3590
|
+
surfaces: { mcp: true },
|
|
2411
3591
|
summary: "Fetch one workflow's full graph — steps (kind, config, position) and edges (with conditions)",
|
|
2412
3592
|
request: z.object({ workflowId: z.string() }),
|
|
2413
3593
|
response: z.object({
|
|
@@ -2428,6 +3608,7 @@ export const RECEIVERS = {
|
|
|
2428
3608
|
}),
|
|
2429
3609
|
"workflow:runs": receiver({
|
|
2430
3610
|
receiver: "workflow:runs",
|
|
3611
|
+
surfaces: { mcp: true },
|
|
2431
3612
|
summary: "List recent runs of a workflow (D1 workflow_run) for the monitor + history surfaces",
|
|
2432
3613
|
request: z.object({ workflowId: z.string(), runId: z.string().optional(), limit: z.number().int().min(1).max(200).optional() }),
|
|
2433
3614
|
response: z.object({
|
|
@@ -2444,6 +3625,25 @@ export const RECEIVERS = {
|
|
|
2444
3625
|
effect: "ask", cost: "free", idempotent: true, simulatable: false, auth: "view_workflows",
|
|
2445
3626
|
examples: [{ workflowId: "wf_abc", limit: 20 }, { workflowId: "wf_abc", runId: "run_1" }],
|
|
2446
3627
|
}),
|
|
3628
|
+
"workflow:step-stats": receiver({
|
|
3629
|
+
receiver: "workflow:step-stats",
|
|
3630
|
+
surfaces: { mcp: true },
|
|
3631
|
+
summary: "Per-step run counts and latency for one workflow (aggregated over D1 workflow_run_event) — what the canvas prints on each step card",
|
|
3632
|
+
request: z.object({ workflowId: z.string(), sinceMs: z.number().int().optional() }),
|
|
3633
|
+
response: z.object({
|
|
3634
|
+
stats: z.array(z.object({
|
|
3635
|
+
stepId: z.string(),
|
|
3636
|
+
runs: z.number().int(),
|
|
3637
|
+
failures: z.number().int(),
|
|
3638
|
+
avgMs: z.number().nullable(),
|
|
3639
|
+
maxMs: z.number().nullable(),
|
|
3640
|
+
lastAt: z.number().nullable(),
|
|
3641
|
+
})),
|
|
3642
|
+
error: z.string().optional(),
|
|
3643
|
+
}),
|
|
3644
|
+
effect: "ask", cost: "free", idempotent: true, simulatable: false, auth: "view_workflows",
|
|
3645
|
+
examples: [{ workflowId: "wf_abc" }],
|
|
3646
|
+
}),
|
|
2447
3647
|
"workflow:preview-step": receiver({
|
|
2448
3648
|
receiver: "workflow:preview-step",
|
|
2449
3649
|
summary: "Resolve one step's receiver + args through the real substitution code, with an optional mock trigger/step payload standing in for run history — never dispatches, reads no run history",
|
|
@@ -2463,6 +3663,7 @@ export const RECEIVERS = {
|
|
|
2463
3663
|
}),
|
|
2464
3664
|
"workflow:create": receiver({
|
|
2465
3665
|
receiver: "workflow:create",
|
|
3666
|
+
surfaces: { mcp: true },
|
|
2466
3667
|
summary: "Create a blank workflow (group group-type=workflow) — optionally cloned from a template",
|
|
2467
3668
|
request: z.object({
|
|
2468
3669
|
slug: z.string().optional(),
|
|
@@ -2495,6 +3696,7 @@ export const RECEIVERS = {
|
|
|
2495
3696
|
}),
|
|
2496
3697
|
"workflow:apply-diff": receiver({
|
|
2497
3698
|
receiver: "workflow:apply-diff",
|
|
3699
|
+
surfaces: { mcp: true },
|
|
2498
3700
|
summary: "Apply a WorkflowDiff (add/remove/connect/disconnect/update). simulate=true validates the DAG without persisting",
|
|
2499
3701
|
request: z.object({
|
|
2500
3702
|
workflowId: z.string(),
|
|
@@ -2515,6 +3717,7 @@ export const RECEIVERS = {
|
|
|
2515
3717
|
}),
|
|
2516
3718
|
"workflow:validate": receiver({
|
|
2517
3719
|
receiver: "workflow:validate",
|
|
3720
|
+
surfaces: { mcp: true },
|
|
2518
3721
|
summary: "Run the §9 gate on a WorkflowDiff without persisting — kinds/config shapes, exactly-one-trigger, edges reference real steps, chain cycles; advisories ride back as warnings (grammar: code | code:stepId, never block). Every save door calls this.",
|
|
2519
3722
|
request: z.object({
|
|
2520
3723
|
workflowId: z.string(),
|
|
@@ -2532,6 +3735,7 @@ export const RECEIVERS = {
|
|
|
2532
3735
|
}),
|
|
2533
3736
|
"workflow:run": receiver({
|
|
2534
3737
|
receiver: "workflow:run",
|
|
3738
|
+
surfaces: { mcp: true },
|
|
2535
3739
|
summary: "Start a run — spawns the WorkflowRun DO, executes each step, marks path strength on traversed edges",
|
|
2536
3740
|
request: z.object({
|
|
2537
3741
|
workflowId: z.string(),
|
|
@@ -2648,6 +3852,7 @@ export const RECEIVERS = {
|
|
|
2648
3852
|
}),
|
|
2649
3853
|
"workflow:stop": receiver({
|
|
2650
3854
|
receiver: "workflow:stop",
|
|
3855
|
+
surfaces: { mcp: true },
|
|
2651
3856
|
summary: "Park a live run at its current step (status → paused); preserves the resume cursor so human:resolve or a re-run continues it",
|
|
2652
3857
|
request: z.object({ runId: z.string() }),
|
|
2653
3858
|
response: z.object({ ok: z.boolean(), status: z.string().optional(), error: z.string().optional() }),
|
|
@@ -2656,6 +3861,7 @@ export const RECEIVERS = {
|
|
|
2656
3861
|
}),
|
|
2657
3862
|
"workflow:trigger": receiver({
|
|
2658
3863
|
receiver: "workflow:trigger",
|
|
3864
|
+
surfaces: { mcp: true },
|
|
2659
3865
|
summary: "Fire every workspace workflow whose trigger step matches a channel source (e.g. webhook:telegram)",
|
|
2660
3866
|
request: z.object({
|
|
2661
3867
|
source: z.string(),
|
|
@@ -2676,6 +3882,7 @@ export const RECEIVERS = {
|
|
|
2676
3882
|
}),
|
|
2677
3883
|
"workflow:update": receiver({
|
|
2678
3884
|
receiver: "workflow:update",
|
|
3885
|
+
surfaces: { mcp: true },
|
|
2679
3886
|
summary: "Rename a workflow or change its status (draft|active|paused)",
|
|
2680
3887
|
request: z.object({ workflowId: z.string(), name: z.string().optional(), status: z.enum(["draft", "active", "paused"]).optional() }),
|
|
2681
3888
|
response: z.object({ ok: z.boolean(), error: z.string().optional() }),
|
|
@@ -2690,11 +3897,44 @@ export const RECEIVERS = {
|
|
|
2690
3897
|
effect: "ask", cost: "free", reversible: false, idempotent: false, auth: "manage_workflows",
|
|
2691
3898
|
examples: [{ workflowId: "wf_abc" }, { rate: 0.1 }, {}],
|
|
2692
3899
|
}),
|
|
3900
|
+
// ── The .tql text form of a workflow (workflows-tql). Export/import as a
|
|
3901
|
+
// single-insert TypeQL document — the graph a canvas draws, as text an
|
|
3902
|
+
// agent can read, diff and hand back. apply-tql never writes D1 itself: it
|
|
3903
|
+
// parses, gates the whole document as an all-add diff, reconciles against
|
|
3904
|
+
// what is persisted, and goes through workflow:apply-diff — the one door.
|
|
3905
|
+
"workflow:tql": receiver({
|
|
3906
|
+
receiver: "workflow:tql",
|
|
3907
|
+
surfaces: { mcp: true },
|
|
3908
|
+
summary: "Emit one workflow as its .tql text form — a single-insert TypeQL document (group header + step things + path edges) that round-trips back through workflow:apply-tql",
|
|
3909
|
+
request: z.object({ workflowId: z.string() }),
|
|
3910
|
+
response: z.object({ ok: z.boolean(), tql: z.string().optional(), version: z.number().optional(), error: z.string().optional() }),
|
|
3911
|
+
effect: "ask", cost: "free", idempotent: true, simulatable: false, auth: "view_workflows",
|
|
3912
|
+
examples: [{ workflowId: "wf_abc" }],
|
|
3913
|
+
}),
|
|
3914
|
+
"workflow:apply-tql": receiver({
|
|
3915
|
+
receiver: "workflow:apply-tql",
|
|
3916
|
+
surfaces: { mcp: true },
|
|
3917
|
+
summary: "Parse a .tql workflow document and apply it to an existing workflow THROUGH workflow:apply-diff — the sole validated persist door, so the import inherits the DAG check, the one-trigger law, the config gate and the authority walk. simulate:true validates and persists nothing",
|
|
3918
|
+
request: z.object({ workflowId: z.string(), tql: z.string(), simulate: z.boolean().optional(), version: z.number().optional() }),
|
|
3919
|
+
response: z.object({
|
|
3920
|
+
ok: z.boolean(),
|
|
3921
|
+
stepCount: z.number().optional(),
|
|
3922
|
+
idMap: z.record(z.string(), z.string()).optional(),
|
|
3923
|
+
warnings: z.array(z.string()).optional(),
|
|
3924
|
+
notes: z.array(z.string()).optional(),
|
|
3925
|
+
error: z.string().optional(),
|
|
3926
|
+
line: z.number().optional(),
|
|
3927
|
+
detail: z.string().optional(),
|
|
3928
|
+
}),
|
|
3929
|
+
effect: "ask", cost: "free", reversible: false, idempotent: false, simulatable: true, auth: "manage_workflows",
|
|
3930
|
+
examples: [{ workflowId: "wf_abc", tql: "insert\n$w isa group, has gid \"group:wf-wf_abc\", has name \"Demo\", has group-type \"team\";\n", simulate: true }],
|
|
3931
|
+
}),
|
|
2693
3932
|
// ── The autonomy ladder's leaf runners — a workflow skill/agent step compiles
|
|
2694
3933
|
// to one of these (workflow-executor.ts stepBinding). Both proxy to the
|
|
2695
3934
|
// channels runtime; web owns the catalog + tenant authority (workflows-lock C2/C3).
|
|
2696
3935
|
"skills:list": receiver({
|
|
2697
3936
|
receiver: "skills:list",
|
|
3937
|
+
surfaces: { mcp: true },
|
|
2698
3938
|
summary: "List the caller's workspace skill catalog (R2) — powers the canvas SkillPicker",
|
|
2699
3939
|
request: z.object({ name: z.string().optional() }),
|
|
2700
3940
|
response: z.object({
|
|
@@ -2713,6 +3953,7 @@ export const RECEIVERS = {
|
|
|
2713
3953
|
}),
|
|
2714
3954
|
"skill:run": receiver({
|
|
2715
3955
|
receiver: "skill:run",
|
|
3956
|
+
surfaces: { mcp: true },
|
|
2716
3957
|
summary: "Run a workspace skill — loads its body from R2, runs one bounded turn via channels, returns { text }",
|
|
2717
3958
|
request: z.object({ skill: z.string() }).catchall(z.unknown()), // extra keys = the skill's input
|
|
2718
3959
|
response: z.object({ ok: z.boolean(), text: z.string().optional(), skill: z.string().optional(), error: z.string().optional() }),
|
|
@@ -2721,9 +3962,12 @@ export const RECEIVERS = {
|
|
|
2721
3962
|
}),
|
|
2722
3963
|
"agent:run": receiver({
|
|
2723
3964
|
receiver: "agent:run",
|
|
3965
|
+
surfaces: { mcp: true },
|
|
2724
3966
|
summary: "Invoke a bound actor (optionally skill-constrained) for one bounded turn via channels; returns { text }",
|
|
2725
3967
|
request: z.object({
|
|
2726
|
-
|
|
3968
|
+
// Optional by design: an omitted actorId resolves to the workspace CEO
|
|
3969
|
+
// (C9). Required here, the binder rejected the very call that default exists to serve.
|
|
3970
|
+
actorId: z.string().optional(),
|
|
2727
3971
|
skill: z.string().optional(),
|
|
2728
3972
|
instructions: z.string().optional(),
|
|
2729
3973
|
}).catchall(z.unknown()), // extra keys = the agent's input
|
|
@@ -2759,10 +4003,11 @@ export const RECEIVERS = {
|
|
|
2759
4003
|
examples: [{ workspace: "acme", roomSlug: "algebra-101", name: "Algebra 101", type: "classroom" }],
|
|
2760
4004
|
// Fan-out pilot (actions-fan-out C2): offered to chat via chatToolsFor — no
|
|
2761
4005
|
// curated workspace tool needed; the registry derivation is the whole tool.
|
|
2762
|
-
surfaces: { chat: true },
|
|
4006
|
+
surfaces: { chat: true, mcp: { name: "create_room" } },
|
|
2763
4007
|
}),
|
|
2764
4008
|
"video:delete-room": receiver({
|
|
2765
4009
|
receiver: "video:delete-room",
|
|
4010
|
+
surfaces: { mcp: { name: "delete_room" } },
|
|
2766
4011
|
summary: "Disable a video room and its 100ms counterpart; room is no longer joinable",
|
|
2767
4012
|
request: z.object({ workspace: z.string(), roomSlug: z.string() }),
|
|
2768
4013
|
response: z.object({ ok: z.boolean(), error: z.string().optional() }),
|
|
@@ -2771,6 +4016,7 @@ export const RECEIVERS = {
|
|
|
2771
4016
|
}),
|
|
2772
4017
|
"video:contact-call": receiver({
|
|
2773
4018
|
receiver: "video:contact-call",
|
|
4019
|
+
surfaces: { mcp: { name: "contact_call" } },
|
|
2774
4020
|
summary: "Provision a one-to-one video room for a contact; links the room thread to the contact CRM record",
|
|
2775
4021
|
request: z.object({
|
|
2776
4022
|
workspace: z.string(),
|
|
@@ -2790,6 +4036,7 @@ export const RECEIVERS = {
|
|
|
2790
4036
|
}),
|
|
2791
4037
|
"video:invite": receiver({
|
|
2792
4038
|
receiver: "video:invite",
|
|
4039
|
+
surfaces: { mcp: { name: "invite_to_call" } },
|
|
2793
4040
|
summary: "Generate a tracked /go/ join link for an actor into an existing room",
|
|
2794
4041
|
request: z.object({
|
|
2795
4042
|
workspace: z.string(),
|
|
@@ -2822,10 +4069,11 @@ export const RECEIVERS = {
|
|
|
2822
4069
|
}),
|
|
2823
4070
|
effect: "ask", cost: "free", idempotent: false, auth: "manage_clients",
|
|
2824
4071
|
examples: [{ workspace: "acme", name: "Product Launch", actorIds: ["actor_1", "actor_2"] }],
|
|
2825
|
-
surfaces: { chat: true }, // fan-out:
|
|
4072
|
+
surfaces: { chat: true, mcp: { name: "schedule_webinar" } }, // fan-out: chat (actions-fan-out C5) + MCP (agent-native C1)
|
|
2826
4073
|
}),
|
|
2827
4074
|
"video:create-session": receiver({
|
|
2828
4075
|
receiver: "video:create-session",
|
|
4076
|
+
surfaces: { mcp: { name: "create_session" } },
|
|
2829
4077
|
summary: "Create a video session with host + guest tracked join links; optionally schedules the call",
|
|
2830
4078
|
request: z.object({
|
|
2831
4079
|
workspace: z.string(),
|
|
@@ -2861,7 +4109,7 @@ export const RECEIVERS = {
|
|
|
2861
4109
|
}),
|
|
2862
4110
|
effect: "ask", cost: "variable", idempotent: false, auth: "manage_clients",
|
|
2863
4111
|
examples: [{ threadId: "thr_abc", transcript: "Host: Hello... Guest: Hi..." }],
|
|
2864
|
-
surfaces: { chat: true }, // fan-out:
|
|
4112
|
+
surfaces: { chat: true, mcp: { name: "video_summary" } }, // fan-out: chat (actions-fan-out C5) + MCP (agent-native C1)
|
|
2865
4113
|
}),
|
|
2866
4114
|
"video:quick-room": receiver({
|
|
2867
4115
|
receiver: "video:quick-room",
|
|
@@ -2882,7 +4130,43 @@ export const RECEIVERS = {
|
|
|
2882
4130
|
effect: "ask", cost: "free", idempotent: false, auth: "manage_clients",
|
|
2883
4131
|
examples: [{ workspace: "acme", threadId: "thr_xyz", name: "Quick sync" }],
|
|
2884
4132
|
// Fan-out pilot (actions-fan-out C2).
|
|
2885
|
-
surfaces: { chat: true },
|
|
4133
|
+
surfaces: { chat: true, mcp: { name: "quick_call" } },
|
|
4134
|
+
}),
|
|
4135
|
+
// The human-handoff door. A visitor in chat asks for a person; the agent fires
|
|
4136
|
+
// this ONE receiver, which creates (or reuses) the meeting room, drops the
|
|
4137
|
+
// reason into the room's thread, and notifies the workspace wherever it
|
|
4138
|
+
// listens — Telegram/Discord/web inbox/web push, all through `notify`. It is
|
|
4139
|
+
// idempotent per CONVERSATION: the same chat asking twice joins the same room
|
|
4140
|
+
// instead of provisioning a second one.
|
|
4141
|
+
"video:request-meeting": receiver({
|
|
4142
|
+
receiver: "video:request-meeting",
|
|
4143
|
+
summary: "Visitor asks to meet a human: creates/reuses a meeting room for this conversation and notifies the workspace",
|
|
4144
|
+
request: z.object({
|
|
4145
|
+
workspace: z.string(),
|
|
4146
|
+
conversation: z.string().optional(), // chat group/thread id — the idempotency key
|
|
4147
|
+
roomSlug: z.string().optional(), // an EXISTING room to send them to (the standing front desk); never creates
|
|
4148
|
+
visitorName: z.string().optional(),
|
|
4149
|
+
reason: z.string().optional(), // what they want to talk about
|
|
4150
|
+
channel: z.string().optional(), // web | telegram | discord | api
|
|
4151
|
+
}),
|
|
4152
|
+
response: z.object({
|
|
4153
|
+
ok: z.boolean(),
|
|
4154
|
+
roomSlug: z.string().optional(),
|
|
4155
|
+
workspace: z.string().optional(),
|
|
4156
|
+
name: z.string().optional(),
|
|
4157
|
+
threadId: z.string().nullable().optional(),
|
|
4158
|
+
path: z.string().optional(), // /u/<workspace>/meet/<roomSlug>
|
|
4159
|
+
reused: z.boolean().optional(), // an existing room for this conversation
|
|
4160
|
+
notified: z.boolean().optional(),
|
|
4161
|
+
error: z.string().optional(), // 'invalid_request' | 'forbidden' | 'rate_limited' | …
|
|
4162
|
+
}),
|
|
4163
|
+
effect: "ask", cost: "free", idempotent: true, auth: "manage_clients",
|
|
4164
|
+
examples: [{ workspace: "acme", conversation: "conv:abc123", visitorName: "Sarah", reason: "pricing for 20 seats" }],
|
|
4165
|
+
// Deliberately NOT `surfaces: { chat: true }`. The registry-derived tool
|
|
4166
|
+
// (from-registry.ts) would be a second, unshaped door onto the same
|
|
4167
|
+
// receiver — no `public` flag (so no visitor could reach it), no room block
|
|
4168
|
+
// on the way back, and a name the model would pick by accident. The curated
|
|
4169
|
+
// `meet_human` (channels/src/tools/meetings.ts) is the chat surface.
|
|
2886
4170
|
}),
|
|
2887
4171
|
"video:join-event": receiver({
|
|
2888
4172
|
receiver: "video:join-event",
|
|
@@ -2908,7 +4192,29 @@ export const RECEIVERS = {
|
|
|
2908
4192
|
}),
|
|
2909
4193
|
effect: "ask", cost: "free", idempotent: false, auth: "manage_clients",
|
|
2910
4194
|
examples: [{ workspace: "acme", roomSlug: "algebra-101" }],
|
|
2911
|
-
surfaces: { chat: true }, // fan-out:
|
|
4195
|
+
surfaces: { chat: true, mcp: { name: "start_recording" } }, // fan-out: chat (actions-fan-out C5) + MCP (agent-native C1)
|
|
4196
|
+
}),
|
|
4197
|
+
// video:room-status — the presence read, one lib function behind two doors.
|
|
4198
|
+
// NOT flagged surfaces.mcp on purpose: packages/mcp still hand-writes
|
|
4199
|
+
// `get_room_status` over this receiver, and flagging it here would derive a
|
|
4200
|
+
// second tool of the same name. Flag it in the same change that deletes that file.
|
|
4201
|
+
"video:room-status": receiver({
|
|
4202
|
+
receiver: "video:room-status",
|
|
4203
|
+
summary: "Who is in a room right now — staff names, a count of everyone else, and when the first staff peer joined",
|
|
4204
|
+
request: z.object({ workspace: z.string(), roomSlug: z.string() }),
|
|
4205
|
+
response: z.object({
|
|
4206
|
+
ok: z.boolean(),
|
|
4207
|
+
// `unknown` is a first-class answer: an upstream that could not be read is
|
|
4208
|
+
// never reported as an empty room.
|
|
4209
|
+
state: z.enum(["live", "empty", "unknown"]).optional(),
|
|
4210
|
+
staff: z.array(z.string()).optional(),
|
|
4211
|
+
others: z.number().optional(),
|
|
4212
|
+
since: z.string().nullable().optional(),
|
|
4213
|
+
reason: z.string().optional(),
|
|
4214
|
+
error: z.string().optional(),
|
|
4215
|
+
}),
|
|
4216
|
+
effect: "ask", cost: "free", idempotent: true, auth: "manage_clients",
|
|
4217
|
+
examples: [{ workspace: "acme", roomSlug: "meeting" }],
|
|
2912
4218
|
}),
|
|
2913
4219
|
"video:start-stream": receiver({
|
|
2914
4220
|
receiver: "video:start-stream",
|
|
@@ -2925,7 +4231,7 @@ export const RECEIVERS = {
|
|
|
2925
4231
|
}),
|
|
2926
4232
|
effect: "ask", cost: "free", idempotent: false, auth: "manage_clients",
|
|
2927
4233
|
examples: [{ workspace: "acme", roomSlug: "product-launch" }],
|
|
2928
|
-
surfaces: { chat: true }, // fan-out:
|
|
4234
|
+
surfaces: { chat: true, mcp: { name: "start_stream" } }, // fan-out: chat (actions-fan-out C5) + MCP (agent-native C1)
|
|
2929
4235
|
}),
|
|
2930
4236
|
// connect:channel — token-paste connect for a channel (Connect plan C7). The
|
|
2931
4237
|
// typed contract over the shipped /api/connect/telegram route: the caller pastes
|
|
@@ -2978,6 +4284,7 @@ export const RECEIVERS = {
|
|
|
2978
4284
|
// ── Broadcast (newsletter) ────────────────────────────────────────────────
|
|
2979
4285
|
"broadcast:create": receiver({
|
|
2980
4286
|
receiver: "broadcast:create",
|
|
4287
|
+
surfaces: { mcp: true },
|
|
2981
4288
|
summary: "Create a broadcast draft — subject, body_md, audience_tag, channel",
|
|
2982
4289
|
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() }),
|
|
2983
4290
|
response: z.object({ broadcastId: z.string() }),
|
|
@@ -2985,6 +4292,7 @@ export const RECEIVERS = {
|
|
|
2985
4292
|
}),
|
|
2986
4293
|
"broadcast:update": receiver({
|
|
2987
4294
|
receiver: "broadcast:update",
|
|
4295
|
+
surfaces: { mcp: { name: "newsletter_update" } },
|
|
2988
4296
|
summary: "Update a broadcast draft — subject, body, audience, schedule",
|
|
2989
4297
|
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() }),
|
|
2990
4298
|
response: z.object({ ok: z.boolean() }),
|
|
@@ -2992,6 +4300,7 @@ export const RECEIVERS = {
|
|
|
2992
4300
|
}),
|
|
2993
4301
|
"broadcast:list": receiver({
|
|
2994
4302
|
receiver: "broadcast:list",
|
|
4303
|
+
surfaces: { mcp: true },
|
|
2995
4304
|
summary: "List broadcasts for a workspace with status and sent_at",
|
|
2996
4305
|
request: z.object({ workspace: z.string(), status: z.enum(["draft", "scheduled", "sending", "sent", "cancelled"]).optional(), limit: z.number().int().max(100).optional() }),
|
|
2997
4306
|
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() })) }),
|
|
@@ -2999,6 +4308,7 @@ export const RECEIVERS = {
|
|
|
2999
4308
|
}),
|
|
3000
4309
|
"broadcast:get": receiver({
|
|
3001
4310
|
receiver: "broadcast:get",
|
|
4311
|
+
surfaces: { mcp: true },
|
|
3002
4312
|
summary: "Get a single broadcast with recipient counts",
|
|
3003
4313
|
request: z.object({ workspace: z.string(), broadcastId: z.string() }),
|
|
3004
4314
|
response: z.object({ broadcast: z.record(z.string(), z.unknown()) }),
|
|
@@ -3020,6 +4330,7 @@ export const RECEIVERS = {
|
|
|
3020
4330
|
}),
|
|
3021
4331
|
"segment:list": receiver({
|
|
3022
4332
|
receiver: "segment:list",
|
|
4333
|
+
surfaces: { mcp: true },
|
|
3023
4334
|
summary: "List audience segments for a workspace",
|
|
3024
4335
|
request: z.object({ workspace: z.string() }),
|
|
3025
4336
|
response: z.object({ segments: z.array(z.record(z.string(), z.unknown())) }),
|
|
@@ -3027,6 +4338,7 @@ export const RECEIVERS = {
|
|
|
3027
4338
|
}),
|
|
3028
4339
|
"segment:get": receiver({
|
|
3029
4340
|
receiver: "segment:get",
|
|
4341
|
+
surfaces: { mcp: true },
|
|
3030
4342
|
summary: "Get one audience segment with its rule definition",
|
|
3031
4343
|
request: z.object({ workspace: z.string(), id: z.string() }),
|
|
3032
4344
|
response: z.object({ segment: z.record(z.string(), z.unknown()) }),
|
|
@@ -3034,6 +4346,7 @@ export const RECEIVERS = {
|
|
|
3034
4346
|
}),
|
|
3035
4347
|
"segment:preview": receiver({
|
|
3036
4348
|
receiver: "segment:preview",
|
|
4349
|
+
surfaces: { mcp: true },
|
|
3037
4350
|
summary: "Preview an audience segment — live count + up to 10 sample addresses (read-only)",
|
|
3038
4351
|
request: z.object({ workspace: z.string(), definition: z.record(z.string(), z.unknown()), channel: z.enum(["email", "sms", "whatsapp"]).optional() }),
|
|
3039
4352
|
response: z.object({ count: z.number(), sample: z.array(z.string()) }),
|
|
@@ -3055,6 +4368,7 @@ export const RECEIVERS = {
|
|
|
3055
4368
|
}),
|
|
3056
4369
|
"broadcast:send": receiver({
|
|
3057
4370
|
receiver: "broadcast:send",
|
|
4371
|
+
surfaces: { mcp: true },
|
|
3058
4372
|
summary: "Seed recipients from audience tag, apply suppression, enqueue the send batch",
|
|
3059
4373
|
request: z.object({ workspace: z.string(), broadcastId: z.string() }),
|
|
3060
4374
|
response: z.object({ enqueued: z.number() }),
|
|
@@ -3157,18 +4471,21 @@ export const RECEIVERS = {
|
|
|
3157
4471
|
// ── Send Link — actor-bound tracked URL (CRM personalisation) ──────────────
|
|
3158
4472
|
"links:create": receiver({
|
|
3159
4473
|
receiver: "links:create",
|
|
3160
|
-
summary: "Create
|
|
4474
|
+
summary: "Create a tracked link in one of two shapes. ACTOR-BOUND (`actorId` given): the click elevates that contact to rung-4 identity at `/go/:id`, pre-warms their snapshot, and personalises the page + chat; the caller must hold authority over the contact's workspace. CAMPAIGN-ONLY (`campaignId` given, no `actorId`): the shape cold ad traffic needs, since an ad click is anonymous by definition and can never name a contact at mint time — the contact-owner walk is skipped, `actor_id` is stored null, and the click is attributed to the campaign alone. Exactly one of `actorId` / `campaignId` is required; supplying neither is refused. Minting is authed in both shapes; only the CLICK is anonymous. `slug` and `createdBy` are stamped from the attested session, never the body.",
|
|
3161
4475
|
request: z.object({
|
|
3162
|
-
actorId: z.string().describe("Contact (actor) the link is bound to — must belong to the caller's workspace or a descendant"),
|
|
4476
|
+
actorId: z.string().optional().describe("Contact (actor) the link is bound to — must belong to the caller's workspace or a descendant. Omit for cold ad traffic, in which case `campaignId` is REQUIRED so the click is never fully unattributed"),
|
|
3163
4477
|
destination: z.string().optional().describe("Path the click lands on (must start with '/'); default '/'"),
|
|
3164
4478
|
context: z.string().optional().describe("Extra context injected into the agent's system prompt for this session"),
|
|
3165
4479
|
greeting: z.string().optional().describe("Override the chat agent's opening line"),
|
|
3166
|
-
campaignId: z.string().optional().describe("Group clicks under a campaign for the learning loop"),
|
|
4480
|
+
campaignId: z.string().optional().describe("Group clicks under a campaign for the learning loop. REQUIRED when `actorId` is absent. Forwarded onto the destination URL as `utm_campaign` so the landing page and the click row report the same campaign; capped at 128 chars"),
|
|
3167
4481
|
expiresInDays: z.number().optional().describe("Link TTL in days; omitted = never expires"),
|
|
3168
4482
|
}),
|
|
3169
4483
|
response: z.object({ id: z.string(), sig: z.string(), url: z.string() }),
|
|
3170
4484
|
effect: "ask", cost: "free", reversible: false, idempotent: false, auth: "member",
|
|
3171
|
-
examples: [
|
|
4485
|
+
examples: [
|
|
4486
|
+
{ actorId: "u-123", destination: "/pricing", expiresInDays: 90 },
|
|
4487
|
+
{ campaignId: "spring-ads", destination: "/movers", expiresInDays: 30 },
|
|
4488
|
+
],
|
|
3172
4489
|
}),
|
|
3173
4490
|
"links:bulk-create": receiver({
|
|
3174
4491
|
receiver: "links:bulk-create",
|
|
@@ -3353,6 +4670,7 @@ export const RECEIVERS = {
|
|
|
3353
4670
|
// ── seo — handlers in resolvers/seo.ts (C1 live; C2 async) ─────────────────
|
|
3354
4671
|
"seo:backlinks-summary": receiver({
|
|
3355
4672
|
receiver: "seo:backlinks-summary",
|
|
4673
|
+
surfaces: { mcp: { name: "seo_backlinks" } },
|
|
3356
4674
|
summary: "Pull live backlink summary for a domain via DataForSEO; marks domain→seo:backlinks path",
|
|
3357
4675
|
request: z.object({ target: z.string(), limit: z.number().int().min(1).max(1000).optional() }),
|
|
3358
4676
|
response: z.object({ ok: z.boolean(), data: z.unknown().optional(), error: z.string().optional() }),
|
|
@@ -3361,6 +4679,7 @@ export const RECEIVERS = {
|
|
|
3361
4679
|
}),
|
|
3362
4680
|
"seo:llm-mentions": receiver({
|
|
3363
4681
|
receiver: "seo:llm-mentions",
|
|
4682
|
+
surfaces: { mcp: { name: "seo_ai_visibility" } },
|
|
3364
4683
|
summary: "Pull live AI-visibility (LLM mentions) data for a domain via DataForSEO; marks domain→seo:llm-mentions path",
|
|
3365
4684
|
request: z.object({ target: z.string() }),
|
|
3366
4685
|
response: z.object({ ok: z.boolean(), data: z.unknown().optional(), error: z.string().optional() }),
|
|
@@ -3369,6 +4688,7 @@ export const RECEIVERS = {
|
|
|
3369
4688
|
}),
|
|
3370
4689
|
"seo:keywords-for-keyword": receiver({
|
|
3371
4690
|
receiver: "seo:keywords-for-keyword",
|
|
4691
|
+
surfaces: { mcp: { name: "seo_research_keywords" } },
|
|
3372
4692
|
summary: "Return related keyword suggestions for a seed keyword via DataForSEO Google Ads; marks keyword→seo:keywords path",
|
|
3373
4693
|
request: z.object({ keyword: z.string(), location_code: z.number().int().optional(), language_code: z.string().optional() }),
|
|
3374
4694
|
response: z.object({ ok: z.boolean(), data: z.unknown().optional(), error: z.string().optional() }),
|
|
@@ -3377,6 +4697,7 @@ export const RECEIVERS = {
|
|
|
3377
4697
|
}),
|
|
3378
4698
|
"seo:serp": receiver({
|
|
3379
4699
|
receiver: "seo:serp",
|
|
4700
|
+
surfaces: { mcp: true },
|
|
3380
4701
|
summary: "Return full Google Organic SERP for a keyword via DataForSEO async task (submit→poll); marks keyword→seo:serp path",
|
|
3381
4702
|
request: z.object({ keyword: z.string(), location_code: z.number().int().optional(), language_code: z.string().optional(), device: z.string().optional() }),
|
|
3382
4703
|
response: z.object({ ok: z.boolean(), data: z.unknown().optional(), error: z.string().optional() }),
|
|
@@ -3385,6 +4706,7 @@ export const RECEIVERS = {
|
|
|
3385
4706
|
}),
|
|
3386
4707
|
"seo:keyword-metrics": receiver({
|
|
3387
4708
|
receiver: "seo:keyword-metrics",
|
|
4709
|
+
surfaces: { mcp: true },
|
|
3388
4710
|
summary: "Return monthly search volume, CPC, and competition for a list of keywords via DataForSEO async task",
|
|
3389
4711
|
request: z.object({ keywords: z.array(z.string()), location_code: z.number().int().optional(), language_code: z.string().optional() }),
|
|
3390
4712
|
response: z.object({ ok: z.boolean(), data: z.unknown().optional(), error: z.string().optional() }),
|
|
@@ -3393,6 +4715,7 @@ export const RECEIVERS = {
|
|
|
3393
4715
|
}),
|
|
3394
4716
|
"seo:gsc-performance": receiver({
|
|
3395
4717
|
receiver: "seo:gsc-performance",
|
|
4718
|
+
surfaces: { mcp: { name: "seo_gsc" } },
|
|
3396
4719
|
summary: "Return Google Search Console click/impression/CTR/position data for a site via Composio GSC toolkit",
|
|
3397
4720
|
request: z.object({ site_url: z.string(), start_date: z.string(), end_date: z.string(), dimensions: z.array(z.string()).optional(), row_limit: z.number().int().optional() }),
|
|
3398
4721
|
response: z.object({ ok: z.boolean(), data: z.unknown().optional(), error: z.string().optional() }),
|