@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
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
DEFAULT_COHORT_BASE,
|
|
26
26
|
} from "./tool-surface.mjs";
|
|
27
27
|
import { methodDef, PROTOCOL_VERSION, READS } from "./protocol.mjs";
|
|
28
|
+
import { ADMIN_TOOLS, expectedAccessFor, normalizeToolAccess } from "./tool-access.mjs";
|
|
28
29
|
|
|
29
30
|
// ── env hygiene ────────────────────────────────────────────────────────────
|
|
30
31
|
const ENV_KEYS = [
|
|
@@ -59,7 +60,7 @@ function fakeFetch(body = { ok: true, result: { fine: true } }, { status = 200,
|
|
|
59
60
|
|
|
60
61
|
// ── table shape ────────────────────────────────────────────────────────────
|
|
61
62
|
|
|
62
|
-
test("table: curated
|
|
63
|
+
test("table: curated 54 + 5 email + 5 artifact + 77 desk tools, snake_case names, valid access + schemas", () => {
|
|
63
64
|
// 2026-08 mobile-parity delta: +5 mail (report_spam/react/move_mailbox/
|
|
64
65
|
// summarise/ask) and +3 asks (files/calendar/crm) → desk 56 → 64; the
|
|
65
66
|
// call-working-sessions delta adds the 5 calls-desk tools → 69.
|
|
@@ -84,8 +85,40 @@ test("table: curated 43 + 5 email + 5 artifact + 69 desk tools, snake_case names
|
|
|
84
85
|
// (channel.addMember), and engage_colleagues — the judged path that decides
|
|
85
86
|
// WHETHER anyone needs involving before it opens anything at all.
|
|
86
87
|
// Curated 38 → 42, +1 for work_track → 43.
|
|
87
|
-
|
|
88
|
-
|
|
88
|
+
// The VOICE delta adds messaging_send_voice_note — hq could synthesise a
|
|
89
|
+
// colleague's voice note only as a reflex (answering a human's spoken note in
|
|
90
|
+
// kind), so an agent ASKED in text for one had no verb and truthfully said it
|
|
91
|
+
// could not. A `local` binding: it records (messaging.synthesizeVoiceNote)
|
|
92
|
+
// then posts (plain messaging.send with the file attached). Curated 43 → 44.
|
|
93
|
+
// The MANDATE + REGISTRY + PREFERENCE delta (+10 → 54). Three families that
|
|
94
|
+
// were protocol-declared and curated NOWHERE, so a session could reach them
|
|
95
|
+
// only through `org_rpc` — access:"admin", i.e. not at all below the CEO tier:
|
|
96
|
+
// mandate (5) — hq's own chat colleague has carried the whole belt
|
|
97
|
+
// since the spine landed, under a docblock naming SDK
|
|
98
|
+
// parity as its reason; the SDK plane had none of it, so
|
|
99
|
+
// the daemon knew the seat's objectives and the session it
|
|
100
|
+
// spawned did not. adopt/retire are deliberately absent —
|
|
101
|
+
// `mandate.write` is not in DEFAULT_AGENT_SCOPES.
|
|
102
|
+
// subagent (3) — READS only. hq keeps the registry, maestro generates
|
|
103
|
+
// sub-agents locally, and the two halves never met. The
|
|
104
|
+
// writes stay with lib/subagents/client.mjs, which owns
|
|
105
|
+
// the offline outbox and the lock file a second writer
|
|
106
|
+
// would silently bypass.
|
|
107
|
+
// preference (2) — hq built this family FOR this plane and said so in the
|
|
108
|
+
// handler; no tool was ever added, so the same workspace
|
|
109
|
+
// remembered a standing preference when hq answered and
|
|
110
|
+
// forgot it when the seat's own daemon did.
|
|
111
|
+
// The RESOURCE delta (+8 desk → 77, table 133 → 141). A venture's artifacts
|
|
112
|
+
// cross from the portal and are filed as real WorkspaceFile rows, and the
|
|
113
|
+
// OrgResource catalogue that indexes them was reachable from NO agent code
|
|
114
|
+
// path in EITHER plane — the venture's own colleagues could not list, add,
|
|
115
|
+
// correct, reorder or remove one of their own deliverables. Unlike every
|
|
116
|
+
// block above it, this one is GENERATED from the vendored protocol
|
|
117
|
+
// declaration (lib/org/resource-tools.mjs) rather than transcribed from hq's
|
|
118
|
+
// desk, so it cannot acquire the 239-vs-69 drift the hand-copied desks
|
|
119
|
+
// already carry; resource-tools.test.mjs is the parity that holds it there.
|
|
120
|
+
assert.equal(ORG_TOOLS.length, 141, "54 curated + 5 email + 5 artifact + 77 desk");
|
|
121
|
+
assert.equal(ORG_TOOLS.filter((t) => t.desk).length, 77, "exactly seventy-seven desk tools");
|
|
89
122
|
assert.equal(ORG_TOOLS.filter((t) => t.email).length, 5, "exactly five email tools");
|
|
90
123
|
assert.equal(ORG_TOOLS.filter((t) => t.artifact).length, 5, "exactly five artifact tools");
|
|
91
124
|
for (const t of ORG_TOOLS) {
|
|
@@ -117,26 +150,37 @@ test("table: curated 43 + 5 email + 5 artifact + 69 desk tools, snake_case names
|
|
|
117
150
|
|
|
118
151
|
test("OUTBOUND_METHODS is DERIVED from outbound:true tools (messaging/email/mail-desk/share-step sends)", () => {
|
|
119
152
|
assert.deepEqual([...OUTBOUND_METHODS].sort(), ["calling.appShareAct", "email.draftSend", "email.send", "messaging.send"]);
|
|
153
|
+
// messaging_send_voice_note is outbound-flagged but adds NOTHING to
|
|
154
|
+
// OUTBOUND_METHODS: it is a `local` binding, and the derivation reads rpc
|
|
155
|
+
// bindings only. Its POST leg is messaging.send, which is already in the set,
|
|
156
|
+
// so the escape hatch stays covered too — the flag here buys the screen on
|
|
157
|
+
// the SPOKEN words, before a billable character is synthesised.
|
|
120
158
|
const outboundTools = ORG_TOOLS.filter((t) => t.outbound).map((t) => t.name).sort();
|
|
121
|
-
assert.deepEqual(outboundTools, [
|
|
159
|
+
assert.deepEqual(outboundTools, [
|
|
160
|
+
"email_draft_send",
|
|
161
|
+
"email_send",
|
|
162
|
+
"messaging_send",
|
|
163
|
+
"messaging_send_voice_note",
|
|
164
|
+
"org_call_share_step",
|
|
165
|
+
]);
|
|
122
166
|
});
|
|
123
167
|
|
|
124
168
|
test("email gating: family present in the vendored protocol → tools active; override excludes", () => {
|
|
125
169
|
// The email family landed in the vendored protocol (sync-protocol phase 1).
|
|
126
170
|
assert.equal(emailFamilyAvailable(), true, "vendored protocol carries email.send");
|
|
127
|
-
assert.equal(getOrgTools().length,
|
|
171
|
+
assert.equal(getOrgTools().length, 141);
|
|
128
172
|
const without = getOrgTools({ emailAvailable: false });
|
|
129
|
-
assert.equal(without.length,
|
|
173
|
+
assert.equal(without.length, 136);
|
|
130
174
|
assert.ok(!without.some((t) => t.email), "email tools excluded when family absent");
|
|
131
175
|
});
|
|
132
176
|
|
|
133
177
|
test("artifact gating: family present → tools active; override excludes (email precedent)", () => {
|
|
134
178
|
assert.equal(artifactFamilyAvailable(), true, "vendored protocol carries artifact.act");
|
|
135
179
|
const without = getOrgTools({ artifactAvailable: false });
|
|
136
|
-
assert.equal(without.length,
|
|
180
|
+
assert.equal(without.length, 136);
|
|
137
181
|
assert.ok(!without.some((t) => t.artifact), "artifact tools excluded when family absent");
|
|
138
182
|
const neither = getOrgTools({ emailAvailable: false, artifactAvailable: false, desksAvailable: false });
|
|
139
|
-
assert.equal(neither.length,
|
|
183
|
+
assert.equal(neither.length, 54, "all additive families off → the 54 always-on tools");
|
|
140
184
|
});
|
|
141
185
|
|
|
142
186
|
test("isOrgTool / orgToolDef cover the full table; unknown names rejected", () => {
|
|
@@ -776,3 +820,336 @@ test("messaging_send passes mentions straight through to the wire", async () =>
|
|
|
776
820
|
// fails hq's zod and 400s the WHOLE message, tags and body alike.
|
|
777
821
|
assert.deepEqual(body.mentions, [{ memberId: "M-2" }]);
|
|
778
822
|
});
|
|
823
|
+
|
|
824
|
+
// ── speaking: messaging_send_voice_note ────────────────────────────────────
|
|
825
|
+
//
|
|
826
|
+
// The verb that closes "I can't send audio voice notes". hq could always
|
|
827
|
+
// synthesise an agent's voice note, but only as a REFLEX answering a human's
|
|
828
|
+
// spoken note in kind — asked in text, an agent had nothing to call.
|
|
829
|
+
//
|
|
830
|
+
// What is pinned here is the composition: record, then post the recording as an
|
|
831
|
+
// ORDINARY message attachment. There is no audio-only send path, so the post
|
|
832
|
+
// leg carries the same ACL, audit and dedup as any typed message; and a refused
|
|
833
|
+
// recording must never be followed by a post that implies one was made.
|
|
834
|
+
|
|
835
|
+
test("messaging_send_voice_note: records, then posts the file as an ordinary attachment", async () => {
|
|
836
|
+
let n = 0;
|
|
837
|
+
const calls = [];
|
|
838
|
+
const fetchImpl = async (url, init) => {
|
|
839
|
+
calls.push({ url: String(url), body: JSON.parse(init.body) });
|
|
840
|
+
n += 1;
|
|
841
|
+
return {
|
|
842
|
+
ok: true,
|
|
843
|
+
status: 200,
|
|
844
|
+
headers: { get: () => undefined },
|
|
845
|
+
json: async () =>
|
|
846
|
+
n === 1
|
|
847
|
+
? { ok: true, result: { fileId: "file_v1", durationMs: 4200 } }
|
|
848
|
+
: { ok: true, result: { messageId: "m1" } },
|
|
849
|
+
};
|
|
850
|
+
};
|
|
851
|
+
const frame = await executeOrgTool(
|
|
852
|
+
"messaging_send_voice_note",
|
|
853
|
+
{ channelId: "C1", text: "Shipping Friday." },
|
|
854
|
+
{ orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl, screenImpl: async () => ({ allow: true }) },
|
|
855
|
+
);
|
|
856
|
+
assert.equal(frame.ok, true);
|
|
857
|
+
assert.match(calls[0].url, /messaging\.synthesizeVoiceNote$/);
|
|
858
|
+
assert.equal(calls[0].body.text, "Shipping Friday.");
|
|
859
|
+
assert.match(calls[1].url, /messaging\.send$/);
|
|
860
|
+
assert.deepEqual(calls[1].body.attachments, [{ fileId: "file_v1" }]);
|
|
861
|
+
// The written body defaults to the spoken words, never an empty message.
|
|
862
|
+
assert.equal(calls[1].body.body, "Shipping Friday.");
|
|
863
|
+
assert.ok(calls[1].body.idempotencyId, "the post is dedup-keyed like any send");
|
|
864
|
+
assert.equal(frame.result.fileId, "file_v1");
|
|
865
|
+
});
|
|
866
|
+
|
|
867
|
+
test("messaging_send_voice_note: a refused recording is NOT followed by a post", async () => {
|
|
868
|
+
const calls = [];
|
|
869
|
+
const fetchImpl = async (url, init) => {
|
|
870
|
+
calls.push(String(url));
|
|
871
|
+
return {
|
|
872
|
+
ok: false,
|
|
873
|
+
status: 403,
|
|
874
|
+
headers: { get: () => undefined },
|
|
875
|
+
json: async () => ({
|
|
876
|
+
ok: false,
|
|
877
|
+
error: { code: "FORBIDDEN_SCOPE", message: "voice note not recorded (no-key): ..." },
|
|
878
|
+
}),
|
|
879
|
+
};
|
|
880
|
+
};
|
|
881
|
+
const frame = await executeOrgTool(
|
|
882
|
+
"messaging_send_voice_note",
|
|
883
|
+
{ channelId: "C1", text: "hi" },
|
|
884
|
+
{ orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl, screenImpl: async () => ({ allow: true }) },
|
|
885
|
+
);
|
|
886
|
+
assert.equal(frame.ok, false);
|
|
887
|
+
assert.match(frame.error.message, /no-key/);
|
|
888
|
+
assert.equal(calls.length, 1, "no post claiming a voice note that was never recorded");
|
|
889
|
+
});
|
|
890
|
+
|
|
891
|
+
test("messaging_send_voice_note: the send-gate screens the SPOKEN words before synthesis", async () => {
|
|
892
|
+
const fetchImpl = fakeFetch();
|
|
893
|
+
const seen = [];
|
|
894
|
+
const frame = await executeOrgTool(
|
|
895
|
+
"messaging_send_voice_note",
|
|
896
|
+
{ channelId: "C1", text: "the secret is hunter2", body: "see note" },
|
|
897
|
+
{
|
|
898
|
+
orgConfig: CFG,
|
|
899
|
+
agentRoot: "/tmp/none",
|
|
900
|
+
fetchImpl,
|
|
901
|
+
screenImpl: async (args) => {
|
|
902
|
+
seen.push(args);
|
|
903
|
+
return args.text.includes("hunter2") ? { allow: false, reason: "credential" } : { allow: true };
|
|
904
|
+
},
|
|
905
|
+
},
|
|
906
|
+
);
|
|
907
|
+
assert.equal(frame.ok, false);
|
|
908
|
+
assert.equal(frame.error.code, "FORBIDDEN_SCOPE");
|
|
909
|
+
// The gate sees the PROSE — the spoken words and the written body — not a
|
|
910
|
+
// stringified params blob. A gate judging JSON would pass sentences it never
|
|
911
|
+
// actually read.
|
|
912
|
+
assert.equal(seen[0].text, "the secret is hunter2\nsee note");
|
|
913
|
+
assert.equal(seen[0].recipient, "C1");
|
|
914
|
+
// Blocked BEFORE the network: audio is the hardest content to retract, and a
|
|
915
|
+
// blocked note must not cost a billable synthesis either.
|
|
916
|
+
assert.equal(fetchImpl.calls.length, 0);
|
|
917
|
+
});
|
|
918
|
+
|
|
919
|
+
test("messaging_send_voice_note: channelId and text are both required", async () => {
|
|
920
|
+
const fetchImpl = fakeFetch();
|
|
921
|
+
const frame = await executeOrgTool(
|
|
922
|
+
"messaging_send_voice_note",
|
|
923
|
+
{ channelId: "C1" },
|
|
924
|
+
{ orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl, screenImpl: async () => ({ allow: true }) },
|
|
925
|
+
);
|
|
926
|
+
assert.equal(frame.ok, false);
|
|
927
|
+
assert.equal(frame.error.code, "BAD_REQUEST");
|
|
928
|
+
assert.equal(fetchImpl.calls.length, 0);
|
|
929
|
+
});
|
|
930
|
+
|
|
931
|
+
// ── mandate / sub-agent registry / preferences (2026-08 parity delta) ───────
|
|
932
|
+
|
|
933
|
+
test("mandate tools ride their mandate.* methods and POST the params verbatim", async () => {
|
|
934
|
+
const seen = [];
|
|
935
|
+
const fetchImpl = async (url, init) => {
|
|
936
|
+
seen.push({ url: String(url), body: JSON.parse(init.body) });
|
|
937
|
+
return { ok: true, status: 200, headers: { get: () => undefined }, json: async () => ({ ok: true, result: {} }) };
|
|
938
|
+
};
|
|
939
|
+
fetchImpl.calls = seen;
|
|
940
|
+
const o = { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl };
|
|
941
|
+
await executeOrgTool("mandate_read", {}, o);
|
|
942
|
+
await executeOrgTool("mandate_objectives", { state: "active", includeRetired: false }, o);
|
|
943
|
+
await executeOrgTool("mandate_kpi_series", { objectiveId: "ob-1", limit: 20 }, o);
|
|
944
|
+
await executeOrgTool("mandate_propose_objective", { key: "pipeline-coverage", text: "3x cover", target: 3 }, o);
|
|
945
|
+
await executeOrgTool(
|
|
946
|
+
"mandate_record_kpi_sample",
|
|
947
|
+
{ objectiveId: "ob-1", value: 2.4, window: "2026-W33", evidence: { method: "crm.listDeals" } },
|
|
948
|
+
o,
|
|
949
|
+
);
|
|
950
|
+
assert.deepEqual(seen.map((c) => c.url), [
|
|
951
|
+
"https://org.example/api/v1/mandate.get",
|
|
952
|
+
"https://org.example/api/v1/mandate.tree",
|
|
953
|
+
"https://org.example/api/v1/mandate.series",
|
|
954
|
+
"https://org.example/api/v1/mandate.propose",
|
|
955
|
+
"https://org.example/api/v1/mandate.sample",
|
|
956
|
+
]);
|
|
957
|
+
// Pass-through, byte for byte: hq's mandate schemas read these names, and the
|
|
958
|
+
// evidence OBJECT is what the server's honesty gate inspects — flattening it
|
|
959
|
+
// to a string here would silently downgrade every sample to `llm`.
|
|
960
|
+
assert.deepEqual(seen[3].body, { key: "pipeline-coverage", text: "3x cover", target: 3 });
|
|
961
|
+
assert.deepEqual(seen[4].body.evidence, { method: "crm.listDeals" });
|
|
962
|
+
});
|
|
963
|
+
|
|
964
|
+
test("the mandate belt stops at PROPOSE — no adopt/retire tool exists at any tier", () => {
|
|
965
|
+
const names = new Set(ORG_TOOLS.map((t) => t.name));
|
|
966
|
+
for (const forbidden of ["mandate_adopt", "mandate_retire"]) {
|
|
967
|
+
assert.equal(names.has(forbidden), false, `${forbidden} must not exist`);
|
|
968
|
+
}
|
|
969
|
+
// The absence is structural, not incidental: no curated tool may bind a
|
|
970
|
+
// method whose scope is missing from DEFAULT_AGENT_SCOPES, because a tool the
|
|
971
|
+
// seat's own credential can never execute is a promise the model will make
|
|
972
|
+
// and fail to keep. `mandate.write` is the anti-Goodhart scope.
|
|
973
|
+
const bound = ORG_TOOLS.filter((t) => t.binding.kind === "rpc").map((t) => t.binding.method);
|
|
974
|
+
for (const m of bound) {
|
|
975
|
+
assert.notEqual(methodDef(m).scope, "mandate.write", `${m} needs mandate.write — not an agent's to hold`);
|
|
976
|
+
}
|
|
977
|
+
});
|
|
978
|
+
|
|
979
|
+
test("subagent tools are READS ONLY — the writes stay with the outbox-owning client", () => {
|
|
980
|
+
const subagentTools = ORG_TOOLS.filter(
|
|
981
|
+
(t) => t.binding.kind === "rpc" && t.binding.method.startsWith("subagent."),
|
|
982
|
+
);
|
|
983
|
+
assert.deepEqual(
|
|
984
|
+
subagentTools.map((t) => t.name).sort(),
|
|
985
|
+
["subagent_get", "subagent_list", "subagent_resolve"],
|
|
986
|
+
);
|
|
987
|
+
for (const t of subagentTools) {
|
|
988
|
+
assert.equal(t.access, "read", `${t.name} is a read`);
|
|
989
|
+
assert.equal(methodDef(t.binding.method).sideEffecting, false, `${t.binding.method} mutates nothing`);
|
|
990
|
+
}
|
|
991
|
+
});
|
|
992
|
+
|
|
993
|
+
test("preference tools ship as a PAIR — a write-only memory is not parity", async () => {
|
|
994
|
+
const names = ORG_TOOLS.filter(
|
|
995
|
+
(t) => t.binding.kind === "rpc" && t.binding.method.startsWith("preference."),
|
|
996
|
+
);
|
|
997
|
+
assert.deepEqual(names.map((t) => t.name).sort(), ["preference_list", "preference_remember"]);
|
|
998
|
+
assert.equal(orgToolDef("preference_list").access, "read");
|
|
999
|
+
assert.equal(orgToolDef("preference_remember").access, "write");
|
|
1000
|
+
// `why` is required in the SCHEMA, not just server-side: the evidence rule
|
|
1001
|
+
// has to reach the model, or it learns the rule by BAD_REQUEST.
|
|
1002
|
+
assert.ok(orgToolDef("preference_remember").input_schema.required.includes("why"));
|
|
1003
|
+
|
|
1004
|
+
const fetchImpl = fakeFetch({ ok: true, result: { id: "pref-1" } });
|
|
1005
|
+
const frame = await executeOrgTool(
|
|
1006
|
+
"preference_remember",
|
|
1007
|
+
{ domain: "travel", key: "seat", value: "aisle", why: "said so in #travel" },
|
|
1008
|
+
{ orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl },
|
|
1009
|
+
);
|
|
1010
|
+
assert.equal(frame.ok, true);
|
|
1011
|
+
assert.equal(fetchImpl.calls[0].url, "https://org.example/api/v1/preference.upsert");
|
|
1012
|
+
// sideEffecting:true at the dispatcher → a fresh header key per invocation.
|
|
1013
|
+
assert.ok(fetchImpl.calls[0].init.headers["x-idempotency-key"], "dispatcher key attached");
|
|
1014
|
+
});
|
|
1015
|
+
|
|
1016
|
+
// ── the access mechanism, TABLE-WIDE ───────────────────────────────────────
|
|
1017
|
+
//
|
|
1018
|
+
// The per-delta assertion below ("the new tools reach the tiers that need
|
|
1019
|
+
// them") lists ten names by hand, so it polices the tools whose author
|
|
1020
|
+
// remembered to add them and nothing else — every family that ships after it is
|
|
1021
|
+
// unguarded by default. These three assertions are the table-wide version:
|
|
1022
|
+
// they iterate ORG_TOOLS, so a capability cannot be parked at the CEO-only tier
|
|
1023
|
+
// or given a tier its protocol scope contradicts without one of them going red,
|
|
1024
|
+
// whether or not anyone updates a list.
|
|
1025
|
+
|
|
1026
|
+
test("ACCESS MECHANISM: `admin` is a CLOSED set of two escape hatches, not a parking space", () => {
|
|
1027
|
+
const admins = ORG_TOOLS.filter((t) => t.access === "admin").map((t) => t.name).sort();
|
|
1028
|
+
assert.deepEqual(
|
|
1029
|
+
admins,
|
|
1030
|
+
[...ADMIN_TOOLS].sort(),
|
|
1031
|
+
"only org_rpc/org_read may be admin-tier — anything else is a CAPABILITY, and a capability " +
|
|
1032
|
+
"at admin is CEO-only, i.e. exactly as unreachable for a leadership or default agent as " +
|
|
1033
|
+
"the org_rpc route a curated tool exists to replace",
|
|
1034
|
+
);
|
|
1035
|
+
for (const name of ADMIN_TOOLS) {
|
|
1036
|
+
const def = orgToolDef(name);
|
|
1037
|
+
assert.ok(def, `${name} is in the table`);
|
|
1038
|
+
assert.equal(def.access, "admin", `${name} is the escape hatch it claims to be`);
|
|
1039
|
+
// Both hatches are `local`: they take a method/path NAME and validate it
|
|
1040
|
+
// against the frozen table before any I/O. A desk verb can never be one.
|
|
1041
|
+
assert.equal(def.binding.kind, "local", `${name} is a console, not a capability`);
|
|
1042
|
+
}
|
|
1043
|
+
});
|
|
1044
|
+
|
|
1045
|
+
test("ACCESS MECHANISM: every rpc-bound tool's tier is DERIVED from its protocol scope", () => {
|
|
1046
|
+
// hq gates on SCOPE, so the native clamp has to mirror the scope lane or it
|
|
1047
|
+
// either withholds a tool hq would have allowed or offers one hq will refuse.
|
|
1048
|
+
// Note the rule this is NOT: `sideEffecting ? "write" : "read"` is wrong for
|
|
1049
|
+
// seven tools already in the table — knowledge.search, directory.ask and the
|
|
1050
|
+
// three branding renders/exports are AUDITED READS (side-effecting, `.read`
|
|
1051
|
+
// scope, runnable by a VIEWER seat), and artifact.create/act are the mirror
|
|
1052
|
+
// case. Deriving from sideEffecting would demote all five audited reads out
|
|
1053
|
+
// of the lookup set every tier gets.
|
|
1054
|
+
let checked = 0;
|
|
1055
|
+
for (const t of ORG_TOOLS) {
|
|
1056
|
+
if (!t.binding || t.binding.kind !== "rpc") continue;
|
|
1057
|
+
checked += 1;
|
|
1058
|
+
const want = expectedAccessFor(t.binding.method);
|
|
1059
|
+
assert.ok(want, `${t.name} binds a vendored method`);
|
|
1060
|
+
assert.equal(
|
|
1061
|
+
t.access,
|
|
1062
|
+
want,
|
|
1063
|
+
`${t.name} (${t.binding.method}, scope ${methodDef(t.binding.method).scope}) must be access:"${want}"`,
|
|
1064
|
+
);
|
|
1065
|
+
}
|
|
1066
|
+
assert.ok(checked >= 120, "the whole rpc-bound belt was checked, not a sample");
|
|
1067
|
+
});
|
|
1068
|
+
|
|
1069
|
+
test("ACCESS MECHANISM: no tool can fall out of every tier — the partition is exhaustive", () => {
|
|
1070
|
+
// Exact string equality used to bucket the table, so a missing or mis-cased
|
|
1071
|
+
// `access` matched NO bucket and reached NO native access level, while the
|
|
1072
|
+
// MCP plane kept publishing it. normalizeToolAccess is the tool-side twin of
|
|
1073
|
+
// normalizeAccessLevel: it normalises casing and sends a genuinely
|
|
1074
|
+
// unrecognised tier to the most-restricted bucket, loudly.
|
|
1075
|
+
for (const t of ORG_TOOLS) {
|
|
1076
|
+
assert.ok(
|
|
1077
|
+
["read", "write", "admin"].includes(t.access),
|
|
1078
|
+
`${t.name} declares a known tier verbatim (the normaliser is a safety net, not a licence)`,
|
|
1079
|
+
);
|
|
1080
|
+
const warnings = [];
|
|
1081
|
+
assert.equal(normalizeToolAccess(t.access, t.name, (m) => warnings.push(m)), t.access);
|
|
1082
|
+
assert.equal(warnings.length, 0, `${t.name} normalises silently`);
|
|
1083
|
+
}
|
|
1084
|
+
// And the safety net itself: a typo fails CLOSED and NOISY, never invisible.
|
|
1085
|
+
const warnings = [];
|
|
1086
|
+
assert.equal(normalizeToolAccess("Read", "x_tool", (m) => warnings.push(m)), "read", "casing is forgiven");
|
|
1087
|
+
assert.equal(normalizeToolAccess(undefined, "x_tool", (m) => warnings.push(m)), "admin", "a missing tier fails closed");
|
|
1088
|
+
assert.equal(warnings.length, 1, "and says so out loud");
|
|
1089
|
+
assert.match(warnings[0], /x_tool/);
|
|
1090
|
+
});
|
|
1091
|
+
|
|
1092
|
+
test("the new tools reach the tiers that need them — reads everywhere, no new admin surface", () => {
|
|
1093
|
+
const added = [
|
|
1094
|
+
"mandate_read", "mandate_objectives", "mandate_kpi_series",
|
|
1095
|
+
"mandate_propose_objective", "mandate_record_kpi_sample",
|
|
1096
|
+
"subagent_list", "subagent_get", "subagent_resolve",
|
|
1097
|
+
"preference_remember", "preference_list",
|
|
1098
|
+
];
|
|
1099
|
+
for (const n of added) {
|
|
1100
|
+
const def = orgToolDef(n);
|
|
1101
|
+
assert.ok(def, `${n} is in the table`);
|
|
1102
|
+
// The whole point of the delta: NONE of these may land at access:"admin",
|
|
1103
|
+
// which is the CEO-only escape-hatch tier. A capability parked there is
|
|
1104
|
+
// exactly as unreachable as the org_rpc route it was meant to replace.
|
|
1105
|
+
assert.notEqual(def.access, "admin", `${n} must not be admin-tier`);
|
|
1106
|
+
assert.equal(def.outbound, undefined, `${n} sends nothing a human reads as a message`);
|
|
1107
|
+
}
|
|
1108
|
+
assert.equal(
|
|
1109
|
+
added.filter((n) => orgToolDef(n).access === "read").length,
|
|
1110
|
+
7,
|
|
1111
|
+
"seven reads (3 mandate, 3 subagent, 1 preference) land in EVERY tier's lookup set",
|
|
1112
|
+
);
|
|
1113
|
+
});
|
|
1114
|
+
|
|
1115
|
+
test("the new tools FAIL CLOSED with no credential — refusal, never a silent success", async () => {
|
|
1116
|
+
const fetchImpl = fakeFetch();
|
|
1117
|
+
for (const n of ["mandate_read", "subagent_list", "preference_remember"]) {
|
|
1118
|
+
const frame = await executeOrgTool(n, {}, { orgConfig: {}, agentRoot: "/tmp/none", fetchImpl });
|
|
1119
|
+
assert.equal(frame.ok, false, `${n} refuses`);
|
|
1120
|
+
assert.equal(frame.error.code, "UNAUTHORIZED");
|
|
1121
|
+
assert.match(frame.error.message, /COHORT_API_TOKEN/);
|
|
1122
|
+
}
|
|
1123
|
+
assert.equal(fetchImpl.calls.length, 0, "nothing dispatched without a credential");
|
|
1124
|
+
});
|
|
1125
|
+
|
|
1126
|
+
test("a 401/403 from hq surfaces VERBATIM on the new tools (a refusal is an answer)", async () => {
|
|
1127
|
+
const denied = fakeFetch(
|
|
1128
|
+
{ ok: false, error: { code: "FORBIDDEN_SCOPE", message: "resolving another member's sub-agents requires admin" } },
|
|
1129
|
+
{ ok: false, status: 403 },
|
|
1130
|
+
);
|
|
1131
|
+
const frame = await executeOrgTool(
|
|
1132
|
+
"subagent_resolve",
|
|
1133
|
+
{ memberId: "someone-else" },
|
|
1134
|
+
{ orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl: denied },
|
|
1135
|
+
);
|
|
1136
|
+
assert.equal(frame.ok, false);
|
|
1137
|
+
assert.equal(frame.error.code, "FORBIDDEN_SCOPE");
|
|
1138
|
+
assert.match(frame.error.message, /admin/);
|
|
1139
|
+
|
|
1140
|
+
const unauth = fakeFetch({ ok: false, error: { code: "UNAUTHORIZED", message: "bad key" } }, { ok: false, status: 401 });
|
|
1141
|
+
const f2 = await executeOrgTool("mandate_read", {}, { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl: unauth });
|
|
1142
|
+
assert.equal(f2.ok, false);
|
|
1143
|
+
assert.equal(f2.error.code, "UNAUTHORIZED");
|
|
1144
|
+
});
|
|
1145
|
+
|
|
1146
|
+
test("transport failure on a new tool fails open into an INTERNAL frame, never a throw", async () => {
|
|
1147
|
+
const thrower = async () => { throw new Error("econnrefused"); };
|
|
1148
|
+
const frame = await executeOrgTool(
|
|
1149
|
+
"mandate_objectives",
|
|
1150
|
+
{},
|
|
1151
|
+
{ orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl: thrower },
|
|
1152
|
+
);
|
|
1153
|
+
assert.equal(frame.ok, false);
|
|
1154
|
+
assert.equal(frame.error.code, "INTERNAL");
|
|
1155
|
+
});
|
package/lib/org/ui-parity.mjs
CHANGED
|
@@ -1,13 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* lib/org/ui-parity.mjs — Agent UI-parity helpers (full human-action mirror).
|
|
3
3
|
*
|
|
4
|
-
* A 1:1 mirror of the human Cohort UI:
|
|
4
|
+
* A 1:1 mirror of the human Cohort UI: 373 named convenience wrappers over the
|
|
5
5
|
* /v1 RPC binding, one per method behind a HUMAN SURFACE — the things a person
|
|
6
6
|
* can do by clicking (charter, member, team, channel, persona, profile, sop,
|
|
7
7
|
* escalation, annotation, contact, file, memory, compact, notification,
|
|
8
8
|
* decision, messaging, calling, board, invitation, user, settings, billing, org,
|
|
9
|
-
* integration, email + the Inbox app, artifact, and the
|
|
10
|
-
* calendar, crm, directory, files, meetings.recapFile).
|
|
9
|
+
* integration, email + the Inbox app, artifact, mandate, preference, and the
|
|
10
|
+
* agent desks: books, calendar, crm, directory, files, meetings.recapFile).
|
|
11
|
+
*
|
|
12
|
+
* THE FAMILIES THIS MODULE DOES NOT CLAIM are no longer a silence. Every family
|
|
13
|
+
* in the protocol table is now either claimed here or carries a written line in
|
|
14
|
+
* `ui-parity.test.mjs`'s UNCLAIMED_FAMILIES naming the surface that carries it
|
|
15
|
+
* (client.mjs for the org spine, lib/subagents/client.mjs for the registry, or
|
|
16
|
+
* "hq refuses an api-key principal outright" for genesis/architecture). Before
|
|
17
|
+
* that ledger existed a newly-declared family simply failed no test — which is
|
|
18
|
+
* how genesis, subagent and mandate all landed dark in one fortnight.
|
|
11
19
|
*
|
|
12
20
|
* "ONE PER HUMAN SURFACE", NOT "one per method in the family" — and the
|
|
13
21
|
* difference is load-bearing, because the flat family list above reads like the
|
|
@@ -3322,6 +3330,29 @@ export function messagingTyping(params, o = {}) {
|
|
|
3322
3330
|
return call("messaging.typing", params || {}, o);
|
|
3323
3331
|
}
|
|
3324
3332
|
|
|
3333
|
+
/**
|
|
3334
|
+
* Record a VOICE NOTE in this seat's own designed voice
|
|
3335
|
+
* (messaging.synthesizeVoiceNote) → `{fileId, durationMs, sizeBytes}`.
|
|
3336
|
+
*
|
|
3337
|
+
* The human parity here is the hold-to-record button: a person speaks into the
|
|
3338
|
+
* composer and the note lands as an attachment on an ordinary message. This is
|
|
3339
|
+
* the same artifact by the same route — POST it with `messagingSendMessage` and
|
|
3340
|
+
* `attachments:[{fileId}]`. There is no audio-only send path on either plane.
|
|
3341
|
+
*
|
|
3342
|
+
* hq splits record from post because synthesis is a provider round-trip that
|
|
3343
|
+
* cannot ride its 5s transactional send lane; `messaging_send_voice_note`
|
|
3344
|
+
* (tool-surface) composes the two so a model sees one verb.
|
|
3345
|
+
*
|
|
3346
|
+
* NOT ok is a real answer: no synthesis key on the deployment, no designed
|
|
3347
|
+
* voice for this seat, or the hourly cap. Relay the reason — never report a
|
|
3348
|
+
* voice note as sent when the frame was not ok.
|
|
3349
|
+
* @param {object} params - { text }
|
|
3350
|
+
* @param {object} o - { base, token, fetchImpl? }
|
|
3351
|
+
*/
|
|
3352
|
+
export function messagingSynthesizeVoiceNote(params, o = {}) {
|
|
3353
|
+
return call("messaging.synthesizeVoiceNote", params || {}, o);
|
|
3354
|
+
}
|
|
3355
|
+
|
|
3325
3356
|
/**
|
|
3326
3357
|
* Add or remove YOUR reaction on a message (messaging.react). `op:add` upserts,
|
|
3327
3358
|
* `op:remove` deletes; both are no-ops when already in that state.
|
|
@@ -3561,3 +3592,165 @@ export function callingGetVisualContext(params, o = {}) {
|
|
|
3561
3592
|
export function callingGetCallEvents(params, o = {}) {
|
|
3562
3593
|
return call("calling.getCallEvents", params || {}, o);
|
|
3563
3594
|
}
|
|
3595
|
+
|
|
3596
|
+
// ---------------------------------------------------------------------------
|
|
3597
|
+
// mandate — the accountability spine (objectives, KPI samples, the adoption act)
|
|
3598
|
+
// ---------------------------------------------------------------------------
|
|
3599
|
+
//
|
|
3600
|
+
// The human surface is the objectives pane: a manager reads a seat's mandate,
|
|
3601
|
+
// adopts or retires an objective, and watches the KPI series move. The family
|
|
3602
|
+
// was protocol-declared with none of it wrapped, so the SDK could reach it only
|
|
3603
|
+
// through `call()` by hand — which is how `lib/mandate/refresh.mjs` ended up
|
|
3604
|
+
// spelling out "mandate.get" as a string constant of its own.
|
|
3605
|
+
//
|
|
3606
|
+
// The read/propose/write SPLIT is the anti-Goodhart law and it survives here
|
|
3607
|
+
// intact: `mandateAdopt`/`mandateRetire` bind methods whose scope
|
|
3608
|
+
// (`mandate.write`) is deliberately absent from DEFAULT_AGENT_SCOPES, so an
|
|
3609
|
+
// agent credential calling them gets FORBIDDEN_SCOPE — as it should. They are
|
|
3610
|
+
// wrapped anyway because ui-parity mirrors what a HUMAN does at the surface,
|
|
3611
|
+
// and an operator tool driving this SDK with an admin key is exactly that human.
|
|
3612
|
+
|
|
3613
|
+
/**
|
|
3614
|
+
* A seat's ADOPTED mandate snapshot (mandate.get) — {version, checksum, body}.
|
|
3615
|
+
* `version: 0` with a null body means nothing is adopted yet: a legitimate
|
|
3616
|
+
* steady state for a new seat, never an error.
|
|
3617
|
+
* @param {object} params - { memberId? }
|
|
3618
|
+
* @param {object} o - { base, token, fetchImpl? }
|
|
3619
|
+
*/
|
|
3620
|
+
export function mandateGet(params, o = {}) {
|
|
3621
|
+
return call("mandate.get", params || {}, o);
|
|
3622
|
+
}
|
|
3623
|
+
|
|
3624
|
+
/**
|
|
3625
|
+
* The cheap ETag poll (mandate.version) — {version, checksum} only, so a daemon
|
|
3626
|
+
* can ask "did my mandate move?" every beat without paying for the body.
|
|
3627
|
+
* @param {object} params - { memberId? }
|
|
3628
|
+
* @param {object} o - { base, token, fetchImpl? }
|
|
3629
|
+
*/
|
|
3630
|
+
export function mandateVersion(params, o = {}) {
|
|
3631
|
+
return call("mandate.version", params || {}, o);
|
|
3632
|
+
}
|
|
3633
|
+
|
|
3634
|
+
/**
|
|
3635
|
+
* The org objective tree (mandate.tree) — a flat node list plus `childIds` and
|
|
3636
|
+
* `roots`. Filters AND together; `includeRetired` defaults false.
|
|
3637
|
+
* @param {object} params - { memberId?, kind?, state?, workstreamId?, includeRetired?, limit? }
|
|
3638
|
+
* @param {object} o - { base, token, fetchImpl? }
|
|
3639
|
+
*/
|
|
3640
|
+
export function mandateTree(params, o = {}) {
|
|
3641
|
+
return call("mandate.tree", params || {}, o);
|
|
3642
|
+
}
|
|
3643
|
+
|
|
3644
|
+
/**
|
|
3645
|
+
* One objective's KPI samples plus the derived {value, target, gap,
|
|
3646
|
+
* normalizedGap, trend, staleness} (mandate.series).
|
|
3647
|
+
* @param {object} params - { objectiveId, limit? }
|
|
3648
|
+
* @param {object} o - { base, token, fetchImpl? }
|
|
3649
|
+
*/
|
|
3650
|
+
export function mandateSeries(params, o = {}) {
|
|
3651
|
+
return call("mandate.series", params || {}, o);
|
|
3652
|
+
}
|
|
3653
|
+
|
|
3654
|
+
/**
|
|
3655
|
+
* Create or update an objective in `state:'proposed'` (mandate.propose).
|
|
3656
|
+
* Idempotent on `(memberId, key)` — the same key UPDATES the proposal rather
|
|
3657
|
+
* than duplicating it, so no `o.idempotencyKey` is needed for dedup. Refuses to
|
|
3658
|
+
* touch a row that is already adopted.
|
|
3659
|
+
* @param {object} params - { key, text, kind?, metric?, unit?, direction?, target?, baseline?, tolerance?, weight?, cadence?, dueAt?, parentId?, charterSectionId?, workstreamId?, sensor?, memberId? }
|
|
3660
|
+
* @param {object} o - { base, token, idempotencyKey?, fetchImpl? }
|
|
3661
|
+
*/
|
|
3662
|
+
export function mandatePropose(params, o = {}) {
|
|
3663
|
+
return call("mandate.propose", params || {}, o);
|
|
3664
|
+
}
|
|
3665
|
+
|
|
3666
|
+
/**
|
|
3667
|
+
* ADOPT an objective — make it live, work-creating and graded (mandate.adopt).
|
|
3668
|
+
* Governance-gated and anti-self-grading: hq refuses the objective's owner and
|
|
3669
|
+
* every agent principal that is not the owner's supervisor, and the scope
|
|
3670
|
+
* (`mandate.write`) is not in DEFAULT_AGENT_SCOPES. Expect FORBIDDEN_SCOPE from
|
|
3671
|
+
* an ordinary agent key; that refusal is the feature.
|
|
3672
|
+
* @param {object} params - { objectiveId }
|
|
3673
|
+
* @param {object} o - { base, token, idempotencyKey?, fetchImpl? }
|
|
3674
|
+
*/
|
|
3675
|
+
export function mandateAdopt(params, o = {}) {
|
|
3676
|
+
return call("mandate.adopt", params || {}, o);
|
|
3677
|
+
}
|
|
3678
|
+
|
|
3679
|
+
/**
|
|
3680
|
+
* Retire an adopted objective (mandate.retire). Same authority ladder as adopt.
|
|
3681
|
+
* @param {object} params - { objectiveId, reason? }
|
|
3682
|
+
* @param {object} o - { base, token, idempotencyKey?, fetchImpl? }
|
|
3683
|
+
*/
|
|
3684
|
+
export function mandateRetire(params, o = {}) {
|
|
3685
|
+
return call("mandate.retire", params || {}, o);
|
|
3686
|
+
}
|
|
3687
|
+
|
|
3688
|
+
/**
|
|
3689
|
+
* Report one KPI measurement (mandate.sample). `source` is decided SERVER-side
|
|
3690
|
+
* from `evidence` and any value sent for it is ignored; `(objectiveId, window)`
|
|
3691
|
+
* is unique, so a repeated report for the same window updates rather than
|
|
3692
|
+
* inflating the series.
|
|
3693
|
+
* @param {object} params - { objectiveId, value, window, evidence? }
|
|
3694
|
+
* @param {object} o - { base, token, idempotencyKey?, fetchImpl? }
|
|
3695
|
+
*/
|
|
3696
|
+
export function mandateSample(params, o = {}) {
|
|
3697
|
+
return call("mandate.sample", params || {}, o);
|
|
3698
|
+
}
|
|
3699
|
+
|
|
3700
|
+
/**
|
|
3701
|
+
* Declare a probe-verified capability of this seat (mandate.declareCapability) —
|
|
3702
|
+
* the manifest half of the mandate/plan split. Idempotent: pass a stable
|
|
3703
|
+
* `o.idempotencyKey`.
|
|
3704
|
+
* @param {object} params - { capability, kind?, evidence?, memberId? }
|
|
3705
|
+
* @param {object} o - { base, token, idempotencyKey?, fetchImpl? }
|
|
3706
|
+
*/
|
|
3707
|
+
export function mandateDeclareCapability(params, o = {}) {
|
|
3708
|
+
return call("mandate.declareCapability", params || {}, o);
|
|
3709
|
+
}
|
|
3710
|
+
|
|
3711
|
+
/**
|
|
3712
|
+
* Publish the agent's compiled PLAN DIGEST (mandate.publishPlan) — the
|
|
3713
|
+
* obligation set it is actually executing, and the mandate version it compiled
|
|
3714
|
+
* from. This is what hq's `read_agent_plan` reads back. Idempotent: pass a
|
|
3715
|
+
* stable `o.idempotencyKey`.
|
|
3716
|
+
* @param {object} params - { planVersion, mandateVersion, obligations[], memberId? }
|
|
3717
|
+
* @param {object} o - { base, token, idempotencyKey?, fetchImpl? }
|
|
3718
|
+
*/
|
|
3719
|
+
export function mandatePublishPlan(params, o = {}) {
|
|
3720
|
+
return call("mandate.publishPlan", params || {}, o);
|
|
3721
|
+
}
|
|
3722
|
+
|
|
3723
|
+
/**
|
|
3724
|
+
* Report a drift between the mandate and what the seat can actually do
|
|
3725
|
+
* (mandate.reportDrift) — e.g. a sensor that no longer resolves.
|
|
3726
|
+
* @param {object} params - { kind, detail?, objectiveId?, memberId? }
|
|
3727
|
+
* @param {object} o - { base, token, idempotencyKey?, fetchImpl? }
|
|
3728
|
+
*/
|
|
3729
|
+
export function mandateReportDrift(params, o = {}) {
|
|
3730
|
+
return call("mandate.reportDrift", params || {}, o);
|
|
3731
|
+
}
|
|
3732
|
+
|
|
3733
|
+
// ---------------------------------------------------------------------------
|
|
3734
|
+
// preference — the durable facts hq injects into every conversation
|
|
3735
|
+
// ---------------------------------------------------------------------------
|
|
3736
|
+
|
|
3737
|
+
/**
|
|
3738
|
+
* Store (or update in place) one member-scoped preference (preference.upsert).
|
|
3739
|
+
* Converges on ONE row per (subject, domain, key); `why` (the evidence) is
|
|
3740
|
+
* mandatory server-side. Idempotent — pass a stable `o.idempotencyKey`.
|
|
3741
|
+
* @param {object} params - { domain, key, value, why, confidence?, messageId?, subjectMemberId? }
|
|
3742
|
+
* @param {object} o - { base, token, idempotencyKey?, fetchImpl? }
|
|
3743
|
+
*/
|
|
3744
|
+
export function preferenceUpsert(params, o = {}) {
|
|
3745
|
+
return call("preference.upsert", params || {}, o);
|
|
3746
|
+
}
|
|
3747
|
+
|
|
3748
|
+
/**
|
|
3749
|
+
* The preferences remembered about one member (preference.list). READ-ONLY.
|
|
3750
|
+
* Defaults to the acting seat.
|
|
3751
|
+
* @param {object} params - { memberId?, domain? }
|
|
3752
|
+
* @param {object} o - { base, token, fetchImpl? }
|
|
3753
|
+
*/
|
|
3754
|
+
export function preferenceList(params, o = {}) {
|
|
3755
|
+
return call("preference.list", params || {}, o);
|
|
3756
|
+
}
|