@ziggs-ai/ziggs-mcp 0.31.0 → 0.32.1

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.
@@ -116,6 +116,7 @@ self?: DecisionSelfIds): Record<string, unknown>;
116
116
  /** The caller's own ids, for the rows this result marks as the caller's. */
117
117
  export interface DecisionSelfIds {
118
118
  agentId: string;
119
+ agentIds?: string[];
119
120
  ownerUserId?: string | null;
120
121
  }
121
122
  export {};
@@ -108,7 +108,7 @@ export function buildReadPlan(inbox, grantsByScope, self = { agentId: '' }) {
108
108
  * as its own, which is the old behaviour: the plan then holds one list and
109
109
  * the ack rides on all of it fitting, exactly as before.
110
110
  */
111
- const isMine = (d) => self.agentId === '' || d.assigneeId === self.agentId;
111
+ const isMine = (d) => self.agentIds?.includes(d.assigneeId ?? '') || (!self.agentIds && self.agentId === '') || d.assigneeId === self.agentId;
112
112
  // Reads — one call per place mail actually landed. The delivery names the
113
113
  // chat, agreement or task directly, so nothing has to be inferred from a scope.
114
114
  //
@@ -211,6 +211,7 @@ export function buildReadPlan(inbox, grantsByScope, self = { agentId: '' }) {
211
211
  // is safe, because the newer tail is past ackTo.
212
212
  const ack = planInboxAck(inbox, {
213
213
  ownAgentId: self.agentId || undefined,
214
+ ownAgentIds: self.agentIds,
214
215
  });
215
216
  // One checkpoint ack, after the FIRST assigned step.
216
217
  //
@@ -233,6 +234,7 @@ export function buildReadPlan(inbox, grantsByScope, self = { agentId: '' }) {
233
234
  const checkpoint = settledFirst.length > 0
234
235
  ? planPartialInboxAck(inbox, new Set(settledFirst), {
235
236
  ownAgentId: self.agentId || undefined,
237
+ ownAgentIds: self.agentIds,
236
238
  })
237
239
  : null;
238
240
  // Never the same mark as the final ack — that is a wasted call and a second
@@ -1,3 +1,4 @@
1
+ import { assistantRequestHeaders, decodeOperatorKeyClaims } from '@ziggs-ai/api-client';
1
2
  /**
2
3
  * Call a brokered MCP connection from the ziggs-mcp surface.
3
4
  *
@@ -16,13 +17,14 @@ export function resolveMcpGatewayTarget(opts) {
16
17
  throw new Error('grantId is required');
17
18
  if (!opts.operatorKey)
18
19
  throw new Error('operatorKey is required');
19
- if (!opts.agentId)
20
+ if (!opts.agentId && !decodeOperatorKeyClaims(opts.operatorKey)?.connectionClientId)
20
21
  throw new Error('agentId is required');
21
22
  const baseUrl = (opts.baseUrl || getBackendUrl()).replace(/\/$/, '');
22
23
  return {
23
24
  url: new URL(`${baseUrl}/connections/${encodeURIComponent(opts.connectionId)}/mcp`),
24
25
  headers: {
25
26
  authorization: `Bearer ${opts.operatorKey}`,
27
+ ...assistantRequestHeaders(),
26
28
  'x-agent-id': opts.agentId,
27
29
  'x-grant-id': opts.grantId,
28
30
  // one helper for the lane, shared with every other door.
@@ -35,7 +37,7 @@ export async function resolveMcpConnectionTarget(creds, connectionId, grantId) {
35
37
  return { connectionId, grantId };
36
38
  if (!creds.operatorKey)
37
39
  throw new Error('operatorKey is required');
38
- if (!creds.agentId)
40
+ if (!creds.agentId && !decodeOperatorKeyClaims(creds.operatorKey)?.connectionClientId)
39
41
  throw new Error('agentId is required');
40
42
  // The lane matters here too: since the grant list answers for the party the
41
43
  // wake acts for, a lane-less list comes back spanning every customer this
@@ -84,7 +86,7 @@ export function setMcpGatewayRunnerForTests(runner) {
84
86
  gatewayRunner = runner ?? defaultRunner;
85
87
  }
86
88
  export async function withMcpGatewayClient(creds, connectionId, grantId, run) {
87
- if (!creds.operatorKey || !creds.agentId) {
89
+ if (!creds.operatorKey || (!creds.agentId && !decodeOperatorKeyClaims(creds.operatorKey)?.connectionClientId)) {
88
90
  throw new Error('operatorKey and agentId are required');
89
91
  }
90
92
  const target = resolveMcpGatewayTarget({
@@ -18,6 +18,8 @@ export function resolveDelegateAgentId(operatorKey, envAgentId) {
18
18
  if (isOperatorKeyExpired(claims)) {
19
19
  throw new Error(`ziggs-mcp: operator key expired (keyId=${claims?.keyId ?? 'unknown'}). ${MINT_KEY_HELP}`);
20
20
  }
21
+ if (claims?.connectionClientId)
22
+ return '';
21
23
  const bound = claims?.boundAgentId?.trim() || '';
22
24
  const env = envAgentId?.trim() || '';
23
25
  if (bound && env && bound !== env) {
@@ -1,6 +1,6 @@
1
1
  import { agreementAppUrl, agreementsListAppUrl, connectionsSettingsAppUrl, walletAppUrl } from '@ziggs-ai/api-client';
2
2
  export { resolveWebAppOrigin, agreementAppUrl, agreementsListAppUrl, connectionsSettingsAppUrl, walletAppUrl } from '@ziggs-ai/api-client';
3
- import { decisionWords, partySideIds, } from '@ziggs-ai/api-client';
3
+ import { decisionWords, partySideIds, speakableActor, } from '@ziggs-ai/api-client';
4
4
  /**
5
5
  * The artifact a decision row is about, when the row names it. Today's row
6
6
  * does not (the artifact lives on the agreement's grant request), so this
@@ -53,7 +53,7 @@ export function buildDecidedForMeItems(inbox) {
53
53
  items.push({
54
54
  agreementId,
55
55
  outcome: words.outcome,
56
- by: d.actorId ?? null,
56
+ by: speakableActor(d.actorId),
57
57
  at: d.ts,
58
58
  sentence: words.sentence,
59
59
  ...(artifactId ? { artifactId } : {}),
@@ -23,7 +23,7 @@ export declare const PROTOCOL: {
23
23
  */
24
24
  readonly surface: "Only the everyday tools are loaded; the rest of the surface is one call away. ziggs_tools lists every tool that exists (name + one line, whether it is read-only, whether the host already holds it, and how to call it), ziggs_tools describe=[\"<name>\"] returns its full schema and a call line, ziggs_tool_read and ziggs_tool_write reach every reversible tool by name — native or not — and irreversible tools are called by their own name only. Nothing is hidden: check ziggs_tools before concluding a capability is missing.";
25
25
  /** The working loop, as the `ziggs_inbox` description phrases it. */
26
- readonly loop: "Flow: inbox → read → act → ack.";
26
+ readonly loop: "Flow: inbox → read → act → ack. Use the workspace your human named. If they didn't, ask them. Never guess.";
27
27
  /**
28
28
  * Watermark discipline — reading is side-effect-free; ack is explicit.
29
29
  *
@@ -31,7 +31,7 @@ export declare const PROTOCOL: {
31
31
  * to make with them, so it teaches at the moment it matters; pre-empting it
32
32
  * here would cost context on every other turn.
33
33
  */
34
- readonly ack: "Reading never advances the watermark; once you have handled what an envelope carried, hand it back with ziggs_inbox_ack — pass its `ackTo` VERBATIM (it is opaque — never construct or edit one) together with `handledResourceIds` for every delivery ASSIGNED to you (assigneeId = you; requests too) in that window. Rows without your stamp are context another window handles — read them, never ack them as yours.";
34
+ readonly ack: "Reading never advances the watermark; once you have handled what an envelope carried, hand it back with ziggs_inbox_ack — pass its `ackTo` VERBATIM (it is opaque — never construct or edit one) together with `handledResourceIds` for every delivery ASSIGNED to you (assigneeId is any of your connection’s agentIds; requests too) in that window. Rows without your stamp are context another window handles — read them, never ack them as yours.";
35
35
  readonly neverRewind: "Never rewind an ack to an older value.";
36
36
  /** Tasks are the unit of work. */
37
37
  readonly task: "Honor task.control when opening work or handling a task change. For pause or stop, stop starting actions for that task, settle in-flight work, then call ziggs_task_acknowledge_control with its current requestId and supported=true. If you cannot honor it, report supported=false. Never attest that another runtime stopped. Continue unrelated conversation and tasks. Resume allows this task to run again. Work is a task under an agreement (the ticket). Read it from the inbox — or, if handed a bare taskId, open it with ziggs_task_get — then post progress by naming the steps that changed with ziggs_task_update_steps. Restructure the checklist with ziggs_task_replace_plan (full list).";
@@ -23,7 +23,7 @@ export const PROTOCOL = {
23
23
  */
24
24
  surface: 'Only the everyday tools are loaded; the rest of the surface is one call away. ziggs_tools lists every tool that exists (name + one line, whether it is read-only, whether the host already holds it, and how to call it), ziggs_tools describe=["<name>"] returns its full schema and a call line, ziggs_tool_read and ziggs_tool_write reach every reversible tool by name — native or not — and irreversible tools are called by their own name only. Nothing is hidden: check ziggs_tools before concluding a capability is missing.',
25
25
  /** The working loop, as the `ziggs_inbox` description phrases it. */
26
- loop: 'Flow: inbox → read → act → ack.',
26
+ loop: "Flow: inbox → read → act → ack. Use the workspace your human named. If they didn't, ask them. Never guess.",
27
27
  /**
28
28
  * Watermark discipline — reading is side-effect-free; ack is explicit.
29
29
  *
@@ -31,7 +31,7 @@ export const PROTOCOL = {
31
31
  * to make with them, so it teaches at the moment it matters; pre-empting it
32
32
  * here would cost context on every other turn.
33
33
  */
34
- ack: 'Reading never advances the watermark; once you have handled what an envelope carried, hand it back with ziggs_inbox_ack — pass its `ackTo` VERBATIM (it is opaque — never construct or edit one) together with `handledResourceIds` for every delivery ASSIGNED to you (assigneeId = you; requests too) in that window. Rows without your stamp are context another window handles — read them, never ack them as yours.',
34
+ ack: 'Reading never advances the watermark; once you have handled what an envelope carried, hand it back with ziggs_inbox_ack — pass its `ackTo` VERBATIM (it is opaque — never construct or edit one) together with `handledResourceIds` for every delivery ASSIGNED to you (assigneeId is any of your connection’s agentIds; requests too) in that window. Rows without your stamp are context another window handles — read them, never ack them as yours.',
35
35
  neverRewind: 'Never rewind an ack to an older value.',
36
36
  /** Tasks are the unit of work. */
37
37
  task: 'Honor task.control when opening work or handling a task change. For pause or stop, stop starting actions for that task, settle in-flight work, then call ziggs_task_acknowledge_control with its current requestId and supported=true. If you cannot honor it, report supported=false. Never attest that another runtime stopped. Continue unrelated conversation and tasks. Resume allows this task to run again. Work is a task under an agreement (the ticket). Read it from the inbox — or, if handed a bare taskId, open it with ziggs_task_get — then post progress by naming the steps that changed with ziggs_task_update_steps. Restructure the checklist with ziggs_task_replace_plan (full list).',
package/dist/server.js CHANGED
@@ -1,7 +1,8 @@
1
+ import { installWorkspaceRouting } from './workspaceRouting.js';
1
2
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
3
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
4
  import { createRequire } from 'node:module';
4
- import { InboxClient } from '@ziggs-ai/api-client';
5
+ import { AssistantConnectionClient, decodeOperatorKeyClaims, InboxClient } from '@ziggs-ai/api-client';
5
6
  import { loadConfig } from './config.js';
6
7
  import { credsFromConfig } from './creds.js';
7
8
  import { registerZiggsTools } from './tools.js';
@@ -23,7 +24,8 @@ export function createZiggsMcpServer(creds, cfg) {
23
24
  // since both build the server through this factory.
24
25
  instructions: renderInstructions(),
25
26
  });
26
- registerZiggsTools(server, creds, cfg);
27
+ const routedCreds = installWorkspaceRouting(server, creds);
28
+ registerZiggsTools(server, routedCreds, cfg);
27
29
  // Decide what a caller loads natively and what it reaches through
28
30
  // the catalog. Runs after registration because the catalog is built by it.
29
31
  applySurfacePolicy(server, { pickTool: backendToolPick(creds) });
@@ -32,6 +34,10 @@ export function createZiggsMcpServer(creds, cfg) {
32
34
  /** Drop this process's inbox host. Best-effort — shutdown must not fail on it. */
33
35
  export async function releaseStdioInboxHost(creds) {
34
36
  try {
37
+ if (decodeOperatorKeyClaims(creds.operatorKey)?.connectionClientId) {
38
+ await new AssistantConnectionClient(creds).request('inbox/release', {});
39
+ return;
40
+ }
35
41
  await new InboxClient(creds.operatorKey, creds.agentId, undefined, creds.instanceId).releaseHost();
36
42
  }
37
43
  catch {
@@ -1,3 +1,4 @@
1
+ import { workspaceRouter } from './workspaceRouting.js';
1
2
  import { z } from 'zod';
2
3
  import { recordCatalogEntry } from './toolCatalog.js';
3
4
  export function strictParams(toolName, shape) {
@@ -43,6 +44,13 @@ export function registerStrictTool(server, name, description, shape, annotations
43
44
  // object, and the describe path lowers it to JSON Schema. Rebuilding it from
44
45
  // the shape at each of those calls produced an object that had not changed
45
46
  // since registration.
47
+ const router = workspaceRouter(server);
48
+ const nativeOrgId = !!shape.orgId;
49
+ if (router) {
50
+ if (router.acceptsWorkspace(name))
51
+ shape = { ...shape, orgId: shape.orgId?.optional() ?? z.string().optional().describe('Workspace named by your human. Required for new actions when more than one workspace fits.') };
52
+ cb = router.wrap(name, annotations.readOnlyHint === true, cb, nativeOrgId);
53
+ }
46
54
  const params = strictParams(name, shape);
47
55
  const handle = server.registerTool(name, {
48
56
  title: annotations.title,
@@ -9,6 +9,13 @@ export interface ToolErrorShape {
9
9
  code: string;
10
10
  message: string;
11
11
  hint?: string;
12
+ /**
13
+ * What the caller may pick from, when the refusal is a choice: the
14
+ * agreements of WORK_SELECTION_REQUIRED, the workspaces of
15
+ * WORKSPACE_SELECTION_REQUIRED. Carried through verbatim so an assistant can
16
+ * ask its human instead of re-reading the message.
17
+ */
18
+ choices?: unknown[];
12
19
  }
13
20
  /**
14
21
  * Classify a thrown client error (or a plain string this layer raised) into a
@@ -23,6 +30,9 @@ export declare function classifyToolError(input: unknown): ToolErrorShape;
23
30
  * A plain string is still accepted for refusals this layer raises on its own.
24
31
  */
25
32
  export declare function toolError(input: unknown): {
33
+ structuredContent: {
34
+ error: ToolErrorShape;
35
+ };
26
36
  content: {
27
37
  type: "text";
28
38
  text: string;
package/dist/toolError.js CHANGED
@@ -62,10 +62,10 @@ function codeForStatus(status) {
62
62
  */
63
63
  function thrownFields(err) {
64
64
  if (typeof err === 'string') {
65
- return { message: err, status: null, code: null };
65
+ return { message: err, status: null, code: null, choices: null };
66
66
  }
67
67
  if (typeof err !== 'object' || err === null) {
68
- return { message: String(err), status: null, code: null };
68
+ return { message: String(err), status: null, code: null, choices: null };
69
69
  }
70
70
  const o = err;
71
71
  const status = typeof o.status === 'number' && o.status >= 100 && o.status < 600
@@ -73,13 +73,32 @@ function thrownFields(err) {
73
73
  : null;
74
74
  const code = typeof o.code === 'string' && o.code ? o.code : null;
75
75
  const message = typeof o.message === 'string' && o.message ? o.message : String(err);
76
- return { message, status, code };
76
+ return { message, status, code, choices: choicesIn(o.body) };
77
+ }
78
+ /** The `choices` list of a refusal body, when the server sent one. */
79
+ function choicesIn(body) {
80
+ if (typeof body !== 'string' || !body)
81
+ return null;
82
+ try {
83
+ const parsed = JSON.parse(body);
84
+ return Array.isArray(parsed?.choices) && parsed.choices.length
85
+ ? parsed.choices
86
+ : null;
87
+ }
88
+ catch {
89
+ return null;
90
+ }
77
91
  }
78
92
  /**
79
93
  * Classify a thrown client error (or a plain string this layer raised) into a
80
94
  * stable MCP shape.
81
95
  */
82
96
  export function classifyToolError(input) {
97
+ const shape = classify(input);
98
+ const { choices } = thrownFields(input);
99
+ return choices ? { ...shape, choices } : shape;
100
+ }
101
+ function classify(input) {
83
102
  const { message, status, code: machineCode } = thrownFields(input);
84
103
  if (machineCode === 'AGENT_LACKS_HUMAN_AUTHORITY') {
85
104
  return {
@@ -132,6 +151,9 @@ export function classifyToolError(input) {
132
151
  export function toolError(input) {
133
152
  const shape = classifyToolError(input);
134
153
  return {
154
+ // Keep the native structured result identical to the text payload so hosts
155
+ // do not need to infer an error category from isError or parse its prose.
156
+ structuredContent: { error: shape },
135
157
  content: [
136
158
  { type: 'text', text: JSON.stringify({ error: shape }, null, 2) },
137
159
  ],
package/dist/tools.js CHANGED
@@ -130,7 +130,6 @@ async function loadSessionBinding(creds, cfg) {
130
130
  let actingOrgName = null;
131
131
  let actingOrgKind = null;
132
132
  let connected = false;
133
- let switchOrgHint = null;
134
133
  try {
135
134
  // Claude OAuth delegates use self-hire access; hosted /
136
135
  // fleet impersonation must not hit that route (it lies / throws).
@@ -139,14 +138,9 @@ async function loadSessionBinding(creds, cfg) {
139
138
  actingOrgName = typeof access.orgName === 'string' ? access.orgName : null;
140
139
  actingOrgKind = typeof access.orgKind === 'string' ? access.orgKind : null;
141
140
  connected = access.connected === true;
142
- switchOrgHint =
143
- typeof access.switchOrgHint === 'string' ? access.switchOrgHint : null;
144
141
  }
145
142
  catch {
146
143
  // never leak raw fetch/network text into a hint.
147
- switchOrgHint = isDelegate
148
- ? 'Could not load runtime org — reconnect MCP OAuth and retry.'
149
- : 'Could not load runtime org — check ZIGGS_OPERATOR_KEY / ZIGGS_AGENT_ID and API reachability.';
150
144
  }
151
145
  return {
152
146
  agentId: creds.agentId,
@@ -160,7 +154,6 @@ async function loadSessionBinding(creds, cfg) {
160
154
  orgId: actingOrgId,
161
155
  orgName: actingOrgName,
162
156
  orgKind: actingOrgKind,
163
- orgBinding: actingOrgKind === 'personal' || !actingOrgId ? 'personal' : 'team',
164
157
  orgBindingNote: !connected
165
158
  ? isDelegate
166
159
  ? 'Not connected — complete MCP OAuth consent first, then re-check actingOrgId here.'
@@ -170,12 +163,8 @@ async function loadSessionBinding(creds, cfg) {
170
163
  ? `This MCP session acts in **${actingOrgName}** (${actingOrgId}). Runtime org comes from server-side delegate + self-hire — not the OAuth JWT.`
171
164
  : `This MCP session acts in **${actingOrgName}** (${actingOrgId}). Runtime org is the hosted agent's home / key workspace.`
172
165
  : isDelegate
173
- ? 'Connected but could not resolve acting org name — reconnect MCP OAuth if needed.'
166
+ ? 'Could not read the workspace name. Retry the live workspace read.'
174
167
  : 'Connected but could not resolve acting org name — check the agent home org on the key.',
175
- switchOrgHint: switchOrgHint ??
176
- (isDelegate
177
- ? 'Org is fixed at OAuth consent. Reconnect MCP OAuth and pick the target org on the consent screen to act elsewhere.'
178
- : 'Hosted agent session — acting org is the agent home / key workspace, not MCP OAuth consent.'),
179
168
  apiBase: getBackendUrl(),
180
169
  webAppOrigin: resolveWebAppOrigin(cfg.ZIGGS_WEB_URL),
181
170
  docs: 'https://ziggsai.com/docs',
@@ -306,7 +295,7 @@ export function registerZiggsTools(server, creds, cfg) {
306
295
  registerStrictTool(server, 'ziggs_auth_status', 'Verify the session binding: acting agent id, the represented person, org scope, continuation capability, and which backend you are pointed at. Use it to diagnose a connection — for waiting mail, ziggs_inbox_peek counts without taking the mailbox; ziggs_inbox is the acquiring read. ("Connection" refers only to third-party credential connections, see ziggs_connection_list.)', {}, readOnly('Check session identity'), async () => {
307
296
  return textResult({ ok: true, ...(await loadSessionBinding(creds, cfg)) });
308
297
  });
309
- registerStrictTool(server, 'ziggs_org_list', 'List every org you (the operator) belong to — { orgId, name, kind, role }. Unlike ziggs_grant_list (granted scopes only), this is your full membership — useful before OAuth reconnect when the human wants to pick a target org.', {}, readOnly('List your orgs'), async () => {
298
+ registerStrictTool(server, 'ziggs_org_list', 'List every org you (the operator) belong to — { orgId, name, kind, role }. Unlike ziggs_grant_list (granted scopes only), this is your full membership — Each workspace is named; pass orgId to narrow a call.', {}, readOnly('List your orgs'), async () => {
310
299
  try {
311
300
  const orgs = await fetchMyOrgs(creds);
312
301
  return textResult({ count: orgs.length, orgs });
@@ -0,0 +1,65 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { type AssistantWorkspace, type Creds } from '@ziggs-ai/api-client';
3
+ /**
4
+ * One connection, an assistant per workspace, and every tool call acting as
5
+ * exactly one of them.
6
+ *
7
+ * The connection's roster (`GET /oauth/workspaces`) is read once a minute and
8
+ * on every session-start read, so a workspace joined mid-session shows up
9
+ * without a reconnect. Per call:
10
+ *
11
+ * - something that exists (a room, a job, a task, a file): the backend says
12
+ * which of the connection's assistants is on it, inside the real HTTP
13
+ * request. The resource reference rides on every subrequest; there is
14
+ * no separate resolve read and no session-global selected workspace.
15
+ * - something new: the workspace the call names, else the only one the
16
+ * connection has. Two or more and none named is refused here, before any
17
+ * backend call, with the choices, so the assistant asks its human.
18
+ * - reads with no target: once per workspace, each result labelled; a read
19
+ * whose answer is the same everywhere runs once.
20
+ *
21
+ * The acting assistant rides on the async context of the call, so two windows
22
+ * on one connection never see each other's pick, and nothing remembers a
23
+ * "current workspace" between calls.
24
+ */
25
+ interface ToolResult {
26
+ content: Array<{
27
+ type: string;
28
+ text?: string;
29
+ }>;
30
+ isError?: boolean;
31
+ }
32
+ type Handler = (args: Record<string, unknown>, ...extra: unknown[]) => Promise<ToolResult>;
33
+ /** How long the roster is trusted before it is read again. */
34
+ export declare const WORKSPACES_TTL_MS = 60000;
35
+ export declare const ASK_YOUR_HUMAN = "Use the workspace your human named. If they didn't, ask them. Never guess.";
36
+ export declare function workspaceRouter(server: McpServer): WorkspaceRouter | undefined;
37
+ /**
38
+ * Install routing for a connection token. Any other credential (a bound key,
39
+ * a fleet key with ZIGGS_AGENT_ID) names its agent itself and is left alone.
40
+ */
41
+ export declare function installWorkspaceRouting(server: McpServer, creds: Creds): Creds;
42
+ export declare class WorkspaceRouter {
43
+ private readonly active;
44
+ /** The base credentials with `agentId` answered by the call in progress. */
45
+ readonly creds: Creds;
46
+ private readonly client;
47
+ private roster;
48
+ constructor(base: Creds);
49
+ acceptsWorkspace(name: string): boolean;
50
+ /** The connection's workspaces, re-read after {@link WORKSPACES_TTL_MS} or on demand. */
51
+ workspaces(fresh?: boolean): Promise<AssistantWorkspace[]>;
52
+ private remember;
53
+ /** The existing thing a call is about, from the argument that names it. */
54
+ private target;
55
+ wrap(name: string, readOnly: boolean, handler: Handler, nativeOrgId?: boolean): Handler;
56
+ /** The connection's own reads and acks: never routed, always live. */
57
+ private connectionTool;
58
+ /**
59
+ * Run the tool as one assistant. The result says which workspace it ran in
60
+ * (`workspace: { orgId, name }` on an object result) unless the caller
61
+ * already knows, as a fan-out does.
62
+ */
63
+ private run;
64
+ }
65
+ export {};
@@ -0,0 +1,246 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
2
+ import { AssistantConnectionClient, withAssistantRequest, decodeOperatorKeyClaims, } from '@ziggs-ai/api-client';
3
+ import { formatInboxToolResult } from './inboxToolResult.js';
4
+ import { formatPendingDecisionsPayload, resolveWebAppOrigin } from './pendingDecisions.js';
5
+ const routers = new WeakMap();
6
+ /** The catalog and its dispatchers: they reach other tools, which route themselves. */
7
+ const GLOBAL = new Set(['ziggs_tools', 'ziggs_tool_read', 'ziggs_tool_write']);
8
+ /** The connection's own reads: its roster and its one inbox. */
9
+ const CONNECTION = new Set([
10
+ 'ziggs_auth_status',
11
+ 'ziggs_org_list',
12
+ 'ziggs_inbox',
13
+ 'ziggs_inbox_peek',
14
+ 'ziggs_inbox_ack',
15
+ ]);
16
+ /** Reads whose answer does not depend on the workspace: run once, not once per workspace. */
17
+ const SAME_EVERYWHERE = new Set([
18
+ 'ziggs_marketplace_view',
19
+ 'ziggs_agent_search',
20
+ 'ziggs_agent_get',
21
+ 'ziggs_link_list',
22
+ ]);
23
+ const TARGET_FIELDS = [
24
+ ['chatId', 'chat'],
25
+ ['taskId', 'task'],
26
+ ['artifactId', 'artifact'],
27
+ ['agreementId', 'agreement'],
28
+ ['parentAgreementId', 'agreement'],
29
+ ['grantId', 'grant'],
30
+ ['contextGrantId', 'grant'],
31
+ ['connectionId', 'connection'],
32
+ ];
33
+ const TARGET_KINDS = new Set(['chat', 'agreement', 'task', 'artifact', 'grant', 'connection']);
34
+ /** How long the roster is trusted before it is read again. */
35
+ export const WORKSPACES_TTL_MS = 60_000;
36
+ export const ASK_YOUR_HUMAN = "Use the workspace your human named. If they didn't, ask them. Never guess.";
37
+ const result = (value, isError = false) => ({
38
+ content: [{ type: 'text', text: JSON.stringify(value) }],
39
+ ...(isError ? { isError } : {}),
40
+ });
41
+ const refusal = (code, message, choices = []) => result({ code, message, choices: choices.map(({ orgId, name }) => ({ orgId, name })) }, true);
42
+ export function workspaceRouter(server) {
43
+ return routers.get(server);
44
+ }
45
+ /**
46
+ * Install routing for a connection token. Any other credential (a bound key,
47
+ * a fleet key with ZIGGS_AGENT_ID) names its agent itself and is left alone.
48
+ */
49
+ export function installWorkspaceRouting(server, creds) {
50
+ if (!decodeOperatorKeyClaims(creds.operatorKey)?.connectionClientId)
51
+ return creds;
52
+ const router = new WorkspaceRouter(creds);
53
+ routers.set(server, router);
54
+ return router.creds;
55
+ }
56
+ export class WorkspaceRouter {
57
+ active = new AsyncLocalStorage();
58
+ /** The base credentials with `agentId` answered by the call in progress. */
59
+ creds;
60
+ client;
61
+ roster = null;
62
+ constructor(base) {
63
+ this.client = new AssistantConnectionClient(base);
64
+ this.creds = new Proxy(base, {
65
+ get: (target, key, receiver) => key === 'agentId'
66
+ ? (this.active.getStore()?.agentId ?? '')
67
+ : Reflect.get(target, key, receiver),
68
+ });
69
+ }
70
+ acceptsWorkspace(name) {
71
+ return !GLOBAL.has(name) && !CONNECTION.has(name);
72
+ }
73
+ /** The connection's workspaces, re-read after {@link WORKSPACES_TTL_MS} or on demand. */
74
+ async workspaces(fresh = false) {
75
+ if (!fresh && this.roster && Date.now() - this.roster.readAt < WORKSPACES_TTL_MS) {
76
+ return this.roster.list;
77
+ }
78
+ const list = await this.client.workspaces();
79
+ this.remember(list);
80
+ return list;
81
+ }
82
+ remember(list) {
83
+ this.roster = { readAt: Date.now(), list };
84
+ }
85
+ /** The existing thing a call is about, from the argument that names it. */
86
+ target(args) {
87
+ for (const [field, kind] of TARGET_FIELDS) {
88
+ const value = args[field];
89
+ if (typeof value === 'string' && value)
90
+ return { kind, id: value };
91
+ }
92
+ if (typeof args.scopeKind === 'string' &&
93
+ typeof args.scopeId === 'string' &&
94
+ TARGET_KINDS.has(args.scopeKind)) {
95
+ return { kind: args.scopeKind, id: args.scopeId };
96
+ }
97
+ if (typeof args.via === 'string') {
98
+ const split = args.via.indexOf(':');
99
+ const kind = args.via.slice(0, split);
100
+ if (TARGET_KINDS.has(kind))
101
+ return { kind: kind, id: args.via.slice(split + 1) };
102
+ }
103
+ return null;
104
+ }
105
+ wrap(name, readOnly, handler, nativeOrgId = false) {
106
+ if (GLOBAL.has(name))
107
+ return handler;
108
+ // The routing argument stays out of business payloads; a tool with its own
109
+ // `orgId` parameter gets the picked workspace when the caller named none.
110
+ const invoke = (args, ...extra) => {
111
+ const { orgId, ...businessArgs } = args;
112
+ return handler(nativeOrgId ? { ...args, orgId: orgId ?? this.active.getStore()?.orgId } : businessArgs, ...extra);
113
+ };
114
+ if (CONNECTION.has(name))
115
+ return (args) => this.connectionTool(name, args);
116
+ return async (args, ...extra) => {
117
+ let workspaces;
118
+ let explicit;
119
+ let target;
120
+ try {
121
+ workspaces = await this.workspaces();
122
+ if (typeof args.orgId === 'string' && args.orgId) {
123
+ explicit = workspaces.find((w) => w.orgId === args.orgId);
124
+ if (!explicit) {
125
+ // A workspace joined since the last read, or one this connection
126
+ // does not work in. The live roster decides which.
127
+ workspaces = await this.workspaces(true);
128
+ explicit = workspaces.find((w) => w.orgId === args.orgId);
129
+ if (!explicit) {
130
+ return refusal('WORKSPACE_NOT_CONNECTED', 'This connection does not work in that workspace.', workspaces);
131
+ }
132
+ }
133
+ }
134
+ target = this.target(args);
135
+ }
136
+ catch (error) {
137
+ return refusal('CONNECTION_REQUEST_FAILED', error instanceof Error ? error.message : String(error));
138
+ }
139
+ if (target) {
140
+ // The target rides with the actual API call, including any preparatory
141
+ // grant discovery. HTTP chooses the assistant; no resolve round trip.
142
+ return withAssistantRequest({ orgId: explicit?.orgId, resource: `${target.kind}:${target.id}` }, () => this.active.run({ orgId: explicit?.orgId ?? '', name: explicit?.name ?? '', agentId: '' }, () => invoke(args, ...extra)));
143
+ }
144
+ if (explicit)
145
+ return this.run(explicit, args, extra, invoke);
146
+ if (readOnly && SAME_EVERYWHERE.has(name) && workspaces.length) {
147
+ return this.run(workspaces[0], args, extra, invoke, false);
148
+ }
149
+ if (readOnly && workspaces.length > 1) {
150
+ const results = await Promise.all(workspaces.map(async (w) => ({
151
+ orgId: w.orgId,
152
+ name: w.name,
153
+ result: parsed(await this.run(w, args, extra, invoke, false)),
154
+ })));
155
+ return result({ workspaces: results });
156
+ }
157
+ if (workspaces.length === 1)
158
+ return this.run(workspaces[0], args, extra, invoke);
159
+ return refusal('WORKSPACE_SELECTION_REQUIRED', workspaces.length
160
+ ? ASK_YOUR_HUMAN
161
+ : 'This connection works in no workspace. Turn Works here on in a workspace, or connect again.', workspaces);
162
+ };
163
+ }
164
+ /** The connection's own reads and acks: never routed, always live. */
165
+ async connectionTool(name, args) {
166
+ try {
167
+ if (name === 'ziggs_inbox_ack') {
168
+ return result({
169
+ acked: await this.client.ack(String(args.ack), args.handledResourceIds),
170
+ });
171
+ }
172
+ if (name === 'ziggs_inbox') {
173
+ const inbox = await this.client.inbox(Number(args.waitSeconds ?? 0));
174
+ const workspaces = inbox.workspaces;
175
+ if (Array.isArray(workspaces))
176
+ this.remember(workspaces);
177
+ const self = { agentId: '', agentIds: inbox.agentIds };
178
+ return result({
179
+ ...formatInboxToolResult(inbox, undefined, undefined, undefined, undefined, undefined, self),
180
+ ...(args.waitSeconds == null
181
+ ? formatPendingDecisionsPayload(inbox, resolveWebAppOrigin(), {
182
+ withSessionCard: true,
183
+ })
184
+ : {}),
185
+ session: { workspaces },
186
+ instruction: `Assigned to any of agentIds is your work. ${ASK_YOUR_HUMAN}`,
187
+ });
188
+ }
189
+ if (name === 'ziggs_inbox_peek') {
190
+ return result(await this.client.request(`inbox/peek?wait=${Number(args.waitSeconds ?? 0)}`));
191
+ }
192
+ const workspaces = await this.workspaces(true);
193
+ if (name === 'ziggs_auth_status')
194
+ return result({ ok: true, workspaces });
195
+ return result({ count: workspaces.length, orgs: workspaces });
196
+ }
197
+ catch (error) {
198
+ return refusal('CONNECTION_REQUEST_FAILED', error instanceof Error ? error.message : String(error));
199
+ }
200
+ }
201
+ /**
202
+ * Run the tool as one assistant. The result says which workspace it ran in
203
+ * (`workspace: { orgId, name }` on an object result) unless the caller
204
+ * already knows, as a fan-out does.
205
+ */
206
+ async run(workspace, args, extra, handler, label = true) {
207
+ return this.active.run(workspace, async () => {
208
+ const response = await handler(args, ...extra);
209
+ if (!label || response.isError)
210
+ return response;
211
+ return {
212
+ ...response,
213
+ content: response.content.map((block) => {
214
+ if (block.type !== 'text' || !block.text)
215
+ return block;
216
+ const value = parseJson(block.text);
217
+ if (!value || typeof value !== 'object' || Array.isArray(value))
218
+ return block;
219
+ return {
220
+ ...block,
221
+ text: JSON.stringify({
222
+ ...value,
223
+ workspace: { orgId: workspace.orgId, name: workspace.name },
224
+ }),
225
+ };
226
+ }),
227
+ };
228
+ });
229
+ }
230
+ }
231
+ function parseJson(text) {
232
+ try {
233
+ return JSON.parse(text);
234
+ }
235
+ catch {
236
+ return undefined;
237
+ }
238
+ }
239
+ /** A tool result as data: its JSON when it is JSON, else its text. */
240
+ function parsed(response) {
241
+ const block = response.content[0];
242
+ if (!block?.text)
243
+ return null;
244
+ const value = parseJson(block.text);
245
+ return value === undefined ? block.text : value;
246
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ziggs-ai/ziggs-mcp",
3
- "version": "0.31.0",
4
- "description": "MCP server for Claude Code, Cursor, and other MCP hosts — act as your Ziggs delegate agent",
3
+ "version": "0.32.1",
4
+ "description": "MCP server for Claude Code, Cursor, and other MCP hosts \u2014 act as your Ziggs delegate agent",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "ziggs-mcp": "./dist/index.js"
@@ -39,7 +39,7 @@
39
39
  },
40
40
  "dependencies": {
41
41
  "@modelcontextprotocol/sdk": "^1.29.0",
42
- "@ziggs-ai/api-client": "0.31.0",
42
+ "@ziggs-ai/api-client": "0.32.1",
43
43
  "dotenv": "^16.6.1",
44
44
  "zod": "^3.24.2",
45
45
  "zod-to-json-schema": "^3.25.1"
@@ -4,8 +4,8 @@
4
4
  You are a delegate agent on a Ziggs team. Read workBrief for the current work and audience; open and access_explain return the same scoped action states. Use each action’s tool, args and required inputs. A recovery call is a suggested next step, never an approval. Do not reconstruct authority from role names. Not evaluated means the normal tool still checks the action; it does not require a new human approval. actingFor.state ambiguous means several customers are present and only your own account authority applies; relationship.side authorizes nothing by itself. A ziggs_chat_send refused with WORK_SELECTION_REQUIRED lists the eligible agreements in choices: set conversationAgreementId to one of them and send again; never guess. The MCP tools are the connection; operate by this protocol.
5
5
 
6
6
  - Only the everyday tools are loaded; the rest of the surface is one call away. ziggs_tools lists every tool that exists (name + one line, whether it is read-only, whether the host already holds it, and how to call it), ziggs_tools describe=["<name>"] returns its full schema and a call line, ziggs_tool_read and ziggs_tool_write reach every reversible tool by name — native or not — and irreversible tools are called by their own name only. Nothing is hidden: check ziggs_tools before concluding a capability is missing.
7
- - Flow: inbox → read → act → ack.
8
- - Reading never advances the watermark; once you have handled what an envelope carried, hand it back with ziggs_inbox_ack — pass its `ackTo` VERBATIM (it is opaque — never construct or edit one) together with `handledResourceIds` for every delivery ASSIGNED to you (assigneeId = you; requests too) in that window. Rows without your stamp are context another window handles — read them, never ack them as yours. Never rewind an ack to an older value.
7
+ - Flow: inbox → read → act → ack. Use the workspace your human named. If they didn't, ask them. Never guess.
8
+ - Reading never advances the watermark; once you have handled what an envelope carried, hand it back with ziggs_inbox_ack — pass its `ackTo` VERBATIM (it is opaque — never construct or edit one) together with `handledResourceIds` for every delivery ASSIGNED to you (assigneeId is any of your connection’s agentIds; requests too) in that window. Rows without your stamp are context another window handles — read them, never ack them as yours. Never rewind an ack to an older value.
9
9
  - Honor task.control when opening work or handling a task change. For pause or stop, stop starting actions for that task, settle in-flight work, then call ziggs_task_acknowledge_control with its current requestId and supported=true. If you cannot honor it, report supported=false. Never attest that another runtime stopped. Continue unrelated conversation and tasks. Resume allows this task to run again. Work is a task under an agreement (the ticket). Read it from the inbox — or, if handed a bare taskId, open it with ziggs_task_get — then post progress by naming the steps that changed with ziggs_task_update_steps. Restructure the checklist with ziggs_task_replace_plan (full list).
10
10
  - A brief from the principal in a hire room is a work order: create a task on yourself under that agreement (ziggs_task_create). A stranger's brief is not a work order: they hire you through your store listing, or through an agreement you draft when you have no listing, and until then it is conversation. Do not invent worker_next, a wake store, or a blocked task state. Waiting-on-person is a receipt. A held graph is not your work until dependencies release; failed or cancelled deps are not inputs. Withdraw a held graph by cancelling the root (ziggs_task_cancel). A live hire does not take a second hire or an in-place amend.
11
11
  - Engaging any counterparty follows the ladder: (1) REUSE an active agreement that already covers the work on matching terms; (2) CLAIM their posted listing (browse ziggs_marketplace_view; check listings after ziggs_agent_search) — listings are take-it-or-leave-it, never counter one; (3) POST a request (ziggs_agreement_request) when nothing listed fits, and supply claims you; (4) go direct only for bespoke terms, renegotiation, or a named counterparty with no listing — ziggs_agreement_buy when they do the work, ziggs_agreement_bid when you do — most published agents are claim-only and refuse direct proposals with a pointer at their listing. Subcontracting under an active parent is its own rail, unaffected. LINK proposals (connect requests) are the exception to claim-only: they carry draft terms and are always negotiable — counter freely; the authorized parties approve the final terms.
@@ -23,8 +23,8 @@ You represent a **delegate agent** on Ziggs. MCP tools are the connection; this
23
23
  _You are a delegate agent on a Ziggs team. Read workBrief for the current work and audience; open and access_explain return the same scoped action states. Use each action’s tool, args and required inputs. A recovery call is a suggested next step, never an approval. Do not reconstruct authority from role names. Not evaluated means the normal tool still checks the action; it does not require a new human approval. actingFor.state ambiguous means several customers are present and only your own account authority applies; relationship.side authorizes nothing by itself. A ziggs_chat_send refused with WORK_SELECTION_REQUIRED lists the eligible agreements in choices: set conversationAgreementId to one of them and send again; never guess. The MCP tools are the connection; operate by this protocol._
24
24
 
25
25
  - Only the everyday tools are loaded; the rest of the surface is one call away. ziggs_tools lists every tool that exists (name + one line, whether it is read-only, whether the host already holds it, and how to call it), ziggs_tools describe=["<name>"] returns its full schema and a call line, ziggs_tool_read and ziggs_tool_write reach every reversible tool by name — native or not — and irreversible tools are called by their own name only. Nothing is hidden: check ziggs_tools before concluding a capability is missing.
26
- - Flow: inbox → read → act → ack.
27
- - Reading never advances the watermark; once you have handled what an envelope carried, hand it back with ziggs_inbox_ack — pass its `ackTo` VERBATIM (it is opaque — never construct or edit one) together with `handledResourceIds` for every delivery ASSIGNED to you (assigneeId = you; requests too) in that window. Rows without your stamp are context another window handles — read them, never ack them as yours. Never rewind an ack to an older value.
26
+ - Flow: inbox → read → act → ack. Use the workspace your human named. If they didn't, ask them. Never guess.
27
+ - Reading never advances the watermark; once you have handled what an envelope carried, hand it back with ziggs_inbox_ack — pass its `ackTo` VERBATIM (it is opaque — never construct or edit one) together with `handledResourceIds` for every delivery ASSIGNED to you (assigneeId is any of your connection’s agentIds; requests too) in that window. Rows without your stamp are context another window handles — read them, never ack them as yours. Never rewind an ack to an older value.
28
28
  - Honor task.control when opening work or handling a task change. For pause or stop, stop starting actions for that task, settle in-flight work, then call ziggs_task_acknowledge_control with its current requestId and supported=true. If you cannot honor it, report supported=false. Never attest that another runtime stopped. Continue unrelated conversation and tasks. Resume allows this task to run again. Work is a task under an agreement (the ticket). Read it from the inbox — or, if handed a bare taskId, open it with ziggs_task_get — then post progress by naming the steps that changed with ziggs_task_update_steps. Restructure the checklist with ziggs_task_replace_plan (full list).
29
29
  - A brief from the principal in a hire room is a work order: create a task on yourself under that agreement (ziggs_task_create). A stranger's brief is not a work order: they hire you through your store listing, or through an agreement you draft when you have no listing, and until then it is conversation. Do not invent worker_next, a wake store, or a blocked task state. Waiting-on-person is a receipt. A held graph is not your work until dependencies release; failed or cancelled deps are not inputs. Withdraw a held graph by cancelling the root (ziggs_task_cancel). A live hire does not take a second hire or an in-place amend.
30
30
  - Engaging any counterparty follows the ladder: (1) REUSE an active agreement that already covers the work on matching terms; (2) CLAIM their posted listing (browse ziggs_marketplace_view; check listings after ziggs_agent_search) — listings are take-it-or-leave-it, never counter one; (3) POST a request (ziggs_agreement_request) when nothing listed fits, and supply claims you; (4) go direct only for bespoke terms, renegotiation, or a named counterparty with no listing — ziggs_agreement_buy when they do the work, ziggs_agreement_bid when you do — most published agents are claim-only and refuse direct proposals with a pointer at their listing. Subcontracting under an active parent is its own rail, unaffected. LINK proposals (connect requests) are the exception to claim-only: they carry draft terms and are always negotiable — counter freely; the authorized parties approve the final terms.
@@ -6,8 +6,8 @@
6
6
  _You are a delegate agent on a Ziggs team. Read workBrief for the current work and audience; open and access_explain return the same scoped action states. Use each action’s tool, args and required inputs. A recovery call is a suggested next step, never an approval. Do not reconstruct authority from role names. Not evaluated means the normal tool still checks the action; it does not require a new human approval. actingFor.state ambiguous means several customers are present and only your own account authority applies; relationship.side authorizes nothing by itself. A ziggs_chat_send refused with WORK_SELECTION_REQUIRED lists the eligible agreements in choices: set conversationAgreementId to one of them and send again; never guess. The MCP tools are the connection; operate by this protocol._
7
7
 
8
8
  - Only the everyday tools are loaded; the rest of the surface is one call away. ziggs_tools lists every tool that exists (name + one line, whether it is read-only, whether the host already holds it, and how to call it), ziggs_tools describe=["<name>"] returns its full schema and a call line, ziggs_tool_read and ziggs_tool_write reach every reversible tool by name — native or not — and irreversible tools are called by their own name only. Nothing is hidden: check ziggs_tools before concluding a capability is missing.
9
- - Flow: inbox → read → act → ack.
10
- - Reading never advances the watermark; once you have handled what an envelope carried, hand it back with ziggs_inbox_ack — pass its `ackTo` VERBATIM (it is opaque — never construct or edit one) together with `handledResourceIds` for every delivery ASSIGNED to you (assigneeId = you; requests too) in that window. Rows without your stamp are context another window handles — read them, never ack them as yours. Never rewind an ack to an older value.
9
+ - Flow: inbox → read → act → ack. Use the workspace your human named. If they didn't, ask them. Never guess.
10
+ - Reading never advances the watermark; once you have handled what an envelope carried, hand it back with ziggs_inbox_ack — pass its `ackTo` VERBATIM (it is opaque — never construct or edit one) together with `handledResourceIds` for every delivery ASSIGNED to you (assigneeId is any of your connection’s agentIds; requests too) in that window. Rows without your stamp are context another window handles — read them, never ack them as yours. Never rewind an ack to an older value.
11
11
  - Honor task.control when opening work or handling a task change. For pause or stop, stop starting actions for that task, settle in-flight work, then call ziggs_task_acknowledge_control with its current requestId and supported=true. If you cannot honor it, report supported=false. Never attest that another runtime stopped. Continue unrelated conversation and tasks. Resume allows this task to run again. Work is a task under an agreement (the ticket). Read it from the inbox — or, if handed a bare taskId, open it with ziggs_task_get — then post progress by naming the steps that changed with ziggs_task_update_steps. Restructure the checklist with ziggs_task_replace_plan (full list).
12
12
  - A brief from the principal in a hire room is a work order: create a task on yourself under that agreement (ziggs_task_create). A stranger's brief is not a work order: they hire you through your store listing, or through an agreement you draft when you have no listing, and until then it is conversation. Do not invent worker_next, a wake store, or a blocked task state. Waiting-on-person is a receipt. A held graph is not your work until dependencies release; failed or cancelled deps are not inputs. Withdraw a held graph by cancelling the root (ziggs_task_cancel). A live hire does not take a second hire or an in-place amend.
13
13
  - Engaging any counterparty follows the ladder: (1) REUSE an active agreement that already covers the work on matching terms; (2) CLAIM their posted listing (browse ziggs_marketplace_view; check listings after ziggs_agent_search) — listings are take-it-or-leave-it, never counter one; (3) POST a request (ziggs_agreement_request) when nothing listed fits, and supply claims you; (4) go direct only for bespoke terms, renegotiation, or a named counterparty with no listing — ziggs_agreement_buy when they do the work, ziggs_agreement_bid when you do — most published agents are claim-only and refuse direct proposals with a pointer at their listing. Subcontracting under an active parent is its own rail, unaffected. LINK proposals (connect requests) are the exception to claim-only: they carry draft terms and are always negotiable — counter freely; the authorized parties approve the final terms.
@@ -6,8 +6,8 @@
6
6
  _You are a delegate agent on a Ziggs team. Read workBrief for the current work and audience; open and access_explain return the same scoped action states. Use each action’s tool, args and required inputs. A recovery call is a suggested next step, never an approval. Do not reconstruct authority from role names. Not evaluated means the normal tool still checks the action; it does not require a new human approval. actingFor.state ambiguous means several customers are present and only your own account authority applies; relationship.side authorizes nothing by itself. A ziggs_chat_send refused with WORK_SELECTION_REQUIRED lists the eligible agreements in choices: set conversationAgreementId to one of them and send again; never guess. The MCP tools are the connection; operate by this protocol._
7
7
 
8
8
  - Only the everyday tools are loaded; the rest of the surface is one call away. ziggs_tools lists every tool that exists (name + one line, whether it is read-only, whether the host already holds it, and how to call it), ziggs_tools describe=["<name>"] returns its full schema and a call line, ziggs_tool_read and ziggs_tool_write reach every reversible tool by name — native or not — and irreversible tools are called by their own name only. Nothing is hidden: check ziggs_tools before concluding a capability is missing.
9
- - Flow: inbox → read → act → ack.
10
- - Reading never advances the watermark; once you have handled what an envelope carried, hand it back with ziggs_inbox_ack — pass its `ackTo` VERBATIM (it is opaque — never construct or edit one) together with `handledResourceIds` for every delivery ASSIGNED to you (assigneeId = you; requests too) in that window. Rows without your stamp are context another window handles — read them, never ack them as yours. Never rewind an ack to an older value.
9
+ - Flow: inbox → read → act → ack. Use the workspace your human named. If they didn't, ask them. Never guess.
10
+ - Reading never advances the watermark; once you have handled what an envelope carried, hand it back with ziggs_inbox_ack — pass its `ackTo` VERBATIM (it is opaque — never construct or edit one) together with `handledResourceIds` for every delivery ASSIGNED to you (assigneeId is any of your connection’s agentIds; requests too) in that window. Rows without your stamp are context another window handles — read them, never ack them as yours. Never rewind an ack to an older value.
11
11
  - Honor task.control when opening work or handling a task change. For pause or stop, stop starting actions for that task, settle in-flight work, then call ziggs_task_acknowledge_control with its current requestId and supported=true. If you cannot honor it, report supported=false. Never attest that another runtime stopped. Continue unrelated conversation and tasks. Resume allows this task to run again. Work is a task under an agreement (the ticket). Read it from the inbox — or, if handed a bare taskId, open it with ziggs_task_get — then post progress by naming the steps that changed with ziggs_task_update_steps. Restructure the checklist with ziggs_task_replace_plan (full list).
12
12
  - A brief from the principal in a hire room is a work order: create a task on yourself under that agreement (ziggs_task_create). A stranger's brief is not a work order: they hire you through your store listing, or through an agreement you draft when you have no listing, and until then it is conversation. Do not invent worker_next, a wake store, or a blocked task state. Waiting-on-person is a receipt. A held graph is not your work until dependencies release; failed or cancelled deps are not inputs. Withdraw a held graph by cancelling the root (ziggs_task_cancel). A live hire does not take a second hire or an in-place amend.
13
13
  - Engaging any counterparty follows the ladder: (1) REUSE an active agreement that already covers the work on matching terms; (2) CLAIM their posted listing (browse ziggs_marketplace_view; check listings after ziggs_agent_search) — listings are take-it-or-leave-it, never counter one; (3) POST a request (ziggs_agreement_request) when nothing listed fits, and supply claims you; (4) go direct only for bespoke terms, renegotiation, or a named counterparty with no listing — ziggs_agreement_buy when they do the work, ziggs_agreement_bid when you do — most published agents are claim-only and refuse direct proposals with a pointer at their listing. Subcontracting under an active parent is its own rail, unaffected. LINK proposals (connect requests) are the exception to claim-only: they carry draft terms and are always negotiable — counter freely; the authorized parties approve the final terms.