@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.
- package/dist/inboxToolResult.d.ts +1 -0
- package/dist/inboxToolResult.js +3 -1
- package/dist/mcpConnectionTools.js +5 -3
- package/dist/operatorKey.js +2 -0
- package/dist/pendingDecisions.js +2 -2
- package/dist/protocol/delegateProtocol.d.ts +2 -2
- package/dist/protocol/delegateProtocol.js +2 -2
- package/dist/server.js +8 -2
- package/dist/strictParams.js +8 -0
- package/dist/toolError.d.ts +10 -0
- package/dist/toolError.js +25 -3
- package/dist/tools.js +2 -13
- package/dist/workspaceRouting.d.ts +65 -0
- package/dist/workspaceRouting.js +246 -0
- package/package.json +3 -3
- package/skills/ziggs/.cursorrules +2 -2
- package/skills/ziggs/SKILL.md +2 -2
- package/skills/ziggs/references/inbox-rhythm.md +2 -2
- package/skills/ziggs/references/reporting-convention.md +2 -2
|
@@ -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 {};
|
package/dist/inboxToolResult.js
CHANGED
|
@@ -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({
|
package/dist/operatorKey.js
CHANGED
|
@@ -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) {
|
package/dist/pendingDecisions.js
CHANGED
|
@@ -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
|
|
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
|
|
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:
|
|
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
|
|
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
|
-
|
|
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 {
|
package/dist/strictParams.js
CHANGED
|
@@ -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,
|
package/dist/toolError.d.ts
CHANGED
|
@@ -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
|
-
? '
|
|
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 —
|
|
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.
|
|
4
|
-
"description": "MCP server for Claude Code, Cursor, and other MCP hosts
|
|
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.
|
|
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
|
|
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.
|
package/skills/ziggs/SKILL.md
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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.
|