@ziggs-ai/agent-sdk 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/AgentHost.d.ts +21 -20
- package/dist/AgentHost.js +60 -136
- package/dist/adapters/McpAdapter.js +14 -13
- package/dist/agent/Agent.js +4 -0
- package/dist/agent/ClaudeSdkAgent.d.ts +5 -0
- package/dist/agent/ClaudeSdkAgent.js +25 -6
- package/dist/agent/claudeCognition.d.ts +7 -0
- package/dist/agent/claudeCognition.js +38 -7
- package/dist/agent/claudeSdkProtocol.d.ts +8 -7
- package/dist/agent/claudeSdkProtocol.js +8 -7
- package/dist/agent/claudeSdkTools.d.ts +4 -2
- package/dist/agent/claudeSdkTools.js +26 -6
- package/dist/agent/toolManagerToSdkMcp.js +1 -0
- package/dist/index.d.ts +5 -9
- package/dist/index.js +12 -6
- package/dist/ingress/normalizeIncoming.d.ts +17 -3
- package/dist/ingress/normalizeIncoming.js +11 -26
- package/dist/runtime/AgentMachine.d.ts +5 -2
- package/dist/runtime/AgentMachine.js +8 -2
- package/dist/runtime/runMechanical.d.ts +3 -1
- package/dist/runtime/runMechanical.js +2 -2
- package/dist/runtime/runTurn.d.ts +3 -1
- package/dist/runtime/runTurn.js +25 -7
- package/dist/runtime/wakeWriteKeys.d.ts +25 -0
- package/dist/runtime/wakeWriteKeys.js +134 -0
- package/dist/server/ConnectionPool.d.ts +7 -4
- package/dist/server/ConnectionPool.js +11 -14
- package/dist/server/ZiggsEffectHandler.js +17 -12
- package/dist/server/inboxLoop.d.ts +50 -0
- package/dist/server/{InboxCatchUp.js → inboxLoop.js} +63 -52
- package/dist/server/runLauncher.js +1 -1
- package/dist/server/tasks/TaskService.d.ts +2 -1
- package/dist/server/tasks/TaskService.js +5 -2
- package/dist/server/tools/index.d.ts +7 -3
- package/dist/server/tools/index.js +13 -3
- package/dist/server/tools/tier1/protocolRunner.d.ts +2 -1
- package/dist/server/tools/tier1/protocolRunner.js +43 -9
- package/dist/server/tools/tier1/protocolTools.js +1 -1
- package/dist/server/tools/tier2/artifactTools.d.ts +3 -0
- package/dist/server/tools/tier2/artifactTools.js +82 -0
- package/dist/server/tools/tier2/chatTools.d.ts +3 -0
- package/dist/server/tools/tier2/chatTools.js +37 -0
- package/dist/server/tools/tier2/connectionTools.d.ts +1 -0
- package/dist/server/tools/tier2/connectionTools.js +52 -5
- package/dist/server/tools/tier2/contextTools.d.ts +4 -2
- package/dist/server/tools/tier2/contextTools.js +105 -12
- package/dist/server/tools/tier2/grantsTools.d.ts +3 -0
- package/dist/server/tools/tier2/grantsTools.js +81 -0
- package/dist/server/tools/tier2/linkTools.d.ts +7 -0
- package/dist/server/tools/tier2/linkTools.js +156 -0
- package/dist/server/tools/tier2/paymentTools.d.ts +0 -1
- package/dist/server/tools/tier2/paymentTools.js +9 -13
- package/dist/types.d.ts +40 -0
- package/package.json +2 -2
- package/dist/server/InboxCatchUp.d.ts +0 -24
- package/dist/server/ziggsconnect/ZiggsConnectClient.d.ts +0 -35
- package/dist/server/ziggsconnect/ZiggsConnectClient.js +0 -95
- package/dist/server/ziggscontext/ZiggsContextClient.d.ts +0 -53
- package/dist/server/ziggscontext/ZiggsContextClient.js +0 -124
- package/dist/server/ziggspay/ZiggsPayClient.d.ts +0 -82
- package/dist/server/ziggspay/ZiggsPayClient.js +0 -203
|
@@ -34,7 +34,7 @@ function normalizePlanInPlace(payload) {
|
|
|
34
34
|
payload.planReviewTiming = 'with_proposal';
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
|
-
export async function dispatchProtocolOp(services, payload, chatId, agents = [], actingAgentId = null) {
|
|
37
|
+
export async function dispatchProtocolOp(services, payload, chatId, agents = [], actingAgentId = null, wakeKeys = null) {
|
|
38
38
|
if (!payload?.operation)
|
|
39
39
|
return null;
|
|
40
40
|
normalizePlanInPlace(payload);
|
|
@@ -43,9 +43,9 @@ export async function dispatchProtocolOp(services, payload, chatId, agents = [],
|
|
|
43
43
|
const actorId = actingAgentId?.trim() || null;
|
|
44
44
|
switch (operation) {
|
|
45
45
|
case 'agreement-propose':
|
|
46
|
-
return proposeAgreement(services, payload, chatId, agents, actorId);
|
|
46
|
+
return proposeAgreement(services, payload, chatId, agents, actorId, wakeKeys);
|
|
47
47
|
case 'agreement-subcontract':
|
|
48
|
-
return subcontractAgreement(services, payload, chatId, agents, actorId);
|
|
48
|
+
return subcontractAgreement(services, payload, chatId, agents, actorId, wakeKeys);
|
|
49
49
|
case 'agreement-respond':
|
|
50
50
|
if (!payload.agreementId || !payload.action)
|
|
51
51
|
return null;
|
|
@@ -146,12 +146,27 @@ export async function dispatchProtocolOp(services, payload, chatId, agents = [],
|
|
|
146
146
|
inputArtifactIds: Array.isArray(payload.inputArtifactIds)
|
|
147
147
|
? payload.inputArtifactIds
|
|
148
148
|
: undefined,
|
|
149
|
+
// Wake-derived idempotency key (delivery law): a crash-replay re-issuing
|
|
150
|
+
// this spawn resolves to the already-created task server-side instead of
|
|
151
|
+
// creating a duplicate. Keyed on what is actually written (the resolved
|
|
152
|
+
// agreement, not the LLM-cited one), so multiple spawns in one wake stay
|
|
153
|
+
// distinct.
|
|
154
|
+
idempotencyKey: wakeKeys?.taskSpawnKey({
|
|
155
|
+
agreementId: spawnAgreementId,
|
|
156
|
+
description: payload.description,
|
|
157
|
+
assigneeId: payload.assigneeId,
|
|
158
|
+
parentTaskId: payload.parentTaskId,
|
|
159
|
+
}),
|
|
149
160
|
});
|
|
150
161
|
}
|
|
151
|
-
case 'task-update':
|
|
162
|
+
case 'task-update': {
|
|
152
163
|
if (!payload.taskId || !payload.status)
|
|
153
164
|
return null;
|
|
154
|
-
|
|
165
|
+
const state = normalizeState(payload.status);
|
|
166
|
+
// Wake-derived idempotency key: a crash-replay re-issuing this transition
|
|
167
|
+
// no-ops server-side instead of erroring on the already-terminal task.
|
|
168
|
+
return taskService.updateState(payload.taskId, state, payload.result, wakeKeys?.taskStateKey(payload.taskId, state));
|
|
169
|
+
}
|
|
155
170
|
case 'task-update-plan-step':
|
|
156
171
|
if (!payload.taskId)
|
|
157
172
|
throw new Error('task-update-plan-step requires `taskId`');
|
|
@@ -173,7 +188,7 @@ export async function dispatchProtocolOp(services, payload, chatId, agents = [],
|
|
|
173
188
|
throw new Error(`Unknown protocol operation: ${operation}`);
|
|
174
189
|
}
|
|
175
190
|
}
|
|
176
|
-
async function proposeAgreement(services, payload, chatId, _agents, actingAgentId) {
|
|
191
|
+
async function proposeAgreement(services, payload, chatId, _agents, actingAgentId, wakeKeys = null) {
|
|
177
192
|
if (!payload.description)
|
|
178
193
|
throw new Error('agreement-propose requires `description`');
|
|
179
194
|
if (!payload.proposedTo)
|
|
@@ -187,7 +202,17 @@ async function proposeAgreement(services, payload, chatId, _agents, actingAgentI
|
|
|
187
202
|
}
|
|
188
203
|
const priced = applyFleetPricingForProtocol('agreement-propose', payload, actingAgentId);
|
|
189
204
|
const { operation: _op, ...fields } = priced;
|
|
190
|
-
|
|
205
|
+
// Wake-derived idempotency key (delivery law): a crash-replay re-issuing this
|
|
206
|
+
// propose resolves to the already-created agreement server-side instead of
|
|
207
|
+
// minting a duplicate proposal. Keyed on what the proposal writes, so
|
|
208
|
+
// multiple proposals in one wake stay distinct.
|
|
209
|
+
const idempotencyKey = wakeKeys?.agreementProposeKey({
|
|
210
|
+
proposedTo: payload.proposedTo,
|
|
211
|
+
providerId: payload.providerId,
|
|
212
|
+
description: payload.description,
|
|
213
|
+
parentAgreementId: payload.parentAgreementId,
|
|
214
|
+
});
|
|
215
|
+
return services.agreementService.propose({ ...fields, chatId, idempotencyKey });
|
|
191
216
|
}
|
|
192
217
|
// ── ZIG-832: deterministic sub-agreement resolution ─────────────────────────
|
|
193
218
|
// The orchestrator delegates one task per worker, but the LLM inconsistently
|
|
@@ -228,7 +253,7 @@ function agActive(a) {
|
|
|
228
253
|
function agIsActiveStatus(a) {
|
|
229
254
|
return String(a.status ?? '') === 'active';
|
|
230
255
|
}
|
|
231
|
-
async function subcontractAgreement(services, payload, chatId, _agents, actingAgentId) {
|
|
256
|
+
async function subcontractAgreement(services, payload, chatId, _agents, actingAgentId, wakeKeys = null) {
|
|
232
257
|
if (!payload.description || !payload.executorId)
|
|
233
258
|
return null;
|
|
234
259
|
// b1 (ZIG-832): idempotent subcontract — if a non-terminal sub-agreement for
|
|
@@ -284,5 +309,14 @@ async function subcontractAgreement(services, payload, chatId, _agents, actingAg
|
|
|
284
309
|
f.lifecycle = 'count-bound';
|
|
285
310
|
f.maxExecutions = 1;
|
|
286
311
|
}
|
|
287
|
-
|
|
312
|
+
// Wake-derived idempotency key (delivery law): a crash-replay that slips past
|
|
313
|
+
// the b1 reuse check above (e.g. no chatId, or the sub not yet visible in the
|
|
314
|
+
// snapshot) resolves to the already-created sub-agreement server-side instead
|
|
315
|
+
// of minting a duplicate.
|
|
316
|
+
const idempotencyKey = wakeKeys?.agreementSubcontractKey({
|
|
317
|
+
executorId: payload.executorId,
|
|
318
|
+
description: payload.description,
|
|
319
|
+
parentAgreementId: payload.parentAgreementId ?? '',
|
|
320
|
+
});
|
|
321
|
+
return services.agreementService.delegate({ ...fields, chatId, idempotencyKey });
|
|
288
322
|
}
|
|
@@ -13,7 +13,7 @@ function handlerFor(operation) {
|
|
|
13
13
|
taskService: c.taskService,
|
|
14
14
|
agreementService: c.agreementService,
|
|
15
15
|
contextReadClient: c.contextReadClient,
|
|
16
|
-
}, { operation, ...args }, c.chatId ?? null, c.agents ?? [], typeof c.agentId === 'string' ? c.agentId : null);
|
|
16
|
+
}, { operation, ...args }, c.chatId ?? null, c.agents ?? [], typeof c.agentId === 'string' ? c.agentId : null, c.wakeKeys ?? null);
|
|
17
17
|
if (result == null)
|
|
18
18
|
throw new Error(`Operation ${operation} returned no result`);
|
|
19
19
|
return result;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { defineTool } from '../../../tools/defineTool.js';
|
|
2
|
+
import { ArtifactsClient } from '@ziggs-ai/api-client';
|
|
3
|
+
/**
|
|
4
|
+
* ZIG-899 — deliberate artifact recording on the SDK surface, same base name
|
|
5
|
+
* as ziggs-mcp's ziggs_record_artifact. Until now artifacts were written only
|
|
6
|
+
* as a host-side effect (recordReport / record-event); the model could not
|
|
7
|
+
* decide "this is a deliverable, record it" — which is exactly what the
|
|
8
|
+
* delegate protocol tells agents to do for heavy results. Rides
|
|
9
|
+
* ArtifactsClient.writeStrict (fail loudly, return the artifactId) — the same
|
|
10
|
+
* wire call as the MCP tool.
|
|
11
|
+
*/
|
|
12
|
+
function creds(ctx) {
|
|
13
|
+
const operatorKey = ctx?.['operatorKey'];
|
|
14
|
+
const agentId = ctx?.['agentId'];
|
|
15
|
+
if (!operatorKey)
|
|
16
|
+
throw new Error('operatorKey missing from tool context');
|
|
17
|
+
if (!agentId)
|
|
18
|
+
throw new Error('agentId missing from tool context');
|
|
19
|
+
return { operatorKey, agentId };
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* ZIG-560 teaching carried over from the MCP tool: name the result slot on the
|
|
23
|
+
* success path so an agent finds the right move unaided — reworded to the SDK
|
|
24
|
+
* task grammar (task_update carries the terminal result on this surface).
|
|
25
|
+
*/
|
|
26
|
+
function reportingHint(contentType, taskId) {
|
|
27
|
+
if (contentType === 'result') {
|
|
28
|
+
return taskId
|
|
29
|
+
? 'Recorded as a task-bound result artifact. Close the task by setting its terminal result with task_update.'
|
|
30
|
+
: 'Recorded as a result artifact, but not bound to a task — pass taskId to bind it, then close the task with task_update.';
|
|
31
|
+
}
|
|
32
|
+
return 'Reporting finished work? Record it with content_type=result bound to the task (taskId), then close the task with task_update — chat messages are conversation only.';
|
|
33
|
+
}
|
|
34
|
+
export const recordArtifactTool = defineTool('record_artifact', {
|
|
35
|
+
text: { type: 'string', required: true, description: 'Artifact body' },
|
|
36
|
+
visibility: {
|
|
37
|
+
type: 'string',
|
|
38
|
+
required: true,
|
|
39
|
+
enum: ['chat', 'agent-private'],
|
|
40
|
+
description: 'chat = visible to scope parties; agent-private = your eyes only',
|
|
41
|
+
},
|
|
42
|
+
chatId: { type: 'string', description: 'Target chat (xor agreementId)' },
|
|
43
|
+
agreementId: { type: 'string', description: 'Target agreement (xor chatId)' },
|
|
44
|
+
taskId: {
|
|
45
|
+
type: 'string',
|
|
46
|
+
description: 'Optional task — creates a TaskArtifactLink alongside the primary scope link',
|
|
47
|
+
},
|
|
48
|
+
content_type: { type: 'string', description: 'Default text; use result for a finished deliverable' },
|
|
49
|
+
}, async (args, ctx) => {
|
|
50
|
+
const chatId = args['chatId'];
|
|
51
|
+
const agreementId = args['agreementId'];
|
|
52
|
+
if ((chatId && agreementId) || (!chatId && !agreementId)) {
|
|
53
|
+
throw new Error('Pass exactly one of chatId or agreementId');
|
|
54
|
+
}
|
|
55
|
+
const visibility = args['visibility'];
|
|
56
|
+
if (visibility !== 'chat' && visibility !== 'agent-private') {
|
|
57
|
+
throw new Error('visibility must be chat or agent-private');
|
|
58
|
+
}
|
|
59
|
+
const contentType = args['content_type'];
|
|
60
|
+
const taskId = args['taskId'];
|
|
61
|
+
const { operatorKey, agentId } = creds(ctx);
|
|
62
|
+
const { artifactId } = await new ArtifactsClient(operatorKey, agentId).writeStrict({
|
|
63
|
+
text: args['text'],
|
|
64
|
+
visibility,
|
|
65
|
+
chatId,
|
|
66
|
+
agreementId,
|
|
67
|
+
taskId,
|
|
68
|
+
content_type: contentType,
|
|
69
|
+
});
|
|
70
|
+
return {
|
|
71
|
+
ok: true,
|
|
72
|
+
artifactId,
|
|
73
|
+
visibility,
|
|
74
|
+
chatId,
|
|
75
|
+
agreementId,
|
|
76
|
+
taskId,
|
|
77
|
+
reportingHint: reportingHint(contentType, taskId),
|
|
78
|
+
};
|
|
79
|
+
}, {
|
|
80
|
+
description: 'Write an artifact to a chat or agreement scope. Set visibility explicitly. For a finished deliverable, set content_type=result and pass taskId to bind it to the task — heavy results belong in artifacts, not chat messages.',
|
|
81
|
+
});
|
|
82
|
+
export const ARTIFACT_TOOLS = [recordArtifactTool];
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { defineTool } from '../../../tools/defineTool.js';
|
|
2
|
+
import { openConversation } from '@ziggs-ai/api-client';
|
|
3
|
+
/**
|
|
4
|
+
* ZIG-900 — the decided chat surface for hosted agents: open_conversation
|
|
5
|
+
* only, same base name as MCP's ziggs_open_conversation. There is deliberately
|
|
6
|
+
* NO chat-listing tool on the SDK: agents read context only via held grants
|
|
7
|
+
* (ZIG-425) and chat membership auto-mints a chat grant (ZIG-428/426), so
|
|
8
|
+
* "what chats can I see" is already answered by list_grants scopeKind=chat —
|
|
9
|
+
* adding a lister would duplicate a context-family read (ZIG-626 rule).
|
|
10
|
+
*/
|
|
11
|
+
function creds(ctx) {
|
|
12
|
+
const operatorKey = ctx?.['operatorKey'];
|
|
13
|
+
const agentId = ctx?.['agentId'];
|
|
14
|
+
if (!operatorKey)
|
|
15
|
+
throw new Error('operatorKey missing from tool context');
|
|
16
|
+
if (!agentId)
|
|
17
|
+
throw new Error('agentId missing from tool context');
|
|
18
|
+
return { operatorKey, agentId };
|
|
19
|
+
}
|
|
20
|
+
export const openConversationTool = defineTool('open_conversation', {
|
|
21
|
+
participantId: {
|
|
22
|
+
type: 'string',
|
|
23
|
+
required: true,
|
|
24
|
+
description: 'User or agent id to converse with (from agent_search — do not guess ids)',
|
|
25
|
+
},
|
|
26
|
+
}, async (args, ctx) => {
|
|
27
|
+
if (!args['participantId'])
|
|
28
|
+
throw new Error('participantId is required');
|
|
29
|
+
const { chatId } = await openConversation(args['participantId'], creds(ctx));
|
|
30
|
+
return {
|
|
31
|
+
chatId,
|
|
32
|
+
note: 'Conversation is open (or reused). Your membership auto-mints a chat grant, so the chat also appears in list_grants scopeKind=chat for context reads.',
|
|
33
|
+
};
|
|
34
|
+
}, {
|
|
35
|
+
description: 'Open or reuse a chat with a user or agent participant. To reach an agent in ANOTHER org, establish a link first — request_link (if you have its agent id) or create_link_invite (if you do not), approved/claimed — otherwise this fails with AGENT_NOT_PUBLISHED. To list chats you can already read, use list_grants scopeKind=chat.',
|
|
36
|
+
});
|
|
37
|
+
export const CHAT_TOOLS = [openConversationTool];
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type ToolDefinition } from '../../../tools/defineTool.js';
|
|
2
2
|
export declare const connectionProxyTool: ToolDefinition;
|
|
3
3
|
export declare const connectionListGrantsTool: ToolDefinition;
|
|
4
|
+
export declare const requestConnectionTool: ToolDefinition;
|
|
4
5
|
export declare const CONNECTION_TOOLS: ToolDefinition[];
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { defineTool } from '../../../tools/defineTool.js';
|
|
2
|
-
import {
|
|
2
|
+
import { ConnectionsClient } from '@ziggs-ai/api-client';
|
|
3
3
|
function clientFor(ctx) {
|
|
4
4
|
const operatorKey = ctx?.['operatorKey'];
|
|
5
5
|
if (!operatorKey)
|
|
6
6
|
throw new Error('operatorKey missing from tool context');
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
// BACKEND_URL / ZIGGS_BACKEND_URL are the SDK runner's overrides; the shared
|
|
8
|
+
// client defaults to HTTP_URL (api-client convention) when neither is set.
|
|
9
|
+
const baseUrl = process.env.BACKEND_URL || process.env.ZIGGS_BACKEND_URL || undefined;
|
|
10
|
+
return new ConnectionsClient(operatorKey, ctx?.['agentId'] || undefined, baseUrl);
|
|
11
11
|
}
|
|
12
12
|
function rethrowWithContext(error, prefix) {
|
|
13
13
|
const e = error;
|
|
@@ -57,7 +57,54 @@ export const connectionListGrantsTool = defineTool('connection_list_grants', { c
|
|
|
57
57
|
rethrowWithContext(e, 'Failed to list connection grants');
|
|
58
58
|
}
|
|
59
59
|
});
|
|
60
|
+
export const requestConnectionTool = defineTool('request_connection', {
|
|
61
|
+
chatId: {
|
|
62
|
+
type: 'string',
|
|
63
|
+
required: true,
|
|
64
|
+
description: 'The chat you are working in — the consent card is opened there',
|
|
65
|
+
},
|
|
66
|
+
serverUrl: { type: 'string', required: true, description: 'Remote MCP server URL (https)' },
|
|
67
|
+
tools: {
|
|
68
|
+
type: 'array',
|
|
69
|
+
items: { type: 'string' },
|
|
70
|
+
required: true,
|
|
71
|
+
description: "Tool names you want — become the grant's allowed_actions caveats",
|
|
72
|
+
},
|
|
73
|
+
reason: {
|
|
74
|
+
type: 'string',
|
|
75
|
+
description: 'Plain-language reason shown to the human deciding',
|
|
76
|
+
},
|
|
77
|
+
}, async (args, ctx) => {
|
|
78
|
+
if (!args['chatId'])
|
|
79
|
+
throw new Error('chatId is required');
|
|
80
|
+
if (!args['serverUrl'])
|
|
81
|
+
throw new Error('serverUrl is required');
|
|
82
|
+
const tools = args['tools'];
|
|
83
|
+
if (!Array.isArray(tools) || tools.length === 0) {
|
|
84
|
+
throw new Error('tools must be a non-empty array of tool names');
|
|
85
|
+
}
|
|
86
|
+
try {
|
|
87
|
+
const result = await clientFor(ctx).requestMcpConnection({
|
|
88
|
+
chatId: args['chatId'],
|
|
89
|
+
serverUrl: args['serverUrl'],
|
|
90
|
+
tools: tools,
|
|
91
|
+
reason: args['reason'],
|
|
92
|
+
});
|
|
93
|
+
return {
|
|
94
|
+
ok: true,
|
|
95
|
+
...result,
|
|
96
|
+
note: 'A connection-consent card is now in the chat awaiting your principal — they approve it right there (ZIG-798 flow). ' +
|
|
97
|
+
'Once approved, the connection + grant appear in list_grants (scopeKind=connection) / connection_list_grants for connection_proxy.',
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
catch (e) {
|
|
101
|
+
rethrowWithContext(e, 'Connection request failed');
|
|
102
|
+
}
|
|
103
|
+
}, {
|
|
104
|
+
description: 'Ask your principal (the human) to connect a remote MCP server and grant you the listed tools. Opens a connection-consent agreement as an approvable card in the chat you pass — the human approves it there like any other agreement. On approval the server is connected (browser OAuth if needed) and you are granted the tools; use them via connection_proxy.',
|
|
105
|
+
});
|
|
60
106
|
export const CONNECTION_TOOLS = [
|
|
61
107
|
connectionProxyTool,
|
|
62
108
|
connectionListGrantsTool,
|
|
109
|
+
requestConnectionTool,
|
|
63
110
|
];
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { type ToolDefinition } from '../../../tools/defineTool.js';
|
|
2
|
-
export declare const
|
|
2
|
+
export declare const contextReadTool: ToolDefinition;
|
|
3
3
|
export declare const contextDelegateTool: ToolDefinition;
|
|
4
|
-
export declare const
|
|
4
|
+
export declare const contextExpandReachTool: ToolDefinition;
|
|
5
|
+
export declare const contextDiscoverGrantableTool: ToolDefinition;
|
|
6
|
+
export declare const CONTEXT_TOOLS: readonly [ToolDefinition, ToolDefinition, ToolDefinition, ToolDefinition];
|
|
@@ -1,16 +1,78 @@
|
|
|
1
1
|
import { defineTool } from '../../../tools/defineTool.js';
|
|
2
|
-
import {
|
|
3
|
-
|
|
2
|
+
import { ContextDiscoveryClient, ContextGrantsClient, ContextReadClient, } from '@ziggs-ai/api-client';
|
|
3
|
+
/**
|
|
4
|
+
* Context grant calls impersonate the grant holder, so both operatorKey (fleet
|
|
5
|
+
* or agent-scoped key) and agentId (X-Agent-Id) must be on the tool context.
|
|
6
|
+
* The same api-client clients back ziggs-mcp — one wire contract, no fork
|
|
7
|
+
* (ZIG-869 / ZIG-775).
|
|
8
|
+
*/
|
|
9
|
+
function creds(ctx) {
|
|
4
10
|
const operatorKey = ctx?.['operatorKey'];
|
|
11
|
+
const agentId = ctx?.['agentId'];
|
|
5
12
|
if (!operatorKey)
|
|
6
13
|
throw new Error('operatorKey missing from tool context');
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
});
|
|
14
|
+
if (!agentId)
|
|
15
|
+
throw new Error('agentId missing from tool context');
|
|
16
|
+
return { operatorKey, agentId };
|
|
11
17
|
}
|
|
12
|
-
|
|
13
|
-
|
|
18
|
+
const CONTEXT_READ_TYPES = [
|
|
19
|
+
'messages',
|
|
20
|
+
'artifacts',
|
|
21
|
+
'agreements',
|
|
22
|
+
'tasks',
|
|
23
|
+
];
|
|
24
|
+
export const contextReadTool = defineTool('context_read', {
|
|
25
|
+
type: {
|
|
26
|
+
type: 'string',
|
|
27
|
+
required: true,
|
|
28
|
+
enum: [...CONTEXT_READ_TYPES],
|
|
29
|
+
description: 'Resource type to read from the scope',
|
|
30
|
+
},
|
|
31
|
+
via: {
|
|
32
|
+
type: 'string',
|
|
33
|
+
required: true,
|
|
34
|
+
description: 'Scope entry you hold, e.g. chat:<id>, agreement:<id>, task:<id>',
|
|
35
|
+
},
|
|
36
|
+
cursor: { type: 'string', description: 'Opaque cursor from a prior nextCursor to page' },
|
|
37
|
+
after: {
|
|
38
|
+
type: 'string',
|
|
39
|
+
description: 'ISO timestamp for forward-delta (messages/artifacts)',
|
|
40
|
+
},
|
|
41
|
+
direction: {
|
|
42
|
+
type: 'string',
|
|
43
|
+
enum: ['forward'],
|
|
44
|
+
description: 'Use "forward" with after for a message forward-delta',
|
|
45
|
+
},
|
|
46
|
+
limit: { type: 'number', description: 'Page size (server default when omitted)' },
|
|
47
|
+
state: { type: 'string', description: 'Task state filter (tasks only)' },
|
|
48
|
+
contextGrantId: {
|
|
49
|
+
type: 'string',
|
|
50
|
+
description: 'Pin a specific grant when you hold several over the same scope',
|
|
51
|
+
},
|
|
52
|
+
}, async (args, ctx) => {
|
|
53
|
+
const { operatorKey, agentId } = creds(ctx);
|
|
54
|
+
const type = args['type'];
|
|
55
|
+
if (!CONTEXT_READ_TYPES.includes(type)) {
|
|
56
|
+
throw new Error(`type must be one of ${CONTEXT_READ_TYPES.join(', ')}`);
|
|
57
|
+
}
|
|
58
|
+
const via = args['via'];
|
|
59
|
+
if (!via)
|
|
60
|
+
throw new Error('via is required');
|
|
61
|
+
const direction = args['direction'];
|
|
62
|
+
if (direction !== undefined && direction !== 'forward') {
|
|
63
|
+
throw new Error('direction must be "forward"');
|
|
64
|
+
}
|
|
65
|
+
return new ContextReadClient(operatorKey, agentId).read(type, {
|
|
66
|
+
via,
|
|
67
|
+
cursor: args['cursor'],
|
|
68
|
+
after: args['after'],
|
|
69
|
+
direction: direction,
|
|
70
|
+
limit: typeof args['limit'] === 'number' ? args['limit'] : undefined,
|
|
71
|
+
state: args['state'],
|
|
72
|
+
contextGrantId: args['contextGrantId'],
|
|
73
|
+
});
|
|
74
|
+
}, {
|
|
75
|
+
description: 'Read the contents of a scope you already hold a grant for: messages | artifacts | agreements | tasks. Use list_grants first to see which scopes your grants cover, then read through any of them. Cursored; all access is grant-fenced server-side.',
|
|
14
76
|
});
|
|
15
77
|
export const contextDelegateTool = defineTool('context_delegate', {
|
|
16
78
|
parentGrantId: { type: 'string', required: true },
|
|
@@ -29,8 +91,8 @@ export const contextDelegateTool = defineTool('context_delegate', {
|
|
|
29
91
|
if (scopeKind !== 'chat' && scopeKind !== 'agreement' && scopeKind !== 'org') {
|
|
30
92
|
throw new Error('scopeKind must be chat, agreement, or org');
|
|
31
93
|
}
|
|
32
|
-
|
|
33
|
-
|
|
94
|
+
const { operatorKey, agentId } = creds(ctx);
|
|
95
|
+
return new ContextGrantsClient(operatorKey, agentId).delegateGrant(args['parentGrantId'], {
|
|
34
96
|
holderId: args['holderId'],
|
|
35
97
|
scope: { kind: scopeKind, id: args['scopeId'] },
|
|
36
98
|
temporal,
|
|
@@ -38,9 +100,40 @@ export const contextDelegateTool = defineTool('context_delegate', {
|
|
|
38
100
|
watermarkAt: args['watermarkAt'],
|
|
39
101
|
});
|
|
40
102
|
}, {
|
|
41
|
-
description: 'Delegate a narrower child context grant (tighter scope, shorter expiry, or from-now). Holder-only.',
|
|
103
|
+
description: 'Delegate a narrower child context grant (tighter scope, shorter expiry, or from-now). Holder-only. Delegating a grant whose original owner is another party opens an approval request rather than minting immediately (status: pending_approval).',
|
|
104
|
+
});
|
|
105
|
+
export const contextExpandReachTool = defineTool('context_expand_reach', {
|
|
106
|
+
grantId: {
|
|
107
|
+
type: 'string',
|
|
108
|
+
required: true,
|
|
109
|
+
description: 'A grant you hold (grantId from list_grants) to expand',
|
|
110
|
+
},
|
|
111
|
+
}, async (args, ctx) => {
|
|
112
|
+
const { operatorKey, agentId } = creds(ctx);
|
|
113
|
+
const grantId = args['grantId'];
|
|
114
|
+
if (!grantId)
|
|
115
|
+
throw new Error('grantId is required');
|
|
116
|
+
return new ContextGrantsClient(operatorKey, agentId).getReach(grantId);
|
|
117
|
+
}, {
|
|
118
|
+
description: 'Expand a grant you hold into the chat/agreement ids inside its scope, so you can read through it. list_grants tells you that you hold e.g. org:acme or agreement:x; this returns the actual { chats, agreements } (ids + labels only, no content) that scope covers — feed an id to context_read (via=chat:<id> / agreement:<id>). Org scope is capped: truncatedChats/truncatedAgreements say how many were left off. Holder-only, grant-fenced.',
|
|
119
|
+
});
|
|
120
|
+
// ZIG-754 — the negative-space complement to the reach tools. list_grants /
|
|
121
|
+
// context_expand_reach answer "what CAN I read?"; this answers "what exists
|
|
122
|
+
// just outside my fence that I CAN'T read yet?" so the agent can flag missing
|
|
123
|
+
// context and make an informed widen request instead of silently
|
|
124
|
+
// under-delivering on partial context. The MCP surface has had this as
|
|
125
|
+
// ziggs_discover_grantable; this is its SDK/legibility complement, backed by
|
|
126
|
+
// the same ContextDiscoveryClient (one wire contract, no fork).
|
|
127
|
+
export const contextDiscoverGrantableTool = defineTool('context_discover_grantable', {}, async (_args, ctx) => {
|
|
128
|
+
const { operatorKey, agentId } = creds(ctx);
|
|
129
|
+
const items = await new ContextDiscoveryClient(operatorKey, agentId).discoverGrantable();
|
|
130
|
+
return { count: items.length, items };
|
|
131
|
+
}, {
|
|
132
|
+
description: 'See what context EXISTS in your orgs that you CANNOT read yet — the inverse of list_grants. Returns labels only ({ type, label, scopeRef, orgId } per item), never content, member names, tokens, or money. Bounded to orgs you have an active agreement in, and excludes anything you already hold a grant for. Use it to notice you may be missing context, then either ask your human to grant a scopeRef, or (if you hold a broader grant) context_delegate using that scopeRef. Pair with list_grants (what you hold) and context_expand_reach (what a held grant covers).',
|
|
42
133
|
});
|
|
43
134
|
export const CONTEXT_TOOLS = [
|
|
44
|
-
|
|
135
|
+
contextReadTool,
|
|
45
136
|
contextDelegateTool,
|
|
137
|
+
contextExpandReachTool,
|
|
138
|
+
contextDiscoverGrantableTool,
|
|
46
139
|
];
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { defineTool } from '../../../tools/defineTool.js';
|
|
2
|
+
import { GrantsClient, unreadableGrantRails, } from '@ziggs-ai/api-client';
|
|
3
|
+
/**
|
|
4
|
+
* ZIG-893 — the single "what authority do I hold?" tool. One name, every rail
|
|
5
|
+
* (context chat/agreement/org, connection, wallet), holder-scoped, cross-session.
|
|
6
|
+
* Wraps the unified `GET /grants` (GrantsClient) that already replaced the three
|
|
7
|
+
* per-rail listers server-side; this lifts that unification up to the tool layer
|
|
8
|
+
* so an agent no longer has to know three tool names — or, as in the 2026-07-20
|
|
9
|
+
* incident, tell its principal grant lookup doesn't exist because its config
|
|
10
|
+
* happened to omit the one rail's lister.
|
|
11
|
+
*/
|
|
12
|
+
function creds(ctx) {
|
|
13
|
+
const operatorKey = ctx?.['operatorKey'];
|
|
14
|
+
const agentId = ctx?.['agentId'];
|
|
15
|
+
if (!operatorKey)
|
|
16
|
+
throw new Error('operatorKey missing from tool context');
|
|
17
|
+
if (!agentId)
|
|
18
|
+
throw new Error('agentId missing from tool context');
|
|
19
|
+
return { operatorKey, agentId };
|
|
20
|
+
}
|
|
21
|
+
const SCOPE_KINDS = [
|
|
22
|
+
'chat',
|
|
23
|
+
'agreement',
|
|
24
|
+
'org',
|
|
25
|
+
'connection',
|
|
26
|
+
'wallet',
|
|
27
|
+
];
|
|
28
|
+
const HEALTHS = ['active', 'expired', 'revoked'];
|
|
29
|
+
function parseScopeKinds(raw) {
|
|
30
|
+
if (raw == null)
|
|
31
|
+
return undefined;
|
|
32
|
+
const list = Array.isArray(raw) ? raw : [raw];
|
|
33
|
+
const out = [];
|
|
34
|
+
for (const r of list) {
|
|
35
|
+
if (!SCOPE_KINDS.includes(r)) {
|
|
36
|
+
throw new Error(`scopeKind must be one of ${SCOPE_KINDS.join(', ')}`);
|
|
37
|
+
}
|
|
38
|
+
if (!out.includes(r))
|
|
39
|
+
out.push(r);
|
|
40
|
+
}
|
|
41
|
+
return out.length ? out : undefined;
|
|
42
|
+
}
|
|
43
|
+
export const listGrantsTool = defineTool('list_grants', {
|
|
44
|
+
scopeKind: {
|
|
45
|
+
type: 'array',
|
|
46
|
+
items: { type: 'string', enum: [...SCOPE_KINDS] },
|
|
47
|
+
description: 'Rail filter (repeatable): chat | agreement | org (context) | connection | wallet. Omit for every rail you can read.',
|
|
48
|
+
},
|
|
49
|
+
health: {
|
|
50
|
+
type: 'string',
|
|
51
|
+
enum: [...HEALTHS],
|
|
52
|
+
description: 'Grant health filter. Defaults to active (live grants only).',
|
|
53
|
+
},
|
|
54
|
+
cursor: { type: 'string', description: 'Opaque cursor from a prior nextCursor to page' },
|
|
55
|
+
limit: { type: 'number', description: 'Page size (server default when omitted)' },
|
|
56
|
+
}, async (args, ctx) => {
|
|
57
|
+
const { operatorKey, agentId } = creds(ctx);
|
|
58
|
+
const scopeKind = parseScopeKinds(args['scopeKind']);
|
|
59
|
+
const healthArg = args['health'];
|
|
60
|
+
if (healthArg !== undefined && !HEALTHS.includes(healthArg)) {
|
|
61
|
+
throw new Error(`health must be one of ${HEALTHS.join(', ')}`);
|
|
62
|
+
}
|
|
63
|
+
const { items, nextCursor } = await new GrantsClient(operatorKey, agentId).listGrants({
|
|
64
|
+
scopeKind,
|
|
65
|
+
// Reach = live grants only by default; pass health to see expired/revoked.
|
|
66
|
+
health: healthArg ?? 'active',
|
|
67
|
+
cursor: args['cursor'],
|
|
68
|
+
limit: typeof args['limit'] === 'number' ? args['limit'] : undefined,
|
|
69
|
+
});
|
|
70
|
+
const unreadable = unreadableGrantRails(operatorKey, scopeKind);
|
|
71
|
+
return {
|
|
72
|
+
count: items.length,
|
|
73
|
+
grants: items,
|
|
74
|
+
nextCursor,
|
|
75
|
+
...(unreadable && unreadable.length ? { unreadableRails: unreadable } : {}),
|
|
76
|
+
};
|
|
77
|
+
}, {
|
|
78
|
+
description: 'List every grant this agent holds across all rails in one call — context (chat/agreement/org), connection, and wallet — as canonical grants (grantId, scope, caveats, expiresAt, health; no message/artifact content or credentials). The single answer to "what grants of mine do you hold?", holder-scoped and cross-session. Filter by scopeKind (rail) and health (defaults to active). Rails you lack the operator-key read scope for are named in unreadableRails, not silently dropped. Cursor-paginated: pass cursor from a prior nextCursor. Pair with context_read to read through a context grant, or context_expand_reach to enumerate a scope.',
|
|
79
|
+
isGenericFallback: true,
|
|
80
|
+
});
|
|
81
|
+
export const GRANTS_TOOLS = [listGrantsTool];
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type ToolDefinition } from '../../../tools/defineTool.js';
|
|
2
|
+
export declare const requestLinkTool: ToolDefinition;
|
|
3
|
+
export declare const createLinkInviteTool: ToolDefinition;
|
|
4
|
+
export declare const claimLinkInviteTool: ToolDefinition;
|
|
5
|
+
export declare const listLinksTool: ToolDefinition;
|
|
6
|
+
export declare const revokeLinkTool: ToolDefinition;
|
|
7
|
+
export declare const LINK_TOOLS: ToolDefinition[];
|