@cohortapp/agent-sdk 2.10.0 → 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/mcp/server.test.mjs +16 -4
- package/lib/org/client.mjs +58 -1
- package/lib/org/protocol.checksum +1 -1
- package/lib/org/protocol.mjs +98 -0
- package/lib/org/protocol.test.mjs +19 -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 +38 -15
- 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/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/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
|
@@ -396,13 +396,20 @@ Do NOT modify these sections (keep them exactly as they are, except for agent na
|
|
|
396
396
|
|
|
397
397
|
h. **`scripts/daemon/responder.mjs` `FALLBACK_PREAMBLE`** — System prompt that introduces the agent to Claude. Identity intro line must reference the new agent. Preserve the operational rules.
|
|
398
398
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
399
|
+
> **NO LONGER A MANUAL STEP — these four now read config and must NOT be hand-edited.**
|
|
400
|
+
> `scripts/daemon/prompt-builder.mjs` (`buildFallbackPreamble`), `scripts/daemon/classifier.mjs`
|
|
401
|
+
> (`renderClassifierIdentityLine`), `scripts/huddle/huddle-server.mjs`
|
|
402
|
+
> (`renderHuddleIdentityLine` + `renderHuddleCompanyBlock`) and `scripts/spawn-session.sh`
|
|
403
|
+
> (sources `config/agent.env`) all render identity, employer and — for the preamble — AUTHORITY
|
|
404
|
+
> from `config/agent.json`, `config/company.json` and `policies/action-classification.yaml`,
|
|
405
|
+
> omitting any clause whose field is unset or `UNCONFIGURED`. Editing the literals back in
|
|
406
|
+
> re-creates the fabrication. If one of them prints the wrong company, fix the CONFIG.
|
|
407
|
+
>
|
|
408
|
+
> (Previously items i, j, k and l. Item k asked only for the huddle "Voice agent identity
|
|
409
|
+
> line" and never mentioned the `ABOUT <COMPANY>:` block six lines below it — which described
|
|
410
|
+
> a second, different fictional company and was spoken aloud on auto-joined huddles. That
|
|
411
|
+
> block is now `renderHuddleCompanyBlock(config/company.json)` and emits NOTHING when the
|
|
412
|
+
> company config is empty.)
|
|
406
413
|
|
|
407
414
|
m. **`scripts/continuous-monitor.sh`** — Channel monitor agent prompt.
|
|
408
415
|
|
|
@@ -412,9 +419,9 @@ Do NOT modify these sections (keep them exactly as they are, except for agent na
|
|
|
412
419
|
|
|
413
420
|
p. **`scripts/rag-indexer.py`, `scripts/user-context-search.py`** — Author docstring at the top. Set to the new agent's full name.
|
|
414
421
|
|
|
415
|
-
q. **`scripts/
|
|
422
|
+
q. **`scripts/validate_outbound.py`** — ONLY the embedded `--self-test` harness at the bottom, whose fixtures look up example people in the entity index. Everything on the production path (the employer entity-id set, the possessive-claim regex, the AI-self-disclosure business exceptions) now resolves from `config/company.json` at load and must NOT be hand-edited. Leave the generic third-party pronoun regex UNCHANGED — it's a detector, not an identity reference.
|
|
416
423
|
|
|
417
|
-
**Verification step (identity AND company):** After rewrites, run `grep -rniE "robin|alex|jordan|northwind" scripts/ config/ agents/ policies/ 2>&1` and confirm NO placeholder identity remains — neither the placeholder persona (Robin / Alex) NOR the placeholder company ("Northwind"). Everything must be the new agent + the company from `config/company.json`. Report any remaining matches you cannot safely auto-resolve so the main agent can decide.
|
|
424
|
+
**Verification step (identity AND company):** After rewrites, run `node scripts/ci/check-no-residual-identity.mjs` — it now FAILS on fictional placeholder identity anywhere under `scripts/`, `lib/`, `policies/`, `agents/`, `bin/`, `services/` and friends (tests, fixtures, `docs/`, `scaffold/` and `*.example` are exempt by construction, and the remaining known-dirty files are printed from `PLACEHOLDER_PENDING`). Then run `grep -rniE "robin|alex|jordan|northwind" scripts/ config/ agents/ policies/ 2>&1` for anything the guard's patterns do not cover, and confirm NO placeholder identity remains — neither the placeholder persona (Robin / Alex) NOR the placeholder company ("Northwind"). Everything must be the new agent + the company from `config/company.json`. Report any remaining matches you cannot safely auto-resolve so the main agent can decide.
|
|
418
425
|
|
|
419
426
|
### Sub-agent 5: Update agent definitions
|
|
420
427
|
|
package/docs/guides/mac-mini.md
CHANGED
|
@@ -48,10 +48,20 @@ Two lanes — pick one:
|
|
|
48
48
|
pair it to the member, then:
|
|
49
49
|
|
|
50
50
|
```bash
|
|
51
|
-
export COHORT_API_KEY=nlk_… COHORT_ORG_ID=<org-
|
|
51
|
+
export COHORT_API_KEY=nlk_… COHORT_ORG_ID=<org-ID> COHORT_AGENT_ID=<member-slug>
|
|
52
52
|
cohort setup
|
|
53
53
|
```
|
|
54
54
|
|
|
55
|
+
> **`COHORT_ORG_ID` is the org's ID, not its slug** — this line said
|
|
56
|
+
> `<org-slug>` and that would 401 every call. The value rides as `x-org-id`
|
|
57
|
+
> (`lib/org/client.mjs#baseHeaders`) and hq compares it with strict equality
|
|
58
|
+
> against the key's own `Org.id`
|
|
59
|
+
> (`src/server/auth/org-api-key.ts:163` — no slug fallback, no normalisation).
|
|
60
|
+
> For Adaptic the two differ: the ID is `org_default_adaptic`, the slug is
|
|
61
|
+
> `adaptic`. `COHORT_AGENT_ID` **is** a slug (`A001`, `A038`) — the two
|
|
62
|
+
> variables sitting side by side take different kinds of identifier, which is
|
|
63
|
+
> exactly why this was easy to get wrong.
|
|
64
|
+
|
|
55
65
|
Pull-enrollment populates `config/agent.json` from the member profile and
|
|
56
66
|
writes `config/org.yaml` (`org.cohort.{enabled,base,orgId,token}`).
|
|
57
67
|
|
|
@@ -55,6 +55,22 @@ machine's `config/org.yaml` pins `server.url` (e.g.
|
|
|
55
55
|
service changes the `*.up.railway.app` host and strands the fleet. Move the
|
|
56
56
|
fleet to a stable custom domain first (§2), then rename freely.
|
|
57
57
|
|
|
58
|
+
> ⚠︎ **2026-08-16 — this warning appears to have come true, and it is unresolved.**
|
|
59
|
+
> `https://neolith.up.railway.app` is now UNROUTED: Railway's edge returns
|
|
60
|
+
> `x-railway-fallback: true` with a 404 on `/`, `/v1` and `/v1/ops`, meaning no
|
|
61
|
+
> service is attached to that hostname. Contrast the app, whose rename DID
|
|
62
|
+
> retain its old host — `cohort-app` still answers on
|
|
63
|
+
> `neolith-app-production.up.railway.app`. So the hq-side rename was safe and
|
|
64
|
+
> this one was not.
|
|
65
|
+
>
|
|
66
|
+
> Any fleet machine still pinning that URL is talking to nothing, and
|
|
67
|
+
> `DEPLOYMENT.md` §5.1 was handing the dead host to every new agent as its setup
|
|
68
|
+
> template until this was found. The replacement host was NOT determinable from
|
|
69
|
+
> either repo, from `cohort-os` (the org server does not live there), or from the
|
|
70
|
+
> app's env — a separate Railway project `neolith-production` still exists in the
|
|
71
|
+
> workspace and is the first place to look. Establish the real host, fix §5.1,
|
|
72
|
+
> and sweep every `config/org.yaml` before assuming the fleet is healthy.
|
|
73
|
+
|
|
58
74
|
---
|
|
59
75
|
|
|
60
76
|
## 2. Custom domain for the org server
|
package/lib/mcp/server.test.mjs
CHANGED
|
@@ -154,19 +154,31 @@ test("tools/list: full active surface (email tools present — family is vendore
|
|
|
154
154
|
// engage_colleagues is the judged version of that; work_track puts the ask on
|
|
155
155
|
// the board and moves it. The line above is the real invariant — this one just
|
|
156
156
|
// pins the number so a silent table change is visible in review.
|
|
157
|
-
|
|
157
|
+
// 122 → 133: the voice delta (messaging_send_voice_note) plus the mandate /
|
|
158
|
+
// sub-agent-registry / preference delta — ten curated tools for three
|
|
159
|
+
// protocol families that were declared, implemented on hq, and curated
|
|
160
|
+
// NOWHERE, so this transport served them only through the admin-tier
|
|
161
|
+
// `org_rpc` hatch. One table, two transports: they appear here for free.
|
|
162
|
+
// 133 → 141: the venture-deliverable desk. OrgResource — a venture's own
|
|
163
|
+
// catalogue of the artifacts it has produced — was reachable from NO agent
|
|
164
|
+
// code path in either plane, so the venture's own colleagues could not list,
|
|
165
|
+
// add, correct, reorder or remove one of their deliverables. Unlike every
|
|
166
|
+
// other desk block, these eight are GENERATED from the vendored protocol
|
|
167
|
+
// declaration (lib/org/resource-tools.mjs), so this transport gets them, and
|
|
168
|
+
// every future resource method, without a second hand-written table.
|
|
169
|
+
assert.equal(tools.length, 141, "email + artifact + desk families vendored → 141 tools");
|
|
158
170
|
const names = tools.map((t) => t.name);
|
|
159
|
-
for (const expected of ["org_whoami", "org_describe", "org_rpc", "org_read", "messaging_send", "task_assign", "board_ready", "email_send", "email_inbox", "artifact_create", "artifact_act", "artifact_catalog", "email_mailboxes", "email_draft_send", "files_list", "calendar_find_a_time", "crm_list_deals", "books_reports", "meetings_recap_file"]) {
|
|
171
|
+
for (const expected of ["org_whoami", "org_describe", "org_rpc", "org_read", "messaging_send", "task_assign", "board_ready", "email_send", "email_inbox", "artifact_create", "artifact_act", "artifact_catalog", "email_mailboxes", "email_draft_send", "files_list", "calendar_find_a_time", "crm_list_deals", "books_reports", "meetings_recap_file", "resource_list", "resource_attach_file"]) {
|
|
160
172
|
assert.ok(names.includes(expected), `${expected} listed`);
|
|
161
173
|
}
|
|
162
174
|
assert.ok(tools.every((t) => t.inputSchema && t.inputSchema.type === "object"));
|
|
163
175
|
h.server.stop();
|
|
164
176
|
});
|
|
165
177
|
|
|
166
|
-
test("tools/list honours the email gate (emailAvailable:false →
|
|
178
|
+
test("tools/list honours the email gate (emailAvailable:false → 136 tools)", async () => {
|
|
167
179
|
const h = harness({ emailAvailable: false });
|
|
168
180
|
const r = await h.request("tools/list", {});
|
|
169
|
-
assert.equal(r.result.tools.length,
|
|
181
|
+
assert.equal(r.result.tools.length, 136, "the 5 own-mailbox email tools drop out");
|
|
170
182
|
const names = new Set(r.result.tools.map((t) => t.name));
|
|
171
183
|
// The five own-mailbox tools (email:true) are gated out …
|
|
172
184
|
for (const gated of ["email_send", "email_inbox", "email_message", "email_thread", "email_mark_read"]) {
|
package/lib/org/client.mjs
CHANGED
|
@@ -109,9 +109,18 @@ export function isEnabled(cfg) {
|
|
|
109
109
|
* Distil the org client config from an agent config object. Resolves:
|
|
110
110
|
* - base: the server origin for the /v1 binding (explicit `org.cohort.base`,
|
|
111
111
|
* else derived from `apiUrl` [+ `orgId` path] for back-compat).
|
|
112
|
-
* - orgId: the org
|
|
112
|
+
* - orgId: the org's ID — config `org.cohort.orgId` else COHORT_ORG_ID env
|
|
113
113
|
* (hq's documented var). Used by the legacy directory-path derivation
|
|
114
114
|
* and, when known, sent as the `x-org-id` pin header (defence-in-depth).
|
|
115
|
+
*
|
|
116
|
+
* NOT THE SLUG, though this said "the org slug" until 2026-08-21 and
|
|
117
|
+
* `docs/guides/mac-mini.md` told operators to export one. hq compares
|
|
118
|
+
* the pin with STRICT EQUALITY against the key's `Org.id`
|
|
119
|
+
* (`src/server/auth/org-api-key.ts:163`) — no slug fallback — so a
|
|
120
|
+
* slug here 401s every call. The two are not interchangeable: the
|
|
121
|
+
* ID has the form `org_default_<slug>` (e.g. slug `acme` → ID
|
|
122
|
+
* `org_default_acme`), never the bare slug. Note `COHORT_AGENT_ID`
|
|
123
|
+
* beside it genuinely IS a slug.
|
|
115
124
|
* - token: bearer credential — the RAW OrgApiKey. Resolution order:
|
|
116
125
|
* config `token` → COHORT_API_TOKEN → COHORT_TOKEN → COHORT_API_KEY.
|
|
117
126
|
* hq's auth doc names the agent var COHORT_API_KEY; the older
|
|
@@ -1314,6 +1323,54 @@ export function messagingSend(params, o = {}) {
|
|
|
1314
1323
|
return call("messaging.send", params || {}, o);
|
|
1315
1324
|
}
|
|
1316
1325
|
|
|
1326
|
+
/**
|
|
1327
|
+
* Ask hq who — if anyone — should respond to a message in a channel
|
|
1328
|
+
* (messaging.electResponder). This is the SHOULD-I-RESPOND decision, moved off
|
|
1329
|
+
* the daemon and onto hq's `routeResponders`, which for an undirected group
|
|
1330
|
+
* message elects exactly ONE responder or NONE, and already fails safe (no model
|
|
1331
|
+
* → quiet). hq derives the roster + trigger server-side from just
|
|
1332
|
+
* `{messageId, channelId}`, org-scoped, computes the verdict ONCE per messageId
|
|
1333
|
+
* and caches it in a durable ledger event, so concurrent daemons read the same
|
|
1334
|
+
* election (the loser of a race reads the winner).
|
|
1335
|
+
*
|
|
1336
|
+
* Read-style: agent-tier `messaging.read` scope (any paired seat may ask),
|
|
1337
|
+
* `sideEffecting:false` — no client idempotency key, and the server owns the
|
|
1338
|
+
* write-once ledger append. Returns a res frame whose `result` is
|
|
1339
|
+
* `{ election, mode, responders:[{slug,dimension}], deferredToHuman }`.
|
|
1340
|
+
*
|
|
1341
|
+
* FAIL-OPEN AT THE TRANSPORT ONLY: an unreachable/disabled server yields an
|
|
1342
|
+
* error frame, never a throw. The CALLER owns the fail-SAFE inversion — for an
|
|
1343
|
+
* ambient channel, "no verdict / not me" means STAY SILENT, never respond-to-all.
|
|
1344
|
+
*
|
|
1345
|
+
* @param {{messageId:string, channelId:string}} params
|
|
1346
|
+
* @param {object} [o] { base, token, orgId?, fetchImpl? }
|
|
1347
|
+
*/
|
|
1348
|
+
export function electResponder(params, o = {}) {
|
|
1349
|
+
return call("messaging.electResponder", params || {}, o);
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
/**
|
|
1353
|
+
* Record a voice note in THIS seat's own designed voice
|
|
1354
|
+
* (messaging.synthesizeVoiceNote) and return the READY `{fileId, durationMs,
|
|
1355
|
+
* sizeBytes}` to attach.
|
|
1356
|
+
*
|
|
1357
|
+
* This is the RECORD half only. Posting is an ordinary `messagingSend` with
|
|
1358
|
+
* `attachments:[{fileId}]` — hq deliberately keeps the two apart because
|
|
1359
|
+
* synthesis is a provider round-trip that cannot ride its transactional send
|
|
1360
|
+
* lane, and because a spoken message must be an ordinary message with a file on
|
|
1361
|
+
* it rather than a second send path with its own governance.
|
|
1362
|
+
*
|
|
1363
|
+
* Refusals come back as error frames naming the cause (no synthesis key on the
|
|
1364
|
+
* deployment, this seat has no designed voice, the hourly recording cap). Relay
|
|
1365
|
+
* the cause — never claim a voice note was sent when the frame was not ok.
|
|
1366
|
+
*
|
|
1367
|
+
* `messaging_send_voice_note` (tool-surface) composes both halves into one verb.
|
|
1368
|
+
* @param {{text:string}} params
|
|
1369
|
+
*/
|
|
1370
|
+
export function messagingSynthesizeVoiceNote(params, o = {}) {
|
|
1371
|
+
return call("messaging.synthesizeVoiceNote", params || {}, o);
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1317
1374
|
/** React to an org message (messaging.react). */
|
|
1318
1375
|
export function messagingReact(params, o = {}) {
|
|
1319
1376
|
return call("messaging.react", params || {}, o);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
b2cbf124c99800a0ad1b19d7aea6a88c3c72f07b53838eb44ef6f82d888fda43
|
package/lib/org/protocol.mjs
CHANGED
|
@@ -95,6 +95,15 @@ export const FAMILIES = Object.freeze([
|
|
|
95
95
|
// because the same surface outlives the run — the Org › Architecture view
|
|
96
96
|
// reads this state for a workspace that has long since committed.
|
|
97
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",
|
|
98
107
|
]);
|
|
99
108
|
|
|
100
109
|
/**
|
|
@@ -311,7 +320,35 @@ export const METHODS = Object.freeze({
|
|
|
311
320
|
// the transactional lane. Still messaging.write scope: faking "X is
|
|
312
321
|
// composing" in a room is a social write a read-only key must not have. ---
|
|
313
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 },
|
|
314
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 },
|
|
315
352
|
// --- calling (SP3): call lifecycle in the human app. NOT reserved-admin. ---
|
|
316
353
|
"calling.start": { family: "calling", scope: "calling.write", sideEffecting: true },
|
|
317
354
|
"calling.join": { family: "calling", scope: "calling.write", sideEffecting: true },
|
|
@@ -1008,6 +1045,67 @@ export const METHODS = Object.freeze({
|
|
|
1008
1045
|
"architecture.status": { family: "architecture", scope: "org.read", sideEffecting: false },
|
|
1009
1046
|
"architecture.accept": { family: "architecture", scope: "org.write", sideEffecting: true, idempotent: true },
|
|
1010
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 },
|
|
1011
1109
|
});
|
|
1012
1110
|
|
|
1013
1111
|
/**
|
|
@@ -139,7 +139,11 @@ test("protocol: frozen family + method counts (additive evolution guard)", () =>
|
|
|
139
139
|
// and architecture (the four tier review decisions and the one whole-
|
|
140
140
|
// architecture approval, which outlive the run as the Org › Architecture
|
|
141
141
|
// view) → 53.
|
|
142
|
-
|
|
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");
|
|
143
147
|
// 357 = the mesh-protocol + agent UI-parity + calling + branding + email
|
|
144
148
|
// methods, plus the Live Integrations surface: integration.toolsetVersion +
|
|
145
149
|
// integration.listAgentTools (SP1) and integration.invokeTool (SP5, execute a
|
|
@@ -188,7 +192,20 @@ test("protocol: frozen family + method counts (additive evolution guard)", () =>
|
|
|
188
192
|
// had shipped WITHOUT descriptors, which made all 13 unreachable — dispatch
|
|
189
193
|
// rejects unknown methods and scopeForMethod resolves them to `admin`, which
|
|
190
194
|
// no principal holds → 474.
|
|
191
|
-
|
|
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");
|
|
192
209
|
});
|
|
193
210
|
|
|
194
211
|
test("protocol SP3: messaging + calling families/methods/scopes", async () => {
|