@oneie/sdk 0.14.12 → 0.14.14
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/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 +1 -1
- package/dist/fn-allowlist.d.ts.map +1 -1
- package/dist/fn-allowlist.js +10 -1
- package/dist/fn-allowlist.js.map +1 -1
- package/dist/generated/fn-map.d.ts +1 -1
- package/dist/generated/fn-map.js +2 -2
- package/dist/generated/schemas/index.d.ts +1 -1
- package/dist/generated/schemas.d.ts +34 -6
- package/dist/generated/schemas.d.ts.map +1 -1
- package/dist/generated/schemas.js +24 -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 +975 -22
- package/dist/receivers.d.ts.map +1 -1
- package/dist/receivers.js +1547 -37
- 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 +62 -0
- package/package.json +18 -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)",
|
|
@@ -398,6 +440,73 @@ export const RECEIVERS = {
|
|
|
398
440
|
effect: "ask", auth: "manage_integrations", reversible: false,
|
|
399
441
|
examples: [{ slug: "acme", toolkit: "stripe", credentials: { api_key: "sk_test_..." } }],
|
|
400
442
|
}),
|
|
443
|
+
// git:pr — open or update ONE pull request on the repository a workspace owns.
|
|
444
|
+
//
|
|
445
|
+
// THE REPO IS NOT A PARAMETER, and that is the security property, not an
|
|
446
|
+
// ergonomic one. `bind-receiver.ts` splits the work: the binder asks "does this
|
|
447
|
+
// caller hold this CLASS of authority?", the handler asks "…over this
|
|
448
|
+
// PARTICULAR object?". Every other tenant receiver can answer the second
|
|
449
|
+
// because its object is a node in the six dimensions. A GitHub repository is
|
|
450
|
+
// not one — grep every schema/*.tql and the only hit is a `front-door`
|
|
451
|
+
// attribute whose VALUE may be the string "github". So `git:pr({ repo, … })`
|
|
452
|
+
// would take its target from the body and its credential from env: workspace
|
|
453
|
+
// authority anywhere becoming a write to any repo the token can reach, signed
|
|
454
|
+
// by ONE. That is the confused deputy, and it is the same defect class as
|
|
455
|
+
// trusting an actorId from a request body.
|
|
456
|
+
//
|
|
457
|
+
// So the caller never names the repository. The object of the decision is the
|
|
458
|
+
// WORKSPACE — dimension 1, a real node — resolved from the attested
|
|
459
|
+
// `ctx.ownerSlug` exactly as `tools:composio` resolves its entity ("never a
|
|
460
|
+
// body field", in that resolver's own words), and the repo is looked up FROM
|
|
461
|
+
// the workspace. No binding is a refusal, never a fallback to a default repo.
|
|
462
|
+
//
|
|
463
|
+
// `auth` is `manage_integrations` because it must be: AUTH_POLICY maps that to
|
|
464
|
+
// the `tenant` floor, and an unrecognised label falls to `authenticated`
|
|
465
|
+
// (bind-receiver.ts:120) — a novel name would have shipped this WEAKER, and
|
|
466
|
+
// nothing in the suite would have said so.
|
|
467
|
+
//
|
|
468
|
+
// It cannot derive its own title or body: there is no git and no shell in a
|
|
469
|
+
// Worker. Those are the caller's, which is why this receiver is the DOOR and
|
|
470
|
+
// `.claude/scripts/pr-body.sh` is the document.
|
|
471
|
+
"git:pr": receiver({
|
|
472
|
+
receiver: "git:pr",
|
|
473
|
+
summary: "Open or update one pull request on the repository this workspace is bound to — the repo is resolved from the workspace, never from the payload",
|
|
474
|
+
request: z.object({
|
|
475
|
+
workspace: z.string().optional().describe("Workspace slug; defaults to the attested caller's. The bound repo is resolved FROM this."),
|
|
476
|
+
head: z.string().describe("Branch to merge from — must already be pushed"),
|
|
477
|
+
base: z.string().optional().describe("Branch to merge into; defaults to main"),
|
|
478
|
+
title: z.string().describe("PR title"),
|
|
479
|
+
body: z.string().optional().describe("PR body — derive it with .claude/scripts/pr-body.sh; a Worker cannot"),
|
|
480
|
+
draft: z.boolean().optional().describe("Open as a draft"),
|
|
481
|
+
}),
|
|
482
|
+
response: z.object({
|
|
483
|
+
ok: z.boolean(),
|
|
484
|
+
url: z.string().optional(),
|
|
485
|
+
number: z.number().optional(),
|
|
486
|
+
updated: z.boolean().optional().describe("true = an open PR was updated rather than a new one created"),
|
|
487
|
+
repo: z.string().optional(),
|
|
488
|
+
error: z.string().optional(),
|
|
489
|
+
detail: z.string().optional(),
|
|
490
|
+
}),
|
|
491
|
+
effect: "ask",
|
|
492
|
+
auth: "manage_integrations",
|
|
493
|
+
// A PR is closable, and a re-run updates the open one rather than opening a
|
|
494
|
+
// second — so a retried workflow step is safe as-is.
|
|
495
|
+
reversible: true,
|
|
496
|
+
idempotent: true,
|
|
497
|
+
cost: "free",
|
|
498
|
+
settles: "none",
|
|
499
|
+
version: "1.0.0",
|
|
500
|
+
examples: [{ head: "feat/x", base: "main", title: "feat: x" }],
|
|
501
|
+
}),
|
|
502
|
+
"tools:composio": receiver({
|
|
503
|
+
receiver: "tools:composio",
|
|
504
|
+
summary: "Execute one connected Composio tool from a workflow `tool` step (config.composio)",
|
|
505
|
+
request: z.object({ workspace: z.string().optional().describe("Workspace slug; defaults to the attested caller's"), tool: z.string().describe("Composio tool slug to execute"), args: z.record(z.string(), z.unknown()).optional().describe("Arguments passed to the tool") }),
|
|
506
|
+
response: z.object({ ok: z.boolean(), tool: z.string().optional(), result: z.unknown().optional(), error: z.string().optional() }),
|
|
507
|
+
effect: "ask", auth: "manage_integrations", reversible: false, idempotent: false,
|
|
508
|
+
examples: [{ workspace: "acme", tool: "GMAIL_SEND_EMAIL", args: { to: "x@y.com" } }],
|
|
509
|
+
}),
|
|
401
510
|
"brand:set": receiver({
|
|
402
511
|
receiver: "brand:set",
|
|
403
512
|
summary: "Write up to 6 brand color tokens (primary/secondary/accent/bg/text/border) to a workspace theme",
|
|
@@ -408,12 +517,14 @@ export const RECEIVERS = {
|
|
|
408
517
|
}),
|
|
409
518
|
"world:create-thing": receiver({
|
|
410
519
|
receiver: "world:create-thing",
|
|
520
|
+
surfaces: { mcp: true },
|
|
411
521
|
summary: "Create a thing (skill, token, product) in the world",
|
|
412
522
|
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
523
|
response: z.object({ tid: z.string() }), effect: "ask", auth: "manage_things",
|
|
414
524
|
}),
|
|
415
525
|
"world:update-thing": receiver({
|
|
416
526
|
receiver: "world:update-thing",
|
|
527
|
+
surfaces: { mcp: true },
|
|
417
528
|
summary: "Update a thing's name, tags, price, or meta",
|
|
418
529
|
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
530
|
response: ok, effect: "ask", auth: "manage_things",
|
|
@@ -432,6 +543,7 @@ export const RECEIVERS = {
|
|
|
432
543
|
}),
|
|
433
544
|
"world:list-things": receiver({
|
|
434
545
|
receiver: "world:list-things",
|
|
546
|
+
surfaces: { mcp: true },
|
|
435
547
|
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
548
|
request: z.object({ group: z.string(), type: z.string(), ids: z.array(z.string()).optional() }),
|
|
437
549
|
response: z.object({ items: z.array(z.object({ tid: z.string(), name: z.string() })) }),
|
|
@@ -439,6 +551,7 @@ export const RECEIVERS = {
|
|
|
439
551
|
}),
|
|
440
552
|
"world:list-actors": receiver({
|
|
441
553
|
receiver: "world:list-actors",
|
|
554
|
+
surfaces: { mcp: true },
|
|
442
555
|
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
556
|
request: z.object({ group: z.string(), type: z.string(), ids: z.array(z.string()).optional() }),
|
|
444
557
|
response: z.object({ items: z.array(z.object({ aid: z.string(), name: z.string() })) }),
|
|
@@ -572,6 +685,7 @@ export const RECEIVERS = {
|
|
|
572
685
|
// ── identity ──
|
|
573
686
|
"identity:address": receiver({
|
|
574
687
|
receiver: "identity:address",
|
|
688
|
+
surfaces: { mcp: true },
|
|
575
689
|
summary: "Resolve an actor's wallet address",
|
|
576
690
|
request: z.object({ uid: z.string() }),
|
|
577
691
|
response: z.object({ uid: z.string(), address: z.string() }),
|
|
@@ -592,6 +706,7 @@ export const RECEIVERS = {
|
|
|
592
706
|
// ── groups ──
|
|
593
707
|
"groups:join": receiver({
|
|
594
708
|
receiver: "groups:join",
|
|
709
|
+
surfaces: { mcp: true },
|
|
595
710
|
summary: "Join a group",
|
|
596
711
|
request: z.object({ gid: z.string() }),
|
|
597
712
|
response: z.object({ ok: z.boolean(), gid: z.string(), role: z.string() }),
|
|
@@ -599,6 +714,7 @@ export const RECEIVERS = {
|
|
|
599
714
|
}),
|
|
600
715
|
"groups:leave": receiver({
|
|
601
716
|
receiver: "groups:leave",
|
|
717
|
+
surfaces: { mcp: true },
|
|
602
718
|
summary: "Leave a group",
|
|
603
719
|
request: z.object({ gid: z.string() }),
|
|
604
720
|
response: ok,
|
|
@@ -613,6 +729,7 @@ export const RECEIVERS = {
|
|
|
613
729
|
}),
|
|
614
730
|
"groups:members": receiver({
|
|
615
731
|
receiver: "groups:members",
|
|
732
|
+
surfaces: { mcp: true },
|
|
616
733
|
summary: "List a group's members",
|
|
617
734
|
request: z.object({ gid: z.string() }),
|
|
618
735
|
response: z.object({
|
|
@@ -647,7 +764,16 @@ export const RECEIVERS = {
|
|
|
647
764
|
receiver: "subscriptions:register",
|
|
648
765
|
summary: "Subscribe to a receiver's events by tag",
|
|
649
766
|
request: z.object({
|
|
650
|
-
|
|
767
|
+
// OPTIONAL, and the default is the common case. `subscriptions.ts` reads
|
|
768
|
+
// this as `data['receiver'] || 'world:announce'` — an agent staking on
|
|
769
|
+
// tags omits it, which is how every `subscribes:` frontmatter seam works.
|
|
770
|
+
// Declaring it required was harmless while nothing validated the request;
|
|
771
|
+
// once `bind-receiver` began enforcing the declared schema it started
|
|
772
|
+
// rejecting the documented call with a 400 at the edge, before the
|
|
773
|
+
// resolver's default could apply. Caught by tests/e2e/marketplace.test.ts,
|
|
774
|
+
// which is the marketplace promise's proof observable — the seller's
|
|
775
|
+
// stake never registered, so no deal opened.
|
|
776
|
+
receiver: z.string().optional(),
|
|
651
777
|
tags: z.array(z.string()),
|
|
652
778
|
scope: z.enum(["private", "public"]).optional(),
|
|
653
779
|
// Shared cross-tenant board — the one workspace two different ctx.ownerSlug
|
|
@@ -779,6 +905,7 @@ export const RECEIVERS = {
|
|
|
779
905
|
}),
|
|
780
906
|
"tasks:announce": receiver({
|
|
781
907
|
receiver: "tasks:announce",
|
|
908
|
+
surfaces: { mcp: true },
|
|
782
909
|
summary: "Announce a task into the world by its tags — the first caller of world:announce",
|
|
783
910
|
request: z.object({
|
|
784
911
|
taskId: z.string(),
|
|
@@ -792,6 +919,7 @@ export const RECEIVERS = {
|
|
|
792
919
|
}),
|
|
793
920
|
"tasks:mine": receiver({
|
|
794
921
|
receiver: "tasks:mine",
|
|
922
|
+
surfaces: { mcp: true },
|
|
795
923
|
summary: "My work queue: the open tasks whose words match what I subscribed to, ranked by learned weight (context-strength), not static priority",
|
|
796
924
|
// `workspace` is not decoration — /api/ask reads a SERVICE caller's nominated slug off
|
|
797
925
|
// validation.payload.slug|workspace, and zod .object() STRIPS unknown keys. Omitting the
|
|
@@ -822,6 +950,7 @@ export const RECEIVERS = {
|
|
|
822
950
|
}),
|
|
823
951
|
"tasks:everywhere": receiver({
|
|
824
952
|
receiver: "tasks:everywhere",
|
|
953
|
+
surfaces: { mcp: true },
|
|
825
954
|
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)",
|
|
826
955
|
// See tasks:mine — a service caller nominates its workspace here, and zod strips any
|
|
827
956
|
// field the schema does not declare.
|
|
@@ -885,6 +1014,37 @@ export const RECEIVERS = {
|
|
|
885
1014
|
}),
|
|
886
1015
|
effect: "ask", idempotent: true,
|
|
887
1016
|
}),
|
|
1017
|
+
"factory:size": receiver({
|
|
1018
|
+
receiver: "factory:size",
|
|
1019
|
+
summary: "How big is this change — the tier `.claude/scripts/do-tier.sh` returns for the same paths, answered where a Worker can ask (a Worker cannot run a shell script, which is the only reason the Size card at factory flow.ts:306 is a gap). Zero real paths is REFUSED as `unsized`, mirroring the script's exit 3: absence of recon must not read as simplicity. The pin ladder in the response is a pure function of the tier; the surfaces are additive obligation, never a second gate selector",
|
|
1020
|
+
request: z.object({
|
|
1021
|
+
// The changed paths, repo-relative. do-tier sizes a DIFF, never a sentence,
|
|
1022
|
+
// so this is the only field that can decide the answer.
|
|
1023
|
+
paths: z.array(z.string()).max(2000),
|
|
1024
|
+
// Free text. It can only RAISE the answer to FEATURE, and is ignored entirely
|
|
1025
|
+
// when `paths` is empty — "add a null check" and "add a settings page" share a verb.
|
|
1026
|
+
intent: z.string().max(2000).optional(),
|
|
1027
|
+
}),
|
|
1028
|
+
response: z.object({
|
|
1029
|
+
ok: z.boolean(),
|
|
1030
|
+
tier: z.enum(["PATCH", "FIX", "FEATURE", "SCHEMA"]).optional(),
|
|
1031
|
+
spine: z.string().optional(),
|
|
1032
|
+
classifier: z.string().optional(),
|
|
1033
|
+
ceilingTokens: z.number().optional(),
|
|
1034
|
+
// The pinned suites this tier buys — derived from `tier` alone, mirroring
|
|
1035
|
+
// .claude/scripts/verify-fast.sh:302-306. There is no second selector.
|
|
1036
|
+
pins: z.array(z.string()).optional(),
|
|
1037
|
+
// What the paths touch, and the ceremony rung after every surface floor is
|
|
1038
|
+
// applied. Obligations, not gate selection.
|
|
1039
|
+
surfaces: z.array(z.string()).optional(),
|
|
1040
|
+
risk: z.string().optional(),
|
|
1041
|
+
ratcheted: z.boolean().optional(),
|
|
1042
|
+
produces: z.array(z.object({ what: z.string(), source: z.string() })).optional(),
|
|
1043
|
+
gates: z.array(z.object({ what: z.string(), source: z.string() })).optional(),
|
|
1044
|
+
error: z.string().optional(),
|
|
1045
|
+
}),
|
|
1046
|
+
effect: "ask", idempotent: true, reversible: true,
|
|
1047
|
+
}),
|
|
888
1048
|
"factory:attempt": receiver({
|
|
889
1049
|
receiver: "factory:attempt",
|
|
890
1050
|
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",
|
|
@@ -924,6 +1084,82 @@ export const RECEIVERS = {
|
|
|
924
1084
|
}),
|
|
925
1085
|
effect: "ask", idempotent: true,
|
|
926
1086
|
}),
|
|
1087
|
+
"factory:event": receiver({
|
|
1088
|
+
receiver: "factory:event",
|
|
1089
|
+
summary: "One factory-executor stage lands on the same run/event stream the canvas already reads — the six-stage pipeline (ready·claim·build·review·prove·close) that until now touched the substrate only at claim and close, so every surface could show a job before and after but never during. An unknown stage or status is REFUSED, never recorded as a default frame",
|
|
1090
|
+
request: z.object({
|
|
1091
|
+
// The task id being built. It IS the run key (`factory:<job>`), which is why
|
|
1092
|
+
// a retried stage appends to its own run instead of minting a second one.
|
|
1093
|
+
job: z.string(),
|
|
1094
|
+
// Must equal FACTORY_SPINE_STEPS (one.ie/web/src/lib/factory/event.ts).
|
|
1095
|
+
// Retyped because packages/sdk cannot import from one.ie/web; pinned by
|
|
1096
|
+
// tests/unit/factory/event-receiver.test.ts so the two cannot drift.
|
|
1097
|
+
stage: z.enum(["ready", "claim", "build", "review", "prove", "close"]),
|
|
1098
|
+
// There is deliberately no "skip": a stage that did not run must be ABSENT.
|
|
1099
|
+
status: z.enum(["start", "ok", "fail"]),
|
|
1100
|
+
// Board workspace. TAGS ONLY *inside the resolver* — effectiveWorkspace
|
|
1101
|
+
// (resolvers/tasks.ts:35) reads `workspace` and never `slug`, so the resolver
|
|
1102
|
+
// itself cannot be steered by this field.
|
|
1103
|
+
//
|
|
1104
|
+
// BUT NOT BEFORE IT. For a VERIFIED SERVICE CALLER the ask route nominates
|
|
1105
|
+
// the identity from the body and `slug` WINS:
|
|
1106
|
+
// pages/api/ask/[...receiver].ts:237
|
|
1107
|
+
// nominated = typeof p.slug === 'string' ? p.slug
|
|
1108
|
+
// : typeof p.workspace === 'string' ? p.workspace : undefined
|
|
1109
|
+
// :324 ownerSlug = locals.slug ?? callerUid ?? serviceOwnerSlug
|
|
1110
|
+
// A sessionless curl (no `locals.slug`, and `callerUid` is only resolved in the
|
|
1111
|
+
// `bearerToken && !isServiceCaller` branch) therefore lands ctx.ownerSlug =
|
|
1112
|
+
// data.slug, and effectiveWorkspace returns that owner unchanged. So on the
|
|
1113
|
+
// shipped emitter — .claude/scripts/factory-emit.sh, which posts
|
|
1114
|
+
// `Authorization: Bearer $GATEWAY_API_KEY` (isVerifiedServiceCaller case 2,
|
|
1115
|
+
// gateway-guard.ts:118) and sends `--slug` but never `workspace` — the `slug`
|
|
1116
|
+
// COLUMN is data.slug, chosen by the caller. That is by design for a caller
|
|
1117
|
+
// holding a shared service secret, and it is NOT what "never authz" says.
|
|
1118
|
+
// The tags/authz split is a property of the RESOLVER, not of this field.
|
|
1119
|
+
slug: z.string().optional(),
|
|
1120
|
+
model: z.string().optional(),
|
|
1121
|
+
detail: z.record(z.string(), z.unknown()).optional(),
|
|
1122
|
+
reason: z.string().optional(),
|
|
1123
|
+
// "dev" flags the projected run is_test so a worktree never pollutes the list.
|
|
1124
|
+
env: z.string().optional(),
|
|
1125
|
+
// Which authorized workspace the `slug` COLUMN resolves to (effectiveWorkspace,
|
|
1126
|
+
// resolvers/tasks.ts). Never identity: an unauthorized value falls back to the
|
|
1127
|
+
// attested ownerSlug rather than escalating.
|
|
1128
|
+
workspace: z.string().optional(),
|
|
1129
|
+
}),
|
|
1130
|
+
response: z.object({
|
|
1131
|
+
ok: z.boolean(),
|
|
1132
|
+
runId: z.string().optional(),
|
|
1133
|
+
stage: z.string().optional(),
|
|
1134
|
+
status: z.string().optional(),
|
|
1135
|
+
events: z.number().optional(),
|
|
1136
|
+
slug: z.string().optional(),
|
|
1137
|
+
error: z.string().optional(),
|
|
1138
|
+
}),
|
|
1139
|
+
// VERIFIED, not assumed: authClassFor(undefined) returns 'open'
|
|
1140
|
+
// (bind-receiver.ts:117-120), and the 'authenticated' floor tests exactly
|
|
1141
|
+
// `ctx.staff === true || Boolean(ctx.ownerSlug)` (bind-receiver.ts:159) — the
|
|
1142
|
+
// same predicate the handler's own guard uses, one layer earlier, never
|
|
1143
|
+
// stricter. The four siblings omit the label; declaring it here makes the
|
|
1144
|
+
// fail-closed intent survive a refactor of the handler.
|
|
1145
|
+
//
|
|
1146
|
+
// HOW THE FLOOR ACTUALLY CLEARS — the previous note here said "the executor's
|
|
1147
|
+
// world-key call carries `data.workspace`, which the ask route turns into
|
|
1148
|
+
// ctx.ownerSlug". Both halves were wrong. The shipped emitter
|
|
1149
|
+
// (.claude/scripts/factory-emit.sh) is NOT a world-key call: it posts
|
|
1150
|
+
// `Authorization: Bearer $GATEWAY_API_KEY`, which is isVerifiedServiceCaller
|
|
1151
|
+
// case 2 (gateway-guard.ts:118) — the SERVICE-secret door, not the per-actor
|
|
1152
|
+
// one. A world-key bearer takes the other branch entirely
|
|
1153
|
+
// (`bearerToken && !isServiceCaller`, [...receiver].ts:238) and resolves
|
|
1154
|
+
// `callerUid`, never `serviceOwnerSlug`. And it does not send `workspace` at
|
|
1155
|
+
// all; it sends `slug`, which is the FIRST nomination at :237 and therefore
|
|
1156
|
+
// wins for a service caller. So the floor clears on a caller-supplied `slug`
|
|
1157
|
+
// reaching ctx.ownerSlug — see the note on the `slug` field above.
|
|
1158
|
+
auth: "required",
|
|
1159
|
+
// Two `build/ok` events for one job are two real frames with rising seq, not
|
|
1160
|
+
// a dedupe. The RUN row dedupes (INSERT OR IGNORE); the events do not.
|
|
1161
|
+
effect: "ask", idempotent: false,
|
|
1162
|
+
}),
|
|
927
1163
|
"do:halt": receiver({
|
|
928
1164
|
receiver: "do:halt",
|
|
929
1165
|
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",
|
|
@@ -953,6 +1189,7 @@ export const RECEIVERS = {
|
|
|
953
1189
|
}),
|
|
954
1190
|
"tasks:create": receiver({
|
|
955
1191
|
receiver: "tasks:create",
|
|
1192
|
+
surfaces: { mcp: true },
|
|
956
1193
|
summary: "Write one open task to the substrate and announce it by its tags in the same act — the quick-add entry point",
|
|
957
1194
|
request: z.object({
|
|
958
1195
|
title: z.string(),
|
|
@@ -961,17 +1198,23 @@ export const RECEIVERS = {
|
|
|
961
1198
|
// Prose goal / what "done" looks like — the context a human attaches at create so
|
|
962
1199
|
// the task is pickable by an agent or a Claude Code session without a round trip.
|
|
963
1200
|
notes: z.string().optional(),
|
|
1201
|
+
// Optional parent tid — writes the `containment` edge in the SAME pipeline as the
|
|
1202
|
+
// row, so the factory's downward walk (fn ready-tasks -> derived-from) can reach the
|
|
1203
|
+
// task. Without it the task is an orphan: on the board, unreachable from any plan.
|
|
1204
|
+
// Refused unless the caller has operate access to the parent.
|
|
1205
|
+
parent: z.string().optional(),
|
|
964
1206
|
// The viewed /u/<slug> workspace to file the task under. Honored only when the
|
|
965
1207
|
// caller is authorized for it (attested staff or owner-tree control); otherwise
|
|
966
1208
|
// the resolver falls back to the caller's own slug. Reconciles the create tag
|
|
967
1209
|
// with the /api/things read filter so a created task survives reload.
|
|
968
1210
|
workspace: z.string().optional(),
|
|
969
1211
|
}),
|
|
970
|
-
response: z.object({ ok: z.boolean(), tid: z.string().optional(), tags: z.array(z.string()).optional() }),
|
|
1212
|
+
response: z.object({ ok: z.boolean(), tid: z.string().optional(), tags: z.array(z.string()).optional(), parent: z.string().optional() }),
|
|
971
1213
|
effect: "ask", idempotent: false,
|
|
972
1214
|
}),
|
|
973
1215
|
"tasks:claim": receiver({
|
|
974
1216
|
receiver: "tasks:claim",
|
|
1217
|
+
surfaces: { mcp: true },
|
|
975
1218
|
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",
|
|
976
1219
|
// Addressable two ways, and BOTH must be optional here. The resolver has always
|
|
977
1220
|
// supported slug (resolveTaskBySlug) because create returns a random task:<traceId>
|
|
@@ -997,8 +1240,375 @@ export const RECEIVERS = {
|
|
|
997
1240
|
}),
|
|
998
1241
|
effect: "ask", idempotent: true,
|
|
999
1242
|
}),
|
|
1243
|
+
"tasks:reap": receiver({
|
|
1244
|
+
receiver: "tasks:reap",
|
|
1245
|
+
surfaces: { mcp: true },
|
|
1246
|
+
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",
|
|
1247
|
+
// The other half of the claim seam. tasks:claim is atomic, so two workers can
|
|
1248
|
+
// never double-lease — but a lease whose OWNER DIED is `picked` forever:
|
|
1249
|
+
// `claimable` requires `status === 'open' && ats.length === 0`, so the view
|
|
1250
|
+
// never surfaces it, so `leaseIsStale` (called only inside claim) is never
|
|
1251
|
+
// reached on the rows it was written for. Meanwhile ready-tasks blocks every
|
|
1252
|
+
// dependent on a `picked` blocker. This receiver queries `picked` DIRECTLY —
|
|
1253
|
+
// that is the whole point; asking the claimable view returns nothing.
|
|
1254
|
+
request: z.object({
|
|
1255
|
+
// Viewed workspace — same authorization contract as tasks:claim's workspace.
|
|
1256
|
+
workspace: z.string().optional(),
|
|
1257
|
+
}),
|
|
1258
|
+
response: z.object({
|
|
1259
|
+
ok: z.boolean().optional(),
|
|
1260
|
+
workspace: z.string().optional(),
|
|
1261
|
+
scanned: z.number().optional(),
|
|
1262
|
+
reaped: z.array(z.string()).optional(),
|
|
1263
|
+
failed: z.array(z.string()).optional(),
|
|
1264
|
+
error: z.string().optional(),
|
|
1265
|
+
}),
|
|
1266
|
+
// Idempotent: a second sweep finds nothing left stale and reaps nothing.
|
|
1267
|
+
effect: "ask", idempotent: true,
|
|
1268
|
+
}),
|
|
1269
|
+
// ── The rest of the board's verbs ────────────────────────────────────────────
|
|
1270
|
+
//
|
|
1271
|
+
// These twelve shipped as RESOLVERS ONLY (`tasksResolvers`, one.ie/web) and were
|
|
1272
|
+
// absent from this registry, so `bindReceiver` had no request schema to validate
|
|
1273
|
+
// and no auth label to apply — every one of them reached its handler with an
|
|
1274
|
+
// unchecked payload. They are also what the UI's detail pane, status menu, tag
|
|
1275
|
+
// chips and Gantt call, so the contract that was missing is the contract for most
|
|
1276
|
+
// of the board. `auth: "member"` is the label the family already resolves to
|
|
1277
|
+
// (an absent label falls to `authenticated`, same class) — stated, not changed.
|
|
1278
|
+
//
|
|
1279
|
+
// Every one takes an optional `workspace`, and it means the same thing here as on
|
|
1280
|
+
// tasks:create: the VIEWED workspace, honoured only when the caller is staff or
|
|
1281
|
+
// controls that tree, otherwise ignored in favour of the caller's own slug
|
|
1282
|
+
// (`effectiveWorkspace`). Declaring it is not decoration — zod `.object()` strips
|
|
1283
|
+
// undeclared keys, which is exactly how tasks:mine/everywhere were silently
|
|
1284
|
+
// un-nominatable until 2026-07-25.
|
|
1285
|
+
"tasks:status": receiver({
|
|
1286
|
+
receiver: "tasks:status",
|
|
1287
|
+
surfaces: { mcp: true },
|
|
1288
|
+
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",
|
|
1289
|
+
request: z.object({
|
|
1290
|
+
tid: z.string(),
|
|
1291
|
+
status: z.enum(["open", "blocked", "picked", "done", "verified", "failed", "dissolved"]),
|
|
1292
|
+
workspace: z.string().optional(),
|
|
1293
|
+
}),
|
|
1294
|
+
response: z.object({
|
|
1295
|
+
ok: z.boolean().optional(),
|
|
1296
|
+
tid: z.string().optional(),
|
|
1297
|
+
status: z.string().optional(),
|
|
1298
|
+
// Set when a repeating task re-armed rather than closed.
|
|
1299
|
+
recurred: z.boolean().optional(),
|
|
1300
|
+
dueAt: z.string().optional(),
|
|
1301
|
+
error: z.string().optional(),
|
|
1302
|
+
}),
|
|
1303
|
+
effect: "ask", idempotent: true, auth: "member",
|
|
1304
|
+
}),
|
|
1305
|
+
"tasks:rename": receiver({
|
|
1306
|
+
receiver: "tasks:rename",
|
|
1307
|
+
surfaces: { mcp: true },
|
|
1308
|
+
summary: "Retitle a task in place. The tid is unchanged, so every link, blocker and follow survives the rename",
|
|
1309
|
+
request: z.object({
|
|
1310
|
+
tid: z.string(),
|
|
1311
|
+
title: z.string(),
|
|
1312
|
+
workspace: z.string().optional(),
|
|
1313
|
+
}),
|
|
1314
|
+
response: z.object({
|
|
1315
|
+
ok: z.boolean().optional(),
|
|
1316
|
+
tid: z.string().optional(),
|
|
1317
|
+
title: z.string().optional(),
|
|
1318
|
+
error: z.string().optional(),
|
|
1319
|
+
}),
|
|
1320
|
+
effect: "ask", idempotent: true, auth: "member",
|
|
1321
|
+
}),
|
|
1322
|
+
// tasks:generate — read what a page says, propose the work it implies, and file it.
|
|
1323
|
+
//
|
|
1324
|
+
// The one task verb that does not take a title: the caller hands over the PAGE
|
|
1325
|
+
// (its title, its own summary, its visible text) and gets back real rows on the
|
|
1326
|
+
// board. Every row is written through `tasks:create`, so the workspace gate, the
|
|
1327
|
+
// announce and the write-failure semantics are the ones that verb already holds —
|
|
1328
|
+
// this receiver only decides WHAT to file, never who may file it.
|
|
1329
|
+
//
|
|
1330
|
+
// No URL is fetched server-side. The caller sends the text it can already see
|
|
1331
|
+
// (the browser reads its own DOM, or same-origin-fetches a page under the
|
|
1332
|
+
// viewer's own session); a receiver that fetched an arbitrary `url` would be an
|
|
1333
|
+
// SSRF door on a surface that spends LLM tokens. `url` is a LABEL here — it
|
|
1334
|
+
// names the source and seeds the dedupe key, and is never dereferenced.
|
|
1335
|
+
//
|
|
1336
|
+
// Idempotent by (source, title): each row carries a derived `slug:` tag, which
|
|
1337
|
+
// tasks:create dedupes on. Clicking twice on the same page cannot double the
|
|
1338
|
+
// board, while a genuinely new suggestion still lands.
|
|
1339
|
+
"tasks:generate": receiver({
|
|
1340
|
+
receiver: "tasks:generate",
|
|
1341
|
+
surfaces: { mcp: true },
|
|
1342
|
+
summary: "Turn a page into tasks: read the page text the caller supplies, propose 3-7 concrete next actions, and write each one through tasks:create (deduped by source+title). Never fetches the URL — `url` labels the source and seeds the dedupe key",
|
|
1343
|
+
request: z.object({
|
|
1344
|
+
/** Source label + dedupe seed. Never fetched. */
|
|
1345
|
+
url: z.string().optional(),
|
|
1346
|
+
title: z.string().optional(),
|
|
1347
|
+
/** The page's own description / heading trail — what `readPageSummary()` returns. */
|
|
1348
|
+
summary: z.string().optional(),
|
|
1349
|
+
/** Visible page text, if the caller has it. Capped server-side. */
|
|
1350
|
+
text: z.string().optional(),
|
|
1351
|
+
/** How many tasks to aim for. Clamped to 1-8. */
|
|
1352
|
+
count: z.number().optional(),
|
|
1353
|
+
/** Extra tags every generated task carries, on top of `from-page`. */
|
|
1354
|
+
tags: z.array(z.string()).optional(),
|
|
1355
|
+
/** The viewed /u/<slug> workspace — same authorization contract as tasks:create. */
|
|
1356
|
+
workspace: z.string().optional(),
|
|
1357
|
+
}),
|
|
1358
|
+
response: z.object({
|
|
1359
|
+
ok: z.boolean(),
|
|
1360
|
+
created: z.number().optional(),
|
|
1361
|
+
deduped: z.number().optional(),
|
|
1362
|
+
/** The workspace the rows actually landed in — the resolver's own answer,
|
|
1363
|
+
* not the caller's request. A caller that guessed this instead would link
|
|
1364
|
+
* the operator at a board their tasks are not on whenever the workspace
|
|
1365
|
+
* override was refused. */
|
|
1366
|
+
workspace: z.string().optional(),
|
|
1367
|
+
tasks: z.array(z.object({ tid: z.string(), title: z.string(), deduped: z.boolean().optional() })).optional(),
|
|
1368
|
+
error: z.string().optional(),
|
|
1369
|
+
}),
|
|
1370
|
+
// Spends LLM tokens and writes rows. An undeclared label is an anonymous door
|
|
1371
|
+
// (see receiver-envelope.ts § requiresAttestedCaller) — this one refuses.
|
|
1372
|
+
effect: "ask", auth: "member", reversible: false, idempotent: true,
|
|
1373
|
+
}),
|
|
1374
|
+
"tasks:list": receiver({
|
|
1375
|
+
receiver: "tasks:list",
|
|
1376
|
+
surfaces: { mcp: true },
|
|
1377
|
+
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",
|
|
1378
|
+
request: z.object({
|
|
1379
|
+
tag: z.string(),
|
|
1380
|
+
// Optional task-status filter (open|blocked|picked|done|verified|failed|dissolved).
|
|
1381
|
+
// Declared here because the HTTP edge dispatches zod's PARSED output — an
|
|
1382
|
+
// undeclared field is silently stripped before the resolver reads it, and the
|
|
1383
|
+
// factory turn (one.ie/ai/workflows/factory-turn.tql step:rows) needs the
|
|
1384
|
+
// plan's OPEN rows to fan tasks:launch over.
|
|
1385
|
+
status: z.string().optional(),
|
|
1386
|
+
workspace: z.string().optional(),
|
|
1387
|
+
}),
|
|
1388
|
+
response: z.object({
|
|
1389
|
+
ok: z.boolean().optional(),
|
|
1390
|
+
tag: z.string().optional(),
|
|
1391
|
+
tasks: z.array(z.object({
|
|
1392
|
+
tid: z.string(),
|
|
1393
|
+
status: z.string(),
|
|
1394
|
+
name: z.string().optional(),
|
|
1395
|
+
})).optional(),
|
|
1396
|
+
error: z.string().optional(),
|
|
1397
|
+
}),
|
|
1398
|
+
effect: "ask", idempotent: true, auth: "member",
|
|
1399
|
+
}),
|
|
1400
|
+
"tasks:notes": receiver({
|
|
1401
|
+
receiver: "tasks:notes",
|
|
1402
|
+
surfaces: { mcp: true },
|
|
1403
|
+
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",
|
|
1404
|
+
request: z.object({
|
|
1405
|
+
tid: z.string(),
|
|
1406
|
+
notes: z.string().nullable().optional(),
|
|
1407
|
+
workspace: z.string().optional(),
|
|
1408
|
+
}),
|
|
1409
|
+
response: z.object({
|
|
1410
|
+
ok: z.boolean().optional(),
|
|
1411
|
+
tid: z.string().optional(),
|
|
1412
|
+
notes: z.string().nullable().optional(),
|
|
1413
|
+
error: z.string().optional(),
|
|
1414
|
+
}),
|
|
1415
|
+
effect: "ask", idempotent: true, auth: "member",
|
|
1416
|
+
}),
|
|
1417
|
+
"tasks:priority": receiver({
|
|
1418
|
+
receiver: "tasks:priority",
|
|
1419
|
+
surfaces: { mcp: true },
|
|
1420
|
+
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",
|
|
1421
|
+
request: z.object({
|
|
1422
|
+
tid: z.string(),
|
|
1423
|
+
priority: z.number(),
|
|
1424
|
+
workspace: z.string().optional(),
|
|
1425
|
+
}),
|
|
1426
|
+
response: z.object({
|
|
1427
|
+
ok: z.boolean().optional(),
|
|
1428
|
+
tid: z.string().optional(),
|
|
1429
|
+
// The stored 0–1 value, not the 1–100 slider that was sent.
|
|
1430
|
+
priority: z.number().optional(),
|
|
1431
|
+
error: z.string().optional(),
|
|
1432
|
+
}),
|
|
1433
|
+
effect: "ask", idempotent: true, auth: "member",
|
|
1434
|
+
}),
|
|
1435
|
+
"tasks:schedule": receiver({
|
|
1436
|
+
receiver: "tasks:schedule",
|
|
1437
|
+
surfaces: { mcp: true },
|
|
1438
|
+
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",
|
|
1439
|
+
request: z.object({
|
|
1440
|
+
tid: z.string(),
|
|
1441
|
+
dueAt: z.string().nullable().optional(),
|
|
1442
|
+
startAt: z.string().nullable().optional(),
|
|
1443
|
+
workspace: z.string().optional(),
|
|
1444
|
+
}),
|
|
1445
|
+
response: z.object({
|
|
1446
|
+
ok: z.boolean().optional(),
|
|
1447
|
+
tid: z.string().optional(),
|
|
1448
|
+
dueAt: z.string().nullable().optional(),
|
|
1449
|
+
startAt: z.string().nullable().optional(),
|
|
1450
|
+
error: z.string().optional(),
|
|
1451
|
+
}),
|
|
1452
|
+
effect: "ask", idempotent: true, auth: "member",
|
|
1453
|
+
}),
|
|
1454
|
+
"tasks:tag": receiver({
|
|
1455
|
+
receiver: "tasks:tag",
|
|
1456
|
+
surfaces: { mcp: true },
|
|
1457
|
+
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",
|
|
1458
|
+
request: z.object({
|
|
1459
|
+
tid: z.string(),
|
|
1460
|
+
add: z.array(z.string()).optional(),
|
|
1461
|
+
remove: z.array(z.string()).optional(),
|
|
1462
|
+
workspace: z.string().optional(),
|
|
1463
|
+
}),
|
|
1464
|
+
response: z.object({
|
|
1465
|
+
ok: z.boolean().optional(),
|
|
1466
|
+
tid: z.string().optional(),
|
|
1467
|
+
add: z.array(z.string()).optional(),
|
|
1468
|
+
remove: z.array(z.string()).optional(),
|
|
1469
|
+
error: z.string().optional(),
|
|
1470
|
+
}),
|
|
1471
|
+
effect: "ask", idempotent: true, auth: "member",
|
|
1472
|
+
}),
|
|
1473
|
+
"tasks:reassign": receiver({
|
|
1474
|
+
receiver: "tasks:reassign",
|
|
1475
|
+
surfaces: { mcp: true },
|
|
1476
|
+
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",
|
|
1477
|
+
request: z.object({
|
|
1478
|
+
tid: z.string(),
|
|
1479
|
+
// Actor slug. Empty/null unassigns. A leading '@' is accepted and stripped.
|
|
1480
|
+
assignee: z.string().nullable().optional(),
|
|
1481
|
+
workspace: z.string().optional(),
|
|
1482
|
+
}),
|
|
1483
|
+
response: z.object({
|
|
1484
|
+
ok: z.boolean().optional(),
|
|
1485
|
+
tid: z.string().optional(),
|
|
1486
|
+
assignee: z.string().nullable().optional(),
|
|
1487
|
+
// Who held it before, so the caller can say "moved from X to Y" without a re-read.
|
|
1488
|
+
previous: z.string().nullable().optional(),
|
|
1489
|
+
tags: z.array(z.string()).optional(),
|
|
1490
|
+
unchanged: z.boolean().optional(),
|
|
1491
|
+
// Set only when unassigning reopened a `picked` task.
|
|
1492
|
+
status: z.string().optional(),
|
|
1493
|
+
reopened: z.boolean().optional(),
|
|
1494
|
+
error: z.string().optional(),
|
|
1495
|
+
}),
|
|
1496
|
+
effect: "ask", idempotent: true, auth: "member",
|
|
1497
|
+
}),
|
|
1498
|
+
"tasks:comment": receiver({
|
|
1499
|
+
receiver: "tasks:comment",
|
|
1500
|
+
surfaces: { mcp: true },
|
|
1501
|
+
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",
|
|
1502
|
+
request: z.object({
|
|
1503
|
+
tid: z.string().optional(),
|
|
1504
|
+
slug: z.string().optional(),
|
|
1505
|
+
body: z.string().optional(),
|
|
1506
|
+
workspace: z.string().optional(),
|
|
1507
|
+
}).refine((v) => !!v.tid || !!v.slug, {
|
|
1508
|
+
message: "address the task by tid or slug",
|
|
1509
|
+
path: ["tid"],
|
|
1510
|
+
}),
|
|
1511
|
+
response: z.object({
|
|
1512
|
+
ok: z.boolean().optional(),
|
|
1513
|
+
tid: z.string().optional(),
|
|
1514
|
+
messages: z.array(z.record(z.string(), z.unknown())).optional(),
|
|
1515
|
+
error: z.string().optional(),
|
|
1516
|
+
}),
|
|
1517
|
+
effect: "ask", idempotent: false, auth: "member",
|
|
1518
|
+
}),
|
|
1519
|
+
"tasks:subtask": receiver({
|
|
1520
|
+
receiver: "tasks:subtask",
|
|
1521
|
+
surfaces: { mcp: true },
|
|
1522
|
+
summary: "Create a child task COMPLETE — row, notes, tags, its `containment` edge to the parent and every `blockedBy` prerequisite — in ONE pipeline, so a child never appears claimable with an empty body or missing ordering",
|
|
1523
|
+
request: z.object({
|
|
1524
|
+
parent: z.string(),
|
|
1525
|
+
title: z.string(),
|
|
1526
|
+
tags: z.array(z.string()).optional(),
|
|
1527
|
+
assignee: z.string().optional(),
|
|
1528
|
+
// The prose goal, written at creation. A child that arrives with an empty body is
|
|
1529
|
+
// claimable-but-unbuildable; notes here close that window.
|
|
1530
|
+
notes: z.string().optional(),
|
|
1531
|
+
// One tid or a list (max 16) of prerequisites. Each writes a `blocks` edge in the
|
|
1532
|
+
// same pipeline as the row, so ordering is created at intake rather than remembered
|
|
1533
|
+
// later. Validated exactly as tasks:depend does: operate-role on each prerequisite
|
|
1534
|
+
// and the transitive cycle walk.
|
|
1535
|
+
blockedBy: z.union([z.string(), z.array(z.string())]).optional(),
|
|
1536
|
+
workspace: z.string().optional(),
|
|
1537
|
+
}),
|
|
1538
|
+
response: z.object({
|
|
1539
|
+
ok: z.boolean().optional(),
|
|
1540
|
+
tid: z.string().optional(),
|
|
1541
|
+
parent: z.string().optional(),
|
|
1542
|
+
tags: z.array(z.string()).optional(),
|
|
1543
|
+
blockedBy: z.array(z.string()).optional(),
|
|
1544
|
+
error: z.string().optional(),
|
|
1545
|
+
}),
|
|
1546
|
+
effect: "ask", idempotent: false, auth: "member",
|
|
1547
|
+
}),
|
|
1548
|
+
"tasks:depend": receiver({
|
|
1549
|
+
receiver: "tasks:depend",
|
|
1550
|
+
surfaces: { mcp: true },
|
|
1551
|
+
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",
|
|
1552
|
+
request: z.object({
|
|
1553
|
+
tid: z.string(),
|
|
1554
|
+
blockedBy: z.string(),
|
|
1555
|
+
workspace: z.string().optional(),
|
|
1556
|
+
}),
|
|
1557
|
+
response: z.object({
|
|
1558
|
+
ok: z.boolean().optional(),
|
|
1559
|
+
tid: z.string().optional(),
|
|
1560
|
+
blockedBy: z.string().optional(),
|
|
1561
|
+
error: z.string().optional(),
|
|
1562
|
+
}),
|
|
1563
|
+
effect: "ask", idempotent: true, auth: "member",
|
|
1564
|
+
}),
|
|
1565
|
+
"tasks:follow": receiver({
|
|
1566
|
+
receiver: "tasks:follow",
|
|
1567
|
+
surfaces: { mcp: true },
|
|
1568
|
+
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",
|
|
1569
|
+
request: z.object({
|
|
1570
|
+
tid: z.string(),
|
|
1571
|
+
workspace: z.string().optional(),
|
|
1572
|
+
}),
|
|
1573
|
+
response: z.object({
|
|
1574
|
+
ok: z.boolean().optional(),
|
|
1575
|
+
tid: z.string().optional(),
|
|
1576
|
+
tags: z.array(z.string()).optional(),
|
|
1577
|
+
actor: z.string().optional(),
|
|
1578
|
+
error: z.string().optional(),
|
|
1579
|
+
}),
|
|
1580
|
+
effect: "ask", idempotent: true, auth: "member",
|
|
1581
|
+
}),
|
|
1582
|
+
"tasks:unfollow": receiver({
|
|
1583
|
+
receiver: "tasks:unfollow",
|
|
1584
|
+
surfaces: { mcp: true },
|
|
1585
|
+
summary: "Stop following one task — deletes exactly that task-grain subscription row, leaving tag-level subscriptions alone",
|
|
1586
|
+
request: z.object({
|
|
1587
|
+
tid: z.string(),
|
|
1588
|
+
workspace: z.string().optional(),
|
|
1589
|
+
}),
|
|
1590
|
+
response: z.object({
|
|
1591
|
+
ok: z.boolean().optional(),
|
|
1592
|
+
tid: z.string().optional(),
|
|
1593
|
+
actor: z.string().optional(),
|
|
1594
|
+
error: z.string().optional(),
|
|
1595
|
+
}),
|
|
1596
|
+
effect: "ask", idempotent: true, auth: "member",
|
|
1597
|
+
}),
|
|
1598
|
+
"tasks:follows": receiver({
|
|
1599
|
+
receiver: "tasks:follows",
|
|
1600
|
+
summary: "List the tasks the caller follows (task-grain subscriptions only, newest first, cap 100)",
|
|
1601
|
+
request: z.object({
|
|
1602
|
+
workspace: z.string().optional(),
|
|
1603
|
+
}),
|
|
1604
|
+
response: z.object({
|
|
1605
|
+
follows: z.array(z.record(z.string(), z.unknown())),
|
|
1606
|
+
}),
|
|
1607
|
+
effect: "ask", idempotent: true, auth: "member",
|
|
1608
|
+
}),
|
|
1000
1609
|
"tasks:undepend": receiver({
|
|
1001
1610
|
receiver: "tasks:undepend",
|
|
1611
|
+
surfaces: { mcp: true },
|
|
1002
1612
|
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",
|
|
1003
1613
|
request: z.object({
|
|
1004
1614
|
tid: z.string(),
|
|
@@ -1026,13 +1636,21 @@ export const RECEIVERS = {
|
|
|
1026
1636
|
tid: z.string(),
|
|
1027
1637
|
ok: z.boolean(),
|
|
1028
1638
|
actorId: z.string().optional(),
|
|
1029
|
-
|
|
1639
|
+
// `unspecced` = the task has no Proof: row (factory/spec-gate.ts).
|
|
1640
|
+
// `spec_unreadable` = we could not READ the task to decide — an outage,
|
|
1641
|
+
// kept distinct from a verdict per factory-spec.md demand 6.
|
|
1642
|
+
reason: z.enum(["unassigned", "human-assignee", "blocked", "forbidden", "fire_failed", "unspecced", "spec_unreadable"]).optional(),
|
|
1643
|
+
/** Why the gate refused, in one human-readable line. */
|
|
1644
|
+
detail: z.string().optional(),
|
|
1645
|
+
/** Set when the task launched only on the spec gate's grandfather clause. */
|
|
1646
|
+
warning: z.string().optional(),
|
|
1030
1647
|
})),
|
|
1031
1648
|
}),
|
|
1032
1649
|
effect: "ask", idempotent: false,
|
|
1033
1650
|
}),
|
|
1034
1651
|
"tasks:link": receiver({
|
|
1035
1652
|
receiver: "tasks:link",
|
|
1653
|
+
surfaces: { mcp: true },
|
|
1036
1654
|
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",
|
|
1037
1655
|
// The slug IS the handle here — the resolver requires it and resolves the origin tid
|
|
1038
1656
|
// from the `slug:` tag, treating an explicit tid as an optional override for direct
|
|
@@ -1057,18 +1675,31 @@ export const RECEIVERS = {
|
|
|
1057
1675
|
effect: "ask", idempotent: true,
|
|
1058
1676
|
}),
|
|
1059
1677
|
// ── agents (TRADE / lifecycle) ──
|
|
1678
|
+
// THE PUBLIC DOOR. An arriving agent joins here with no human in the loop and
|
|
1679
|
+
// no prior credential, and gets a SCOPED rung: it may list capabilities and be
|
|
1680
|
+
// paid; it may not spend the compute float until one x402 challenge is
|
|
1681
|
+
// satisfied, and it may never touch another workspace.
|
|
1682
|
+
//
|
|
1683
|
+
// `uid` is OPTIONAL and is never authority. An anonymous caller's identity is
|
|
1684
|
+
// MINTED by the handler and its `uid` is ignored outright; an attested caller
|
|
1685
|
+
// may only name an identity it already controls (callerControlsWorkspace).
|
|
1686
|
+
// That is the whole IDOR rule: the subject is derived, never declared.
|
|
1060
1687
|
"agents:register": receiver({
|
|
1061
1688
|
receiver: "agents:register",
|
|
1062
|
-
|
|
1689
|
+
surfaces: { mcp: { name: "register" } },
|
|
1690
|
+
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).",
|
|
1063
1691
|
request: z.object({
|
|
1064
|
-
uid
|
|
1692
|
+
/** Omit when anonymous — the handler mints one. Naming another agent's uid is refused. */
|
|
1693
|
+
uid: z.string().optional(),
|
|
1694
|
+
/** Display handle for a fresh anonymous registration. Never authority. */
|
|
1695
|
+
name: z.string().optional(),
|
|
1065
1696
|
kind: z.string().optional(),
|
|
1066
1697
|
capabilities: z.array(z.object({ skill: z.string(), price: z.number().optional() })).optional(),
|
|
1067
1698
|
wallet: z.string().optional(),
|
|
1068
1699
|
chain: z.string().optional(),
|
|
1069
1700
|
}),
|
|
1070
1701
|
response: RegisterResponseSchema,
|
|
1071
|
-
effect: "ask", idempotent: true,
|
|
1702
|
+
effect: "ask", auth: "public", idempotent: true,
|
|
1072
1703
|
}),
|
|
1073
1704
|
"agents:commend": receiver({
|
|
1074
1705
|
receiver: "agents:commend",
|
|
@@ -1102,26 +1733,35 @@ export const RECEIVERS = {
|
|
|
1102
1733
|
// ── pay (TRANSACT — onchain settlement) ──
|
|
1103
1734
|
"pay:weight": receiver({
|
|
1104
1735
|
receiver: "pay:weight",
|
|
1736
|
+
surfaces: { mcp: { name: "pay" } },
|
|
1105
1737
|
summary: "Pay for a task by weighting the path between two actors",
|
|
1106
|
-
request: z.object({
|
|
1738
|
+
request: z.object({
|
|
1739
|
+
// DERIVED, never sent: the handler sets `from = ctx.ownerSlug` and never reads
|
|
1740
|
+
// data.from. Declaring it required made the binder reject every valid call
|
|
1741
|
+
// before the handler could derive it — and before its own auth check ran.
|
|
1742
|
+
from: z.string().optional(),
|
|
1743
|
+
to: z.string(), task: z.string(), amount: z.number(),
|
|
1744
|
+
}),
|
|
1107
1745
|
response: PayResponseSchema,
|
|
1108
1746
|
effect: "ask", cost: "variable", settles: "onchain", reversible: false, simulatable: true,
|
|
1109
1747
|
}),
|
|
1110
1748
|
"tasks:stake": receiver({
|
|
1111
1749
|
receiver: "tasks:stake",
|
|
1112
|
-
summary: "
|
|
1750
|
+
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.",
|
|
1113
1751
|
request: z.object({
|
|
1114
|
-
|
|
1115
|
-
|
|
1752
|
+
tags: z.array(z.string().min(1)).min(1).max(8),
|
|
1753
|
+
addWeight: z.number().int().optional(),
|
|
1754
|
+
digest: z.string().min(20),
|
|
1116
1755
|
}),
|
|
1117
1756
|
response: z.object({
|
|
1118
1757
|
ok: z.boolean(),
|
|
1119
1758
|
digest: z.string().optional(),
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1759
|
+
newWeight: z.number().optional(),
|
|
1760
|
+
tags: z.array(z.string()).optional(),
|
|
1761
|
+
error: z.string().optional(),
|
|
1123
1762
|
}),
|
|
1124
|
-
effect: "ask", cost: "variable", settles: "onchain", reversible: false, simulatable: false,
|
|
1763
|
+
effect: "ask", auth: "required", cost: "variable", settles: "onchain", reversible: false, simulatable: false,
|
|
1764
|
+
examples: [{ tags: ["engineering", "ontology", "marketing"], addWeight: 20, digest: "VisitorSignedDigestFromSuiTestnetTx" }],
|
|
1125
1765
|
}),
|
|
1126
1766
|
// ── market (A2A negotiation, C2) ──
|
|
1127
1767
|
"market:offer": receiver({
|
|
@@ -1266,7 +1906,11 @@ export const RECEIVERS = {
|
|
|
1266
1906
|
receiver: "market:bounty",
|
|
1267
1907
|
summary: "Post a bounty for a skill, backed by an escrow",
|
|
1268
1908
|
request: z.object({
|
|
1269
|
-
skillId: z.string(), sellerUid: z.string(),
|
|
1909
|
+
skillId: z.string(), sellerUid: z.string(),
|
|
1910
|
+
// DERIVED, never sent: the handler sets `posterUid = ctx.ownerSlug`
|
|
1911
|
+
// (resolvers/market.ts). Required here, it rejected every valid post.
|
|
1912
|
+
posterUid: z.string().optional(),
|
|
1913
|
+
price: z.number(),
|
|
1270
1914
|
tags: z.array(z.string()).optional(),
|
|
1271
1915
|
content: z.record(z.string(), z.unknown()).optional(),
|
|
1272
1916
|
rubric: z.object({
|
|
@@ -1301,6 +1945,32 @@ export const RECEIVERS = {
|
|
|
1301
1945
|
]),
|
|
1302
1946
|
effect: "ask", cost: "free", reversible: false, idempotent: false,
|
|
1303
1947
|
}),
|
|
1948
|
+
"market:search": receiver({
|
|
1949
|
+
receiver: "market:search",
|
|
1950
|
+
summary: "Search the whole marketplace catalog — agents, skills, plugins, live listings and open bounties — and get back named, card-shaped results",
|
|
1951
|
+
request: z.object({
|
|
1952
|
+
q: z.string().describe("What the shopper is looking for, in their own words"),
|
|
1953
|
+
kind: z.enum(["agent", "skill", "plugin", "listing", "bounty"]).optional(),
|
|
1954
|
+
limit: z.number().int().positive().optional(),
|
|
1955
|
+
}),
|
|
1956
|
+
response: z.object({
|
|
1957
|
+
ok: z.boolean(),
|
|
1958
|
+
total: z.number(),
|
|
1959
|
+
matched: z.number(),
|
|
1960
|
+
results: z.array(z.object({
|
|
1961
|
+
kind: z.string(), ref: z.string(), title: z.string(), blurb: z.string(),
|
|
1962
|
+
tags: z.array(z.string()),
|
|
1963
|
+
// null means "carries no price" — never render it as 0. `priceLabel` is
|
|
1964
|
+
// the honest string for a card ("from 25 credits", "$49", or null).
|
|
1965
|
+
price: z.number().nullable(), unit: z.string().nullable(),
|
|
1966
|
+
sellers: z.number(), priceLabel: z.string().nullable(), url: z.string(),
|
|
1967
|
+
})),
|
|
1968
|
+
}),
|
|
1969
|
+
// Browse is public on /marketplace and public here — the concierge answers
|
|
1970
|
+
// "who sells X" for a signed-out visitor, which is the whole point.
|
|
1971
|
+
auth: "open",
|
|
1972
|
+
effect: "ask", cost: "free", idempotent: true,
|
|
1973
|
+
}),
|
|
1304
1974
|
"market:list": receiver({
|
|
1305
1975
|
receiver: "market:list",
|
|
1306
1976
|
summary: "List the capability market",
|
|
@@ -1313,7 +1983,13 @@ export const RECEIVERS = {
|
|
|
1313
1983
|
receiver: "capabilities:publish",
|
|
1314
1984
|
summary: "Publish a capability (skill listing) to the market",
|
|
1315
1985
|
request: z.object({
|
|
1316
|
-
skillId: z.string(), name: z.string(),
|
|
1986
|
+
skillId: z.string(), name: z.string(),
|
|
1987
|
+
// OPTIONAL — `market.ts` reads it as `Math.max(0, num(...))`, so an
|
|
1988
|
+
// omitted price is a free listing, not a refusal. Declaring it required
|
|
1989
|
+
// meant the edge rejected a call the resolver was written to accept, the
|
|
1990
|
+
// same drift that took `subscriptions:register` down once bind-receiver
|
|
1991
|
+
// began enforcing declared schemas.
|
|
1992
|
+
price: z.number().optional(),
|
|
1317
1993
|
mode: z.string().optional(), visibility: z.string().optional(), scope: z.string().optional(), entitlement: z.string().optional(),
|
|
1318
1994
|
tags: z.array(z.string()).optional(),
|
|
1319
1995
|
rubricThresholds: z.object({
|
|
@@ -1341,6 +2017,7 @@ export const RECEIVERS = {
|
|
|
1341
2017
|
}),
|
|
1342
2018
|
"stats:current": receiver({
|
|
1343
2019
|
receiver: "stats:current",
|
|
2020
|
+
surfaces: { mcp: { name: "stats" } },
|
|
1344
2021
|
summary: "Current world stats — units, skills, highways, revenue, signals",
|
|
1345
2022
|
request: z.object({}),
|
|
1346
2023
|
response: StatsSchema,
|
|
@@ -1371,6 +2048,7 @@ export const RECEIVERS = {
|
|
|
1371
2048
|
// ════════════════════════════════════════════════════════════════════════
|
|
1372
2049
|
"meta:catalog": receiver({
|
|
1373
2050
|
receiver: "meta:catalog",
|
|
2051
|
+
surfaces: { mcp: true },
|
|
1374
2052
|
summary: "List every receiver the caller can use (with cost/reversibility/settlement), or a goal's recipe",
|
|
1375
2053
|
request: z.object({ goal: z.enum(["spine", "build", "trade", "transact"]).optional() }),
|
|
1376
2054
|
response: z.union([
|
|
@@ -1385,6 +2063,7 @@ export const RECEIVERS = {
|
|
|
1385
2063
|
}),
|
|
1386
2064
|
"meta:schema": receiver({
|
|
1387
2065
|
receiver: "meta:schema",
|
|
2066
|
+
surfaces: { mcp: true },
|
|
1388
2067
|
summary: "JSON Schema for one receiver's request + response — read it before you call",
|
|
1389
2068
|
request: z.object({ receiver: z.string() }),
|
|
1390
2069
|
response: z.object({
|
|
@@ -1395,6 +2074,7 @@ export const RECEIVERS = {
|
|
|
1395
2074
|
}),
|
|
1396
2075
|
"meta:recall": receiver({
|
|
1397
2076
|
receiver: "meta:recall",
|
|
2077
|
+
surfaces: { mcp: true },
|
|
1398
2078
|
summary: "Recall the caller's hypotheses (memory), optionally filtered by a search term",
|
|
1399
2079
|
request: z.object({ match: z.string().optional(), limit: z.number().optional() }),
|
|
1400
2080
|
response: z.object({
|
|
@@ -1422,6 +2102,7 @@ export const RECEIVERS = {
|
|
|
1422
2102
|
}),
|
|
1423
2103
|
"meta:types": receiver({
|
|
1424
2104
|
receiver: "meta:types",
|
|
2105
|
+
surfaces: { mcp: true },
|
|
1425
2106
|
summary: "Read the resource-type manifest for the caller's workspace, plus the built-in industry templates",
|
|
1426
2107
|
request: z.object({}),
|
|
1427
2108
|
response: z.object({
|
|
@@ -1542,6 +2223,13 @@ export const RECEIVERS = {
|
|
|
1542
2223
|
}),
|
|
1543
2224
|
"notify": receiver({
|
|
1544
2225
|
receiver: "notify",
|
|
2226
|
+
// NOT surfaces.mcp — the curated `message` tool in packages/mcp/src/tools/
|
|
2227
|
+
// messaging.ts already fronts this receiver and carries shaping the registry
|
|
2228
|
+
// cannot infer: it defaults `kind: "message"` and names the argument
|
|
2229
|
+
// `content` in a description written for a sender, not a schema. Flagging it
|
|
2230
|
+
// produced two tools called `message`, of which the curated one won
|
|
2231
|
+
// registration and the derived one was dead weight. Curated survives here;
|
|
2232
|
+
// the registry does not claim a surface something else already serves.
|
|
1545
2233
|
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.",
|
|
1546
2234
|
request: z.object({
|
|
1547
2235
|
receiver: z.string(),
|
|
@@ -1848,6 +2536,55 @@ export const RECEIVERS = {
|
|
|
1848
2536
|
}),
|
|
1849
2537
|
effect: "ask", cost: "free", reversible: true, idempotent: false, auth: "member",
|
|
1850
2538
|
}),
|
|
2539
|
+
// ── EVENTS (dim 5) — the analytics ingress ───────────────────────────────────
|
|
2540
|
+
// event:track — the write behind POST /api/events, the browser pixel's door.
|
|
2541
|
+
// PUBLIC by construction: the pixel runs on third-party pages with no session,
|
|
2542
|
+
// no key and no cookie the substrate controls, so an auth label here would
|
|
2543
|
+
// throw in bindReceiver's applyAuth and silently kill 46k events/30d.
|
|
2544
|
+
// Identity is never taken from this payload; the route attests the caller and
|
|
2545
|
+
// hands the resolver an already-enriched row.
|
|
2546
|
+
"event:track": receiver({
|
|
2547
|
+
receiver: "event:track",
|
|
2548
|
+
summary: "Record one analytics event (agent_events_warm) and broadcast it to the workspace AnalyticsRelay",
|
|
2549
|
+
request: z.object({
|
|
2550
|
+
id: z.string(),
|
|
2551
|
+
ts: z.number(),
|
|
2552
|
+
slug: z.string(),
|
|
2553
|
+
event: z.string(),
|
|
2554
|
+
source: z.string(),
|
|
2555
|
+
visitor_hash: z.string().optional().nullable(),
|
|
2556
|
+
agent_id: z.string().optional().nullable(),
|
|
2557
|
+
variant: z.string().optional().nullable(),
|
|
2558
|
+
thread_id: z.string().optional().nullable(),
|
|
2559
|
+
actor_id: z.string().optional().nullable(),
|
|
2560
|
+
actor_type: z.string().optional().nullable(),
|
|
2561
|
+
channel: z.string().optional().nullable(),
|
|
2562
|
+
campaign: z.string().optional().nullable(),
|
|
2563
|
+
link_id: z.string().optional().nullable(),
|
|
2564
|
+
referrer: z.string().optional().nullable(),
|
|
2565
|
+
user_agent_class: z.string().optional().nullable(),
|
|
2566
|
+
locale: z.string().optional().nullable(),
|
|
2567
|
+
payload: z.record(z.string(), z.unknown()).optional().nullable(),
|
|
2568
|
+
consent_state: z.string().optional().nullable(),
|
|
2569
|
+
region: z.string().optional().nullable(),
|
|
2570
|
+
tags: z.array(z.string()).optional().nullable(),
|
|
2571
|
+
}),
|
|
2572
|
+
response: z.object({ ok: z.boolean(), id: z.string().optional(), error: z.string().optional() }),
|
|
2573
|
+
examples: [
|
|
2574
|
+
{
|
|
2575
|
+
id: "01J8ZQ3K2P0000000000000000",
|
|
2576
|
+
ts: 1767225600000,
|
|
2577
|
+
slug: "one",
|
|
2578
|
+
event: "pageview",
|
|
2579
|
+
source: "web",
|
|
2580
|
+
visitor_hash: "d41d8cd98f00b204",
|
|
2581
|
+
consent_state: "granted",
|
|
2582
|
+
region: "IE",
|
|
2583
|
+
payload: { page: "/pricing" },
|
|
2584
|
+
},
|
|
2585
|
+
],
|
|
2586
|
+
effect: "signal", cost: "free", reversible: false, idempotent: true, auth: "public",
|
|
2587
|
+
}),
|
|
1851
2588
|
// ── FUNNELS (text/funnels-plan.md) — author + run funnel definitions ──────────
|
|
1852
2589
|
// funnel:create — create a new funnel definition (draft).
|
|
1853
2590
|
"funnel:create": receiver({
|
|
@@ -1971,14 +2708,30 @@ export const RECEIVERS = {
|
|
|
1971
2708
|
images: z.array(z.string()).optional(),
|
|
1972
2709
|
product_type: z.string().optional(), // default 'one_time'
|
|
1973
2710
|
collection: z.string().optional(),
|
|
2711
|
+
// Same stripped-at-the-edge trap as the two artifact fields below, and it
|
|
2712
|
+
// cost more: resolvers/commerce.ts has read `data.status` since it shipped,
|
|
2713
|
+
// but the key was never declared here, so zod dropped it and EVERY product
|
|
2714
|
+
// created through this receiver came back 'draft'. Proven behaviourally
|
|
2715
|
+
// 2026-08-23 — POST with status:"active" returned 200, the row read 'draft'.
|
|
2716
|
+
// An agent could list a product and had no way to make it sellable.
|
|
2717
|
+
status: z.enum(["draft", "active", "archived"]).optional(),
|
|
2718
|
+
// The ARTIFACT listing door. Declared here because validateReceiver returns
|
|
2719
|
+
// zod's PARSED output, so an undeclared key is stripped at the HTTP edge and
|
|
2720
|
+
// never reaches the handler. `delivery_r2_key` is accepted ONLY as a
|
|
2721
|
+
// `{kind}:{ref}` install descriptor — the plain-R2-key write door stays shut.
|
|
2722
|
+
product_kind: z.literal("artifact").optional(),
|
|
2723
|
+
delivery_r2_key: z.string().optional(),
|
|
1974
2724
|
}),
|
|
1975
2725
|
response: z.object({ pid: z.string().optional(), ppid: z.string().optional(), name: z.string().optional(), workspace: z.string().optional(), error: z.string().optional() }),
|
|
1976
2726
|
effect: "ask", cost: "free", reversible: true, idempotent: false, auth: "member",
|
|
1977
2727
|
}),
|
|
1978
2728
|
"products:update": receiver({
|
|
1979
2729
|
receiver: "products:update",
|
|
1980
|
-
summary: "Update a product's name, description, images, or
|
|
1981
|
-
|
|
2730
|
+
summary: "Update a product's name, description, images, collection, or status",
|
|
2731
|
+
// `status` carries the same history as on products:create — the resolver has
|
|
2732
|
+
// always read it, the declaration never listed it, so zod stripped it and a
|
|
2733
|
+
// draft product could not be published through this receiver either.
|
|
2734
|
+
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() }),
|
|
1982
2735
|
response: z.object({ pid: z.string().optional(), workspace: z.string().optional(), error: z.string().optional() }),
|
|
1983
2736
|
effect: "ask", cost: "free", reversible: false, idempotent: true, auth: "member",
|
|
1984
2737
|
}),
|
|
@@ -2003,6 +2756,429 @@ export const RECEIVERS = {
|
|
|
2003
2756
|
response: z.object({ pid: z.string().optional(), workspace: z.string().optional(), status: z.string().optional(), error: z.string().optional() }),
|
|
2004
2757
|
effect: "ask", cost: "free", reversible: true, idempotent: true, auth: "member",
|
|
2005
2758
|
}),
|
|
2759
|
+
// ── variants (product options: size/colour) — handlers in resolvers/commerce.ts ─
|
|
2760
|
+
// stock IS NULL means UNTRACKED, never zero: a service or digital product is
|
|
2761
|
+
// never blocked by an inventory check it did not opt into. On update, an ABSENT
|
|
2762
|
+
// stock field leaves the value unchanged; an explicit null sets untracked.
|
|
2763
|
+
"variants:list": receiver({
|
|
2764
|
+
receiver: "variants:list",
|
|
2765
|
+
summary: "List a product's active variants with SKU, price, weight and stock (public)",
|
|
2766
|
+
request: z.object({ slug: z.string(), pid: z.string() }),
|
|
2767
|
+
response: z.object({ variants: z.array(z.record(z.string(), z.unknown())).optional(), error: z.string().optional() }),
|
|
2768
|
+
effect: "ask", cost: "free", reversible: true, idempotent: true, auth: "public",
|
|
2769
|
+
}),
|
|
2770
|
+
"variants:create": receiver({
|
|
2771
|
+
receiver: "variants:create",
|
|
2772
|
+
summary: "Add a variant to a product — its own SKU, price, weight and stock",
|
|
2773
|
+
request: z.object({
|
|
2774
|
+
slug: z.string(),
|
|
2775
|
+
pid: z.string(),
|
|
2776
|
+
title: z.string(), // "Blue / M"
|
|
2777
|
+
unit_amount: z.number().int(), // cents
|
|
2778
|
+
sku: z.string().optional(),
|
|
2779
|
+
options: z.record(z.string(), z.string()).optional(),
|
|
2780
|
+
currency: z.string().optional(), // default 'usd'
|
|
2781
|
+
weight_grams: z.number().int().optional(),
|
|
2782
|
+
stock: z.number().int().nullable().optional(), // null/absent = untracked
|
|
2783
|
+
position: z.number().int().optional(),
|
|
2784
|
+
}),
|
|
2785
|
+
response: z.object({ vid: z.string().optional(), pid: z.string().optional(), workspace: z.string().optional(), error: z.string().optional() }),
|
|
2786
|
+
effect: "ask", cost: "free", reversible: true, idempotent: false, auth: "member",
|
|
2787
|
+
}),
|
|
2788
|
+
"variants:update": receiver({
|
|
2789
|
+
receiver: "variants:update",
|
|
2790
|
+
summary: "Update a variant — omit stock to leave it unchanged, send null to make it untracked",
|
|
2791
|
+
request: z.object({
|
|
2792
|
+
slug: z.string(),
|
|
2793
|
+
vid: z.string(),
|
|
2794
|
+
title: z.string().optional(),
|
|
2795
|
+
unit_amount: z.number().int().optional(),
|
|
2796
|
+
sku: z.string().optional(),
|
|
2797
|
+
options: z.record(z.string(), z.string()).optional(),
|
|
2798
|
+
currency: z.string().optional(),
|
|
2799
|
+
weight_grams: z.number().int().optional(),
|
|
2800
|
+
stock: z.number().int().nullable().optional(), // absent is not null: absent leaves it unchanged
|
|
2801
|
+
position: z.number().int().optional(),
|
|
2802
|
+
active: z.boolean().optional(),
|
|
2803
|
+
}),
|
|
2804
|
+
response: z.object({ vid: z.string().optional(), workspace: z.string().optional(), error: z.string().optional() }),
|
|
2805
|
+
effect: "ask", cost: "free", reversible: true, idempotent: true, auth: "member",
|
|
2806
|
+
}),
|
|
2807
|
+
"variants:archive": receiver({
|
|
2808
|
+
receiver: "variants:archive",
|
|
2809
|
+
summary: "Archive a variant (reversible via variants:update { active: true })",
|
|
2810
|
+
request: z.object({ slug: z.string(), vid: z.string() }),
|
|
2811
|
+
response: z.object({ vid: z.string().optional(), workspace: z.string().optional(), active: z.number().optional(), error: z.string().optional() }),
|
|
2812
|
+
effect: "ask", cost: "free", reversible: true, idempotent: true, auth: "member",
|
|
2813
|
+
}),
|
|
2814
|
+
// ── cart (server-truth basket) — handlers land in resolvers/cart.ts (C3) ───────
|
|
2815
|
+
// Registered here, not by C3: signals-parity counts RECEIVERS against
|
|
2816
|
+
// text/signals-catalog.md and is a live accept: on another kept promise.
|
|
2817
|
+
// `cid` is a server-minted bearer capability (never client-chosen), which is
|
|
2818
|
+
// what makes an anonymous cart safe at auth: "public". No request carries a
|
|
2819
|
+
// unit_amount — every line is re-priced server-side from D1 by ppid/vid.
|
|
2820
|
+
"cart:get": receiver({
|
|
2821
|
+
receiver: "cart:get",
|
|
2822
|
+
summary: "Read a cart and its lines by server-minted cart id",
|
|
2823
|
+
request: z.object({ slug: z.string(), cid: z.string() }),
|
|
2824
|
+
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() }),
|
|
2825
|
+
effect: "ask", cost: "free", reversible: true, idempotent: true, auth: "public",
|
|
2826
|
+
}),
|
|
2827
|
+
"cart:add": receiver({
|
|
2828
|
+
receiver: "cart:add",
|
|
2829
|
+
summary: "Add a product (or variant) line to a cart, minting the cart when cid is absent — price is read server-side, never sent",
|
|
2830
|
+
request: z.object({
|
|
2831
|
+
slug: z.string(),
|
|
2832
|
+
pid: z.string(),
|
|
2833
|
+
cid: z.string().optional(), // absent mints a new cart
|
|
2834
|
+
ppid: z.string().optional(),
|
|
2835
|
+
vid: z.string().optional(),
|
|
2836
|
+
quantity: z.number().int().positive().optional(), // default 1
|
|
2837
|
+
}),
|
|
2838
|
+
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() }),
|
|
2839
|
+
effect: "ask", cost: "free", reversible: true, idempotent: false, auth: "public",
|
|
2840
|
+
}),
|
|
2841
|
+
"cart:update": receiver({
|
|
2842
|
+
receiver: "cart:update",
|
|
2843
|
+
summary: "Set a cart line's quantity (0 removes the line)",
|
|
2844
|
+
request: z.object({ slug: z.string(), cid: z.string(), ciid: z.string(), quantity: z.number().int().min(0) }),
|
|
2845
|
+
response: z.object({ cid: z.string().optional(), items: z.array(z.record(z.string(), z.unknown())).optional(), error: z.string().optional() }),
|
|
2846
|
+
effect: "ask", cost: "free", reversible: true, idempotent: true, auth: "public",
|
|
2847
|
+
}),
|
|
2848
|
+
"cart:remove": receiver({
|
|
2849
|
+
receiver: "cart:remove",
|
|
2850
|
+
summary: "Remove one line from a cart",
|
|
2851
|
+
request: z.object({ slug: z.string(), cid: z.string(), ciid: z.string() }),
|
|
2852
|
+
response: z.object({ cid: z.string().optional(), items: z.array(z.record(z.string(), z.unknown())).optional(), error: z.string().optional() }),
|
|
2853
|
+
effect: "ask", cost: "free", reversible: false, idempotent: true, auth: "public",
|
|
2854
|
+
}),
|
|
2855
|
+
"cart:clear": receiver({
|
|
2856
|
+
receiver: "cart:clear",
|
|
2857
|
+
summary: "Empty a cart, keeping the cart itself",
|
|
2858
|
+
request: z.object({ slug: z.string(), cid: z.string() }),
|
|
2859
|
+
response: z.object({ cid: z.string().optional(), cleared: z.number().optional(), error: z.string().optional() }),
|
|
2860
|
+
effect: "ask", cost: "free", reversible: false, idempotent: true, auth: "public",
|
|
2861
|
+
}),
|
|
2862
|
+
// ── orders (the one operator write door) — handler lands in resolvers (C5) ─────
|
|
2863
|
+
// data-bindings.ts:62 marks `order` create/update/archive as null by design:
|
|
2864
|
+
// orders are webhook-minted and otherwise read-only. This is the deliberate
|
|
2865
|
+
// exception. `status` is z.string(), not an enum — the D1 column is bare TEXT
|
|
2866
|
+
// (0118_orders.sql:15) and C5's resolver owns the allowlist.
|
|
2867
|
+
"orders:status": receiver({
|
|
2868
|
+
receiver: "orders:status",
|
|
2869
|
+
summary: "Set an order's status — the one operator write on a webhook-minted record",
|
|
2870
|
+
request: z.object({ slug: z.string(), oid: z.string(), status: z.string() }),
|
|
2871
|
+
response: z.object({ oid: z.string().optional(), workspace: z.string().optional(), status: z.string().optional(), error: z.string().optional() }),
|
|
2872
|
+
effect: "ask", cost: "free", reversible: true, idempotent: true, auth: "member",
|
|
2873
|
+
}),
|
|
2874
|
+
// ── order:claim — the buyer's delivery door (handler: resolvers/commerce.ts) ──
|
|
2875
|
+
// Declared so the route's envelope guard fires: an undeclared receiver skips
|
|
2876
|
+
// the `envelope_missing` check, and a flat body silently validates as {}.
|
|
2877
|
+
// `auth: "public"` because an anonymous buyer claims with the order's own
|
|
2878
|
+
// session_id as bearer proof; the handler holds the real attestation (session
|
|
2879
|
+
// match OR ctx-attested workspace control), never a body-supplied identity.
|
|
2880
|
+
"order:claim": receiver({
|
|
2881
|
+
receiver: "order:claim",
|
|
2882
|
+
summary: "Claim a paid order's delivery — returns a scoped download URL for the digital good",
|
|
2883
|
+
request: z.object({
|
|
2884
|
+
oid: z.string(),
|
|
2885
|
+
workspace: z.string().optional(),
|
|
2886
|
+
session_id: z.string().optional(),
|
|
2887
|
+
}),
|
|
2888
|
+
response: z.object({
|
|
2889
|
+
ok: z.boolean().optional(),
|
|
2890
|
+
url: z.string().optional(),
|
|
2891
|
+
name: z.string().optional(),
|
|
2892
|
+
error: z.string().optional(),
|
|
2893
|
+
}),
|
|
2894
|
+
effect: "ask", cost: "free", reversible: true, idempotent: true, settles: "none", auth: "public",
|
|
2895
|
+
examples: [{ oid: "ord_1", session_id: "cs_test_1" }],
|
|
2896
|
+
}),
|
|
2897
|
+
// ── order:install — the buyer's ARTIFACT door (handler: resolvers/install.ts) ──
|
|
2898
|
+
// The sibling of order:claim, and deliberately NOT the same receiver. order:claim
|
|
2899
|
+
// is `auth: "public"` because it hands back a FILE: knowing (oid, session_id) is
|
|
2900
|
+
// the whole proof, which is right for a crypto buyer who never signs in. Install
|
|
2901
|
+
// inverts the direction of authority — a SELLER's product causes a WRITE into a
|
|
2902
|
+
// BUYER's workspace — so it is the IDOR shape ~80 workspace routes were already
|
|
2903
|
+
// fixed for, and it gets a tenant-class label (`manage_things`) so bind-receiver's
|
|
2904
|
+
// AUTH_POLICY floor is ENFORCED before the handler runs. The handler then walks
|
|
2905
|
+
// the destination with callerControlsWorkspace(ctx.ownerSlug, …); `workspace` in
|
|
2906
|
+
// the body NAMES a target, it never authorizes one. order:claim is untouched.
|
|
2907
|
+
"order:install": receiver({
|
|
2908
|
+
receiver: "order:install",
|
|
2909
|
+
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",
|
|
2910
|
+
request: z.object({
|
|
2911
|
+
oid: z.string(),
|
|
2912
|
+
workspace: z.string().optional(), // destination; defaults to the attested caller
|
|
2913
|
+
session_id: z.string().optional(), // the order's own receipt, as in order:claim
|
|
2914
|
+
}),
|
|
2915
|
+
response: z.object({
|
|
2916
|
+
ok: z.boolean().optional(),
|
|
2917
|
+
kind: z.enum(["skill", "agent", "workflow", "view", "playbook"]).optional(),
|
|
2918
|
+
ref: z.string().optional(),
|
|
2919
|
+
workspace: z.string().optional(),
|
|
2920
|
+
installed: z.record(z.string(), z.unknown()).optional(),
|
|
2921
|
+
error: z.string().optional(),
|
|
2922
|
+
}),
|
|
2923
|
+
effect: "ask", cost: "free", reversible: false, idempotent: false, settles: "none", auth: "manage_things",
|
|
2924
|
+
examples: [{ oid: "ord_1", session_id: "cs_test_1" }, { oid: "ord_1", workspace: "elitemoversca" }],
|
|
2925
|
+
}),
|
|
2926
|
+
// ── wallet:create — register RECEIVING ADDRESSES only (handler: resolvers/wallet.ts) ──
|
|
2927
|
+
// Custody invariant: `addresses` carries public addresses and nothing else. No
|
|
2928
|
+
// field here accepts a private key or a mnemonic, and none is emitted back.
|
|
2929
|
+
// Omitting `addresses` provisions an address-less row (the pre-existing
|
|
2930
|
+
// idempotent path); supplying it merges first-write-wins per chain.
|
|
2931
|
+
"wallet:create": receiver({
|
|
2932
|
+
receiver: "wallet:create",
|
|
2933
|
+
summary: "Provision a wallet row for an actor and register its public receiving addresses (first-write-wins per chain)",
|
|
2934
|
+
request: z.object({
|
|
2935
|
+
workspace: z.string().optional(),
|
|
2936
|
+
actor: z.string().optional(),
|
|
2937
|
+
kind: z.enum(["human", "agent"]).optional(),
|
|
2938
|
+
addresses: z.object({
|
|
2939
|
+
sui: z.string().nullable().optional(),
|
|
2940
|
+
evm: z.string().nullable().optional(),
|
|
2941
|
+
solana: z.string().nullable().optional(),
|
|
2942
|
+
btc: z.string().nullable().optional(),
|
|
2943
|
+
}).optional(),
|
|
2944
|
+
}),
|
|
2945
|
+
response: z.object({
|
|
2946
|
+
ok: z.boolean().optional(),
|
|
2947
|
+
actor: z.string().optional(),
|
|
2948
|
+
workspace: z.string().optional(),
|
|
2949
|
+
kind: z.string().optional(),
|
|
2950
|
+
addresses: z.object({
|
|
2951
|
+
sui: z.string().nullable().optional(),
|
|
2952
|
+
evm: z.string().nullable().optional(),
|
|
2953
|
+
solana: z.string().nullable().optional(),
|
|
2954
|
+
btc: z.string().nullable().optional(),
|
|
2955
|
+
}).optional(),
|
|
2956
|
+
error: z.string().optional(),
|
|
2957
|
+
field: z.string().optional(),
|
|
2958
|
+
}),
|
|
2959
|
+
effect: "ask", cost: "free", reversible: false, idempotent: true, settles: "none", auth: "member",
|
|
2960
|
+
examples: [{ workspace: "acme", kind: "human", addresses: { sui: "0xabc", evm: "0xdef" } }],
|
|
2961
|
+
}),
|
|
2962
|
+
// ── wallet:rehearse — READ a TESTNET transaction back (handler: resolvers/wallet.ts) ──
|
|
2963
|
+
// The rehearsal step of the /u/one funnel: a visitor sends play money on Sui
|
|
2964
|
+
// testnet from their own derived address, and this reads the digest back so the
|
|
2965
|
+
// chat can say what actually happened on chain.
|
|
2966
|
+
//
|
|
2967
|
+
// IT GRANTS NOTHING, and that is the whole contract — `credits` is a
|
|
2968
|
+
// z.literal(0), so the response schema itself forbids a grant. The reason is
|
|
2969
|
+
// not squeamishness: `currentBalance` (web/src/lib/credits.ts) sums
|
|
2970
|
+
// `credit_grants` with NO `test_mode` filter, so a credit granted for testnet
|
|
2971
|
+
// money would be real, fully spendable credit minted out of play money.
|
|
2972
|
+
//
|
|
2973
|
+
// `network` is a z.literal("testnet") on BOTH sides. There is deliberately no
|
|
2974
|
+
// parameter by which a caller could point this at mainnet — the resolver
|
|
2975
|
+
// hardcodes the testnet RPC. A mainnet read belongs to the settling rail
|
|
2976
|
+
// (pay's payment_link_claim), which moves money and therefore takes attestation.
|
|
2977
|
+
//
|
|
2978
|
+
// auth "public" because it neither writes nor grants: a chain read with a rate
|
|
2979
|
+
// limit, placed in front of anonymous workspace-root traffic. The resolver
|
|
2980
|
+
// limits per ATTESTED `ctx.visitorHash` — the shape `movers:funnel-provision`
|
|
2981
|
+
// already uses. A caller may name an address; it can never name a spend.
|
|
2982
|
+
"wallet:rehearse": receiver({
|
|
2983
|
+
receiver: "wallet:rehearse",
|
|
2984
|
+
summary: "Read a TESTNET Sui transaction back and report it. Grants nothing, settles nothing, never writes a credit_grants row.",
|
|
2985
|
+
request: z.object({
|
|
2986
|
+
chain: z.literal("sui"),
|
|
2987
|
+
network: z.literal("testnet"),
|
|
2988
|
+
digest: z.string().min(1).describe("The transaction digest the sender broadcast"),
|
|
2989
|
+
address: z.string().optional().describe("The sender address — PUBLIC only, never a key"),
|
|
2990
|
+
}),
|
|
2991
|
+
response: z.object({
|
|
2992
|
+
verified: z.boolean(),
|
|
2993
|
+
network: z.literal("testnet"),
|
|
2994
|
+
credits: z.literal(0).describe("Always 0. Testnet money buys nothing."),
|
|
2995
|
+
amount: z.string().nullable().optional(),
|
|
2996
|
+
sender: z.string().nullable().optional(),
|
|
2997
|
+
explorerUrl: z.string().nullable().optional(),
|
|
2998
|
+
error: z.string().optional(),
|
|
2999
|
+
}),
|
|
3000
|
+
effect: "ask", cost: "free", reversible: true, idempotent: true, settles: "none", auth: "public",
|
|
3001
|
+
examples: [{ chain: "sui", network: "testnet", digest: "8xKvQ1", address: "0xabc" }],
|
|
3002
|
+
}),
|
|
3003
|
+
// ── The wallet estate, declared ────────────────────────────────────────────
|
|
3004
|
+
//
|
|
3005
|
+
// wallet:get / :send / :transactions / :invoice have run in production since
|
|
3006
|
+
// wallets-workspace shipped (handlers: resolvers/wallet.ts:643) with NO entry
|
|
3007
|
+
// here at all. Undeclared, they were invisible to `meta:catalog`, absent from
|
|
3008
|
+
// MCP, and passed through the /api/ask edge with no schema gate whatsoever.
|
|
3009
|
+
//
|
|
3010
|
+
// DECLARING A LIVE RECEIVER IS NOT FREE. The ask route dispatches
|
|
3011
|
+
// `validation.payload` — zod's PARSED output (pages/api/ask/[...receiver].ts:358)
|
|
3012
|
+
// — so a field a real caller sends and a schema here omits is SILENTLY
|
|
3013
|
+
// STRIPPED before the handler reads it. Every field below was read back off
|
|
3014
|
+
// the resolver and off its callers (packages/cli/src/wallet.ts,
|
|
3015
|
+
// template/site/src/pages/wallet.astro). Add a field to a handler ⇒ add it
|
|
3016
|
+
// here in the same diff, or the handler stops seeing it.
|
|
3017
|
+
//
|
|
3018
|
+
// `surfaces: { mcp: true }` is what actually creates the MCP tool.
|
|
3019
|
+
// `mcpToolsFromRegistry()` is default-closed (`if (!mcp) continue`,
|
|
3020
|
+
// packages/mcp/src/tools/from-registry.ts:79), so the declaration alone would
|
|
3021
|
+
// have left MCP with zero wallet tools — the exact gap this cycle closes.
|
|
3022
|
+
"wallet:get": receiver({
|
|
3023
|
+
receiver: "wallet:get",
|
|
3024
|
+
surfaces: { mcp: true },
|
|
3025
|
+
summary: "Read one workspace's wallet estate — credits, spend ceiling, wallet rows with payTo, live chain balances, delegated wallets",
|
|
3026
|
+
request: z.object({
|
|
3027
|
+
workspace: z.string().optional().describe("Defaults to the attested caller's workspace"),
|
|
3028
|
+
actor: z.string().optional().describe("Actor whose spend ceiling is read; defaults to the workspace"),
|
|
3029
|
+
}),
|
|
3030
|
+
response: z.object({
|
|
3031
|
+
workspace: z.string().optional(),
|
|
3032
|
+
actor: z.string().optional(),
|
|
3033
|
+
credits: z.number().optional(),
|
|
3034
|
+
wallets: z.array(z.record(z.string(), z.unknown())).optional(),
|
|
3035
|
+
spendCeiling: z.object({
|
|
3036
|
+
limitCredits: z.number(), spentCredits: z.number(), remainingCredits: z.number(),
|
|
3037
|
+
}).nullable().optional(),
|
|
3038
|
+
balances: z.array(z.object({
|
|
3039
|
+
actor: z.string(), chain: z.string(), address: z.string(), balance: z.string().nullable(),
|
|
3040
|
+
})).optional(),
|
|
3041
|
+
addresses: z.array(z.object({ actor: z.string(), chain: z.string(), address: z.string() })).optional(),
|
|
3042
|
+
delegated: z.array(z.record(z.string(), z.unknown())).optional(),
|
|
3043
|
+
error: z.string().optional(),
|
|
3044
|
+
}),
|
|
3045
|
+
effect: "ask", cost: "free", reversible: true, idempotent: true, settles: "none", auth: "member",
|
|
3046
|
+
examples: [{}, { workspace: "acme" }, { workspace: "acme", actor: "agent:scout" }],
|
|
3047
|
+
}),
|
|
3048
|
+
"wallet:transactions": receiver({
|
|
3049
|
+
receiver: "wallet:transactions",
|
|
3050
|
+
surfaces: { mcp: true },
|
|
3051
|
+
summary: "The unified settlement feed for one workspace — every rail, money in and out, newest first",
|
|
3052
|
+
request: z.object({
|
|
3053
|
+
workspace: z.string().optional().describe("Defaults to the attested caller's workspace"),
|
|
3054
|
+
limit: z.number().optional().describe("Rows to return; the handler caps at 500"),
|
|
3055
|
+
}),
|
|
3056
|
+
response: z.object({
|
|
3057
|
+
transactions: z.array(z.object({
|
|
3058
|
+
ts: z.number(), rail: z.string(), direction: z.enum(["in", "out"]), counterparty: z.string(),
|
|
3059
|
+
amountCents: z.number(), currency: z.string(), ref: z.string(), status: z.string(),
|
|
3060
|
+
})).optional(),
|
|
3061
|
+
error: z.string().optional(),
|
|
3062
|
+
}),
|
|
3063
|
+
effect: "ask", cost: "free", reversible: true, idempotent: true, settles: "none", auth: "member",
|
|
3064
|
+
examples: [{ limit: 20 }, { workspace: "acme", limit: 100 }],
|
|
3065
|
+
}),
|
|
3066
|
+
// The caller signs and broadcasts the transfer CLIENT-SIDE and passes the
|
|
3067
|
+
// resulting `paymentTx`; this composes pay.one.ie's create → quote → claim and
|
|
3068
|
+
// never holds a key. It VERIFIES that an on-chain transfer settled, so it
|
|
3069
|
+
// declares `settles: "onchain"` and `reversible: false`. A catalog that said
|
|
3070
|
+
// otherwise would put a lie where the omission used to be.
|
|
3071
|
+
"wallet:send": receiver({
|
|
3072
|
+
receiver: "wallet:send",
|
|
3073
|
+
surfaces: { mcp: true },
|
|
3074
|
+
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",
|
|
3075
|
+
request: z.object({
|
|
3076
|
+
to: z.string().describe("Recipient address — public only"),
|
|
3077
|
+
chain: z.string().describe("SOL | ETH | BASE | ARB | OPT | BTC | SUI"),
|
|
3078
|
+
amount: z.number().describe("Must be > 0"),
|
|
3079
|
+
paymentTx: z.string().describe("The tx hash from signing and broadcasting client-side"),
|
|
3080
|
+
workspace: z.string().optional().describe("Source workspace; falls back to `from`, then the attested caller"),
|
|
3081
|
+
from: z.string().optional().describe("Alias for `workspace` — the source the authority walk runs against"),
|
|
3082
|
+
unit: z.enum(["usd", "token"]).optional(),
|
|
3083
|
+
currency: z.enum(["NATIVE", "USDC"]).optional(),
|
|
3084
|
+
userAddress: z.string().optional().describe("Sender address; defaults to `to`"),
|
|
3085
|
+
memo: z.string().optional(),
|
|
3086
|
+
product: z.string().optional().describe("Falls back to `memo`, then a generated transfer label"),
|
|
3087
|
+
}),
|
|
3088
|
+
response: z.object({
|
|
3089
|
+
ok: z.boolean(),
|
|
3090
|
+
from: z.string().optional(),
|
|
3091
|
+
to: z.string().optional(),
|
|
3092
|
+
chain: z.string().optional(),
|
|
3093
|
+
amount: z.number().optional(),
|
|
3094
|
+
link: z.string().optional(),
|
|
3095
|
+
quoteId: z.string().optional(),
|
|
3096
|
+
paymentTx: z.string().optional(),
|
|
3097
|
+
treasury: z.string().optional(),
|
|
3098
|
+
receipt: z.object({ id: z.string(), url: z.string() }).optional(),
|
|
3099
|
+
claimed: z.boolean().optional(),
|
|
3100
|
+
stage: z.string().optional().describe("Which pay call failed: create | quote | claim"),
|
|
3101
|
+
error: z.string().optional(),
|
|
3102
|
+
}),
|
|
3103
|
+
effect: "ask", cost: "variable", reversible: false, idempotent: false, settles: "onchain", auth: "manage_workspace",
|
|
3104
|
+
examples: [{ to: "0xabc", chain: "SUI", amount: 5, paymentTx: "8xKvQ1" }],
|
|
3105
|
+
}),
|
|
3106
|
+
// A pending, attributed `settlements` row is written before a cent moves —
|
|
3107
|
+
// INSERT OR IGNORE keyed on the pay link id, so a retry with the same link is a
|
|
3108
|
+
// no-op. An unpayable actor is a clean refusal: no link minted, no row written.
|
|
3109
|
+
"wallet:invoice": receiver({
|
|
3110
|
+
receiver: "wallet:invoice",
|
|
3111
|
+
surfaces: { mcp: true },
|
|
3112
|
+
summary: "Hand a payable actor a working payment link, with the money attributed to it before a cent moves",
|
|
3113
|
+
request: z.object({
|
|
3114
|
+
actor: z.string().describe("The actor being paid — attributed here even when custody is via-treasury"),
|
|
3115
|
+
amountCents: z.number().describe("Must be > 0"),
|
|
3116
|
+
workspace: z.string().optional().describe("Defaults to the attested caller's workspace"),
|
|
3117
|
+
currency: z.string().optional().describe("Defaults to usd"),
|
|
3118
|
+
}),
|
|
3119
|
+
response: z.object({
|
|
3120
|
+
url: z.string().optional(),
|
|
3121
|
+
linkId: z.string().optional(),
|
|
3122
|
+
payTo: z.object({
|
|
3123
|
+
// An OPEN string, never an enum. 'self' | 'via-treasury' | 'none' today;
|
|
3124
|
+
// wallet-authority's ScopedWallet adds 'scoped' (payee = an object id,
|
|
3125
|
+
// no chain), and that must not need a registry change to be
|
|
3126
|
+
// representable. An UNKNOWN value is refused BY NAME at payee resolution
|
|
3127
|
+
// (`unsupported_custody:<value>`, resolvers/wallet.ts) — never a crash,
|
|
3128
|
+
// never a silent fallback to 'self' or to the SUI rail.
|
|
3129
|
+
custody: z.string(),
|
|
3130
|
+
chain: z.string().optional(),
|
|
3131
|
+
address: z.string().optional(),
|
|
3132
|
+
attributeTo: z.string().optional(),
|
|
3133
|
+
}).optional(),
|
|
3134
|
+
error: z.string().optional(),
|
|
3135
|
+
}),
|
|
3136
|
+
effect: "ask", cost: "free", reversible: true, idempotent: true, settles: "none", auth: "manage_workspace",
|
|
3137
|
+
examples: [
|
|
3138
|
+
{ actor: "agent:scout", amountCents: 2500 },
|
|
3139
|
+
{ workspace: "acme", actor: "agent:scout", amountCents: 2500, currency: "usd" },
|
|
3140
|
+
],
|
|
3141
|
+
}),
|
|
3142
|
+
// wallet:portfolio — the authority walk projected onto wallet rows. One call
|
|
3143
|
+
// answers "what does my whole estate hold", which no client could ask before:
|
|
3144
|
+
// every wallet read was scoped to one workspace at a time.
|
|
3145
|
+
//
|
|
3146
|
+
// Deliberately NO chain balances. `wallet:get` fans out one RPC per address,
|
|
3147
|
+
// which is right for one workspace and quadratic across an estate. Portfolio
|
|
3148
|
+
// NAMES the estate; `wallet:get` prices one workspace in it.
|
|
3149
|
+
"wallet:portfolio": receiver({
|
|
3150
|
+
receiver: "wallet:portfolio",
|
|
3151
|
+
surfaces: { mcp: true },
|
|
3152
|
+
summary: "Every wallet row in every workspace the caller controls — one call, no per-address chain reads",
|
|
3153
|
+
request: z.object({
|
|
3154
|
+
limit: z.number().optional().describe("Max child workspaces to include; capped at 100"),
|
|
3155
|
+
}),
|
|
3156
|
+
response: z.object({
|
|
3157
|
+
caller: z.string().optional(),
|
|
3158
|
+
workspaces: z.array(z.object({
|
|
3159
|
+
workspace: z.string(),
|
|
3160
|
+
wallets: z.array(z.record(z.string(), z.unknown())),
|
|
3161
|
+
})).optional(),
|
|
3162
|
+
count: z.number().optional(),
|
|
3163
|
+
error: z.string().optional(),
|
|
3164
|
+
}),
|
|
3165
|
+
effect: "ask", cost: "free", reversible: true, idempotent: true, settles: "none", auth: "member",
|
|
3166
|
+
examples: [{}, { limit: 25 }],
|
|
3167
|
+
}),
|
|
3168
|
+
// The door that lets a workspace open its own shop. Until this existed, the
|
|
3169
|
+
// only writers of `owners.storefront_enabled` lived under the Stripe Connect
|
|
3170
|
+
// OAuth callbacks, so selling anything required a human with a browser and a
|
|
3171
|
+
// business — including on the crypto rail, which needs no Stripe account at
|
|
3172
|
+
// all (deriveRails gives a treasury-only seller the crypto rail already).
|
|
3173
|
+
// Authorized by the owners-tree walk, so a parent can open a client's shop.
|
|
3174
|
+
"storefront:enable": receiver({
|
|
3175
|
+
receiver: "storefront:enable",
|
|
3176
|
+
summary: "Open (or close) a workspace's storefront — the gate every checkout mint reads",
|
|
3177
|
+
request: z.object({ slug: z.string().optional(), enabled: z.boolean().optional() }),
|
|
3178
|
+
response: z.object({ ok: z.boolean().optional(), slug: z.string().optional(), enabled: z.boolean().optional(), error: z.string().optional() }),
|
|
3179
|
+
effect: "ask", cost: "free", reversible: true, idempotent: true, auth: "member",
|
|
3180
|
+
examples: [{ slug: "acme" }, { slug: "acme", enabled: false }],
|
|
3181
|
+
}),
|
|
2006
3182
|
"storefront:stats": receiver({
|
|
2007
3183
|
receiver: "storefront:stats",
|
|
2008
3184
|
summary: "Storefront funnel stats — views, checkouts, purchases, revenue, per-product breakdown",
|
|
@@ -2020,6 +3196,7 @@ export const RECEIVERS = {
|
|
|
2020
3196
|
// ── view:* — saved views (custom-views C1). A view is a workspace-scoped Thing (world_things type 'view'). ──
|
|
2021
3197
|
"view:create": receiver({
|
|
2022
3198
|
receiver: "view:create",
|
|
3199
|
+
surfaces: { mcp: { name: "create_view" } },
|
|
2023
3200
|
summary: "Save a named view — dimension × tags × kind × config — as a workspace Thing",
|
|
2024
3201
|
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() }),
|
|
2025
3202
|
response: z.object({ ok: z.boolean(), id: z.string().optional(), name: z.string().optional(), error: z.string().optional() }),
|
|
@@ -2027,6 +3204,7 @@ export const RECEIVERS = {
|
|
|
2027
3204
|
}),
|
|
2028
3205
|
"view:list": receiver({
|
|
2029
3206
|
receiver: "view:list",
|
|
3207
|
+
surfaces: { mcp: { name: "list_views" } },
|
|
2030
3208
|
summary: "List a workspace's saved views (cold flag derived at read time)",
|
|
2031
3209
|
request: z.object({ slug: z.string() }),
|
|
2032
3210
|
response: z.object({ views: z.array(z.object({ id: z.string(), name: z.string(), kind: z.string().optional(), pinned: z.boolean(), cold: z.boolean() })) }),
|
|
@@ -2056,6 +3234,7 @@ export const RECEIVERS = {
|
|
|
2056
3234
|
// ── pages + workspace settings — handlers in resolvers/pages.ts ────────────────
|
|
2057
3235
|
"pages:create": receiver({
|
|
2058
3236
|
receiver: "pages:create",
|
|
3237
|
+
surfaces: { mcp: true },
|
|
2059
3238
|
summary: "Create a workspace page (draft) from a title + sections",
|
|
2060
3239
|
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() }),
|
|
2061
3240
|
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() }),
|
|
@@ -2070,6 +3249,7 @@ export const RECEIVERS = {
|
|
|
2070
3249
|
}),
|
|
2071
3250
|
"pages:list": receiver({
|
|
2072
3251
|
receiver: "pages:list",
|
|
3252
|
+
surfaces: { mcp: true },
|
|
2073
3253
|
summary: "List a workspace's pages with status",
|
|
2074
3254
|
request: z.object({ slug: z.string() }),
|
|
2075
3255
|
response: z.object({ pages: z.array(z.object({ slug: z.string(), title: z.string(), status: z.string() })) }),
|
|
@@ -2180,10 +3360,24 @@ export const RECEIVERS = {
|
|
|
2180
3360
|
response: z.object({ ok: z.boolean(), slug: z.string().optional(), message: z.string().optional(), error: z.string().optional() }),
|
|
2181
3361
|
effect: "ask", cost: "free", reversible: true, idempotent: false, auth: "member",
|
|
2182
3362
|
}),
|
|
3363
|
+
"settings:set-home-page": receiver({
|
|
3364
|
+
receiver: "settings:set-home-page",
|
|
3365
|
+
summary: "Name which published page a workspace serves at its root",
|
|
3366
|
+
request: z.object({ slug: z.string(), page: z.string() }),
|
|
3367
|
+
response: z.object({ ok: z.boolean(), page: z.string().optional(), error: z.string().optional() }),
|
|
3368
|
+
effect: "ask", cost: "free", reversible: true, idempotent: true, auth: "member",
|
|
3369
|
+
}),
|
|
2183
3370
|
"domain:set": receiver({
|
|
2184
3371
|
receiver: "domain:set",
|
|
2185
3372
|
summary: "Bind a custom domain to a workspace — returns the verification TXT/CNAME records to add",
|
|
2186
|
-
request: z.object({
|
|
3373
|
+
request: z.object({
|
|
3374
|
+
slug: z.string(),
|
|
3375
|
+
// DERIVED, never sent: "the body used to carry `actorId`" — it was removed
|
|
3376
|
+
// by the IDOR fix in resolvers/pages.ts, which authorizes from the ATTESTED
|
|
3377
|
+
// caller only. Requiring it here demanded a field the fix stopped trusting.
|
|
3378
|
+
actorId: z.string().optional(),
|
|
3379
|
+
host: z.string(),
|
|
3380
|
+
}),
|
|
2187
3381
|
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() }),
|
|
2188
3382
|
effect: "ask", cost: "free", reversible: true, idempotent: false, auth: "verify_domain",
|
|
2189
3383
|
}),
|
|
@@ -2244,6 +3438,159 @@ export const RECEIVERS = {
|
|
|
2244
3438
|
response: z.object({ ok: z.boolean(), skillRef: z.string().optional() }),
|
|
2245
3439
|
effect: "ask", cost: "free", reversible: true, idempotent: false, auth: "member",
|
|
2246
3440
|
}),
|
|
3441
|
+
// ── company — the COMPANY WORKFLOW's crawl steps, handlers in resolvers/company.ts ──
|
|
3442
|
+
//
|
|
3443
|
+
// The crawl ladder underneath these is plain fetch -> Cloudflare Browser Rendering
|
|
3444
|
+
// (only when the page is a JS shell). `via` names the rung that answered and
|
|
3445
|
+
// `degraded` names why a lower one did, so a caller is never told a partial read
|
|
3446
|
+
// was a full one. None of them writes; foundation:save is the single write.
|
|
3447
|
+
"company:render": receiver({
|
|
3448
|
+
receiver: "company:render",
|
|
3449
|
+
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",
|
|
3450
|
+
request: z.object({ url: z.string(), waitFor: z.number().int().optional() }),
|
|
3451
|
+
response: z.object({
|
|
3452
|
+
ok: z.boolean(),
|
|
3453
|
+
url: z.string().optional(),
|
|
3454
|
+
finalUrl: z.string().optional(),
|
|
3455
|
+
domain: z.string().optional(),
|
|
3456
|
+
via: z.enum(["fetch", "cloudflare"]).optional(),
|
|
3457
|
+
degraded: z.string().optional(),
|
|
3458
|
+
patch: z.unknown().optional(),
|
|
3459
|
+
evidence: z.string().optional(),
|
|
3460
|
+
error: z.string().optional(),
|
|
3461
|
+
}),
|
|
3462
|
+
effect: "ask", cost: "free", reversible: true, idempotent: true, auth: "public",
|
|
3463
|
+
}),
|
|
3464
|
+
"company:map": receiver({
|
|
3465
|
+
receiver: "company:map",
|
|
3466
|
+
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",
|
|
3467
|
+
request: z.object({ url: z.string(), limit: z.number().int().min(1).max(50).optional() }),
|
|
3468
|
+
response: z.object({
|
|
3469
|
+
ok: z.boolean(),
|
|
3470
|
+
url: z.string().optional(),
|
|
3471
|
+
via: z.enum(["fetch", "cloudflare"]).optional(),
|
|
3472
|
+
degraded: z.string().optional(),
|
|
3473
|
+
links: z.unknown().optional(),
|
|
3474
|
+
priority: z.array(z.string()).optional(),
|
|
3475
|
+
count: z.number().optional(),
|
|
3476
|
+
error: z.string().optional(),
|
|
3477
|
+
}),
|
|
3478
|
+
effect: "ask", cost: "free", reversible: true, idempotent: true, auth: "public",
|
|
3479
|
+
}),
|
|
3480
|
+
"company:seo": receiver({
|
|
3481
|
+
receiver: "company:seo",
|
|
3482
|
+
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",
|
|
3483
|
+
request: z.object({ domain: z.string(), slug: z.string().optional() }),
|
|
3484
|
+
response: z.object({
|
|
3485
|
+
ok: z.boolean(),
|
|
3486
|
+
patch: z.unknown().optional(),
|
|
3487
|
+
domain: z.string().optional(),
|
|
3488
|
+
skipped: z.string().optional(),
|
|
3489
|
+
error: z.string().optional(),
|
|
3490
|
+
}),
|
|
3491
|
+
effect: "ask", cost: "variable", reversible: true, idempotent: false, auth: "member",
|
|
3492
|
+
}),
|
|
3493
|
+
// ── foundation — handlers in resolvers/foundation.ts ──────────────────────────
|
|
3494
|
+
"foundation:deep": receiver({
|
|
3495
|
+
receiver: "foundation:deep",
|
|
3496
|
+
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",
|
|
3497
|
+
request: z.object({
|
|
3498
|
+
slug: z.string().optional(),
|
|
3499
|
+
source: z.enum(["inferred", "scrape"]).optional(),
|
|
3500
|
+
}),
|
|
3501
|
+
response: z.object({
|
|
3502
|
+
ok: z.boolean(),
|
|
3503
|
+
slug: z.string().optional(),
|
|
3504
|
+
error: z.string().optional(),
|
|
3505
|
+
skillsRun: z.number().optional(),
|
|
3506
|
+
// EMITTED, not written: a field already at chat/document rank is emitted and
|
|
3507
|
+
// then refused by the merge guard. Naming it "written" would overstate it.
|
|
3508
|
+
fieldsEmitted: z.number().optional(),
|
|
3509
|
+
// Per-skill receipt: what each skill actually did. `outcome` distinguishes
|
|
3510
|
+
// "emitted N fields" from "returned nothing" from "the answer did not parse".
|
|
3511
|
+
skills: z.array(z.object({
|
|
3512
|
+
skill: z.string(),
|
|
3513
|
+
outcome: z.enum(["ok", "empty", "parse_failed", "llm_error"]),
|
|
3514
|
+
bodyFound: z.boolean(),
|
|
3515
|
+
fieldsEmitted: z.number(),
|
|
3516
|
+
fields: z.array(z.string()).optional(),
|
|
3517
|
+
dropped: z.array(z.string()).optional(),
|
|
3518
|
+
status: z.number().optional(),
|
|
3519
|
+
})).optional(),
|
|
3520
|
+
}),
|
|
3521
|
+
effect: "ask", cost: "variable", reversible: false, idempotent: false, auth: "member",
|
|
3522
|
+
}),
|
|
3523
|
+
"foundation:start": receiver({
|
|
3524
|
+
receiver: "foundation:start",
|
|
3525
|
+
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",
|
|
3526
|
+
request: z.object({ slug: z.string().optional() }),
|
|
3527
|
+
response: z.object({ ok: z.boolean(), slug: z.string().optional(), fields: z.number().optional(), error: z.string().optional() }),
|
|
3528
|
+
effect: "ask", cost: "variable", reversible: false, idempotent: false, auth: "member",
|
|
3529
|
+
}),
|
|
3530
|
+
"foundation:read": receiver({
|
|
3531
|
+
receiver: "foundation:read",
|
|
3532
|
+
summary: "Read the workspace's Foundation as stored (null when nothing has been written yet)",
|
|
3533
|
+
request: z.object({ slug: z.string().optional() }),
|
|
3534
|
+
response: z.object({ ok: z.boolean(), slug: z.string().optional(), foundation: z.unknown().optional(), error: z.string().optional() }),
|
|
3535
|
+
effect: "ask", cost: "free", reversible: true, idempotent: true, auth: "member",
|
|
3536
|
+
}),
|
|
3537
|
+
"foundation:derive": receiver({
|
|
3538
|
+
receiver: "foundation:derive",
|
|
3539
|
+
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",
|
|
3540
|
+
request: z.object({}),
|
|
3541
|
+
response: z.object({
|
|
3542
|
+
ok: z.boolean(),
|
|
3543
|
+
slug: z.string().optional(),
|
|
3544
|
+
products: z.unknown().optional(),
|
|
3545
|
+
playbook: z.unknown().optional(),
|
|
3546
|
+
error: z.string().optional(),
|
|
3547
|
+
hint: z.string().optional(),
|
|
3548
|
+
}),
|
|
3549
|
+
effect: "ask", cost: "free", reversible: false, idempotent: false, auth: "member",
|
|
3550
|
+
}),
|
|
3551
|
+
"foundation:extract": receiver({
|
|
3552
|
+
receiver: "foundation:extract",
|
|
3553
|
+
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",
|
|
3554
|
+
request: z.object({ home: z.unknown().optional(), pages: z.unknown().optional(), siteMap: z.unknown().optional() }),
|
|
3555
|
+
response: z.object({
|
|
3556
|
+
ok: z.boolean(),
|
|
3557
|
+
patch: z.unknown().optional(),
|
|
3558
|
+
found: z.unknown().optional(),
|
|
3559
|
+
gaps: z.array(z.string()).optional(),
|
|
3560
|
+
pagesRead: z.number().optional(),
|
|
3561
|
+
pagesFailed: z.number().optional(),
|
|
3562
|
+
degraded: z.string().optional(),
|
|
3563
|
+
fencedEvidence: z.string().optional(),
|
|
3564
|
+
}),
|
|
3565
|
+
effect: "ask", cost: "free", reversible: true, idempotent: true, auth: "public",
|
|
3566
|
+
}),
|
|
3567
|
+
"foundation:save": receiver({
|
|
3568
|
+
receiver: "foundation:save",
|
|
3569
|
+
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",
|
|
3570
|
+
request: z.object({
|
|
3571
|
+
patch: z.unknown().optional(),
|
|
3572
|
+
fill: z.unknown().optional(),
|
|
3573
|
+
seo: z.unknown().optional(),
|
|
3574
|
+
draftToken: z.string().optional(),
|
|
3575
|
+
}),
|
|
3576
|
+
response: z.object({
|
|
3577
|
+
ok: z.boolean(),
|
|
3578
|
+
stored: z.enum(["workspace", "draft", "none"]).optional(),
|
|
3579
|
+
slug: z.string().optional(),
|
|
3580
|
+
draftToken: z.string().optional(),
|
|
3581
|
+
fields: z.number().optional(),
|
|
3582
|
+
gaps: z.array(z.string()).optional(),
|
|
3583
|
+
error: z.string().optional(),
|
|
3584
|
+
}),
|
|
3585
|
+
effect: "ask", cost: "free", reversible: false, idempotent: false, auth: "public",
|
|
3586
|
+
}),
|
|
3587
|
+
"foundation:promote-draft": receiver({
|
|
3588
|
+
receiver: "foundation:promote-draft",
|
|
3589
|
+
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",
|
|
3590
|
+
request: z.object({ draftToken: z.string() }),
|
|
3591
|
+
response: z.object({ ok: z.boolean(), slug: z.string().optional(), fields: z.number().optional(), gaps: z.array(z.string()).optional(), error: z.string().optional() }),
|
|
3592
|
+
effect: "ask", cost: "free", reversible: false, idempotent: true, auth: "member",
|
|
3593
|
+
}),
|
|
2247
3594
|
// ── social + crawl — handlers in resolvers/social.ts ───────────────────────────
|
|
2248
3595
|
"social:publish": receiver({
|
|
2249
3596
|
receiver: "social:publish",
|
|
@@ -2305,7 +3652,7 @@ export const RECEIVERS = {
|
|
|
2305
3652
|
}),
|
|
2306
3653
|
"billing:topup": receiver({
|
|
2307
3654
|
receiver: "billing:topup",
|
|
2308
|
-
summary: "Top up workspace AI credits — returns a checkout URL for the amount",
|
|
3655
|
+
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",
|
|
2309
3656
|
request: z.object({ slug: z.string(), actorId: z.string(), amount: z.number(), currency: z.string().optional() }),
|
|
2310
3657
|
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() }),
|
|
2311
3658
|
effect: "ask", cost: "free", reversible: false, idempotent: false, auth: "update_group",
|
|
@@ -2456,7 +3803,10 @@ export const RECEIVERS = {
|
|
|
2456
3803
|
receiver: "booking:list-bookings",
|
|
2457
3804
|
summary: "Provider-gated: list a workspace's bookings for a month (customer, time, service, status)",
|
|
2458
3805
|
request: z.object({
|
|
2459
|
-
|
|
3806
|
+
// Optional: the comment below is the point — the handler does
|
|
3807
|
+
// `const slug = ctx.ownerSlug` and never reads data.slug. Required, the
|
|
3808
|
+
// binder rejected every call that correctly omitted it.
|
|
3809
|
+
slug: z.string().optional(), // workspace (informational; gate re-scopes to caller)
|
|
2460
3810
|
month: z.string().optional(), // 'YYYY-MM'; omitted → current month
|
|
2461
3811
|
}),
|
|
2462
3812
|
response: z.object({
|
|
@@ -2521,6 +3871,7 @@ export const RECEIVERS = {
|
|
|
2521
3871
|
// and commits nothing — the chat-authoring preview path.
|
|
2522
3872
|
"workflow:list": receiver({
|
|
2523
3873
|
receiver: "workflow:list",
|
|
3874
|
+
surfaces: { mcp: true },
|
|
2524
3875
|
summary: "List a workspace's workflows (D1 mirror); pass templates=true for the public SOP catalog",
|
|
2525
3876
|
request: z.object({
|
|
2526
3877
|
slug: z.string().optional(),
|
|
@@ -2538,6 +3889,7 @@ export const RECEIVERS = {
|
|
|
2538
3889
|
}),
|
|
2539
3890
|
"workflow:get": receiver({
|
|
2540
3891
|
receiver: "workflow:get",
|
|
3892
|
+
surfaces: { mcp: true },
|
|
2541
3893
|
summary: "Fetch one workflow's full graph — steps (kind, config, position) and edges (with conditions)",
|
|
2542
3894
|
request: z.object({ workflowId: z.string() }),
|
|
2543
3895
|
response: z.object({
|
|
@@ -2558,6 +3910,7 @@ export const RECEIVERS = {
|
|
|
2558
3910
|
}),
|
|
2559
3911
|
"workflow:runs": receiver({
|
|
2560
3912
|
receiver: "workflow:runs",
|
|
3913
|
+
surfaces: { mcp: true },
|
|
2561
3914
|
summary: "List recent runs of a workflow (D1 workflow_run) for the monitor + history surfaces",
|
|
2562
3915
|
request: z.object({ workflowId: z.string(), runId: z.string().optional(), limit: z.number().int().min(1).max(200).optional() }),
|
|
2563
3916
|
response: z.object({
|
|
@@ -2574,6 +3927,25 @@ export const RECEIVERS = {
|
|
|
2574
3927
|
effect: "ask", cost: "free", idempotent: true, simulatable: false, auth: "view_workflows",
|
|
2575
3928
|
examples: [{ workflowId: "wf_abc", limit: 20 }, { workflowId: "wf_abc", runId: "run_1" }],
|
|
2576
3929
|
}),
|
|
3930
|
+
"workflow:step-stats": receiver({
|
|
3931
|
+
receiver: "workflow:step-stats",
|
|
3932
|
+
surfaces: { mcp: true },
|
|
3933
|
+
summary: "Per-step run counts and latency for one workflow (aggregated over D1 workflow_run_event) — what the canvas prints on each step card",
|
|
3934
|
+
request: z.object({ workflowId: z.string(), sinceMs: z.number().int().optional() }),
|
|
3935
|
+
response: z.object({
|
|
3936
|
+
stats: z.array(z.object({
|
|
3937
|
+
stepId: z.string(),
|
|
3938
|
+
runs: z.number().int(),
|
|
3939
|
+
failures: z.number().int(),
|
|
3940
|
+
avgMs: z.number().nullable(),
|
|
3941
|
+
maxMs: z.number().nullable(),
|
|
3942
|
+
lastAt: z.number().nullable(),
|
|
3943
|
+
})),
|
|
3944
|
+
error: z.string().optional(),
|
|
3945
|
+
}),
|
|
3946
|
+
effect: "ask", cost: "free", idempotent: true, simulatable: false, auth: "view_workflows",
|
|
3947
|
+
examples: [{ workflowId: "wf_abc" }],
|
|
3948
|
+
}),
|
|
2577
3949
|
"workflow:preview-step": receiver({
|
|
2578
3950
|
receiver: "workflow:preview-step",
|
|
2579
3951
|
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",
|
|
@@ -2593,6 +3965,7 @@ export const RECEIVERS = {
|
|
|
2593
3965
|
}),
|
|
2594
3966
|
"workflow:create": receiver({
|
|
2595
3967
|
receiver: "workflow:create",
|
|
3968
|
+
surfaces: { mcp: true },
|
|
2596
3969
|
summary: "Create a blank workflow (group group-type=workflow) — optionally cloned from a template",
|
|
2597
3970
|
request: z.object({
|
|
2598
3971
|
slug: z.string().optional(),
|
|
@@ -2625,6 +3998,7 @@ export const RECEIVERS = {
|
|
|
2625
3998
|
}),
|
|
2626
3999
|
"workflow:apply-diff": receiver({
|
|
2627
4000
|
receiver: "workflow:apply-diff",
|
|
4001
|
+
surfaces: { mcp: true },
|
|
2628
4002
|
summary: "Apply a WorkflowDiff (add/remove/connect/disconnect/update). simulate=true validates the DAG without persisting",
|
|
2629
4003
|
request: z.object({
|
|
2630
4004
|
workflowId: z.string(),
|
|
@@ -2645,6 +4019,7 @@ export const RECEIVERS = {
|
|
|
2645
4019
|
}),
|
|
2646
4020
|
"workflow:validate": receiver({
|
|
2647
4021
|
receiver: "workflow:validate",
|
|
4022
|
+
surfaces: { mcp: true },
|
|
2648
4023
|
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.",
|
|
2649
4024
|
request: z.object({
|
|
2650
4025
|
workflowId: z.string(),
|
|
@@ -2662,6 +4037,7 @@ export const RECEIVERS = {
|
|
|
2662
4037
|
}),
|
|
2663
4038
|
"workflow:run": receiver({
|
|
2664
4039
|
receiver: "workflow:run",
|
|
4040
|
+
surfaces: { mcp: true },
|
|
2665
4041
|
summary: "Start a run — spawns the WorkflowRun DO, executes each step, marks path strength on traversed edges",
|
|
2666
4042
|
request: z.object({
|
|
2667
4043
|
workflowId: z.string(),
|
|
@@ -2778,6 +4154,7 @@ export const RECEIVERS = {
|
|
|
2778
4154
|
}),
|
|
2779
4155
|
"workflow:stop": receiver({
|
|
2780
4156
|
receiver: "workflow:stop",
|
|
4157
|
+
surfaces: { mcp: true },
|
|
2781
4158
|
summary: "Park a live run at its current step (status → paused); preserves the resume cursor so human:resolve or a re-run continues it",
|
|
2782
4159
|
request: z.object({ runId: z.string() }),
|
|
2783
4160
|
response: z.object({ ok: z.boolean(), status: z.string().optional(), error: z.string().optional() }),
|
|
@@ -2786,6 +4163,7 @@ export const RECEIVERS = {
|
|
|
2786
4163
|
}),
|
|
2787
4164
|
"workflow:trigger": receiver({
|
|
2788
4165
|
receiver: "workflow:trigger",
|
|
4166
|
+
surfaces: { mcp: true },
|
|
2789
4167
|
summary: "Fire every workspace workflow whose trigger step matches a channel source (e.g. webhook:telegram)",
|
|
2790
4168
|
request: z.object({
|
|
2791
4169
|
source: z.string(),
|
|
@@ -2806,6 +4184,7 @@ export const RECEIVERS = {
|
|
|
2806
4184
|
}),
|
|
2807
4185
|
"workflow:update": receiver({
|
|
2808
4186
|
receiver: "workflow:update",
|
|
4187
|
+
surfaces: { mcp: true },
|
|
2809
4188
|
summary: "Rename a workflow or change its status (draft|active|paused)",
|
|
2810
4189
|
request: z.object({ workflowId: z.string(), name: z.string().optional(), status: z.enum(["draft", "active", "paused"]).optional() }),
|
|
2811
4190
|
response: z.object({ ok: z.boolean(), error: z.string().optional() }),
|
|
@@ -2820,11 +4199,44 @@ export const RECEIVERS = {
|
|
|
2820
4199
|
effect: "ask", cost: "free", reversible: false, idempotent: false, auth: "manage_workflows",
|
|
2821
4200
|
examples: [{ workflowId: "wf_abc" }, { rate: 0.1 }, {}],
|
|
2822
4201
|
}),
|
|
4202
|
+
// ── The .tql text form of a workflow (workflows-tql). Export/import as a
|
|
4203
|
+
// single-insert TypeQL document — the graph a canvas draws, as text an
|
|
4204
|
+
// agent can read, diff and hand back. apply-tql never writes D1 itself: it
|
|
4205
|
+
// parses, gates the whole document as an all-add diff, reconciles against
|
|
4206
|
+
// what is persisted, and goes through workflow:apply-diff — the one door.
|
|
4207
|
+
"workflow:tql": receiver({
|
|
4208
|
+
receiver: "workflow:tql",
|
|
4209
|
+
surfaces: { mcp: true },
|
|
4210
|
+
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",
|
|
4211
|
+
request: z.object({ workflowId: z.string() }),
|
|
4212
|
+
response: z.object({ ok: z.boolean(), tql: z.string().optional(), version: z.number().optional(), error: z.string().optional() }),
|
|
4213
|
+
effect: "ask", cost: "free", idempotent: true, simulatable: false, auth: "view_workflows",
|
|
4214
|
+
examples: [{ workflowId: "wf_abc" }],
|
|
4215
|
+
}),
|
|
4216
|
+
"workflow:apply-tql": receiver({
|
|
4217
|
+
receiver: "workflow:apply-tql",
|
|
4218
|
+
surfaces: { mcp: true },
|
|
4219
|
+
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",
|
|
4220
|
+
request: z.object({ workflowId: z.string(), tql: z.string(), simulate: z.boolean().optional(), version: z.number().optional() }),
|
|
4221
|
+
response: z.object({
|
|
4222
|
+
ok: z.boolean(),
|
|
4223
|
+
stepCount: z.number().optional(),
|
|
4224
|
+
idMap: z.record(z.string(), z.string()).optional(),
|
|
4225
|
+
warnings: z.array(z.string()).optional(),
|
|
4226
|
+
notes: z.array(z.string()).optional(),
|
|
4227
|
+
error: z.string().optional(),
|
|
4228
|
+
line: z.number().optional(),
|
|
4229
|
+
detail: z.string().optional(),
|
|
4230
|
+
}),
|
|
4231
|
+
effect: "ask", cost: "free", reversible: false, idempotent: false, simulatable: true, auth: "manage_workflows",
|
|
4232
|
+
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 }],
|
|
4233
|
+
}),
|
|
2823
4234
|
// ── The autonomy ladder's leaf runners — a workflow skill/agent step compiles
|
|
2824
4235
|
// to one of these (workflow-executor.ts stepBinding). Both proxy to the
|
|
2825
4236
|
// channels runtime; web owns the catalog + tenant authority (workflows-lock C2/C3).
|
|
2826
4237
|
"skills:list": receiver({
|
|
2827
4238
|
receiver: "skills:list",
|
|
4239
|
+
surfaces: { mcp: true },
|
|
2828
4240
|
summary: "List the caller's workspace skill catalog (R2) — powers the canvas SkillPicker",
|
|
2829
4241
|
request: z.object({ name: z.string().optional() }),
|
|
2830
4242
|
response: z.object({
|
|
@@ -2843,6 +4255,7 @@ export const RECEIVERS = {
|
|
|
2843
4255
|
}),
|
|
2844
4256
|
"skill:run": receiver({
|
|
2845
4257
|
receiver: "skill:run",
|
|
4258
|
+
surfaces: { mcp: true },
|
|
2846
4259
|
summary: "Run a workspace skill — loads its body from R2, runs one bounded turn via channels, returns { text }",
|
|
2847
4260
|
request: z.object({ skill: z.string() }).catchall(z.unknown()), // extra keys = the skill's input
|
|
2848
4261
|
response: z.object({ ok: z.boolean(), text: z.string().optional(), skill: z.string().optional(), error: z.string().optional() }),
|
|
@@ -2851,9 +4264,12 @@ export const RECEIVERS = {
|
|
|
2851
4264
|
}),
|
|
2852
4265
|
"agent:run": receiver({
|
|
2853
4266
|
receiver: "agent:run",
|
|
4267
|
+
surfaces: { mcp: true },
|
|
2854
4268
|
summary: "Invoke a bound actor (optionally skill-constrained) for one bounded turn via channels; returns { text }",
|
|
2855
4269
|
request: z.object({
|
|
2856
|
-
|
|
4270
|
+
// Optional by design: an omitted actorId resolves to the workspace CEO
|
|
4271
|
+
// (C9). Required here, the binder rejected the very call that default exists to serve.
|
|
4272
|
+
actorId: z.string().optional(),
|
|
2857
4273
|
skill: z.string().optional(),
|
|
2858
4274
|
instructions: z.string().optional(),
|
|
2859
4275
|
}).catchall(z.unknown()), // extra keys = the agent's input
|
|
@@ -2889,10 +4305,11 @@ export const RECEIVERS = {
|
|
|
2889
4305
|
examples: [{ workspace: "acme", roomSlug: "algebra-101", name: "Algebra 101", type: "classroom" }],
|
|
2890
4306
|
// Fan-out pilot (actions-fan-out C2): offered to chat via chatToolsFor — no
|
|
2891
4307
|
// curated workspace tool needed; the registry derivation is the whole tool.
|
|
2892
|
-
surfaces: { chat: true },
|
|
4308
|
+
surfaces: { chat: true, mcp: { name: "create_room" } },
|
|
2893
4309
|
}),
|
|
2894
4310
|
"video:delete-room": receiver({
|
|
2895
4311
|
receiver: "video:delete-room",
|
|
4312
|
+
surfaces: { mcp: { name: "delete_room" } },
|
|
2896
4313
|
summary: "Disable a video room and its 100ms counterpart; room is no longer joinable",
|
|
2897
4314
|
request: z.object({ workspace: z.string(), roomSlug: z.string() }),
|
|
2898
4315
|
response: z.object({ ok: z.boolean(), error: z.string().optional() }),
|
|
@@ -2901,6 +4318,7 @@ export const RECEIVERS = {
|
|
|
2901
4318
|
}),
|
|
2902
4319
|
"video:contact-call": receiver({
|
|
2903
4320
|
receiver: "video:contact-call",
|
|
4321
|
+
surfaces: { mcp: { name: "contact_call" } },
|
|
2904
4322
|
summary: "Provision a one-to-one video room for a contact; links the room thread to the contact CRM record",
|
|
2905
4323
|
request: z.object({
|
|
2906
4324
|
workspace: z.string(),
|
|
@@ -2920,6 +4338,7 @@ export const RECEIVERS = {
|
|
|
2920
4338
|
}),
|
|
2921
4339
|
"video:invite": receiver({
|
|
2922
4340
|
receiver: "video:invite",
|
|
4341
|
+
surfaces: { mcp: { name: "invite_to_call" } },
|
|
2923
4342
|
summary: "Generate a tracked /go/ join link for an actor into an existing room",
|
|
2924
4343
|
request: z.object({
|
|
2925
4344
|
workspace: z.string(),
|
|
@@ -2952,10 +4371,11 @@ export const RECEIVERS = {
|
|
|
2952
4371
|
}),
|
|
2953
4372
|
effect: "ask", cost: "free", idempotent: false, auth: "manage_clients",
|
|
2954
4373
|
examples: [{ workspace: "acme", name: "Product Launch", actorIds: ["actor_1", "actor_2"] }],
|
|
2955
|
-
surfaces: { chat: true }, // fan-out:
|
|
4374
|
+
surfaces: { chat: true, mcp: { name: "schedule_webinar" } }, // fan-out: chat (actions-fan-out C5) + MCP (agent-native C1)
|
|
2956
4375
|
}),
|
|
2957
4376
|
"video:create-session": receiver({
|
|
2958
4377
|
receiver: "video:create-session",
|
|
4378
|
+
surfaces: { mcp: { name: "create_session" } },
|
|
2959
4379
|
summary: "Create a video session with host + guest tracked join links; optionally schedules the call",
|
|
2960
4380
|
request: z.object({
|
|
2961
4381
|
workspace: z.string(),
|
|
@@ -2991,7 +4411,7 @@ export const RECEIVERS = {
|
|
|
2991
4411
|
}),
|
|
2992
4412
|
effect: "ask", cost: "variable", idempotent: false, auth: "manage_clients",
|
|
2993
4413
|
examples: [{ threadId: "thr_abc", transcript: "Host: Hello... Guest: Hi..." }],
|
|
2994
|
-
surfaces: { chat: true }, // fan-out:
|
|
4414
|
+
surfaces: { chat: true, mcp: { name: "video_summary" } }, // fan-out: chat (actions-fan-out C5) + MCP (agent-native C1)
|
|
2995
4415
|
}),
|
|
2996
4416
|
"video:quick-room": receiver({
|
|
2997
4417
|
receiver: "video:quick-room",
|
|
@@ -3012,7 +4432,43 @@ export const RECEIVERS = {
|
|
|
3012
4432
|
effect: "ask", cost: "free", idempotent: false, auth: "manage_clients",
|
|
3013
4433
|
examples: [{ workspace: "acme", threadId: "thr_xyz", name: "Quick sync" }],
|
|
3014
4434
|
// Fan-out pilot (actions-fan-out C2).
|
|
3015
|
-
surfaces: { chat: true },
|
|
4435
|
+
surfaces: { chat: true, mcp: { name: "quick_call" } },
|
|
4436
|
+
}),
|
|
4437
|
+
// The human-handoff door. A visitor in chat asks for a person; the agent fires
|
|
4438
|
+
// this ONE receiver, which creates (or reuses) the meeting room, drops the
|
|
4439
|
+
// reason into the room's thread, and notifies the workspace wherever it
|
|
4440
|
+
// listens — Telegram/Discord/web inbox/web push, all through `notify`. It is
|
|
4441
|
+
// idempotent per CONVERSATION: the same chat asking twice joins the same room
|
|
4442
|
+
// instead of provisioning a second one.
|
|
4443
|
+
"video:request-meeting": receiver({
|
|
4444
|
+
receiver: "video:request-meeting",
|
|
4445
|
+
summary: "Visitor asks to meet a human: creates/reuses a meeting room for this conversation and notifies the workspace",
|
|
4446
|
+
request: z.object({
|
|
4447
|
+
workspace: z.string(),
|
|
4448
|
+
conversation: z.string().optional(), // chat group/thread id — the idempotency key
|
|
4449
|
+
roomSlug: z.string().optional(), // an EXISTING room to send them to (the standing front desk); never creates
|
|
4450
|
+
visitorName: z.string().optional(),
|
|
4451
|
+
reason: z.string().optional(), // what they want to talk about
|
|
4452
|
+
channel: z.string().optional(), // web | telegram | discord | api
|
|
4453
|
+
}),
|
|
4454
|
+
response: z.object({
|
|
4455
|
+
ok: z.boolean(),
|
|
4456
|
+
roomSlug: z.string().optional(),
|
|
4457
|
+
workspace: z.string().optional(),
|
|
4458
|
+
name: z.string().optional(),
|
|
4459
|
+
threadId: z.string().nullable().optional(),
|
|
4460
|
+
path: z.string().optional(), // /u/<workspace>/meet/<roomSlug>
|
|
4461
|
+
reused: z.boolean().optional(), // an existing room for this conversation
|
|
4462
|
+
notified: z.boolean().optional(),
|
|
4463
|
+
error: z.string().optional(), // 'invalid_request' | 'forbidden' | 'rate_limited' | …
|
|
4464
|
+
}),
|
|
4465
|
+
effect: "ask", cost: "free", idempotent: true, auth: "manage_clients",
|
|
4466
|
+
examples: [{ workspace: "acme", conversation: "conv:abc123", visitorName: "Sarah", reason: "pricing for 20 seats" }],
|
|
4467
|
+
// Deliberately NOT `surfaces: { chat: true }`. The registry-derived tool
|
|
4468
|
+
// (from-registry.ts) would be a second, unshaped door onto the same
|
|
4469
|
+
// receiver — no `public` flag (so no visitor could reach it), no room block
|
|
4470
|
+
// on the way back, and a name the model would pick by accident. The curated
|
|
4471
|
+
// `meet_human` (channels/src/tools/meetings.ts) is the chat surface.
|
|
3016
4472
|
}),
|
|
3017
4473
|
"video:join-event": receiver({
|
|
3018
4474
|
receiver: "video:join-event",
|
|
@@ -3038,7 +4494,29 @@ export const RECEIVERS = {
|
|
|
3038
4494
|
}),
|
|
3039
4495
|
effect: "ask", cost: "free", idempotent: false, auth: "manage_clients",
|
|
3040
4496
|
examples: [{ workspace: "acme", roomSlug: "algebra-101" }],
|
|
3041
|
-
surfaces: { chat: true }, // fan-out:
|
|
4497
|
+
surfaces: { chat: true, mcp: { name: "start_recording" } }, // fan-out: chat (actions-fan-out C5) + MCP (agent-native C1)
|
|
4498
|
+
}),
|
|
4499
|
+
// video:room-status — the presence read, one lib function behind two doors.
|
|
4500
|
+
// NOT flagged surfaces.mcp on purpose: packages/mcp still hand-writes
|
|
4501
|
+
// `get_room_status` over this receiver, and flagging it here would derive a
|
|
4502
|
+
// second tool of the same name. Flag it in the same change that deletes that file.
|
|
4503
|
+
"video:room-status": receiver({
|
|
4504
|
+
receiver: "video:room-status",
|
|
4505
|
+
summary: "Who is in a room right now — staff names, a count of everyone else, and when the first staff peer joined",
|
|
4506
|
+
request: z.object({ workspace: z.string(), roomSlug: z.string() }),
|
|
4507
|
+
response: z.object({
|
|
4508
|
+
ok: z.boolean(),
|
|
4509
|
+
// `unknown` is a first-class answer: an upstream that could not be read is
|
|
4510
|
+
// never reported as an empty room.
|
|
4511
|
+
state: z.enum(["live", "empty", "unknown"]).optional(),
|
|
4512
|
+
staff: z.array(z.string()).optional(),
|
|
4513
|
+
others: z.number().optional(),
|
|
4514
|
+
since: z.string().nullable().optional(),
|
|
4515
|
+
reason: z.string().optional(),
|
|
4516
|
+
error: z.string().optional(),
|
|
4517
|
+
}),
|
|
4518
|
+
effect: "ask", cost: "free", idempotent: true, auth: "manage_clients",
|
|
4519
|
+
examples: [{ workspace: "acme", roomSlug: "meeting" }],
|
|
3042
4520
|
}),
|
|
3043
4521
|
"video:start-stream": receiver({
|
|
3044
4522
|
receiver: "video:start-stream",
|
|
@@ -3055,7 +4533,7 @@ export const RECEIVERS = {
|
|
|
3055
4533
|
}),
|
|
3056
4534
|
effect: "ask", cost: "free", idempotent: false, auth: "manage_clients",
|
|
3057
4535
|
examples: [{ workspace: "acme", roomSlug: "product-launch" }],
|
|
3058
|
-
surfaces: { chat: true }, // fan-out:
|
|
4536
|
+
surfaces: { chat: true, mcp: { name: "start_stream" } }, // fan-out: chat (actions-fan-out C5) + MCP (agent-native C1)
|
|
3059
4537
|
}),
|
|
3060
4538
|
// connect:channel — token-paste connect for a channel (Connect plan C7). The
|
|
3061
4539
|
// typed contract over the shipped /api/connect/telegram route: the caller pastes
|
|
@@ -3108,6 +4586,7 @@ export const RECEIVERS = {
|
|
|
3108
4586
|
// ── Broadcast (newsletter) ────────────────────────────────────────────────
|
|
3109
4587
|
"broadcast:create": receiver({
|
|
3110
4588
|
receiver: "broadcast:create",
|
|
4589
|
+
surfaces: { mcp: true },
|
|
3111
4590
|
summary: "Create a broadcast draft — subject, body_md, audience_tag, channel",
|
|
3112
4591
|
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() }),
|
|
3113
4592
|
response: z.object({ broadcastId: z.string() }),
|
|
@@ -3115,6 +4594,7 @@ export const RECEIVERS = {
|
|
|
3115
4594
|
}),
|
|
3116
4595
|
"broadcast:update": receiver({
|
|
3117
4596
|
receiver: "broadcast:update",
|
|
4597
|
+
surfaces: { mcp: { name: "newsletter_update" } },
|
|
3118
4598
|
summary: "Update a broadcast draft — subject, body, audience, schedule",
|
|
3119
4599
|
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() }),
|
|
3120
4600
|
response: z.object({ ok: z.boolean() }),
|
|
@@ -3122,6 +4602,7 @@ export const RECEIVERS = {
|
|
|
3122
4602
|
}),
|
|
3123
4603
|
"broadcast:list": receiver({
|
|
3124
4604
|
receiver: "broadcast:list",
|
|
4605
|
+
surfaces: { mcp: true },
|
|
3125
4606
|
summary: "List broadcasts for a workspace with status and sent_at",
|
|
3126
4607
|
request: z.object({ workspace: z.string(), status: z.enum(["draft", "scheduled", "sending", "sent", "cancelled"]).optional(), limit: z.number().int().max(100).optional() }),
|
|
3127
4608
|
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() })) }),
|
|
@@ -3129,6 +4610,7 @@ export const RECEIVERS = {
|
|
|
3129
4610
|
}),
|
|
3130
4611
|
"broadcast:get": receiver({
|
|
3131
4612
|
receiver: "broadcast:get",
|
|
4613
|
+
surfaces: { mcp: true },
|
|
3132
4614
|
summary: "Get a single broadcast with recipient counts",
|
|
3133
4615
|
request: z.object({ workspace: z.string(), broadcastId: z.string() }),
|
|
3134
4616
|
response: z.object({ broadcast: z.record(z.string(), z.unknown()) }),
|
|
@@ -3150,6 +4632,7 @@ export const RECEIVERS = {
|
|
|
3150
4632
|
}),
|
|
3151
4633
|
"segment:list": receiver({
|
|
3152
4634
|
receiver: "segment:list",
|
|
4635
|
+
surfaces: { mcp: true },
|
|
3153
4636
|
summary: "List audience segments for a workspace",
|
|
3154
4637
|
request: z.object({ workspace: z.string() }),
|
|
3155
4638
|
response: z.object({ segments: z.array(z.record(z.string(), z.unknown())) }),
|
|
@@ -3157,6 +4640,7 @@ export const RECEIVERS = {
|
|
|
3157
4640
|
}),
|
|
3158
4641
|
"segment:get": receiver({
|
|
3159
4642
|
receiver: "segment:get",
|
|
4643
|
+
surfaces: { mcp: true },
|
|
3160
4644
|
summary: "Get one audience segment with its rule definition",
|
|
3161
4645
|
request: z.object({ workspace: z.string(), id: z.string() }),
|
|
3162
4646
|
response: z.object({ segment: z.record(z.string(), z.unknown()) }),
|
|
@@ -3164,6 +4648,7 @@ export const RECEIVERS = {
|
|
|
3164
4648
|
}),
|
|
3165
4649
|
"segment:preview": receiver({
|
|
3166
4650
|
receiver: "segment:preview",
|
|
4651
|
+
surfaces: { mcp: true },
|
|
3167
4652
|
summary: "Preview an audience segment — live count + up to 10 sample addresses (read-only)",
|
|
3168
4653
|
request: z.object({ workspace: z.string(), definition: z.record(z.string(), z.unknown()), channel: z.enum(["email", "sms", "whatsapp"]).optional() }),
|
|
3169
4654
|
response: z.object({ count: z.number(), sample: z.array(z.string()) }),
|
|
@@ -3185,6 +4670,7 @@ export const RECEIVERS = {
|
|
|
3185
4670
|
}),
|
|
3186
4671
|
"broadcast:send": receiver({
|
|
3187
4672
|
receiver: "broadcast:send",
|
|
4673
|
+
surfaces: { mcp: true },
|
|
3188
4674
|
summary: "Seed recipients from audience tag, apply suppression, enqueue the send batch",
|
|
3189
4675
|
request: z.object({ workspace: z.string(), broadcastId: z.string() }),
|
|
3190
4676
|
response: z.object({ enqueued: z.number() }),
|
|
@@ -3287,18 +4773,21 @@ export const RECEIVERS = {
|
|
|
3287
4773
|
// ── Send Link — actor-bound tracked URL (CRM personalisation) ──────────────
|
|
3288
4774
|
"links:create": receiver({
|
|
3289
4775
|
receiver: "links:create",
|
|
3290
|
-
summary: "Create
|
|
4776
|
+
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.",
|
|
3291
4777
|
request: z.object({
|
|
3292
|
-
actorId: z.string().describe("Contact (actor) the link is bound to — must belong to the caller's workspace or a descendant"),
|
|
4778
|
+
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"),
|
|
3293
4779
|
destination: z.string().optional().describe("Path the click lands on (must start with '/'); default '/'"),
|
|
3294
4780
|
context: z.string().optional().describe("Extra context injected into the agent's system prompt for this session"),
|
|
3295
4781
|
greeting: z.string().optional().describe("Override the chat agent's opening line"),
|
|
3296
|
-
campaignId: z.string().optional().describe("Group clicks under a campaign for the learning loop"),
|
|
4782
|
+
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"),
|
|
3297
4783
|
expiresInDays: z.number().optional().describe("Link TTL in days; omitted = never expires"),
|
|
3298
4784
|
}),
|
|
3299
4785
|
response: z.object({ id: z.string(), sig: z.string(), url: z.string() }),
|
|
3300
4786
|
effect: "ask", cost: "free", reversible: false, idempotent: false, auth: "member",
|
|
3301
|
-
examples: [
|
|
4787
|
+
examples: [
|
|
4788
|
+
{ actorId: "u-123", destination: "/pricing", expiresInDays: 90 },
|
|
4789
|
+
{ campaignId: "spring-ads", destination: "/movers", expiresInDays: 30 },
|
|
4790
|
+
],
|
|
3302
4791
|
}),
|
|
3303
4792
|
"links:bulk-create": receiver({
|
|
3304
4793
|
receiver: "links:bulk-create",
|
|
@@ -3480,9 +4969,25 @@ export const RECEIVERS = {
|
|
|
3480
4969
|
}),
|
|
3481
4970
|
effect: "ask", auth: "public", cost: "free", reversible: false, idempotent: true,
|
|
3482
4971
|
}),
|
|
4972
|
+
// ── health — the doctor. Handler in resolvers/health.ts ───────────────────
|
|
4973
|
+
"health:diagnose": receiver({
|
|
4974
|
+
receiver: "health:diagnose",
|
|
4975
|
+
summary: "Probe every deployed surface and the receiver failure rates D1 already records, then write the verdict back as weighted paths (mark on up, warn on down or on a sick receiver) so the next sweep starts from what the last one learned. Diagnoses only — it never restarts, redeploys or kills anything, because the remote half has no provably safe remedy. Fired by the health-tick cron trigger.",
|
|
4976
|
+
request: z.object({}),
|
|
4977
|
+
response: z.object({
|
|
4978
|
+
ok: z.boolean(),
|
|
4979
|
+
verdict: z.enum(["healthy", "degraded", "unhealthy"]).optional(),
|
|
4980
|
+
symptoms: z.array(z.string()).optional(),
|
|
4981
|
+
surfaces: z.array(z.unknown()).optional(),
|
|
4982
|
+
sick: z.array(z.unknown()).optional(),
|
|
4983
|
+
checkedAt: z.string().optional(),
|
|
4984
|
+
}),
|
|
4985
|
+
effect: "ask", auth: "public", cost: "free", reversible: false, idempotent: true,
|
|
4986
|
+
}),
|
|
3483
4987
|
// ── seo — handlers in resolvers/seo.ts (C1 live; C2 async) ─────────────────
|
|
3484
4988
|
"seo:backlinks-summary": receiver({
|
|
3485
4989
|
receiver: "seo:backlinks-summary",
|
|
4990
|
+
surfaces: { mcp: { name: "seo_backlinks" } },
|
|
3486
4991
|
summary: "Pull live backlink summary for a domain via DataForSEO; marks domain→seo:backlinks path",
|
|
3487
4992
|
request: z.object({ target: z.string(), limit: z.number().int().min(1).max(1000).optional() }),
|
|
3488
4993
|
response: z.object({ ok: z.boolean(), data: z.unknown().optional(), error: z.string().optional() }),
|
|
@@ -3491,6 +4996,7 @@ export const RECEIVERS = {
|
|
|
3491
4996
|
}),
|
|
3492
4997
|
"seo:llm-mentions": receiver({
|
|
3493
4998
|
receiver: "seo:llm-mentions",
|
|
4999
|
+
surfaces: { mcp: { name: "seo_ai_visibility" } },
|
|
3494
5000
|
summary: "Pull live AI-visibility (LLM mentions) data for a domain via DataForSEO; marks domain→seo:llm-mentions path",
|
|
3495
5001
|
request: z.object({ target: z.string() }),
|
|
3496
5002
|
response: z.object({ ok: z.boolean(), data: z.unknown().optional(), error: z.string().optional() }),
|
|
@@ -3499,6 +5005,7 @@ export const RECEIVERS = {
|
|
|
3499
5005
|
}),
|
|
3500
5006
|
"seo:keywords-for-keyword": receiver({
|
|
3501
5007
|
receiver: "seo:keywords-for-keyword",
|
|
5008
|
+
surfaces: { mcp: { name: "seo_research_keywords" } },
|
|
3502
5009
|
summary: "Return related keyword suggestions for a seed keyword via DataForSEO Google Ads; marks keyword→seo:keywords path",
|
|
3503
5010
|
request: z.object({ keyword: z.string(), location_code: z.number().int().optional(), language_code: z.string().optional() }),
|
|
3504
5011
|
response: z.object({ ok: z.boolean(), data: z.unknown().optional(), error: z.string().optional() }),
|
|
@@ -3507,6 +5014,7 @@ export const RECEIVERS = {
|
|
|
3507
5014
|
}),
|
|
3508
5015
|
"seo:serp": receiver({
|
|
3509
5016
|
receiver: "seo:serp",
|
|
5017
|
+
surfaces: { mcp: true },
|
|
3510
5018
|
summary: "Return full Google Organic SERP for a keyword via DataForSEO async task (submit→poll); marks keyword→seo:serp path",
|
|
3511
5019
|
request: z.object({ keyword: z.string(), location_code: z.number().int().optional(), language_code: z.string().optional(), device: z.string().optional() }),
|
|
3512
5020
|
response: z.object({ ok: z.boolean(), data: z.unknown().optional(), error: z.string().optional() }),
|
|
@@ -3515,6 +5023,7 @@ export const RECEIVERS = {
|
|
|
3515
5023
|
}),
|
|
3516
5024
|
"seo:keyword-metrics": receiver({
|
|
3517
5025
|
receiver: "seo:keyword-metrics",
|
|
5026
|
+
surfaces: { mcp: true },
|
|
3518
5027
|
summary: "Return monthly search volume, CPC, and competition for a list of keywords via DataForSEO async task",
|
|
3519
5028
|
request: z.object({ keywords: z.array(z.string()), location_code: z.number().int().optional(), language_code: z.string().optional() }),
|
|
3520
5029
|
response: z.object({ ok: z.boolean(), data: z.unknown().optional(), error: z.string().optional() }),
|
|
@@ -3523,6 +5032,7 @@ export const RECEIVERS = {
|
|
|
3523
5032
|
}),
|
|
3524
5033
|
"seo:gsc-performance": receiver({
|
|
3525
5034
|
receiver: "seo:gsc-performance",
|
|
5035
|
+
surfaces: { mcp: { name: "seo_gsc" } },
|
|
3526
5036
|
summary: "Return Google Search Console click/impression/CTR/position data for a site via Composio GSC toolkit",
|
|
3527
5037
|
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() }),
|
|
3528
5038
|
response: z.object({ ok: z.boolean(), data: z.unknown().optional(), error: z.string().optional() }),
|