@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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ui-parity.test.mjs — Agent UI-parity helpers (full human-action mirror).
|
|
3
3
|
*
|
|
4
|
-
* The
|
|
4
|
+
* The 360 wrappers in ui-parity.mjs are each a one-liner over call() from
|
|
5
5
|
* client.mjs, so client.test.mjs already proves the transport contract (headers,
|
|
6
6
|
* idempotency, fail-open, frame normalisation). Here we just prove a representative
|
|
7
7
|
* slice across families ROUTES to the correct method name, POSTs the params body,
|
|
@@ -326,7 +326,7 @@ test("a human-gated directory verb surfaces the server's refusal verbatim, never
|
|
|
326
326
|
assert.match(frame.error.message, /human/);
|
|
327
327
|
});
|
|
328
328
|
|
|
329
|
-
test("every desk protocol method has exactly one ui-parity wrapper (196 desks /
|
|
329
|
+
test("every desk protocol method has exactly one ui-parity wrapper (196 desks / 373 total)", async () => {
|
|
330
330
|
const fs = await import("node:fs");
|
|
331
331
|
const path = await import("node:path");
|
|
332
332
|
const url = await import("node:url");
|
|
@@ -340,7 +340,15 @@ test("every desk protocol method has exactly one ui-parity wrapper (196 desks /
|
|
|
340
340
|
// NONE of calling's 22 — so the huddle control, the calendar Join, host
|
|
341
341
|
// mute/lock/remove, hand-raising and in-call chat had no wrapper at all.
|
|
342
342
|
// +9 messaging +22 calling → 328 → 359.
|
|
343
|
-
|
|
343
|
+
// 2026-08 voice delta: messaging.synthesizeVoiceNote — the agent-plane twin
|
|
344
|
+
// of the composer's hold-to-record button, which the messaging family had no
|
|
345
|
+
// wrapper for at all → 360.
|
|
346
|
+
// 2026-08 mandate delta: the whole accountability spine (11 methods) was
|
|
347
|
+
// protocol-declared and wrapped nowhere, so `lib/mandate/refresh.mjs` reached
|
|
348
|
+
// it by spelling the method name out as a string constant of its own → 371.
|
|
349
|
+
// 2026-08 preference delta: hq built `preference.*` FOR this plane (its own
|
|
350
|
+
// handler docblock says so) and the SDK wrapped neither half → 373.
|
|
351
|
+
assert.equal(called.length, 373, "one call() site per wrapper");
|
|
344
352
|
assert.equal(new Set(called).size, called.length, "no duplicate method bindings");
|
|
345
353
|
// The two conversation families are now WHOLE, which is what makes the
|
|
346
354
|
// module docblock's claim true rather than aspirational.
|
|
@@ -348,8 +356,11 @@ test("every desk protocol method has exactly one ui-parity wrapper (196 desks /
|
|
|
348
356
|
const famMethods = Object.entries((await import("./protocol.mjs")).METHODS)
|
|
349
357
|
.filter(([, d]) => d.family === fam)
|
|
350
358
|
.map(([n]) => n);
|
|
351
|
-
|
|
352
|
-
|
|
359
|
+
// Whole MINUS the declared daemon-internal exceptions (the same ledger the
|
|
360
|
+
// sibling test enforces) — e.g. messaging.electResponder is the daemon's
|
|
361
|
+
// responder-election transport, not a human-app verb.
|
|
362
|
+
const unwrapped = famMethods.filter((m) => !new Set(called).has(m) && !DELIBERATELY_UNWRAPPED.has(m));
|
|
363
|
+
assert.deepEqual(unwrapped, [], `every ${fam}.* method is wrapped or declared daemon-internal`);
|
|
353
364
|
}
|
|
354
365
|
const p = await import("./protocol.mjs");
|
|
355
366
|
const deskFams = new Set(["books", "calendar", "crm", "directory", "files"]);
|
|
@@ -398,6 +409,11 @@ const DELIBERATELY_UNWRAPPED = new Map([
|
|
|
398
409
|
["integration.listAgentTools", "runtime toolset plane (daemon poll)"],
|
|
399
410
|
["integration.invokeTool", "runtime toolset plane (executor)"],
|
|
400
411
|
["meetings.record", "knowledge plane writer, not the desk verb"],
|
|
412
|
+
// The responder-election plane: the daemon asks hq who — if anyone — should
|
|
413
|
+
// answer an undirected channel message. Server-decided and daemon-driven
|
|
414
|
+
// (scripts/daemon/agent-daemon.mjs, via lib/org/client.electResponder); a human
|
|
415
|
+
// never elects a responder, so there is no human-app verb to wrap.
|
|
416
|
+
["messaging.electResponder", "responder-election plane — daemon-driven (client.electResponder)"],
|
|
401
417
|
]);
|
|
402
418
|
|
|
403
419
|
test("the claimed families are wrapped WHOLE, minus a declared, self-cleaning exception list", async () => {
|
|
@@ -415,7 +431,8 @@ test("the claimed families are wrapped WHOLE, minus a declared, self-cleaning ex
|
|
|
415
431
|
"escalation", "annotation", "contact", "file", "memory", "compact",
|
|
416
432
|
"notification", "decision", "messaging", "calling", "board", "invitation",
|
|
417
433
|
"user", "settings", "billing", "org", "integration", "email", "artifact",
|
|
418
|
-
"books", "calendar", "crm", "directory", "files",
|
|
434
|
+
"books", "calendar", "crm", "directory", "files", "meetings", "mandate",
|
|
435
|
+
"preference",
|
|
419
436
|
];
|
|
420
437
|
const undeclared = [];
|
|
421
438
|
for (const name of Object.keys(p.METHODS)) {
|
|
@@ -437,5 +454,107 @@ test("the claimed families are wrapped WHOLE, minus a declared, self-cleaning ex
|
|
|
437
454
|
assert.deepEqual(notMethods, [], "these are not protocol methods — delete their exception lines");
|
|
438
455
|
|
|
439
456
|
// And the docblock's own numbers stay honest.
|
|
440
|
-
assert.equal(wrapped.size,
|
|
457
|
+
assert.equal(wrapped.size, 373, "the docblock's wrapper count");
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* THE FAMILIES THIS MODULE DOES NOT CLAIM, and who carries each instead.
|
|
462
|
+
*
|
|
463
|
+
* The guard above is airtight INSIDE `CLAIMED` and blind at its edge: it walks
|
|
464
|
+
* the protocol table and `continue`s on any family not in the list, so a NEWLY
|
|
465
|
+
* DECLARED family lands wrapped by nobody and fails no test. That is not
|
|
466
|
+
* hypothetical — genesis, subagent and mandate all arrived dark in one
|
|
467
|
+
* fortnight and were found by a census, not by CI.
|
|
468
|
+
*
|
|
469
|
+
* So every family in `METHODS` must now appear either in `CLAIMED` or here,
|
|
470
|
+
* with the surface that actually carries it. Two entries deliberately record a
|
|
471
|
+
* REFUSAL rather than a home, and two record a genuine hole; that is the point
|
|
472
|
+
* of a ledger — the honest answer is allowed, silence is not.
|
|
473
|
+
*/
|
|
474
|
+
const UNCLAIMED_FAMILIES = new Map([
|
|
475
|
+
// The org spine: named wrappers live in client.mjs beside the transport they
|
|
476
|
+
// use, and predate this module. Moving them would break every importer.
|
|
477
|
+
["approval", "client.mjs approvalRequest/Resolve/Get/List + lib/org/approvals.mjs; curated approval_request/approval_wait"],
|
|
478
|
+
["branding", "client.mjs (said in this module's own docblock); curated design_* tools"],
|
|
479
|
+
["contacts", "client.mjs contactsUpsert — the knowledge plane's contact writer"],
|
|
480
|
+
["cost", "client.mjs costReport + lib/org/cost-sync.mjs"],
|
|
481
|
+
["credential", "client.mjs credentialPut/Lease/List/Revoke + lib/org/keys.mjs — a secret broker, not a pane"],
|
|
482
|
+
["governance", "client.mjs governanceStatus/setDecisionRights — the §0.4 control plane"],
|
|
483
|
+
["handoff", "client.mjs + lib/org/handoff.mjs — governance-gated delegation"],
|
|
484
|
+
["knowledge", "client.mjs + lib/org/knowledge.mjs; curated knowledge_search/knowledge_append"],
|
|
485
|
+
["lease", "client.mjs + lib/org/leases.mjs — the work kernel's lock primitive"],
|
|
486
|
+
["policy", "client.mjs policyPublish + lib/org/policy.mjs"],
|
|
487
|
+
["presence", "client.mjs presenceBeat + lib/org/registry.mjs — the daemon's own heartbeat"],
|
|
488
|
+
["registry", "client.mjs register/heartbeat + lib/org/registry.mjs — enrolment, not a click"],
|
|
489
|
+
["pairing", "client.mjs pairRequest — PRE-AUTH, deliberately off the authenticated call() path"],
|
|
490
|
+
["agent", "agent.wait is the long-poll TRANSPORT rung (lib/org/push.mjs DEFAULT_WAIT_PATH), not a surface"],
|
|
491
|
+
// The registry family has a home with state a thin wrapper would silently
|
|
492
|
+
// lose. Reads reach a session through the curated subagent_* tools.
|
|
493
|
+
["subagent", "lib/subagents/client.mjs — owns the offline outbox + lock file; curated subagent_list/get/resolve for the reads"],
|
|
494
|
+
// The venture-deliverable catalogue (2026-08). Its maestro surface is the
|
|
495
|
+
// CURATED table, not a thin one-liner per method: the eight `resource_*` desk
|
|
496
|
+
// tools are GENERATED from hq's own desk declaration and parity-tested
|
|
497
|
+
// against it, because hand-copying a desk table is precisely what produced
|
|
498
|
+
// the 239-ops-vs-69-tools drift this wave exists to stop. A hand-written
|
|
499
|
+
// wrapper here would be a third copy of the same eight names.
|
|
500
|
+
["resource", "lib/org/tool-surface.mjs — curated resource_* desk tools, GENERATED from hq's desk declaration (never hand-copied) and parity-tested against it"],
|
|
501
|
+
// REFUSALS, not gaps. hq's `requireGenesisSeat` throws UNAUTHORIZED on any
|
|
502
|
+
// actor whose kind is not "human" (src/server/methods/genesis/_shared.ts),
|
|
503
|
+
// and architecture.* imports that same gate. A Genesis run belongs to
|
|
504
|
+
// (orgId, startedById) — one workspace AND one person — and an api-key
|
|
505
|
+
// principal is neither. Wrappers here would be dead code that always 401s,
|
|
506
|
+
// which is a worse surface than an honest absence.
|
|
507
|
+
["genesis", "hq refuses every non-human actor (requireGenesisSeat) — unreachable from a bearer key by design"],
|
|
508
|
+
["architecture", "same gate as genesis (it imports requireGenesisSeat) — unreachable from a bearer key by design"],
|
|
509
|
+
// HOLES, recorded as holes. Both are admin-scope writes with no SDK caller
|
|
510
|
+
// today; neither is reachable below the CEO tier even through org_rpc.
|
|
511
|
+
["admin", "GAP — the deactivate/reactivate kill switch has no wrapper and no tool; admin scope, operator console only"],
|
|
512
|
+
["hierarchy", "GAP — hierarchy.set (admin scope) has no wrapper; reporting edges are set in hq's own org chart today"],
|
|
513
|
+
]);
|
|
514
|
+
|
|
515
|
+
test("every protocol family is CLAIMED or has a written line saying who carries it", async () => {
|
|
516
|
+
const p = await import("./protocol.mjs");
|
|
517
|
+
|
|
518
|
+
// Kept in step with the guard above by construction: read the same list.
|
|
519
|
+
const CLAIMED = new Set([
|
|
520
|
+
"charter", "member", "team", "channel", "persona", "profile", "sop",
|
|
521
|
+
"escalation", "annotation", "contact", "file", "memory", "compact",
|
|
522
|
+
"notification", "decision", "messaging", "calling", "board", "invitation",
|
|
523
|
+
"user", "settings", "billing", "org", "integration", "email", "artifact",
|
|
524
|
+
"books", "calendar", "crm", "directory", "files", "meetings", "mandate",
|
|
525
|
+
"preference",
|
|
526
|
+
]);
|
|
527
|
+
|
|
528
|
+
const families = new Set(Object.values(p.METHODS).map((d) => d.family));
|
|
529
|
+
const undeclared = [...families]
|
|
530
|
+
.filter((f) => !CLAIMED.has(f) && !UNCLAIMED_FAMILIES.has(f))
|
|
531
|
+
.sort();
|
|
532
|
+
// If this fails, a family was added to protocol.mjs and nothing on this plane
|
|
533
|
+
// noticed. Wrap it (and add it to CLAIMED), or add an UNCLAIMED_FAMILIES line
|
|
534
|
+
// naming the surface that carries it. "It's reachable via org_rpc" is not an
|
|
535
|
+
// answer: org_rpc is access:"admin" and does not exist below the CEO tier.
|
|
536
|
+
assert.deepEqual(undeclared, [], "every protocol family is claimed or declared");
|
|
537
|
+
|
|
538
|
+
// Self-cleaning: a declared family that has since been claimed, or that is no
|
|
539
|
+
// longer a protocol family at all, must lose its line rather than rot.
|
|
540
|
+
const nowClaimed = [...UNCLAIMED_FAMILIES.keys()].filter((f) => CLAIMED.has(f));
|
|
541
|
+
assert.deepEqual(nowClaimed, [], "these are claimed now — delete their ledger lines");
|
|
542
|
+
const gone = [...UNCLAIMED_FAMILIES.keys()].filter((f) => !families.has(f));
|
|
543
|
+
assert.deepEqual(gone, [], "these are not protocol families — delete their ledger lines");
|
|
544
|
+
|
|
545
|
+
// The two CLAIMED lists must stay identical, or one guard silently narrows.
|
|
546
|
+
const src = (await import("node:fs")).readFileSync(
|
|
547
|
+
(await import("node:path")).join(
|
|
548
|
+
(await import("node:path")).dirname(
|
|
549
|
+
(await import("node:url")).fileURLToPath(import.meta.url),
|
|
550
|
+
),
|
|
551
|
+
"ui-parity.test.mjs",
|
|
552
|
+
),
|
|
553
|
+
"utf8",
|
|
554
|
+
);
|
|
555
|
+
const blocks = [...src.matchAll(/const CLAIMED = (?:new Set\()?\[([\s\S]*?)\]/g)].map((m) =>
|
|
556
|
+
[...m[1].matchAll(/"([a-z]+)"/g)].map((x) => x[1]).sort().join(","),
|
|
557
|
+
);
|
|
558
|
+
assert.equal(blocks.length, 2, "both guards declare a CLAIMED list");
|
|
559
|
+
assert.equal(blocks[0], blocks[1], "the two CLAIMED lists must not drift apart");
|
|
441
560
|
});
|
package/lib/tool-definitions.js
CHANGED
|
@@ -30,6 +30,11 @@
|
|
|
30
30
|
*/
|
|
31
31
|
|
|
32
32
|
import { getOrgTools, loadIntegrationToolsFromDisk } from "./org/tool-surface.mjs";
|
|
33
|
+
// The tool-side twin of normalizeAccessLevel below. Exact string equality used to
|
|
34
|
+
// drop any entry whose `access` was missing or mis-cased out of EVERY tier —
|
|
35
|
+
// see lib/org/tool-access.mjs for why that is the same footgun, on the other
|
|
36
|
+
// side of the same comparison.
|
|
37
|
+
import { partitionByAccess } from "./org/tool-access.mjs";
|
|
33
38
|
|
|
34
39
|
// ============================================================================
|
|
35
40
|
// 1. COMMUNICATION TOOLS
|
|
@@ -573,8 +578,24 @@ const generateReport = {
|
|
|
573
578
|
const ORG_TOOL_DEFS = getOrgTools({}).filter((t) => !t.integration);
|
|
574
579
|
const asClaudeTool = ({ name, description, input_schema }) => ({ name, description, input_schema });
|
|
575
580
|
const orgTools = ORG_TOOL_DEFS.map(asClaudeTool);
|
|
576
|
-
|
|
577
|
-
|
|
581
|
+
|
|
582
|
+
// EXHAUSTIVE partition — every curated tool lands in exactly one tier.
|
|
583
|
+
//
|
|
584
|
+
// This replaces three independent `filter(t => t.access === x)` passes, which
|
|
585
|
+
// silently agreed to drop any entry they all disagreed with: a tool whose
|
|
586
|
+
// `access` was absent or mis-cased ("Read", " write ") matched no bucket and
|
|
587
|
+
// therefore reached NO native access level — not ceo, not leadership, not
|
|
588
|
+
// voice, not default — while the MCP plane kept publishing it, because
|
|
589
|
+
// lib/mcp/server.mjs reads `access` only to set `readOnlyHint`. A capability
|
|
590
|
+
// could be fully built, fully tested and invisible on this plane alone, with
|
|
591
|
+
// nothing anywhere saying so. That is the same silent UNDER-privilege footgun
|
|
592
|
+
// `normalizeAccessLevel` (below) was written to kill on the CALLER's side of
|
|
593
|
+
// the very same string comparison, left unfixed on the TOOL's side.
|
|
594
|
+
// partitionByAccess normalises casing and sends a genuinely unrecognised tier
|
|
595
|
+
// to the most-restricted bucket with a loud warning: fail closed, fail noisy,
|
|
596
|
+
// never fail invisible.
|
|
597
|
+
const ORG_TOOLS_BY_TIER = partitionByAccess(ORG_TOOL_DEFS);
|
|
598
|
+
const orgToolsByAccess = (access) => (ORG_TOOLS_BY_TIER[access] || []).map(asClaudeTool);
|
|
578
599
|
|
|
579
600
|
/**
|
|
580
601
|
* The workspace's GRANTED integration tools (SP5), read FRESH from the shared
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cohortapp/agent-sdk",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "Cohort Agent SDK
|
|
3
|
+
"version": "2.11.0",
|
|
4
|
+
"description": "Cohort Agent SDK — autonomous AI colleague runtime. Deploy senior AI colleagues on dedicated Mac minis, wired to the Cohort operating surface.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"agent-sdk": "./bin/maestro.mjs",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
{
|
|
11
11
|
"name": "maestro-skills",
|
|
12
12
|
"source": "./",
|
|
13
|
-
"description": "Maestro operational skills — executive briefings, communications, hiring, strategy, org-desk workflows (mail, files, calendar, CRM, books, directory, design) and Cohort call working sessions."
|
|
13
|
+
"description": "Maestro operational skills — executive briefings, communications, hiring, strategy, org-desk workflows (mail, files, calendar, CRM, books, directory, design, venture deliverables) and Cohort call working sessions."
|
|
14
14
|
}
|
|
15
15
|
]
|
|
16
16
|
}
|
|
@@ -131,6 +131,10 @@
|
|
|
131
131
|
"name": "brand-steward",
|
|
132
132
|
"description": "Work the org's Design section — read the brand foundation and voice, rewrite copy in that voice, render and export templates and the brand kit, regenerate stale imagery within a budget, and propose reviewable foundation changes. Use before writing anything the workspace publishes, to produce a branded asset, or when imagery/templates have gone stale."
|
|
133
133
|
},
|
|
134
|
+
{
|
|
135
|
+
"name": "venture-deliverables",
|
|
136
|
+
"description": "Work the venture's deliverable catalogue — the cards a founder actually sends an investor (deck, one-pager, film, site, data room), their order, their provenance, and their link to the workspace drive. Use to answer \"what have we produced\", to file or correct a deliverable after producing one, to re-sort what an outsider sees first, or to point a card at the file you just wrote."
|
|
137
|
+
},
|
|
134
138
|
{
|
|
135
139
|
"name": "call-working-sessions",
|
|
136
140
|
"description": "Turn a Cohort call into a working session — present a doc/sheet/channel/board/charter on the call stage, step through it (scroll/highlight/type), live-edit docs while talking, read what participants see on screen, and watch reactions and raised hands. Use when you are a participant in a live Cohort call and want to show rather than tell, walk people through a document, or ground \"this chart / that number\" talk in what is actually on screen."
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: venture-deliverables
|
|
3
|
+
description: Work the venture's deliverable catalogue — the cards a founder actually sends an investor (deck, one-pager, film, site, data room), their order, their provenance, and their link to the workspace drive. Use to answer "what have we produced", to file or correct a deliverable after producing one, to re-sort what an outsider sees first, or to point a card at the file you just wrote.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Venture Deliverables
|
|
7
|
+
|
|
8
|
+
`/resources` is the venture's own catalogue: one card per artifact the venture
|
|
9
|
+
has produced — deck, one-pager, film, site, data room, brand kit. It is not a
|
|
10
|
+
folder and not an inbox. It is the short list a founder sends an investor, and
|
|
11
|
+
the order of it is the order they are asked to look in.
|
|
12
|
+
|
|
13
|
+
Until this family shipped, no agent could see it. Colleagues could write a deck
|
|
14
|
+
and had no way to say the venture *had* one. That is the gap these eight tools
|
|
15
|
+
close: `resource_list` / `resource_get` (every seat, including least-privilege
|
|
16
|
+
ones), and `resource_create` / `resource_update` / `resource_delete` /
|
|
17
|
+
`resource_reorder` / `resource_attach_file` / `resource_detach_file` (an
|
|
18
|
+
EDITOR-equivalent seat). All of it is `executeOrgTool` — fail-open frames, never
|
|
19
|
+
a throw.
|
|
20
|
+
|
|
21
|
+
## Read it before you claim anything about it
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
node --input-type=module -e '
|
|
25
|
+
import { executeOrgTool } from "./lib/org/tool-surface.mjs";
|
|
26
|
+
const res = await executeOrgTool("resource_list", { limit: 200 });
|
|
27
|
+
for (const r of res.ok ? res.result.resources : [])
|
|
28
|
+
console.log(r.sortOrder, r.slug, `[${r.kind}]`, r.source, r.fileId ?? "-", r.url);
|
|
29
|
+
'
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
The order you get back is byte-identical to the order a human sees at
|
|
33
|
+
`/resources` — same `sortOrder asc, createdAt asc`. Quote it as-is; a
|
|
34
|
+
re-derivation can only disagree with the page.
|
|
35
|
+
|
|
36
|
+
## Provenance decides whether your edit survives
|
|
37
|
+
|
|
38
|
+
`source` is the single most load-bearing field on a card, and no schema can tell
|
|
39
|
+
you why:
|
|
40
|
+
|
|
41
|
+
- **`ftlab-portal`** — pushed from the venture portal and **re-upserted on its
|
|
42
|
+
natural key on every push**. Your delete comes back. Your retitle is
|
|
43
|
+
overwritten. If a portal card is wrong, the fix is upstream in the portal, not
|
|
44
|
+
here; correcting it here buys you the time until the next push and no longer.
|
|
45
|
+
- **`genesis`** — laid down when the venture was stood up.
|
|
46
|
+
- **`manual`** — created through this desk. These are the ones that are actually
|
|
47
|
+
yours to own.
|
|
48
|
+
|
|
49
|
+
You cannot set `source` yourself. `resource_create` always stamps `manual`, so a
|
|
50
|
+
card an agent made can never masquerade as portal-provenance. And `create` is
|
|
51
|
+
**not** an upsert: an existing slug answers `CONFLICT` rather than quietly
|
|
52
|
+
overwriting a portal row whose slug you guessed. Correct with `resource_update`.
|
|
53
|
+
|
|
54
|
+
## Filing a deliverable you just produced
|
|
55
|
+
|
|
56
|
+
Two steps, in this order — the card is the claim, the file is the object.
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
node --input-type=module -e '
|
|
60
|
+
import { executeOrgTool } from "./lib/org/tool-surface.mjs";
|
|
61
|
+
const made = await executeOrgTool("resource_create", {
|
|
62
|
+
kind: "one-pager",
|
|
63
|
+
title: "Seed one-pager — Aug 2026",
|
|
64
|
+
description: "Two-page summary for warm intros. Supersedes the June version.",
|
|
65
|
+
url: "https://acme.vc/one-pager.pdf",
|
|
66
|
+
});
|
|
67
|
+
console.log(made.ok ? made.result.resource.slug : made.error);
|
|
68
|
+
'
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Then, if the artifact is also filed in the workspace drive (the artifact ingest
|
|
72
|
+
does this automatically for portal-crossed artifacts), cite it:
|
|
73
|
+
`resource_attach_file { slug, fileId }`. Leave `setUrl` at its default `false` —
|
|
74
|
+
a deck, one-pager or film keeps its public URL because the venture's own site
|
|
75
|
+
and outreach link that object, and rewriting it to `/files/<id>` breaks those
|
|
76
|
+
links. Pass `setUrl:true` only when the in-app file genuinely *is* the
|
|
77
|
+
destination.
|
|
78
|
+
|
|
79
|
+
Write the `description` like it will be searched, because it will be: hq indexes
|
|
80
|
+
each card into the org's shared recall with the URL inside the summary text. A
|
|
81
|
+
card described as "deck" is findable by nobody; "Seed deck, 14 slides, used in
|
|
82
|
+
the Sept partner meetings" is.
|
|
83
|
+
|
|
84
|
+
## The catalogue is an index, not a drive
|
|
85
|
+
|
|
86
|
+
A card **cites** a file; a `WorkspaceFile` **is** one. `resource_*` returns the
|
|
87
|
+
card's own columns and a `fileId` — never bytes, name, size, version or share
|
|
88
|
+
list. That is a share boundary, not tidiness: the catalogue is org-wide, the
|
|
89
|
+
drive is seat-scoped, so a resource tool that re-served file content would be a
|
|
90
|
+
one-call way around the drive's own share filter.
|
|
91
|
+
|
|
92
|
+
So: read the card here, read the **object** with `files_get` / `files_doc_read` /
|
|
93
|
+
`org_rpc files.exportRequest`. If the drive answers `NOT_FOUND` on a `fileId` a
|
|
94
|
+
card carries, that is the honest answer — either the file was swept or your seat
|
|
95
|
+
is not shared on it, and those two are deliberately indistinguishable. Ask for
|
|
96
|
+
the share; do not route around it, and do not report "the deliverable is
|
|
97
|
+
missing" when what you hit was an ACL.
|
|
98
|
+
|
|
99
|
+
## Re-sorting is total, and the refusal is the feature
|
|
100
|
+
|
|
101
|
+
`resource_reorder` takes **every** slug in the workspace, exactly once. Anything
|
|
102
|
+
else is a `CONFLICT` that names the missing / unknown / duplicated slugs back to
|
|
103
|
+
you.
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
node --input-type=module -e '
|
|
107
|
+
import { executeOrgTool } from "./lib/org/tool-surface.mjs";
|
|
108
|
+
const cur = await executeOrgTool("resource_list", { limit: 200 });
|
|
109
|
+
const slugs = cur.result.resources.map((r) => r.slug);
|
|
110
|
+
const first = "film-founder-story";
|
|
111
|
+
const res = await executeOrgTool("resource_reorder", {
|
|
112
|
+
slugs: [first, ...slugs.filter((s) => s !== first)],
|
|
113
|
+
});
|
|
114
|
+
console.log(res.ok ? res.result.resources.map((r) => r.slug) : res.error.message);
|
|
115
|
+
'
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
List first, reorder from *that* list, and if it refuses, re-list rather than
|
|
119
|
+
retrying: a `CONFLICT` means the catalogue changed under you — usually a portal
|
|
120
|
+
push — and blind retry would scramble somebody's order. To nudge one card
|
|
121
|
+
without touching the rest, use `resource_update { sortOrder }` instead.
|
|
122
|
+
|
|
123
|
+
## Corrections without collateral damage
|
|
124
|
+
|
|
125
|
+
`resource_update` **shallow-merges** `metadata` (`{...prior, ...patch}`), and a
|
|
126
|
+
key you send as `null` is deleted. This matters more than it looks: the artifact
|
|
127
|
+
ingest writes `sha256`, `runId`, `documentVersion`, `byteState` and `driveUrl`
|
|
128
|
+
into that object, and if it were a whole-object replace, fixing a typo in a
|
|
129
|
+
title would erase the delivery provenance of the artifact. Patch the keys you
|
|
130
|
+
mean and leave the rest alone.
|
|
131
|
+
|
|
132
|
+
Three fields are deliberately not editable, and it is worth knowing why before
|
|
133
|
+
you go looking for them:
|
|
134
|
+
|
|
135
|
+
- **`slug`** — it is both the ref and the portal's re-sync key. Renaming it would
|
|
136
|
+
orphan the upsert and produce a duplicate card on the next push.
|
|
137
|
+
- **`fileId`** — re-pointing a card at a different drive row is its own audited
|
|
138
|
+
act: `resource_attach_file` / `resource_detach_file`.
|
|
139
|
+
- **`source`** — provenance is the server's to assign.
|
|
140
|
+
|
|
141
|
+
## Removing one
|
|
142
|
+
|
|
143
|
+
`resource_delete` removes **the card only**. The `WorkspaceFile` it cited is not
|
|
144
|
+
deleted, not even soft-deleted — the drive keeps its own 30-day recovery lane —
|
|
145
|
+
which is why the reply carries `fileRetained: true`. Say that out loud when you
|
|
146
|
+
report a deletion: "removed the card; the file is still in the drive" is the
|
|
147
|
+
truth, and "deleted the deck" is not.
|
|
148
|
+
|
|
149
|
+
`resource_detach_file` refuses with `CONFLICT` if the card's only destination is
|
|
150
|
+
the file you are detaching, unless you supply a replacement `url`. A card that
|
|
151
|
+
points nowhere is precisely the state this desk exists to prevent.
|
|
152
|
+
|
|
153
|
+
## Rules of the desk
|
|
154
|
+
|
|
155
|
+
- A card is a **claim the venture made this thing**. Never create one for an
|
|
156
|
+
artifact that does not exist yet, and never invent a `url` — if there is no
|
|
157
|
+
destination, there is no card.
|
|
158
|
+
- `metadata` is free JSON with no secret column behind it. Never put a token,
|
|
159
|
+
key or signed URL in it. Only its *key names* ride the event chain; the values
|
|
160
|
+
are yours to keep honest.
|
|
161
|
+
- A `FORBIDDEN` frame is your seat's scope, not a bug. Reads ride `org.read`
|
|
162
|
+
(every seat has it); the six writes ride `org.write` (EDITOR-equivalent). Ask
|
|
163
|
+
for the seat rather than working around it.
|
|
164
|
+
- Cite cards to humans by **slug + title**, and say the `source` when you are
|
|
165
|
+
about to change something a portal push will undo.
|
|
166
|
+
- Every write appends exactly one chain event, so a human can see what you did.
|
|
167
|
+
Write like that is true.
|
|
168
|
+
|
|
169
|
+
## Notes
|
|
170
|
+
|
|
171
|
+
- These tools register only when the vendored protocol carries the `resource`
|
|
172
|
+
family. `org_describe { family: "resource" }` lists the eight methods and their
|
|
173
|
+
scopes offline, with no credential.
|
|
174
|
+
- The human surface for the same rows is `/resources`. If what you report and
|
|
175
|
+
what that page shows ever differ, the page is right and you have a bug —
|
|
176
|
+
`resource_list` is deliberately the same query in the same order.
|
|
@@ -5,10 +5,23 @@
|
|
|
5
5
|
# ─────────────────────────────────────────────
|
|
6
6
|
# Domain Taxonomy
|
|
7
7
|
# Used in provenance context_domain and profile boundaries
|
|
8
|
+
#
|
|
9
|
+
# THESE DESCRIPTIONS ARE EXECUTABLE, NOT DOCUMENTATION.
|
|
10
|
+
# scripts/daemon/context-compiler.mjs#loadDomainDescriptions regex-parses each
|
|
11
|
+
# `description:` string into the compiled prompt, and #buildDomainKeywords
|
|
12
|
+
# turns the same strings into the live keyword matcher that assigns a domain
|
|
13
|
+
# to an item. Every 4+-character word here is therefore a classification
|
|
14
|
+
# keyword on every deployment.
|
|
15
|
+
#
|
|
16
|
+
# They must stay COMPANY-NEUTRAL. They previously named a specific (fictional)
|
|
17
|
+
# employer and a specific (fictional) joint-venture counterparty, so those two
|
|
18
|
+
# invented proper nouns were live classification keywords for every customer —
|
|
19
|
+
# matching nothing real, while the neutral words that should have matched were
|
|
20
|
+
# diluted. Describe the SHAPE of the information, never the name of a party.
|
|
8
21
|
# ─────────────────────────────────────────────
|
|
9
22
|
domains:
|
|
10
23
|
internal-legal:
|
|
11
|
-
description: "
|
|
24
|
+
description: "The company's own legal strategy, general counsel advice, privilege-protected"
|
|
12
25
|
default_sensitivity: high
|
|
13
26
|
financial:
|
|
14
27
|
description: "Cap table, runway, fundraising, financial planning"
|
|
@@ -23,19 +36,19 @@ domains:
|
|
|
23
36
|
description: "Approach to regulators, gap assessments, remediation"
|
|
24
37
|
default_sensitivity: high
|
|
25
38
|
strategic-initiative:
|
|
26
|
-
description: "
|
|
39
|
+
description: "Strategic initiative M&A targets, deal terms, pipeline"
|
|
27
40
|
default_sensitivity: critical
|
|
28
41
|
board-governance:
|
|
29
42
|
description: "Board materials, resolutions, governance"
|
|
30
43
|
default_sensitivity: high
|
|
31
44
|
partner-operations:
|
|
32
|
-
description: "
|
|
45
|
+
description: "Partner or joint-venture operational matters (shared domain)"
|
|
33
46
|
default_sensitivity: low
|
|
34
47
|
partner-commercial:
|
|
35
|
-
description: "
|
|
48
|
+
description: "Partner or joint-venture commercial and revenue matters (shared domain)"
|
|
36
49
|
default_sensitivity: medium
|
|
37
50
|
partner-internal:
|
|
38
|
-
description: "
|
|
51
|
+
description: "Company-side-only partner matters (negotiation position, internal joint-venture strategy)"
|
|
39
52
|
default_sensitivity: high
|
|
40
53
|
investor-relations:
|
|
41
54
|
description: "Investor communications, fundraising pipeline"
|
|
@@ -106,8 +119,22 @@ assessment:
|
|
|
106
119
|
medium: strip_and_send
|
|
107
120
|
low: warn_and_send
|
|
108
121
|
recent_context_hours: 48
|
|
109
|
-
|
|
110
|
-
|
|
122
|
+
|
|
123
|
+
# DEFAULT DENY. A slug listed here is granted blanket clearance across EVERY
|
|
124
|
+
# information domain — scripts/disclosure_boundaries.py#_build_clearance_summary
|
|
125
|
+
# renders "<name> has unrestricted access — all information domains are
|
|
126
|
+
# permitted" for them, and the assessment path treats them as unrestricted.
|
|
127
|
+
#
|
|
128
|
+
# It shipped populated with a fictional example recipient, which handed a
|
|
129
|
+
# person who exists in no customer's org a standing all-domains clearance on
|
|
130
|
+
# every deployment. Empty is the only safe shipped value, and it costs nothing:
|
|
131
|
+
# disclosure_boundaries.py already default-denies an undeclared domain.
|
|
132
|
+
#
|
|
133
|
+
# WIZARD STEP: `/init-maestro` may offer to populate this from
|
|
134
|
+
# config/agent.json `principal` — but ONLY after the operator explicitly
|
|
135
|
+
# confirms that person should bypass every barrier. Never populate it
|
|
136
|
+
# automatically, and never infer it from a contact list.
|
|
137
|
+
unrestricted_recipients: []
|
|
111
138
|
|
|
112
139
|
# ─────────────────────────────────────────────
|
|
113
140
|
# Audit Configuration
|