@cohortapp/agent-sdk 2.9.1 → 2.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/init-maestro.md +16 -9
- package/docs/guides/mac-mini.md +11 -1
- package/docs/runbooks/cohort-cutover.md +16 -0
- package/lib/channels/inbox-item.mjs +4 -0
- package/lib/comms/send-gate.mjs +23 -1
- package/lib/comms/send-gate.test.mjs +24 -0
- package/lib/mcp/server.test.mjs +16 -4
- package/lib/model-router/economics.mjs +53 -1
- package/lib/model-router/economics.test.mjs +76 -0
- package/lib/model-router/resolve.mjs +57 -4
- package/lib/model-router.mjs +95 -8
- package/lib/model-router.test.mjs +305 -5
- package/lib/org/client.mjs +58 -1
- package/lib/org/inbound/project.mjs +9 -5
- package/lib/org/messaging.mjs +6 -1
- package/lib/org/protocol.checksum +1 -1
- package/lib/org/protocol.mjs +176 -3
- package/lib/org/protocol.test.mjs +31 -2
- package/lib/org/resource-tools.mjs +317 -0
- package/lib/org/resource-tools.test.mjs +361 -0
- package/lib/org/tool-access.mjs +176 -0
- package/lib/org/tool-access.test.mjs +144 -0
- package/lib/org/tool-surface.mjs +431 -5
- package/lib/org/tool-surface.test.mjs +385 -8
- package/lib/org/ui-parity.mjs +196 -3
- package/lib/org/ui-parity.test.mjs +126 -7
- package/lib/tool-definitions.js +23 -2
- package/package.json +2 -2
- package/plugins/maestro-skills/.claude-plugin/marketplace.json +1 -1
- package/plugins/maestro-skills/plugin.json +4 -0
- package/plugins/maestro-skills/skills/venture-deliverables.md +176 -0
- package/policies/information-barriers.yaml +34 -7
- package/scripts/ci/check-no-residual-identity.mjs +281 -9
- package/scripts/ci/check-no-residual-identity.test.mjs +115 -2
- package/scripts/cloud-relay/voice/relay-identity.test.mjs +96 -0
- package/scripts/cloud-relay/voice/server.mjs +42 -2
- package/scripts/cost/track-claude-usage-pricing.test.mjs +183 -0
- package/scripts/cost/track-claude-usage.mjs +113 -4
- package/scripts/daemon/agent-daemon.mjs +150 -3
- package/scripts/daemon/agent-daemon.test.mjs +190 -0
- package/scripts/daemon/assurance.mjs +50 -16
- package/scripts/daemon/assurance.test.mjs +39 -1
- package/scripts/daemon/classifier-identity.test.mjs +137 -0
- package/scripts/daemon/classifier.mjs +98 -17
- package/scripts/daemon/deliver.mjs +457 -33
- package/scripts/daemon/deliver.test.mjs +564 -0
- package/scripts/daemon/prompt-builder-preamble.test.mjs +210 -0
- package/scripts/daemon/prompt-builder.mjs +264 -41
- package/scripts/daemon/prompt-builder.test.mjs +5 -5
- package/scripts/daemon/responder-history.test.mjs +18 -2
- package/scripts/daemon/responder.mjs +7 -1
- package/scripts/disclosure_boundaries.py +56 -5
- package/scripts/huddle/huddle-prompt.test.mjs +176 -0
- package/scripts/huddle/huddle-server.mjs +128 -13
- package/scripts/local-triggers/autoupdate.sh +83 -0
- package/scripts/local-triggers/generate-plists.sh +9 -0
- package/scripts/local-triggers/generate-plists.test.mjs +12 -10
- package/scripts/media-generation/brand-clause.test.mjs +135 -0
- package/scripts/media-generation/gemini-image-client.mjs +27 -9
- package/scripts/media-generation/generate-assets.mjs +102 -7
- package/scripts/pre-draft-context.py +91 -15
- package/scripts/spawn-session.sh +36 -6
- package/scripts/test-employer-grounding.py +348 -0
- package/scripts/validate_outbound.py +190 -26
package/lib/org/protocol.mjs
CHANGED
|
@@ -76,6 +76,34 @@ export const FAMILIES = Object.freeze([
|
|
|
76
76
|
// seat can PERSIST one it learned rather than re-deriving it every session.
|
|
77
77
|
// The chain rows carry subject + domain + key only — never the value.
|
|
78
78
|
"preference",
|
|
79
|
+
// GENESIS (2026-08): the founder's pre-commit workspace draft — the ten-stage
|
|
80
|
+
// run, its editable sections, and the conversational planner over them. The
|
|
81
|
+
// family exists so an agent reaches the SAME draft the founder's own form
|
|
82
|
+
// does; every method is a thin shell over the server action behind the
|
|
83
|
+
// button, never a second implementation of its gates.
|
|
84
|
+
//
|
|
85
|
+
// Its chain rows land in the DRAFTING org — the tenant the founder is signed
|
|
86
|
+
// into while they draft — and say only that a mutation happened, by whom, to
|
|
87
|
+
// which section. The content-level provenance is a different spine: the
|
|
88
|
+
// action's own `GenesisFieldEdit` rows, which `bufferGenesisAudit` replays
|
|
89
|
+
// into the NEW workspace's chain at commit. Both are true and neither
|
|
90
|
+
// replaces the other.
|
|
91
|
+
"genesis",
|
|
92
|
+
// ARCHITECTURE (2026-08): the four tier review decisions and the one whole-
|
|
93
|
+
// architecture approval that gate a Genesis commit. Split from `genesis`
|
|
94
|
+
// because these are REVIEW acts over a draft rather than edits to it, and
|
|
95
|
+
// because the same surface outlives the run — the Org › Architecture view
|
|
96
|
+
// reads this state for a workspace that has long since committed.
|
|
97
|
+
"architecture",
|
|
98
|
+
// RESOURCE (2026-08): the venture-deliverable catalogue — `OrgResource`, the
|
|
99
|
+
// workspace's own index of what it has to show for itself (site, deck,
|
|
100
|
+
// one-pager, memo, brand book, film, brand assets, repo). The table has been
|
|
101
|
+
// written by the portal and rendered by `/resources` since the three-tier
|
|
102
|
+
// workforce migration and was reachable from NO agent code path at all: zero
|
|
103
|
+
// hits across hq's methods/, mcp/ and llm-responder/ trees, so the venture's
|
|
104
|
+
// own colleagues could not list, add, correct, reorder or remove one of
|
|
105
|
+
// their own deliverables. This family is that surface.
|
|
106
|
+
"resource",
|
|
79
107
|
]);
|
|
80
108
|
|
|
81
109
|
/**
|
|
@@ -292,7 +320,35 @@ export const METHODS = Object.freeze({
|
|
|
292
320
|
// the transactional lane. Still messaging.write scope: faking "X is
|
|
293
321
|
// composing" in a room is a social write a read-only key must not have. ---
|
|
294
322
|
"messaging.typing": { family: "messaging", scope: "messaging.write", sideEffecting: false },
|
|
323
|
+
// --- messaging.synthesizeVoiceNote (2026-08): a colleague SPEAKS when asked
|
|
324
|
+
// to. hq could already synthesise an agent's voice note, but only as a
|
|
325
|
+
// reflex — it answered a HUMAN's spoken note in kind and nothing else —
|
|
326
|
+
// so an agent asked in TEXT ("send me a voicenote with this update")
|
|
327
|
+
// truthfully answered that it could not. This is the verb that makes the
|
|
328
|
+
// ability reachable: it records the acting seat's OWN designed voice and
|
|
329
|
+
// returns the READY fileId, which `messaging.send` then attaches like any
|
|
330
|
+
// other file (same ACL, same outbound gate, same redacted audit row — no
|
|
331
|
+
// parallel send path). Split in two deliberately: synthesis is a provider
|
|
332
|
+
// round-trip with a 20s timeout and cannot ride hq's 5s transactional
|
|
333
|
+
// lane, so this half is sideEffecting:false and self-audits its own
|
|
334
|
+
// `files`/`media.voice.synthesized` chain row, exactly as `books.ask`
|
|
335
|
+
// does for its model call. messaging.write scope, not read: putting words
|
|
336
|
+
// in a named colleague's mouth is a social write of the first order. ---
|
|
337
|
+
"messaging.synthesizeVoiceNote": { family: "messaging", scope: "messaging.write", sideEffecting: false },
|
|
295
338
|
"messaging.channels": { family: "messaging", scope: "messaging.read", sideEffecting: false },
|
|
339
|
+
// --- messaging.electResponder (2026-08): the SERVER-SIDE "should anyone answer,
|
|
340
|
+
// and who?" election for a multi-participant channel. A fleet daemon calls it
|
|
341
|
+
// with just {messageId, channelId}; hq assembles the RouteInput server-side
|
|
342
|
+
// (roster/trigger/channel, org from the KEY) and runs the SAME response
|
|
343
|
+
// planner the in-process responder uses, returning ONE verdict every seat
|
|
344
|
+
// converges on — replacing each daemon's local, room-blind directed_at_agent
|
|
345
|
+
// gate. messaging.read scope (every paired seat may ask; in DEFAULT_AGENT_
|
|
346
|
+
// SCOPES). Declared sideEffecting:false though it appends: the verdict is a
|
|
347
|
+
// write-once ledger event ("responder"/"responder.election", keyed on
|
|
348
|
+
// messageId) and a CACHE HIT must return with ZERO appends — which the
|
|
349
|
+
// side-effecting exactly-one-append contract forbids — so it self-audits its
|
|
350
|
+
// own chain row like messaging.synthesizeVoiceNote and books.ask do. ---
|
|
351
|
+
"messaging.electResponder": { family: "messaging", scope: "messaging.read", sideEffecting: false },
|
|
296
352
|
// --- calling (SP3): call lifecycle in the human app. NOT reserved-admin. ---
|
|
297
353
|
"calling.start": { family: "calling", scope: "calling.write", sideEffecting: true },
|
|
298
354
|
"calling.join": { family: "calling", scope: "calling.write", sideEffecting: true },
|
|
@@ -424,7 +480,12 @@ export const METHODS = Object.freeze({
|
|
|
424
480
|
// --- file ---
|
|
425
481
|
"file.pin": { family: "file", scope: "org.write", sideEffecting: true, idempotent: true },
|
|
426
482
|
"file.unpin": { family: "file", scope: "org.write", sideEffecting: true },
|
|
427
|
-
|
|
483
|
+
// idempotent: a COMMENT CREATE keyed on the caller's message id. Without
|
|
484
|
+
// the flag hq ignores `x-idempotency-key` entirely (dispatch.ts replays only
|
|
485
|
+
// for `def.idempotent === true`), so a daemon retry after a timeout-post-
|
|
486
|
+
// commit posts the same comment two or three times on the thread. Same
|
|
487
|
+
// reasoning as messaging.send, which has carried the flag from the start.
|
|
488
|
+
"file.addComment": { family: "file", scope: "org.write", sideEffecting: true, idempotent: true },
|
|
428
489
|
"file.listComments": { family: "file", scope: "org.read", sideEffecting: false },
|
|
429
490
|
"file.listPinned": { family: "file", scope: "org.read", sideEffecting: false },
|
|
430
491
|
// --- memory ---
|
|
@@ -458,7 +519,7 @@ export const METHODS = Object.freeze({
|
|
|
458
519
|
"notification.updatePreferences": { family: "notification", scope: "org.write", sideEffecting: true },
|
|
459
520
|
"notification.getPreferences": { family: "notification", scope: "org.read", sideEffecting: false },
|
|
460
521
|
// --- decision ---
|
|
461
|
-
"decision.comment": { family: "decision", scope: "decision.write", sideEffecting: true },
|
|
522
|
+
"decision.comment": { family: "decision", scope: "decision.write", sideEffecting: true, idempotent: true },
|
|
462
523
|
"decision.sign": { family: "decision", scope: "decision.write", sideEffecting: true },
|
|
463
524
|
"decision.reverse": { family: "decision", scope: "decision.write", sideEffecting: true },
|
|
464
525
|
"decision.requestAdjustment": { family: "decision", scope: "decision.write", sideEffecting: true },
|
|
@@ -479,7 +540,7 @@ export const METHODS = Object.freeze({
|
|
|
479
540
|
"board.updateTask": { family: "board", scope: "board.write", sideEffecting: true },
|
|
480
541
|
"board.moveTask": { family: "board", scope: "board.write", sideEffecting: true },
|
|
481
542
|
"board.assignTask": { family: "board", scope: "board.write", sideEffecting: true },
|
|
482
|
-
"board.addTaskComment": { family: "board", scope: "board.write", sideEffecting: true },
|
|
543
|
+
"board.addTaskComment": { family: "board", scope: "board.write", sideEffecting: true, idempotent: true },
|
|
483
544
|
"board.addTaskAttachment": { family: "board", scope: "board.write", sideEffecting: true, idempotent: true },
|
|
484
545
|
"board.updateTaskAttachment": { family: "board", scope: "board.write", sideEffecting: true },
|
|
485
546
|
"board.removeTaskAttachment": { family: "board", scope: "board.write", sideEffecting: true },
|
|
@@ -933,6 +994,118 @@ export const METHODS = Object.freeze({
|
|
|
933
994
|
// dispatcher cannot keep: the protocol advertises the method, callers get
|
|
934
995
|
// through scope resolution, and the call 500s. Register it in the same change
|
|
935
996
|
// that adds src/server/methods/escalation/answer.ts.
|
|
997
|
+
// --- GENESIS + ARCHITECTURE (2026-08): the pre-commit workspace draft and the
|
|
998
|
+
// review gates over it. 1:1 with the founder's own form surface — the ask
|
|
999
|
+
// was parity, so the bar is EQUAL power, never more.
|
|
1000
|
+
//
|
|
1001
|
+
// SCOPES REUSE `org.read` / `org.write` rather than minting a genesis.*
|
|
1002
|
+
// pair. `org.write` is already defined as "EDITOR-equivalent: write
|
|
1003
|
+
// org-structural content (members, teams, personas, ... reporting lines,
|
|
1004
|
+
// chart layout)" and already sits in DEFAULT_AGENT_SCOPES under the
|
|
1005
|
+
// comment "agent UI-parity". A Genesis draft IS that content, one commit
|
|
1006
|
+
// earlier; a private scope would be a second name for a permission the
|
|
1007
|
+
// org already grants, and every operator who had reasoned about who may
|
|
1008
|
+
// edit their structure would have to reason about it twice.
|
|
1009
|
+
//
|
|
1010
|
+
// WHY REGISTERING THEM AT ALL IS THE FIX. `scopeForMethod()` resolves an
|
|
1011
|
+
// UNKNOWN method to `admin`, and `admin` is in neither
|
|
1012
|
+
// HUMAN_DEFAULT_SCOPES nor DEFAULT_AGENT_SCOPES — so an undeclared method
|
|
1013
|
+
// is not merely undocumented, it is unreachable by every principal that
|
|
1014
|
+
// exists. That default is correct and it was firing: the handlers shipped
|
|
1015
|
+
// without descriptors and could not serve one request.
|
|
1016
|
+
//
|
|
1017
|
+
// EVERY WRITE IS `sideEffecting:true` AND APPENDS ONE CHAIN EVENT, which
|
|
1018
|
+
// is not bookkeeping — dispatch REQUIRES exactly one append from a
|
|
1019
|
+
// side-effecting handler and throws INTERNAL on zero. The tempting escape
|
|
1020
|
+
// (declare a durable write `sideEffecting:false` so the requirement never
|
|
1021
|
+
// applies) would buy a green test with a lie the whole protocol reads:
|
|
1022
|
+
// no chain row, no idempotency replay, and `READS`-shaped callers free to
|
|
1023
|
+
// retry a mutation they were told was safe.
|
|
1024
|
+
//
|
|
1025
|
+
// `idempotent:true` on the writes is opt-in at the caller (it engages only
|
|
1026
|
+
// when an idempotencyKey is sent) and it is the honest answer for this
|
|
1027
|
+
// family: every write already carries an `(attemptId, editSeq)` base and a
|
|
1028
|
+
// replay of a committed op is refused CONFLICT by the stale-base gate, so
|
|
1029
|
+
// without the cache a dropped response turns a SUCCEEDED edit into a
|
|
1030
|
+
// confusing conflict the caller cannot distinguish from a real race.
|
|
1031
|
+
//
|
|
1032
|
+
// genesis.plan is a READ that spends tokens: it asks a model to turn an
|
|
1033
|
+
// instruction into ops and mutates nothing. Whether it costs money is not
|
|
1034
|
+
// what `sideEffecting` means. ---
|
|
1035
|
+
"genesis.outline": { family: "genesis", scope: "org.read", sideEffecting: false },
|
|
1036
|
+
"genesis.entity": { family: "genesis", scope: "org.read", sideEffecting: false },
|
|
1037
|
+
"genesis.grid": { family: "genesis", scope: "org.read", sideEffecting: false },
|
|
1038
|
+
"genesis.plan": { family: "genesis", scope: "org.read", sideEffecting: false },
|
|
1039
|
+
"genesis.set": { family: "genesis", scope: "org.write", sideEffecting: true, idempotent: true },
|
|
1040
|
+
"genesis.insert": { family: "genesis", scope: "org.write", sideEffecting: true, idempotent: true },
|
|
1041
|
+
"genesis.remove": { family: "genesis", scope: "org.write", sideEffecting: true, idempotent: true },
|
|
1042
|
+
"genesis.reorder": { family: "genesis", scope: "org.write", sideEffecting: true, idempotent: true },
|
|
1043
|
+
"genesis.revert": { family: "genesis", scope: "org.write", sideEffecting: true, idempotent: true },
|
|
1044
|
+
"genesis.applyPlan": { family: "genesis", scope: "org.write", sideEffecting: true, idempotent: true },
|
|
1045
|
+
"architecture.status": { family: "architecture", scope: "org.read", sideEffecting: false },
|
|
1046
|
+
"architecture.accept": { family: "architecture", scope: "org.write", sideEffecting: true, idempotent: true },
|
|
1047
|
+
"architecture.approve": { family: "architecture", scope: "org.write", sideEffecting: true, idempotent: true },
|
|
1048
|
+
// --- resource (2026-08): THE VENTURE-DELIVERABLE CATALOGUE. `OrgResource` is
|
|
1049
|
+
// the org-scoped index of a venture's own artifacts — one row per
|
|
1050
|
+
// deliverable, keyed `(orgId, slug)`, provisioned by the portal
|
|
1051
|
+
// (`source:"ftlab-portal"`), by Genesis, by the artifact ingest, or by
|
|
1052
|
+
// hand. Eight methods: list/get (reads) and create/update/delete/reorder
|
|
1053
|
+
// + attachFile/detachFile (writes).
|
|
1054
|
+
//
|
|
1055
|
+
// SCOPES REUSE `org.read` / `org.write` rather than minting a
|
|
1056
|
+
// `resource.*` pair — the subagent/genesis precedent, and here it is also
|
|
1057
|
+
// the whole ACCESS-LEVEL point. A catalogue card is org-structural
|
|
1058
|
+
// authored content exactly like a persona, an SOP or an annotation, which
|
|
1059
|
+
// is what `org.write` already names; `org.read` is in hq's VIEWER tier
|
|
1060
|
+
// (src/server/auth/role-scopes.ts) and both are in DEFAULT_AGENT_SCOPES.
|
|
1061
|
+
// So on the day these descriptors land, every seat that exists can READ
|
|
1062
|
+
// its venture's catalogue and every EDITOR seat can maintain it, with no
|
|
1063
|
+
// key reminted and no tier table edited. A private scope pair would have
|
|
1064
|
+
// to be threaded through SCOPES, DEFAULT_AGENT_SCOPES and the VIEWER tier
|
|
1065
|
+
// before ONE existing credential could reach the family — which is the
|
|
1066
|
+
// same unreachable-on-arrival failure this declaration exists to end.
|
|
1067
|
+
// (`org.write` is deliberately NOT in hq's HUMAN_DEFAULT_SCOPES: the
|
|
1068
|
+
// human mobile bearer is intentionally narrower than an agent, and the
|
|
1069
|
+
// founder's own catalogue editing runs through the web app's server
|
|
1070
|
+
// actions under `requireOrgContext()`, never this bearer.)
|
|
1071
|
+
//
|
|
1072
|
+
// THE FILES BRIDGE IS A POINTER, NOT A SECOND DRIVE. The artifact ingest
|
|
1073
|
+
// files a venture's artifacts as real `WorkspaceFile` rows and stamps
|
|
1074
|
+
// `OrgResource.fileId`. `resource.*` returns that id and the card's own
|
|
1075
|
+
// columns and NOTHING derived from the drive row — no bytes, no content,
|
|
1076
|
+
// no name, size, version or share list. That is a share boundary, not
|
|
1077
|
+
// tidiness: the catalogue is ORG-WIDE under `org.read` while the drive is
|
|
1078
|
+
// SEAT-scoped by `driveVisibilityWhere` (hq methods/files/list.ts), so a
|
|
1079
|
+
// `resource.*` that re-served file content would be a one-call bypass of
|
|
1080
|
+
// the share filter — the exact defect that handler's docblock warns
|
|
1081
|
+
// about. A card is a citation; `files.get` / `files.docRead` /
|
|
1082
|
+
// `files.exportRequest` are the retrieval path and already exist.
|
|
1083
|
+
// `resource.attachFile` is the ONE place the families touch, and it
|
|
1084
|
+
// resolves the file through the drive's own `getFileOrThrow` (org pin +
|
|
1085
|
+
// `canSeeDriveFile`, NOT_FOUND either way) so it can neither cite another
|
|
1086
|
+
// tenant's file nor act as an existence oracle for one this seat cannot
|
|
1087
|
+
// see. `files.uploadRegister`'s `.strict()` schema is untouched.
|
|
1088
|
+
//
|
|
1089
|
+
// TENANCY, twice over. Slug-addressed rows go through the compound unique
|
|
1090
|
+
// `orgId_slug` — the tenant is PART OF THE KEY, so another org's card is
|
|
1091
|
+
// unaddressable. Id-addressed rows go through `ctx.scopeWhere({ id })` on
|
|
1092
|
+
// findFirst/updateMany/deleteMany, never `findUnique({ where: { id } })`,
|
|
1093
|
+
// which would cross tenants.
|
|
1094
|
+
//
|
|
1095
|
+
// Writes are `sideEffecting:true` and append exactly ONE chain event each
|
|
1096
|
+
// under `family:"resource"`; `idempotent:true` is opt-in at the caller.
|
|
1097
|
+
// `resource.delete` removes the CARD only — the `WorkspaceFile` keeps its
|
|
1098
|
+
// own 30-day soft-delete lane, which is why `fileId` is a plain String
|
|
1099
|
+
// and not a relation (schema.prisma: a cascade would lose the record that
|
|
1100
|
+
// the venture ever had a deck). ---
|
|
1101
|
+
"resource.list": { family: "resource", scope: "org.read", sideEffecting: false },
|
|
1102
|
+
"resource.get": { family: "resource", scope: "org.read", sideEffecting: false },
|
|
1103
|
+
"resource.create": { family: "resource", scope: "org.write", sideEffecting: true, idempotent: true },
|
|
1104
|
+
"resource.update": { family: "resource", scope: "org.write", sideEffecting: true, idempotent: true },
|
|
1105
|
+
"resource.delete": { family: "resource", scope: "org.write", sideEffecting: true, idempotent: true },
|
|
1106
|
+
"resource.reorder": { family: "resource", scope: "org.write", sideEffecting: true, idempotent: true },
|
|
1107
|
+
"resource.attachFile": { family: "resource", scope: "org.write", sideEffecting: true, idempotent: true },
|
|
1108
|
+
"resource.detachFile": { family: "resource", scope: "org.write", sideEffecting: true, idempotent: true },
|
|
936
1109
|
});
|
|
937
1110
|
|
|
938
1111
|
/**
|
|
@@ -134,7 +134,16 @@ test("protocol: frozen family + method counts (additive evolution guard)", () =>
|
|
|
134
134
|
// The 2026-08 protocol-convergence pass adds 4: subagent (the org-scoped
|
|
135
135
|
// sub-agent registry), agent (the agent.wait control lane), mandate (the
|
|
136
136
|
// MANDATE spine) and preference (member-scoped preferences) → 51.
|
|
137
|
-
|
|
137
|
+
// The 2026-08 Genesis agent-surface pass adds 2: genesis (the founder's
|
|
138
|
+
// pre-commit workspace draft — the ten-stage run and its editable sections)
|
|
139
|
+
// and architecture (the four tier review decisions and the one whole-
|
|
140
|
+
// architecture approval, which outlive the run as the Org › Architecture
|
|
141
|
+
// view) → 53.
|
|
142
|
+
// The 2026-08 venture-deliverable pass adds 1: resource (the OrgResource
|
|
143
|
+
// catalogue — a venture's own deliverables). The table predates every agent
|
|
144
|
+
// plane and was reachable from none of them, so a venture's colleagues could
|
|
145
|
+
// not list, add, correct, reorder or remove one of their own artifacts → 54.
|
|
146
|
+
assert.equal(FAMILIES.length, 54, "family count");
|
|
138
147
|
// 357 = the mesh-protocol + agent UI-parity + calling + branding + email
|
|
139
148
|
// methods, plus the Live Integrations surface: integration.toolsetVersion +
|
|
140
149
|
// integration.listAgentTools (SP1) and integration.invokeTool (SP5, execute a
|
|
@@ -176,7 +185,27 @@ test("protocol: frozen family + method counts (additive evolution guard)", () =>
|
|
|
176
185
|
// `server/work/ledger.ts` directly, the daemon reaches it over this wire, so
|
|
177
186
|
// the threshold and the idempotency cannot fork → 461.
|
|
178
187
|
// the descriptors; the checksum (read at runtime) is the primary drift guard.
|
|
179
|
-
|
|
188
|
+
// The 2026-08 Genesis agent-surface pass adds 13, the 1:1 programmatic
|
|
189
|
+
// parity for the founder's own Generate surface: genesis 10 (outline/entity/
|
|
190
|
+
// grid/plan reads + set/insert/remove/reorder/revert/applyPlan writes) and
|
|
191
|
+
// architecture 3 (status read + accept/approve review gates). The handlers
|
|
192
|
+
// had shipped WITHOUT descriptors, which made all 13 unreachable — dispatch
|
|
193
|
+
// rejects unknown methods and scopeForMethod resolves them to `admin`, which
|
|
194
|
+
// no principal holds → 474.
|
|
195
|
+
// The 2026-08 voice-capability pass adds 1: messaging.synthesizeVoiceNote —
|
|
196
|
+
// hq could always synthesise a colleague's voice note but only as a REFLEX
|
|
197
|
+
// (answering a human's spoken note in kind), so an agent asked in text for a
|
|
198
|
+
// voice note truthfully answered that it could not. This is the verb that
|
|
199
|
+
// makes it askable on every plane; the POST stays plain messaging.send → 475.
|
|
200
|
+
// The 2026-08 venture-deliverable pass adds 8: resource.list/get (reads) and
|
|
201
|
+
// resource.create/update/delete/reorder/attachFile/detachFile (writes). Both
|
|
202
|
+
// reads ride `org.read` and every write `org.write`, so the family is
|
|
203
|
+
// reachable by the VIEWER and EDITOR tiers that already exist rather than by
|
|
204
|
+
// a scope nobody has been minted. attachFile/detachFile are the ONLY bridge
|
|
205
|
+
// to `files.*`: a card carries a fileId POINTER and the drive keeps its own
|
|
206
|
+
// seat-scoped read surface → 483. Then +1: messaging.electResponder (the
|
|
207
|
+
// fleet's server-side respond-election) → 484.
|
|
208
|
+
assert.equal(Object.keys(METHODS).length, 484, "method count");
|
|
180
209
|
});
|
|
181
210
|
|
|
182
211
|
test("protocol SP3: messaging + calling families/methods/scopes", async () => {
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/org/resource-tools.mjs — the `resource_*` desk tools, GENERATED.
|
|
3
|
+
*
|
|
4
|
+
* THE FAMILY. `OrgResource` is a venture's own deliverable catalogue: one
|
|
5
|
+
* org-scoped card per artifact (deck, one-pager, film, site, data room…),
|
|
6
|
+
* keyed `(orgId, slug)`, written by the FTLAB portal, by Genesis, by the
|
|
7
|
+
* artifact ingest, or by hand, and rendered to humans at `/resources`. Until
|
|
8
|
+
* this wave it was reachable from NO agent code path in either plane — a
|
|
9
|
+
* venture's own AI colleagues could not list, add, correct, reorder or remove
|
|
10
|
+
* a single one of the venture's deliverables. Eight protocol methods now
|
|
11
|
+
* exist; these are their curated tools.
|
|
12
|
+
*
|
|
13
|
+
* WHY THIS FILE IS A GENERATOR AND NOT A TABLE. hq's responder desks expose
|
|
14
|
+
* 239 ops across 11 families; maestro's curated table exposed 69 desk tools
|
|
15
|
+
* across 9 — files 29 vs 7, crm 44 vs 8. That divergence has one cause: the two
|
|
16
|
+
* tool tables are hand-written twice, so they drift the moment one side moves
|
|
17
|
+
* and nobody hand-copies the delta. This family refuses to be able to drift the
|
|
18
|
+
* same way. The tool NAME, the ACCESS TIER, the BINDING and the DESK TAG are
|
|
19
|
+
* all DERIVED from the vendored protocol declaration — the same declaration
|
|
20
|
+
* hq's handlers are registered from — and the only thing a human writes here is
|
|
21
|
+
* PRESENTATION: the title, the description, and the input schema. Add a ninth
|
|
22
|
+
* `resource.*` method to `lib/org/protocol.mjs` and sync it, and a ninth tool
|
|
23
|
+
* appears on the next import with no edit to this file; `resource-tools.test.mjs`
|
|
24
|
+
* then goes red until someone gives it a real description. The capability is
|
|
25
|
+
* never dark and the gap is never quiet — which is the whole thesis of the
|
|
26
|
+
* wave that shipped the family.
|
|
27
|
+
*
|
|
28
|
+
* ACCESS. `expectedAccessFor` derives the tier from the method's protocol
|
|
29
|
+
* SCOPE, never from a literal typed here: `resource.list` / `resource.get` ride
|
|
30
|
+
* `org.read` → `access:"read"` (every tier's lookup set, including
|
|
31
|
+
* least-privilege "default"); the six writes ride `org.write` →
|
|
32
|
+
* `access:"write"` (ceo + leadership). NONE is `access:"admin"` — that is the
|
|
33
|
+
* CEO-only escape-hatch tier, and a venture's colleagues are default- and
|
|
34
|
+
* leadership-tier agents, so parking their own deliverables there would ship
|
|
35
|
+
* this family exactly as dark as it was before. See lib/org/tool-access.mjs.
|
|
36
|
+
*
|
|
37
|
+
* NO `outbound` FLAG ANYWHERE IN THIS BLOCK. A card is org-internal metadata;
|
|
38
|
+
* nothing here puts free text a human reads onto an outbound channel, and the
|
|
39
|
+
* send-gate lane stays exactly messaging_send / email_send / email_draft_send /
|
|
40
|
+
* org_call_share_step.
|
|
41
|
+
*
|
|
42
|
+
* THE FILES BRIDGE IS A POINTER, NOT A SECOND DRIVE. A card returns its own
|
|
43
|
+
* columns plus a `fileId`, and NOTHING derived from the `WorkspaceFile` — no
|
|
44
|
+
* bytes, name, mime, size, version or share list. The catalogue is org-wide
|
|
45
|
+
* under `org.read` while the drive is seat-scoped by `driveVisibilityWhere`, so
|
|
46
|
+
* a resource tool that re-served file content would be a one-call bypass of the
|
|
47
|
+
* share filter. Bytes are read with `files_get` / `files_doc_read` /
|
|
48
|
+
* `org_rpc files.exportRequest`. `resource_attach_file` is the one seam, and hq
|
|
49
|
+
* resolves the id through the drive's own `getFileOrThrow` (org pin +
|
|
50
|
+
* `canSeeDriveFile`, NOT_FOUND either way), so it can neither cite another
|
|
51
|
+
* tenant's file nor act as an existence oracle for one this seat cannot see.
|
|
52
|
+
*
|
|
53
|
+
* Node builtins only. ESM.
|
|
54
|
+
*
|
|
55
|
+
* @module lib/org/resource-tools
|
|
56
|
+
*/
|
|
57
|
+
|
|
58
|
+
"use strict";
|
|
59
|
+
|
|
60
|
+
import { METHODS } from "./protocol.mjs";
|
|
61
|
+
import { expectedAccessFor } from "./tool-access.mjs";
|
|
62
|
+
|
|
63
|
+
/** The protocol family these tools are generated from. */
|
|
64
|
+
export const RESOURCE_FAMILY = "resource";
|
|
65
|
+
|
|
66
|
+
/** The desk tag every generated entry carries (gates on DESK_PROBES.resource). */
|
|
67
|
+
export const RESOURCE_DESK = "resource";
|
|
68
|
+
|
|
69
|
+
// Schema helpers — same shapes tool-surface.mjs builds its entries with.
|
|
70
|
+
const S = (properties, required = []) => ({ type: "object", properties, required });
|
|
71
|
+
const str = (description) => ({ type: "string", description });
|
|
72
|
+
const num = (description) => ({ type: "number", description });
|
|
73
|
+
const bool = (description) => ({ type: "boolean", description });
|
|
74
|
+
const obj = (description) => ({ type: "object", description });
|
|
75
|
+
const arr = (items, description) => ({ type: "array", items, description });
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* PRESENTATION ONLY, keyed by protocol method. No `name`, no `access`, no
|
|
79
|
+
* `binding`, no `desk` — those four are derived, and a literal here would be
|
|
80
|
+
* the hand-copy this file exists to prevent.
|
|
81
|
+
*
|
|
82
|
+
* Each description carries what the schema cannot: the semantics an agent gets
|
|
83
|
+
* wrong on the first try (total-vs-partial reorder, shallow-merge metadata,
|
|
84
|
+
* what survives a delete) and the refusals it will otherwise learn by
|
|
85
|
+
* BAD_REQUEST.
|
|
86
|
+
* @type {Readonly<Record<string, {title: string, description: string, input_schema: object}>>}
|
|
87
|
+
*/
|
|
88
|
+
export const RESOURCE_PRESENTATION = Object.freeze({
|
|
89
|
+
"resource.list": {
|
|
90
|
+
title: "List venture deliverables",
|
|
91
|
+
description:
|
|
92
|
+
"This venture's deliverable catalogue (resource.list) — the same cards, in the same " +
|
|
93
|
+
"order, that humans see at /resources: sortOrder asc, then createdAt asc. Each card is " +
|
|
94
|
+
"{id, slug, kind, title, description, url, previewImageUrl, source, fileId, metadata, " +
|
|
95
|
+
"sortOrder, createdAt, updatedAt}. `source` says who put it there — \"ftlab-portal\" " +
|
|
96
|
+
"(pushed by the venture portal and re-upserted on every push), \"genesis\", or " +
|
|
97
|
+
"\"manual\" (created through this desk). `fileId` non-null means the artifact is also " +
|
|
98
|
+
"filed in the workspace drive: read it with files_get / files_doc_read, NOT from here — " +
|
|
99
|
+
"this desk returns the card's own columns and nothing about the file itself. Filters: " +
|
|
100
|
+
"`kind`, `source`, free-text `q` (matches title OR description, case-insensitive), " +
|
|
101
|
+
"`hasFile` (true = only drive-backed cards, false = only link-only cards, omit = both). " +
|
|
102
|
+
"No cursor — raise `limit` (max 200) if the catalogue is long.",
|
|
103
|
+
input_schema: S({
|
|
104
|
+
kind: str("Filter by card kind, e.g. \"deck\", \"one-pager\", \"film\", \"site\" (≤64 chars)."),
|
|
105
|
+
source: str("Filter by provenance: \"ftlab-portal\" | \"genesis\" | \"manual\" (≤64 chars)."),
|
|
106
|
+
q: str("Free text; matches title OR description, case-insensitive (≤200 chars)."),
|
|
107
|
+
hasFile: bool("true = only cards with a drive fileId; false = only link-only cards; omit = both."),
|
|
108
|
+
limit: num("Max cards 1..200 (default 100)."),
|
|
109
|
+
}),
|
|
110
|
+
},
|
|
111
|
+
"resource.get": {
|
|
112
|
+
title: "Get one deliverable",
|
|
113
|
+
description:
|
|
114
|
+
"One catalogue card by `slug` (the stable org-scoped natural key the portal, the " +
|
|
115
|
+
"/resources page and every tool here use) or by `resourceId` — pass EXACTLY ONE; " +
|
|
116
|
+
"neither or both is a BAD_REQUEST. Returns the card's own columns only. A card in " +
|
|
117
|
+
"another workspace answers NOT_FOUND identically to one that never existed, so this is " +
|
|
118
|
+
"not an existence oracle — do not read NOT_FOUND as \"it belongs to someone else\".",
|
|
119
|
+
input_schema: S({
|
|
120
|
+
slug: str("Card slug, e.g. \"deck-seed-round\" (preferred — stable across re-pushes)."),
|
|
121
|
+
resourceId: str("Card id (cuid). Alternative to slug; never pass both."),
|
|
122
|
+
}),
|
|
123
|
+
},
|
|
124
|
+
"resource.create": {
|
|
125
|
+
title: "Add a deliverable",
|
|
126
|
+
description:
|
|
127
|
+
"Add a card to the venture's catalogue (resource.create). `url` OR `fileId` must be " +
|
|
128
|
+
"present — a card pointing nowhere is the exact state this desk exists to end. Omit " +
|
|
129
|
+
"`slug` and the server derives one from kind + title (and suffixes -2, -3 on collision) " +
|
|
130
|
+
"using the SAME transform the portal uses, so a portal push and an agent create agree. " +
|
|
131
|
+
"`source` is NOT an input: the server stamps \"manual\", so an agent cannot relabel its " +
|
|
132
|
+
"own card as portal-provenance. This is NOT an upsert — an existing slug answers " +
|
|
133
|
+
"CONFLICT rather than overwriting a portal row someone guessed the slug of; correct an " +
|
|
134
|
+
"existing card with resource_update. `metadata` is free JSON (object only, ≤16KB) for " +
|
|
135
|
+
"provenance such as sha256/runId/documentVersion — NEVER put a credential in it; only " +
|
|
136
|
+
"its KEY NAMES ride the event chain.",
|
|
137
|
+
input_schema: S(
|
|
138
|
+
{
|
|
139
|
+
kind: str("Card kind, e.g. \"deck\" | \"one-pager\" | \"film\" | \"site\" (≤64 chars)."),
|
|
140
|
+
title: str("Human title as it appears at /resources (≤200 chars)."),
|
|
141
|
+
url: str("Public https URL (≤2048) or an in-app path like \"/files/abc123\". Required unless fileId is given."),
|
|
142
|
+
fileId: str("Workspace drive file id to cite. Required unless url is given; must be a file this seat can already see."),
|
|
143
|
+
slug: str("Optional explicit slug: lowercase a-z0-9 words joined by single hyphens, ≤64. Omit to let the server derive it."),
|
|
144
|
+
description: str("One or two sentences a human reads under the title (≤2000 chars)."),
|
|
145
|
+
previewImageUrl: str("Thumbnail URL (≤2048)."),
|
|
146
|
+
metadata: obj("Free-JSON provenance object, ≤16KB serialized. No secrets."),
|
|
147
|
+
sortOrder: num("Position 0..10000 (ties break on createdAt asc). Omit to append."),
|
|
148
|
+
},
|
|
149
|
+
["kind", "title"],
|
|
150
|
+
),
|
|
151
|
+
},
|
|
152
|
+
"resource.update": {
|
|
153
|
+
title: "Correct a deliverable",
|
|
154
|
+
description:
|
|
155
|
+
"Edit an existing card (resource.update), addressed by EXACTLY ONE of `slug` or " +
|
|
156
|
+
"`resourceId`. At least one mutable field is required, else BAD_REQUEST. `metadata` is a " +
|
|
157
|
+
"SHALLOW MERGE over what is already there — {...prior, ...patch} — and a key you pass as " +
|
|
158
|
+
"null is DELETED. It is deliberately not a whole-object replace: the artifact ingest " +
|
|
159
|
+
"writes sha256/runId/documentVersion/byteState/driveUrl into metadata, and a replace " +
|
|
160
|
+
"would let a one-field edit destroy the delivery provenance. Three fields are NOT " +
|
|
161
|
+
"editable here, each on purpose: `slug` (it is the ref AND the portal's re-sync key — " +
|
|
162
|
+
"renaming it would orphan the upsert and duplicate the card on the next push), `fileId` " +
|
|
163
|
+
"(re-pointing a card at a different drive row is a separately-audited act — use " +
|
|
164
|
+
"resource_attach_file), and `source` (provenance is server-owned).",
|
|
165
|
+
input_schema: S({
|
|
166
|
+
slug: str("Card slug to edit (preferred). Never pass both slug and resourceId."),
|
|
167
|
+
resourceId: str("Card id (cuid). Alternative to slug."),
|
|
168
|
+
kind: str("New kind (≤64 chars)."),
|
|
169
|
+
title: str("New title (≤200 chars)."),
|
|
170
|
+
description: str("New description (≤2000 chars); null clears it."),
|
|
171
|
+
url: str("New https URL (≤2048) or in-app path."),
|
|
172
|
+
previewImageUrl: str("New thumbnail URL (≤2048); null clears it."),
|
|
173
|
+
metadata: obj("Metadata PATCH — shallow-merged; a key set to null is removed. ≤16KB serialized."),
|
|
174
|
+
sortOrder: num("New position 0..10000. This is the PARTIAL nudge; resource_reorder is the total re-sort."),
|
|
175
|
+
}),
|
|
176
|
+
},
|
|
177
|
+
"resource.delete": {
|
|
178
|
+
title: "Remove a deliverable",
|
|
179
|
+
description:
|
|
180
|
+
"Remove a card from the catalogue (resource.delete), addressed by EXACTLY ONE of `slug` " +
|
|
181
|
+
"or `resourceId`. THE CARD ONLY: the WorkspaceFile it cited is NOT deleted and not even " +
|
|
182
|
+
"soft-deleted — the drive owns its own 30-day recovery lane — which is why the reply " +
|
|
183
|
+
"carries `fileRetained: true`. Relay that truth; do not tell a human the file is gone. " +
|
|
184
|
+
"The card's Cortex SOURCES entry goes with it, so a removed deliverable stops being " +
|
|
185
|
+
"discoverable. CAVEAT worth saying out loud: deleting a card whose `source` is " +
|
|
186
|
+
"\"ftlab-portal\" is not permanent — the next portal push re-upserts it on its natural " +
|
|
187
|
+
"key. If a portal card is wrong, fix it upstream or correct it with resource_update.",
|
|
188
|
+
input_schema: S({
|
|
189
|
+
slug: str("Card slug to remove (preferred). Never pass both slug and resourceId."),
|
|
190
|
+
resourceId: str("Card id (cuid). Alternative to slug."),
|
|
191
|
+
}),
|
|
192
|
+
},
|
|
193
|
+
"resource.reorder": {
|
|
194
|
+
title: "Re-sort the catalogue",
|
|
195
|
+
description:
|
|
196
|
+
"Set the whole catalogue's order (resource.reorder) — this is what changes what a human " +
|
|
197
|
+
"sees FIRST at /resources. TOTAL, not partial: `slugs` must name EVERY card in this " +
|
|
198
|
+
"workspace exactly once, and each gets sortOrder = its index. Anything else is a " +
|
|
199
|
+
"CONFLICT that names the missing, unknown and duplicated slugs back to you — which is " +
|
|
200
|
+
"the feature, not the friction: a card created by the portal while you were composing " +
|
|
201
|
+
"the list surfaces as a refusal instead of silently scrambling the order. So list first " +
|
|
202
|
+
"(resource_list), reorder from THAT list, and re-list if it refuses. To nudge one card " +
|
|
203
|
+
"without touching the rest, use resource_update { sortOrder } instead. Returns the full " +
|
|
204
|
+
"catalogue in its new order — no re-list needed.",
|
|
205
|
+
input_schema: S(
|
|
206
|
+
{
|
|
207
|
+
slugs: arr(
|
|
208
|
+
{ type: "string" },
|
|
209
|
+
"Every slug in the workspace, exactly once, in the order you want (1..500).",
|
|
210
|
+
),
|
|
211
|
+
},
|
|
212
|
+
["slugs"],
|
|
213
|
+
),
|
|
214
|
+
},
|
|
215
|
+
"resource.attachFile": {
|
|
216
|
+
title: "Cite a drive file on a card",
|
|
217
|
+
description:
|
|
218
|
+
"Point a catalogue card at a workspace drive file (resource.attachFile), addressed by " +
|
|
219
|
+
"EXACTLY ONE of `slug` or `resourceId`. This is a LINK write, not a byte write: it sets " +
|
|
220
|
+
"fileId and merges {driveUrl:\"/files/<fileId>\"} into metadata. hq resolves the file " +
|
|
221
|
+
"through the drive's own permission check, so you may only cite a file this seat can " +
|
|
222
|
+
"already see, and a file you cannot see answers NOT_FOUND exactly like one that does not " +
|
|
223
|
+
"exist. `setUrl` defaults to FALSE and should usually stay there: a deck, one-pager or " +
|
|
224
|
+
"film keeps its public URL because the venture's own site and outreach link that object " +
|
|
225
|
+
"— pass setUrl:true only when the card's destination really should become the in-app " +
|
|
226
|
+
"file. Attaching does not copy, move or re-share the file.",
|
|
227
|
+
input_schema: S(
|
|
228
|
+
{
|
|
229
|
+
fileId: str("Workspace drive file id to cite."),
|
|
230
|
+
slug: str("Card slug (preferred). Never pass both slug and resourceId."),
|
|
231
|
+
resourceId: str("Card id (cuid). Alternative to slug."),
|
|
232
|
+
setUrl: bool("Also rewrite the card's url to /files/<fileId>. Default false — an external URL is usually load-bearing."),
|
|
233
|
+
},
|
|
234
|
+
["fileId"],
|
|
235
|
+
),
|
|
236
|
+
},
|
|
237
|
+
"resource.detachFile": {
|
|
238
|
+
title: "Stop citing a drive file",
|
|
239
|
+
description:
|
|
240
|
+
"Clear a card's drive citation (resource.detachFile), addressed by EXACTLY ONE of `slug` " +
|
|
241
|
+
"or `resourceId`: fileId → null and the `driveUrl` key drops out of metadata. The " +
|
|
242
|
+
"WorkspaceFile is untouched. The card's `url` is left exactly as it was UNLESS you pass " +
|
|
243
|
+
"a replacement `url`. One guard: if the card's only destination IS the file being " +
|
|
244
|
+
"detached (its url is /files/<fileId>), `url` is REQUIRED and the call answers CONFLICT " +
|
|
245
|
+
"without it — otherwise this verb would manufacture the dead card the whole family " +
|
|
246
|
+
"exists to prevent.",
|
|
247
|
+
input_schema: S({
|
|
248
|
+
slug: str("Card slug (preferred). Never pass both slug and resourceId."),
|
|
249
|
+
resourceId: str("Card id (cuid). Alternative to slug."),
|
|
250
|
+
url: str("Replacement destination — required when the card's url is the /files/<fileId> being detached."),
|
|
251
|
+
}),
|
|
252
|
+
},
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* `resource.attachFile` → `resource_attach_file`. Dots become underscores and
|
|
257
|
+
* camelCase humps split, so the tool name is a pure function of the protocol
|
|
258
|
+
* name and cannot be typed differently from it.
|
|
259
|
+
* @param {string} method
|
|
260
|
+
* @returns {string}
|
|
261
|
+
*/
|
|
262
|
+
export function toolNameForMethod(method) {
|
|
263
|
+
return String(method)
|
|
264
|
+
.replace(/\./g, "_")
|
|
265
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1_$2")
|
|
266
|
+
.toLowerCase();
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Build the curated `resource_*` entries from the VENDORED protocol table.
|
|
271
|
+
*
|
|
272
|
+
* Order follows the protocol declaration, so the tool block reads in the order
|
|
273
|
+
* the family was designed in. Returns `[]` when the vendored protocol carries
|
|
274
|
+
* no `resource` family at all — an older protocol degrades cleanly, exactly
|
|
275
|
+
* like the desk gates.
|
|
276
|
+
*
|
|
277
|
+
* A family method with no presentation entry still gets a tool, built from the
|
|
278
|
+
* descriptor: an under-described capability is a documentation bug, but a
|
|
279
|
+
* MISSING one is the failure this whole wave was called to fix, so drift
|
|
280
|
+
* surfaces as a red test (resource-tools.test.mjs asserts the presentation
|
|
281
|
+
* table covers the family exactly) and never as a dark verb.
|
|
282
|
+
*
|
|
283
|
+
* @param {Record<string, {family?:string, scope?:string, sideEffecting?:boolean}>} [methods]
|
|
284
|
+
* @returns {Array<object>}
|
|
285
|
+
*/
|
|
286
|
+
export function buildResourceTools(methods = METHODS) {
|
|
287
|
+
const out = [];
|
|
288
|
+
for (const [method, def] of Object.entries(methods || {})) {
|
|
289
|
+
if (!def || def.family !== RESOURCE_FAMILY) continue;
|
|
290
|
+
// Scope is the authority (it is what hq gates on). The `??` arm only fires
|
|
291
|
+
// for a method the REAL vendored table does not carry — i.e. a synthetic
|
|
292
|
+
// table under test — and mirrors sideEffecting so such a row still lands on
|
|
293
|
+
// the correct side of the read/write line rather than defaulting open.
|
|
294
|
+
const access = expectedAccessFor(method) ?? (def.sideEffecting ? "write" : "read");
|
|
295
|
+
const p = RESOURCE_PRESENTATION[method];
|
|
296
|
+
out.push({
|
|
297
|
+
name: toolNameForMethod(method),
|
|
298
|
+
title: p ? p.title : method,
|
|
299
|
+
description: p
|
|
300
|
+
? p.description
|
|
301
|
+
: `Undocumented ${RESOURCE_FAMILY} verb (${method}) — generated from the protocol ` +
|
|
302
|
+
`declaration so the capability is reachable; add a presentation entry in ` +
|
|
303
|
+
`lib/org/resource-tools.mjs.`,
|
|
304
|
+
input_schema: p ? p.input_schema : S({}),
|
|
305
|
+
access,
|
|
306
|
+
desk: RESOURCE_DESK,
|
|
307
|
+
binding: { kind: "rpc", method },
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
return out;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* The generated block, spliced into `ORG_TOOLS` by lib/org/tool-surface.mjs.
|
|
315
|
+
* @type {ReadonlyArray<object>}
|
|
316
|
+
*/
|
|
317
|
+
export const RESOURCE_TOOLS = Object.freeze(buildResourceTools());
|