@ziggs-ai/api-client 0.30.3 → 0.32.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/dist/capabilities/agreementVerbs.js +1 -1
- package/dist/capabilities/chat.js +2 -2
- package/dist/capabilities/connections.js +22 -7
- package/dist/capabilities/context.js +25 -18
- package/dist/capabilities/tasks.js +2 -2
- package/dist/capabilities/types.d.ts +0 -16
- package/dist/capabilities/types.js +19 -2
- package/dist/decisionWords.d.ts +10 -0
- package/dist/decisionWords.js +16 -1
- package/dist/http/AgreementClient.js +2 -1
- package/dist/http/AssistantConnectionClient.d.ts +27 -0
- package/dist/http/AssistantConnectionClient.js +49 -0
- package/dist/http/ChatClient.js +2 -1
- package/dist/http/ConnectionsClient.d.ts +7 -1
- package/dist/http/ConnectionsClient.js +6 -5
- package/dist/http/ContextGrantsClient.d.ts +26 -3
- package/dist/http/ContextGrantsClient.js +9 -2
- package/dist/http/MarketplaceClient.js +2 -1
- package/dist/http/MessagesClient.js +4 -3
- package/dist/http/OrgsClient.js +0 -1
- package/dist/http/TaskClient.js +2 -1
- package/dist/http/ToolPickClient.d.ts +25 -0
- package/dist/http/ToolPickClient.js +35 -0
- package/dist/http/index.d.ts +1 -0
- package/dist/http/index.js +1 -0
- package/dist/http/operatorHeaders.d.ts +8 -0
- package/dist/http/operatorHeaders.js +15 -0
- package/dist/inboxAck.d.ts +2 -1
- package/dist/inboxAck.js +6 -6
- package/dist/index.d.ts +4 -1
- package/dist/index.js +4 -1
- package/dist/shared/contextAccessRequest.d.ts +12 -0
- package/dist/shared/contextAccessRequest.js +6 -0
- package/dist/shared/operatorKey.d.ts +1 -0
- package/dist/shared/operatorKey.js +1 -0
- package/package.json +1 -1
|
@@ -214,7 +214,7 @@ export const agreementBidCapability = {
|
|
|
214
214
|
proposedTo: args['counterparty'],
|
|
215
215
|
chatId: args['chatId'],
|
|
216
216
|
// You provide, so the counterparty pays.
|
|
217
|
-
providerId: creds.agentId,
|
|
217
|
+
providerId: creds.agentId || undefined,
|
|
218
218
|
engagementKind: engagementKindFrom(args),
|
|
219
219
|
}, creds);
|
|
220
220
|
return { agreement };
|
|
@@ -138,7 +138,7 @@ export const openConversationCapability = {
|
|
|
138
138
|
return {
|
|
139
139
|
chatId,
|
|
140
140
|
appUrl: chatAppUrl(webAppOrigin(env), chatId),
|
|
141
|
-
actingAgentId: agentId,
|
|
141
|
+
...(agentId ? { actingAgentId: agentId } : {}),
|
|
142
142
|
people,
|
|
143
143
|
pending,
|
|
144
144
|
...(typeof reused === 'boolean' ? { reused } : {}),
|
|
@@ -158,7 +158,7 @@ function roomRoster(args, invited, agentId) {
|
|
|
158
158
|
const pending = (invited ?? []).filter((row) => row.status === 'pending').map((row) => row.id);
|
|
159
159
|
const waiting = new Set(pending);
|
|
160
160
|
const here = (invited ?? []).filter((row) => row.status !== 'pending').map((row) => row.id);
|
|
161
|
-
const people = [...new Set([...named.filter((id) => !waiting.has(id)), ...here, agentId])];
|
|
161
|
+
const people = [...new Set([...named.filter((id) => !waiting.has(id)), ...here, ...(agentId ? [agentId] : [])])];
|
|
162
162
|
return { people, pending };
|
|
163
163
|
}
|
|
164
164
|
/** Say what happened to each invited id, so the agent reports it truthfully. */
|
|
@@ -72,13 +72,15 @@ export const connectionProxyCapability = {
|
|
|
72
72
|
export const requestConnectionCapability = {
|
|
73
73
|
key: "connection_request",
|
|
74
74
|
names: { sdk: "connection_request", mcp: "ziggs_connection_request" },
|
|
75
|
-
title: "Ask
|
|
75
|
+
title: "Ask someone for a connection",
|
|
76
76
|
descriptions: {
|
|
77
|
-
sdk: "Ask
|
|
78
|
-
"
|
|
77
|
+
sdk: "Ask a person to connect a remote MCP server and grant you the listed tools. " +
|
|
78
|
+
"Name who to ask in recipientId: your work brief offers them for this wake — workBrief.actingFor.customer.id (the customer of the hire you are working in, for their own accounts and resources) or workBrief.actingFor.operator.id (your operator, for tools you bring). Anyone else is refused, and the refusal lists who you can ask. " +
|
|
79
|
+
"Opens a connection-consent card in the chat you pass; the answer says who got it and where they approve it — tell that person, not whoever happens to be in the room. " +
|
|
79
80
|
"On approval the server is connected (browser OAuth if needed) and you are granted the tools; call them with mcp_tool_call / mcp_tools_list (not connection_proxy).",
|
|
80
|
-
mcp: "Ask
|
|
81
|
-
"
|
|
81
|
+
mcp: "Ask a person to connect a remote MCP server and grant you the listed tools. " +
|
|
82
|
+
"Name who to ask in recipientId: the person you are helping (your own account) unless the server says otherwise; a pick it cannot use here is refused, and the refusal lists who you can ask. " +
|
|
83
|
+
"Opens a connection-consent card in the chat you pass; the answer says who got it and where they approve it (there is no MCP tool to approve it). " +
|
|
82
84
|
"On approval the server is connected (browser OAuth if needed) and you are granted the tools; the result shows up in ziggs_connection_list for use with ziggs_mcp_tools_list / ziggs_mcp_tool_call.",
|
|
83
85
|
},
|
|
84
86
|
annotation: "write",
|
|
@@ -103,6 +105,11 @@ export const requestConnectionCapability = {
|
|
|
103
105
|
type: "string",
|
|
104
106
|
description: "Plain-language reason shown to the human deciding",
|
|
105
107
|
},
|
|
108
|
+
recipientId: {
|
|
109
|
+
type: "string",
|
|
110
|
+
required: true,
|
|
111
|
+
description: "Who to ask: workBrief.actingFor.customer.id (the customer of this hire, for their own accounts) or workBrief.actingFor.operator.id (your operator, for tools you bring)",
|
|
112
|
+
},
|
|
106
113
|
},
|
|
107
114
|
needsAgentId: true,
|
|
108
115
|
handler: async (args, env) => {
|
|
@@ -114,20 +121,28 @@ export const requestConnectionCapability = {
|
|
|
114
121
|
if (!Array.isArray(tools) || tools.length === 0) {
|
|
115
122
|
throw new Error("tools must be a non-empty array of tool names");
|
|
116
123
|
}
|
|
124
|
+
if (typeof args["recipientId"] !== "string" || !args["recipientId"].trim())
|
|
125
|
+
throw new Error("recipientId is required: who to ask, from your work brief");
|
|
117
126
|
try {
|
|
118
127
|
const result = await client(env).requestMcpConnection({
|
|
119
128
|
chatId: args["chatId"],
|
|
120
129
|
serverUrl: args["serverUrl"],
|
|
121
130
|
tools: tools,
|
|
122
131
|
reason: args["reason"],
|
|
132
|
+
recipientId: args["recipientId"].trim(),
|
|
123
133
|
});
|
|
134
|
+
// Who got the card is the server's answer; say that, never "your
|
|
135
|
+
// principal" — the card often goes to someone else.
|
|
136
|
+
const sent = typeof result["message"] === "string"
|
|
137
|
+
? result["message"]
|
|
138
|
+
: "A connection-consent card is open.";
|
|
124
139
|
return {
|
|
125
140
|
ok: true,
|
|
126
141
|
...result,
|
|
127
142
|
note: env.surface === "mcp"
|
|
128
|
-
?
|
|
143
|
+
? `${sent} Tell that person now (pull-only MCP has no push). ` +
|
|
129
144
|
"Once approved, the connection + grant appear in ziggs_connection_list for ziggs_mcp_tools_list / ziggs_mcp_tool_call."
|
|
130
|
-
:
|
|
145
|
+
: `${sent} Tell that person; only they can approve it. ` +
|
|
131
146
|
"Once approved, the connection + grant appear in grant_list (scopeKind [\"connection\"]) / connection_list_grants for mcp_tools_list / mcp_tool_call.",
|
|
132
147
|
};
|
|
133
148
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isContextAccessRequest } from '../shared/contextAccessRequest.js';
|
|
1
2
|
import { presentResourceActions } from './resourceActions.js';
|
|
2
3
|
import { ContextReadClient, CONTEXT_READ_TYPES, CONTEXT_READ_VIA, parseVia, viaHint, } from '../http/ContextReadClient.js';
|
|
3
4
|
import { ContextOpenClient } from '../http/ContextOpenClient.js';
|
|
@@ -65,7 +66,7 @@ export const contextReadCapability = {
|
|
|
65
66
|
title: 'Read context you hold',
|
|
66
67
|
descriptions: {
|
|
67
68
|
sdk: `Read the contents of a scope you already hold a grant for: ${CONTEXT_READ_TYPES.join(' | ')}. Each type reads through its own entries — ${VIA_BY_TYPE}. Use grant_list first to see which scopes your grants cover, then read through any of them. ${ARTIFACT_VIA_NOTE}. Cursored; all access is grant-fenced server-side.`,
|
|
68
|
-
mcp: `Read the contents of a scope you already hold: ${CONTEXT_READ_TYPES.join(' | ')} (the type param). Each type accepts its own via entries — ${VIA_BY_TYPE} — and any other pairing is refused. ${ARTIFACT_VIA_NOTE} (e.g. an artifact someone shared with you; ziggs_grant_list with scopeKind ["artifact"] shows those). Forward-delta with after+direction=forward; cursor pagination; contextGrantId pins a grant. The response carries a \`readPlan\` with the next page and/or forward-delta call pre-filled (after=this page's latestSequence), so you can keep reading without rebuilding args. This is the single read path for all
|
|
69
|
+
mcp: `Read the contents of a scope you already hold: ${CONTEXT_READ_TYPES.join(' | ')} (the type param). Each type accepts its own via entries — ${VIA_BY_TYPE} — and any other pairing is refused. ${ARTIFACT_VIA_NOTE} (e.g. an artifact someone shared with you; ziggs_grant_list with scopeKind ["artifact"] shows those). Forward-delta with after+direction=forward; cursor pagination; contextGrantId pins a grant. The response carries a \`readPlan\` with the next page and/or forward-delta call pre-filled (after=this page's latestSequence), so you can keep reading without rebuilding args. This is the single read path for all context types — to discover which scopes exist, use the listers: ziggs_chat_list, ziggs_task_list, ziggs_agreement_list, ziggs_grant_list, ziggs_link_list.`,
|
|
69
70
|
},
|
|
70
71
|
annotation: 'read-only',
|
|
71
72
|
params: {
|
|
@@ -301,7 +302,7 @@ function presentOpenResult(result, env) {
|
|
|
301
302
|
return [];
|
|
302
303
|
const row = item;
|
|
303
304
|
const ask = row.grantRequest;
|
|
304
|
-
if (row.status !== 'active' || ask
|
|
305
|
+
if (row.status !== 'active' || !isContextAccessRequest(ask) || ask.holderId !== fullCreds(env).agentId || !ask.scope?.id || !['artifact', 'chat', 'agreement', 'task'].includes(ask.scope.kind ?? ''))
|
|
305
306
|
return [];
|
|
306
307
|
return [{ kind: 'resume', tool: env.surface === 'mcp' ? 'ziggs_open' : 'open', args: { [`${ask.scope.kind}Id`]: ask.scope.id }, effect: 'read_only', note: 'Approval recorded. Open the original scope; the server rechecks expiry and revocation.' }];
|
|
307
308
|
});
|
|
@@ -368,13 +369,13 @@ export const contextRequestCapability = {
|
|
|
368
369
|
title: 'Ask an owner for artifact access',
|
|
369
370
|
descriptions: {
|
|
370
371
|
sdk: 'Ask a human owner for temporary read-only access to one artifact you were given a reference for. Use your existing chat with that owner. Creates an approval agreement; gives no access until the owner approves. You are the only recipient. After approval, open the original artifact. Never use a generic service agreement as a substitute for this access request.',
|
|
371
|
-
mcp: 'Request access to a restricted artifact directly from its human owner. Pass the artifact id you were given, your conversation with the owner, and the
|
|
372
|
+
mcp: 'Request access to a restricted artifact directly from its human owner. Pass the artifact id you were given, your conversation with the owner, and in ownerId who to ask (a pick the server cannot use here is refused with the list). This creates a pending approval agreement, NOT a grant. No prior grant is needed. The human approves at the agreement link. After approval, ziggs_open the original artifact. Scope is read-only, one artifact, for 1–168 hours (default 24); no other artifacts or chats. Include the intended summary recipient in reason. Keep polling for the decision when asked to wait. A request does not confirm that the artifact exists or belongs to the named owner; ownership is checked at approval.',
|
|
372
373
|
},
|
|
373
374
|
annotation: 'write', needsAgentId: true,
|
|
374
375
|
params: {
|
|
375
376
|
artifactId: { type: 'string', required: true },
|
|
376
377
|
chatId: { type: 'string', required: true },
|
|
377
|
-
ownerId: { type: 'string', required: true, description: '
|
|
378
|
+
ownerId: { type: 'string', required: true, description: 'Who to ask, from your work brief: workBrief.actingFor.customer.id or workBrief.actingFor.operator.id. Anyone else is refused, and the refusal lists who you can ask.' },
|
|
378
379
|
reason: { type: 'string', required: true, description: 'Purpose, including whom you will summarize the artifact to' },
|
|
379
380
|
durationHours: { type: 'number', description: 'Integer 1–168 hours after approval; default 24' },
|
|
380
381
|
idempotencyKey: { type: 'string', description: 'Reuse for the same request; do not create duplicate approval cards' },
|
|
@@ -390,7 +391,7 @@ export const contextRequestCapability = {
|
|
|
390
391
|
const result = await new ContextGrantsClient(creds.operatorKey, creds.agentId, env.baseUrl).requestArtifactAccess(args.artifactId, {
|
|
391
392
|
chatId: args.chatId, ownerId: args.ownerId, reason: args.reason, durationHours,
|
|
392
393
|
}, { idempotencyKey: args.idempotencyKey, laneId: creds.laneId });
|
|
393
|
-
return { ...result, outcome: 'pending', state: 'requested', appUrl: `${(env.webUrl ?? 'https://ziggsai.com').replace(/\/$/, '')}/app/agreements/${encodeURIComponent(result.agreementId)}`, note: 'Waiting for the artifact owner. No access granted yet; open the original artifact only after approval
|
|
394
|
+
return { ...result, outcome: 'pending', state: 'requested', appUrl: `${(env.webUrl ?? 'https://ziggsai.com').replace(/\/$/, '')}/app/agreements/${encodeURIComponent(result.agreementId)}`, note: `${result.message ?? 'Waiting for the artifact owner.'} No access granted yet; open the original artifact only after approval.` };
|
|
394
395
|
},
|
|
395
396
|
};
|
|
396
397
|
/** A suggested request still needs a purpose; it is never a grant or an approval. */
|
|
@@ -398,7 +399,7 @@ function requestAccessAction(args, env) {
|
|
|
398
399
|
return {
|
|
399
400
|
kind: 'request_access',
|
|
400
401
|
tool: env.surface === 'mcp' ? 'ziggs_access_request' : 'access_request',
|
|
401
|
-
args, requiredInput: args.scopeKind === 'artifact' ? ['reason', 'chatId', '
|
|
402
|
+
args, requiredInput: args.scopeKind === 'artifact' ? ['reason', 'chatId', 'recipientId'] : ['reason', 'recipientId'], effect: 'request_only',
|
|
402
403
|
requiresApproval: true,
|
|
403
404
|
};
|
|
404
405
|
}
|
|
@@ -406,30 +407,35 @@ export const accessRequestCapability = {
|
|
|
406
407
|
key: 'access_request',
|
|
407
408
|
names: { sdk: 'access_request', mcp: 'ziggs_access_request' },
|
|
408
409
|
title: 'Request access for yourself',
|
|
410
|
+
annotation: 'write', needsAgentId: true,
|
|
409
411
|
descriptions: {
|
|
410
|
-
sdk: '
|
|
411
|
-
mcp: '
|
|
412
|
+
sdk: 'Request optional bounded access to one artifact, chat, agreement context or task. Creates only a pending approval; never grants access or approves. Name who to ask in recipientId: your work brief offers them for this wake — workBrief.actingFor.customer.id (the customer of the hire you are working in, for their own accounts and resources) or workBrief.actingFor.operator.id (your operator, for tools you bring). Anyone else is refused, and the refusal lists who you can ask. Artifact requests also need chatId, the conversation they approve in. Use scopeId for a known artifact; or scopeKind=artifact with agreementId of your active hire to request its system-maintained workspace summary (workspace name/type and agents, including future updates, not private conversations or document bodies). Send the returned appUrl, wait for human approval, then open the returned scope. Hiring alone grants no source access. Revocation and expiry are checked by the system.',
|
|
413
|
+
mcp: 'Request access for this agent, never approve or grant it. Use scopeId for an artifact, chat, agreement or task; scopeKind=artifact with agreementId of your active hire resolves its system-maintained workspace summary artifact. Name who to ask in recipientId; a pick the server cannot use here is refused, and the refusal lists who you can ask. Artifact requests also require chatId. Explain the scope and lifetime; summary access includes future updates only to that artifact. Surface appUrl and wait for human approval; then open the returned scope. Pending grants no access.',
|
|
412
414
|
},
|
|
413
|
-
annotation: 'write', needsAgentId: true,
|
|
414
415
|
params: {
|
|
415
416
|
scopeKind: { type: 'string', required: true, enum: ['artifact', 'chat', 'agreement', 'task'] },
|
|
416
|
-
scopeId: { type: 'string',
|
|
417
|
+
scopeId: { type: 'string', description: 'Original resource id. Omit when using a hire agreementId to request its workspace summary.' },
|
|
418
|
+
agreementId: { type: 'string', description: 'Artifact scope only: current hire id, used instead of scopeId to request the system-maintained workspace summary.' },
|
|
417
419
|
reason: { type: 'string', required: true, description: 'Why access is needed and who will receive the reply or summary' },
|
|
418
420
|
permission: { type: 'string', enum: ['read', 'reply'], description: 'read by default; reply is read and reply and is only valid for a chat' },
|
|
419
421
|
temporal: { type: 'string', enum: ['from-now', 'from-start'], description: 'Chat history requested; default from-now. Earlier messages need from-start. Other resources use from-start.' },
|
|
420
422
|
durationHours: { type: 'number', description: 'Integer 1–168 hours after approval; default 24' },
|
|
421
|
-
chatId: { type: 'string', description: 'Artifact requests only: existing conversation with the owner' },
|
|
422
|
-
|
|
423
|
+
chatId: { type: 'string', description: 'Artifact or workspace requests only: existing conversation with the owner' },
|
|
424
|
+
recipientId: { type: 'string', required: true, description: 'Who to ask: workBrief.actingFor.customer.id (the customer of this hire) or workBrief.actingFor.operator.id (your operator)' },
|
|
423
425
|
idempotencyKey: { type: 'string', description: 'Reuse for the same logical request' },
|
|
424
426
|
},
|
|
425
427
|
handler: async (args, env) => {
|
|
426
|
-
for (const key of ['scopeKind', '
|
|
428
|
+
for (const key of ['scopeKind', 'reason', 'recipientId']) {
|
|
427
429
|
if (typeof args[key] !== 'string' || !String(args[key]).trim())
|
|
428
430
|
throw new Error(`${key} is required`);
|
|
429
431
|
}
|
|
430
432
|
const scopeKind = args.scopeKind;
|
|
431
433
|
if (!['artifact', 'chat', 'agreement', 'task'].includes(scopeKind))
|
|
432
434
|
throw new Error('Unsupported scopeKind');
|
|
435
|
+
const agreementId = typeof args.agreementId === 'string' ? args.agreementId.trim() : undefined;
|
|
436
|
+
const scopeId = typeof args.scopeId === 'string' ? args.scopeId.trim() : undefined;
|
|
437
|
+
if (Boolean(agreementId) === Boolean(scopeId) || (agreementId && scopeKind !== 'artifact'))
|
|
438
|
+
throw new Error('Provide scopeId, or agreementId for its workspace summary artifact');
|
|
433
439
|
const permission = args.permission ?? 'read';
|
|
434
440
|
if (!['read', 'reply'].includes(String(permission)) || (permission === 'reply' && scopeKind !== 'chat'))
|
|
435
441
|
throw new Error('Only chat access may request reply permission');
|
|
@@ -440,16 +446,17 @@ export const accessRequestCapability = {
|
|
|
440
446
|
throw new Error('durationHours must be an integer between 1 and 168');
|
|
441
447
|
const creds = fullCreds(env);
|
|
442
448
|
const result = await new ContextGrantsClient(creds.operatorKey, creds.agentId, env.baseUrl).requestAccess({
|
|
443
|
-
scopeKind, scopeId
|
|
449
|
+
scopeKind, scopeId, agreementId, reason: String(args.reason).trim(),
|
|
444
450
|
permission: permission, temporal: args.temporal,
|
|
445
|
-
durationHours, chatId: args.chatId,
|
|
451
|
+
durationHours, chatId: args.chatId,
|
|
452
|
+
recipientId: typeof args.recipientId === 'string' ? args.recipientId.trim() : undefined,
|
|
446
453
|
}, { idempotencyKey: args.idempotencyKey, laneId: creds.laneId });
|
|
447
454
|
return {
|
|
448
455
|
...result, outcome: 'pending', state: 'requested', actingAgentId: creds.agentId,
|
|
449
|
-
scope: { kind: scopeKind, id:
|
|
456
|
+
scope: result.scope ?? { kind: scopeKind, id: scopeId },
|
|
450
457
|
appUrl: `${(env.webUrl ?? 'https://ziggsai.com').replace(/\/$/, '')}/app/agreements/${encodeURIComponent(result.agreementId)}`,
|
|
451
|
-
resumeAfterApproval: { tool: env.surface === 'mcp' ? 'ziggs_open' : 'open', args: { [`${scopeKind}Id`]:
|
|
452
|
-
note: 'No access granted
|
|
458
|
+
resumeAfterApproval: { tool: env.surface === 'mcp' ? 'ziggs_open' : 'open', args: result.scope ? { [`${result.scope.kind}Id`]: result.scope.id } : { [`${scopeKind}Id`]: scopeId } },
|
|
459
|
+
note: `${result.message ?? 'A request card is open.'} No access granted until they approve; then open the original resource using the same agent identity.`,
|
|
453
460
|
};
|
|
454
461
|
},
|
|
455
462
|
};
|
|
@@ -48,9 +48,9 @@ export const listTasksCapability = {
|
|
|
48
48
|
handler: async (args, env) => {
|
|
49
49
|
const creds = fullCreds(env);
|
|
50
50
|
const assignedTo = args['assignedToMe']
|
|
51
|
-
?
|
|
51
|
+
? 'self'
|
|
52
52
|
: args['assignedTo'];
|
|
53
|
-
const createdBy = args['createdByMe'] ?
|
|
53
|
+
const createdBy = args['createdByMe'] ? 'self' : undefined;
|
|
54
54
|
const listed = await listTasks({
|
|
55
55
|
state: args['state'],
|
|
56
56
|
cursor: args['cursor'],
|
|
@@ -1,19 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* one shared definition per SDK/MCP tool capability.
|
|
3
|
-
*
|
|
4
|
-
* The tool-surface-parity epic (→903) unified the HTTP layer here in
|
|
5
|
-
* api-client but left ~25 shared capabilities hand-written twice: once in the
|
|
6
|
-
* agent-sdk's defineTool DSL, once in ziggs-mcp zod. Each capability below is
|
|
7
|
-
* the single source for the tool's schema, validation, response shaping, and
|
|
8
|
-
* guidance; the two surfaces register it through thin adapters
|
|
9
|
-
* (agent-sdk `toolFromCapability`, ziggs-mcp `registerCapability`).
|
|
10
|
-
*
|
|
11
|
-
* Params are a neutral JSON-schema-flavoured DSL rather than zod because the
|
|
12
|
-
* two surfaces cannot share one zod: agent-sdk is on zod v4, ziggs-mcp on
|
|
13
|
-
* zod v3 (pinned by the MCP SDK's ZodRawShape), and api-client deliberately
|
|
14
|
-
* has no zod dependency. The SDK adapter feeds params straight into
|
|
15
|
-
* defineTool's converter; the MCP adapter lowers them to zod v3.
|
|
16
|
-
*/
|
|
17
1
|
import type { Creds } from '../types.js';
|
|
18
2
|
export type CapabilitySurface = 'sdk' | 'mcp';
|
|
19
3
|
/** read-only / write / destructive — lowered to MCP tool annotations. */
|
|
@@ -1,13 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* one shared definition per SDK/MCP tool capability.
|
|
3
|
+
*
|
|
4
|
+
* The tool-surface-parity epic (→903) unified the HTTP layer here in
|
|
5
|
+
* api-client but left ~25 shared capabilities hand-written twice: once in the
|
|
6
|
+
* agent-sdk's defineTool DSL, once in ziggs-mcp zod. Each capability below is
|
|
7
|
+
* the single source for the tool's schema, validation, response shaping, and
|
|
8
|
+
* guidance; the two surfaces register it through thin adapters
|
|
9
|
+
* (agent-sdk `toolFromCapability`, ziggs-mcp `registerCapability`).
|
|
10
|
+
*
|
|
11
|
+
* Params are a neutral JSON-schema-flavoured DSL rather than zod because the
|
|
12
|
+
* two surfaces cannot share one zod: agent-sdk is on zod v4, ziggs-mcp on
|
|
13
|
+
* zod v3 (pinned by the MCP SDK's ZodRawShape), and api-client deliberately
|
|
14
|
+
* has no zod dependency. The SDK adapter feeds params straight into
|
|
15
|
+
* defineTool's converter; the MCP adapter lowers them to zod v3.
|
|
16
|
+
*/
|
|
17
|
+
import { decodeOperatorKeyClaims } from '../shared/operatorKey.js';
|
|
1
18
|
/** Creds for impersonated calls; adapters guarantee agentId when needsAgentId. */
|
|
2
19
|
export function fullCreds(env) {
|
|
3
20
|
const { operatorKey, agentId, laneId } = env.creds;
|
|
4
21
|
if (!operatorKey)
|
|
5
22
|
throw new Error('operatorKey missing from tool context');
|
|
6
|
-
if (!agentId)
|
|
23
|
+
if (!agentId && !decodeOperatorKeyClaims(operatorKey)?.connectionClientId)
|
|
7
24
|
throw new Error('agentId missing from tool context');
|
|
8
25
|
// the lane rides along so every Creds-based client sends
|
|
9
26
|
// X-Ziggs-Lane without each capability having to remember to.
|
|
10
|
-
return { operatorKey, agentId, ...(laneId ? { laneId } : {}) };
|
|
27
|
+
return { operatorKey, agentId: agentId ?? '', ...(laneId ? { laneId } : {}) };
|
|
11
28
|
}
|
|
12
29
|
/**
|
|
13
30
|
* Re-throw a client error with a capability-level prefix, preserving the HTTP
|
package/dist/decisionWords.d.ts
CHANGED
|
@@ -13,6 +13,16 @@
|
|
|
13
13
|
* counterparty wrote: no titles, no messages, no descriptions. Ids, an actor id
|
|
14
14
|
* and an instant are all it carries.
|
|
15
15
|
*/
|
|
16
|
+
/**
|
|
17
|
+
* The actor as it may appear in words, or null.
|
|
18
|
+
*
|
|
19
|
+
* A person's account id is `username:::email`. The server shows a reader
|
|
20
|
+
* outside that person's org their `psn_` face instead, but an older
|
|
21
|
+
* server, or a row written before that, still hands the raw id over, and the
|
|
22
|
+
* model repeats whatever it is told into the room. So an id shaped like a
|
|
23
|
+
* person's account is left out rather than said.
|
|
24
|
+
*/
|
|
25
|
+
export declare function speakableActor(actorId: string | null | undefined): string | null;
|
|
16
26
|
export type DecisionOutcome = 'approved' | 'rejected' | 'fulfilled' | 'held';
|
|
17
27
|
export interface DecisionWordsInput {
|
|
18
28
|
/** The delivery row's `reason`. Anything that is not a decision maps to null. */
|
package/dist/decisionWords.js
CHANGED
|
@@ -13,6 +13,20 @@
|
|
|
13
13
|
* counterparty wrote: no titles, no messages, no descriptions. Ids, an actor id
|
|
14
14
|
* and an instant are all it carries.
|
|
15
15
|
*/
|
|
16
|
+
/**
|
|
17
|
+
* The actor as it may appear in words, or null.
|
|
18
|
+
*
|
|
19
|
+
* A person's account id is `username:::email`. The server shows a reader
|
|
20
|
+
* outside that person's org their `psn_` face instead, but an older
|
|
21
|
+
* server, or a row written before that, still hands the raw id over, and the
|
|
22
|
+
* model repeats whatever it is told into the room. So an id shaped like a
|
|
23
|
+
* person's account is left out rather than said.
|
|
24
|
+
*/
|
|
25
|
+
export function speakableActor(actorId) {
|
|
26
|
+
if (typeof actorId !== 'string' || !actorId)
|
|
27
|
+
return null;
|
|
28
|
+
return actorId.includes(':::') || actorId.includes('@') ? null : actorId;
|
|
29
|
+
}
|
|
16
30
|
const REFUSED_REASONS = new Set([
|
|
17
31
|
'approval:rejected',
|
|
18
32
|
'context_request_rejected',
|
|
@@ -38,7 +52,8 @@ export function decisionWords(input) {
|
|
|
38
52
|
const on = input.agreementId
|
|
39
53
|
? `agreement ${input.agreementId}`
|
|
40
54
|
: 'this agreement';
|
|
41
|
-
const
|
|
55
|
+
const actor = speakableActor(input.actorId);
|
|
56
|
+
const by = actor ? ` by ${actor}` : '';
|
|
42
57
|
const at = input.ts ? ` at ${input.ts}` : '';
|
|
43
58
|
const artifactId = input.scope?.kind === 'artifact' && input.scope.id ? input.scope.id : null;
|
|
44
59
|
if (REFUSED_REASONS.has(reason)) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { decodeOperatorKeyClaims } from '../shared/operatorKey.js';
|
|
1
2
|
import { runtimeLog } from '../shared/runtimeLog.js';
|
|
2
3
|
import { getBackendUrl } from '../utils/urlUtils.js';
|
|
3
4
|
import { OPEN_AGREEMENT_TARGET, ORG_AGREEMENT_TARGET, isBroadcastTarget, partySideIds } from '../types.js';
|
|
@@ -14,7 +15,7 @@ const buildHeaders = buildCredsHeaders;
|
|
|
14
15
|
function assertCreds(creds, op) {
|
|
15
16
|
if (!creds?.operatorKey)
|
|
16
17
|
throw new Error(`operatorKey is required for ${op}`);
|
|
17
|
-
if (!creds?.agentId)
|
|
18
|
+
if (!creds?.agentId && !decodeOperatorKeyClaims(creds.operatorKey)?.connectionClientId)
|
|
18
19
|
throw new Error(`agentId is required for ${op}`);
|
|
19
20
|
}
|
|
20
21
|
/**
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type Creds } from '../types.js';
|
|
2
|
+
export interface AssistantWorkspace {
|
|
3
|
+
orgId: string;
|
|
4
|
+
name: string;
|
|
5
|
+
agentId: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* The reads a connection makes about itself: which workspaces it works in,
|
|
9
|
+
* and its one inbox across all of them. It never remembers a
|
|
10
|
+
* current workspace and never changes shared credentials; the per-call pick
|
|
11
|
+
* is the server's (see `routing.ts`).
|
|
12
|
+
*/
|
|
13
|
+
export declare class AssistantConnectionClient {
|
|
14
|
+
private readonly creds;
|
|
15
|
+
private readonly baseUrl;
|
|
16
|
+
constructor(creds: Creds, baseUrl?: string);
|
|
17
|
+
request<T>(path: string, body?: unknown): Promise<T>;
|
|
18
|
+
/** Live: a workspace joined since connecting shows up here with its assistant. */
|
|
19
|
+
workspaces(): Promise<AssistantWorkspace[]>;
|
|
20
|
+
/** Which of the connection's assistants is on an existing thing. */
|
|
21
|
+
resolve(kind: string, id: string): Promise<{
|
|
22
|
+
workspace: AssistantWorkspace | null;
|
|
23
|
+
choices: AssistantWorkspace[];
|
|
24
|
+
}>;
|
|
25
|
+
inbox(waitSeconds?: number): Promise<Record<string, unknown>>;
|
|
26
|
+
ack(upTo: string, handledResourceIds?: string[]): Promise<unknown>;
|
|
27
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { getBackendUrl } from '../utils/urlUtils.js';
|
|
2
|
+
import { hostIdentity } from '../instanceIdentity.js';
|
|
3
|
+
import { ApiError } from '../types.js';
|
|
4
|
+
import { parseErrorCode, parseErrorMessage } from '../shared/apiError.js';
|
|
5
|
+
/**
|
|
6
|
+
* The reads a connection makes about itself: which workspaces it works in,
|
|
7
|
+
* and its one inbox across all of them. It never remembers a
|
|
8
|
+
* current workspace and never changes shared credentials; the per-call pick
|
|
9
|
+
* is the server's (see `routing.ts`).
|
|
10
|
+
*/
|
|
11
|
+
export class AssistantConnectionClient {
|
|
12
|
+
creds;
|
|
13
|
+
baseUrl;
|
|
14
|
+
constructor(creds, baseUrl = getBackendUrl()) {
|
|
15
|
+
this.creds = creds;
|
|
16
|
+
this.baseUrl = baseUrl;
|
|
17
|
+
}
|
|
18
|
+
async request(path, body) {
|
|
19
|
+
const response = await fetch(`${this.baseUrl}/oauth/${path}`, {
|
|
20
|
+
method: body === undefined ? 'GET' : 'POST',
|
|
21
|
+
headers: {
|
|
22
|
+
Authorization: `Bearer ${this.creds.operatorKey}`,
|
|
23
|
+
'Content-Type': 'application/json',
|
|
24
|
+
'X-Ziggs-Instance': hostIdentity(this.creds.instanceId),
|
|
25
|
+
},
|
|
26
|
+
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
|
27
|
+
});
|
|
28
|
+
const text = await response.text();
|
|
29
|
+
if (!response.ok) {
|
|
30
|
+
throw new ApiError(parseErrorMessage(text, `${path} failed: ${response.status}`), response.status, text, parseErrorCode(text));
|
|
31
|
+
}
|
|
32
|
+
return JSON.parse(text);
|
|
33
|
+
}
|
|
34
|
+
/** Live: a workspace joined since connecting shows up here with its assistant. */
|
|
35
|
+
async workspaces() {
|
|
36
|
+
return (await this.request('workspaces'))
|
|
37
|
+
.workspaces;
|
|
38
|
+
}
|
|
39
|
+
/** Which of the connection's assistants is on an existing thing. */
|
|
40
|
+
resolve(kind, id) {
|
|
41
|
+
return this.request(`resolve?${new URLSearchParams({ kind, id })}`);
|
|
42
|
+
}
|
|
43
|
+
inbox(waitSeconds = 0) {
|
|
44
|
+
return this.request(`inbox?wait=${waitSeconds}`);
|
|
45
|
+
}
|
|
46
|
+
ack(upTo, handledResourceIds = []) {
|
|
47
|
+
return this.request('inbox/ack', { upTo, handledResourceIds });
|
|
48
|
+
}
|
|
49
|
+
}
|
package/dist/http/ChatClient.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { decodeOperatorKeyClaims } from '../shared/operatorKey.js';
|
|
1
2
|
import { runtimeLog } from '../shared/runtimeLog.js';
|
|
2
3
|
import { getBackendUrl } from '../utils/urlUtils.js';
|
|
3
4
|
import { throwApiError } from '../shared/apiError.js';
|
|
@@ -7,7 +8,7 @@ const buildHeaders = buildCredsHeaders;
|
|
|
7
8
|
function assertCreds(creds, op) {
|
|
8
9
|
if (!creds?.operatorKey)
|
|
9
10
|
throw new Error(`operatorKey is required for ${op}`);
|
|
10
|
-
if (!creds?.agentId)
|
|
11
|
+
if (!creds?.agentId && !decodeOperatorKeyClaims(creds.operatorKey)?.connectionClientId)
|
|
11
12
|
throw new Error(`agentId is required for ${op}`);
|
|
12
13
|
}
|
|
13
14
|
export async function openConversation(participant, creds, { newChat = false, agreementId, createIfMissing } = {}) {
|
|
@@ -32,6 +32,12 @@ export interface McpConnectionRequestParams {
|
|
|
32
32
|
serverUrl: string;
|
|
33
33
|
tools: string[];
|
|
34
34
|
reason?: string;
|
|
35
|
+
/**
|
|
36
|
+
* Who to ask, from the people the server offers this wake (the work brief's
|
|
37
|
+
* `actingFor.customer.id` / `.operator.id`). Required by the server; the
|
|
38
|
+
* per-user relay may send it as `onBehalfOfUserId` instead.
|
|
39
|
+
*/
|
|
40
|
+
recipientId?: string;
|
|
35
41
|
/**
|
|
36
42
|
* Working chat the consent card is opened into. Required by the backend for
|
|
37
43
|
* the agent-tool path; the per-user gateway trigger omits it.
|
|
@@ -119,7 +125,7 @@ export declare class ConnectionsClient {
|
|
|
119
125
|
* connection-consent agreement in the working chat; on approval the
|
|
120
126
|
* server is connected (if needed) and the agent is granted the tools.
|
|
121
127
|
*/
|
|
122
|
-
requestMcpConnection({ serverUrl, tools, reason, chatId, onBehalfOfUserId, }: McpConnectionRequestParams): Promise<Record<string, unknown>>;
|
|
128
|
+
requestMcpConnection({ serverUrl, tools, reason, chatId, recipientId, onBehalfOfUserId, }: McpConnectionRequestParams): Promise<Record<string, unknown>>;
|
|
123
129
|
private _requestRaw;
|
|
124
130
|
private _request;
|
|
125
131
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { decodeOperatorKeyClaims } from '../shared/operatorKey.js';
|
|
1
2
|
import { getBackendUrl } from "../utils/urlUtils.js";
|
|
2
3
|
import { throwApiError } from "../shared/apiError.js";
|
|
3
4
|
import { buildOperatorHeaders } from "./operatorHeaders.js";
|
|
@@ -65,7 +66,7 @@ export class ConnectionsClient {
|
|
|
65
66
|
throw new Error("proxy: grantId is required");
|
|
66
67
|
if (!action)
|
|
67
68
|
throw new Error("proxy: action is required");
|
|
68
|
-
if (!this.agentId) {
|
|
69
|
+
if (!this.agentId && !decodeOperatorKeyClaims(this.operatorKey)?.connectionClientId) {
|
|
69
70
|
throw new Error("proxy: agentId is required — connection broker calls must impersonate the grant holder agent");
|
|
70
71
|
}
|
|
71
72
|
const text = await this._requestRaw("POST", `/connections/${encodeURIComponent(connectionId)}/proxy`, { grantId, action, payload: payload ?? {} });
|
|
@@ -88,12 +89,12 @@ export class ConnectionsClient {
|
|
|
88
89
|
async listGrants({ connectionId, }) {
|
|
89
90
|
if (!connectionId)
|
|
90
91
|
throw new Error("listGrants: connectionId is required");
|
|
91
|
-
if (!this.agentId) {
|
|
92
|
+
if (!this.agentId && !decodeOperatorKeyClaims(this.operatorKey)?.connectionClientId) {
|
|
92
93
|
throw new Error("listGrants: agentId is required — grants are scoped to the impersonated agent");
|
|
93
94
|
}
|
|
94
95
|
const res = (await this._request("GET", `/connections/${encodeURIComponent(connectionId)}/grants`, undefined));
|
|
95
96
|
const grants = res["grants"] || [];
|
|
96
|
-
return grants.filter((g) => g["holderId"] === this.agentId);
|
|
97
|
+
return this.agentId ? grants.filter((g) => g["holderId"] === this.agentId) : grants;
|
|
97
98
|
}
|
|
98
99
|
/**
|
|
99
100
|
* cross-connection discovery over the unified GET /grants:
|
|
@@ -174,10 +175,10 @@ export class ConnectionsClient {
|
|
|
174
175
|
* connection-consent agreement in the working chat; on approval the
|
|
175
176
|
* server is connected (if needed) and the agent is granted the tools.
|
|
176
177
|
*/
|
|
177
|
-
async requestMcpConnection({ serverUrl, tools, reason, chatId, onBehalfOfUserId, }) {
|
|
178
|
+
async requestMcpConnection({ serverUrl, tools, reason, chatId, recipientId, onBehalfOfUserId, }) {
|
|
178
179
|
if (!serverUrl)
|
|
179
180
|
throw new Error("requestMcpConnection: serverUrl is required");
|
|
180
|
-
return (await this._request("POST", "/connections/mcp/requests", { serverUrl, tools, reason, chatId }, onBehalfOfUserId
|
|
181
|
+
return (await this._request("POST", "/connections/mcp/requests", { serverUrl, tools, reason, chatId, ...(recipientId ? { recipientId } : {}) }, onBehalfOfUserId
|
|
181
182
|
? { "X-On-Behalf-Of-User": onBehalfOfUserId }
|
|
182
183
|
: undefined));
|
|
183
184
|
}
|
|
@@ -99,13 +99,16 @@ export declare class ContextGrantsClient {
|
|
|
99
99
|
/** Request-only endpoint: a successful call must still have granted no access. */
|
|
100
100
|
requestAccess(input: {
|
|
101
101
|
scopeKind: 'artifact' | 'chat' | 'agreement' | 'task';
|
|
102
|
-
scopeId
|
|
102
|
+
scopeId?: string;
|
|
103
|
+
agreementId?: string;
|
|
103
104
|
reason: string;
|
|
104
105
|
permission?: 'read' | 'reply';
|
|
105
106
|
temporal?: 'from-now' | 'from-start';
|
|
106
107
|
durationHours?: number;
|
|
107
108
|
chatId?: string;
|
|
108
109
|
ownerId?: string;
|
|
110
|
+
/** Who to ask, from the people the server offers this wake. */
|
|
111
|
+
recipientId?: string;
|
|
109
112
|
}, opts?: {
|
|
110
113
|
idempotencyKey?: string;
|
|
111
114
|
laneId?: string;
|
|
@@ -113,7 +116,11 @@ export declare class ContextGrantsClient {
|
|
|
113
116
|
status: 'pending_approval';
|
|
114
117
|
agreementId: string;
|
|
115
118
|
accessGranted: false;
|
|
116
|
-
|
|
119
|
+
scope?: {
|
|
120
|
+
kind: string;
|
|
121
|
+
id: string;
|
|
122
|
+
};
|
|
123
|
+
} & RequestSentTo>;
|
|
117
124
|
requestArtifactAccess(artifactId: string, input: {
|
|
118
125
|
chatId: string;
|
|
119
126
|
ownerId: string;
|
|
@@ -125,7 +132,7 @@ export declare class ContextGrantsClient {
|
|
|
125
132
|
}): Promise<{
|
|
126
133
|
status: 'pending_approval';
|
|
127
134
|
agreementId: string;
|
|
128
|
-
}>;
|
|
135
|
+
} & RequestSentTo>;
|
|
129
136
|
delegateGrant(parentGrantId: string, input: DelegateContextGrantInput, opts?: {
|
|
130
137
|
idempotencyKey?: string;
|
|
131
138
|
}): Promise<DelegateContextGrantResult>;
|
|
@@ -158,3 +165,19 @@ export declare class ContextGrantsClient {
|
|
|
158
165
|
revokedCount?: number;
|
|
159
166
|
}>;
|
|
160
167
|
}
|
|
168
|
+
/** Who got a request card and where they answer it, as the server says. */
|
|
169
|
+
export interface RequestSentTo {
|
|
170
|
+
sentTo?: {
|
|
171
|
+
id: string;
|
|
172
|
+
name: string | null;
|
|
173
|
+
role: 'customer' | 'operator';
|
|
174
|
+
};
|
|
175
|
+
approveIn?: {
|
|
176
|
+
kind: 'chat';
|
|
177
|
+
chatId: string;
|
|
178
|
+
} | {
|
|
179
|
+
kind: 'inbox';
|
|
180
|
+
};
|
|
181
|
+
/** e.g. "Sent to Ido. Ido approves it in this room." */
|
|
182
|
+
message?: string;
|
|
183
|
+
}
|
|
@@ -80,7 +80,7 @@ export class ContextGrantsClient {
|
|
|
80
80
|
if (result.status !== 'pending_approval' || !result.agreementId || result.accessGranted !== false) {
|
|
81
81
|
throw new Error('Expected a pending access request with accessGranted=false');
|
|
82
82
|
}
|
|
83
|
-
return { status: 'pending_approval', agreementId: result.agreementId, accessGranted: false };
|
|
83
|
+
return { status: 'pending_approval', agreementId: result.agreementId, accessGranted: false, ...(result.scope ? { scope: result.scope } : {}), ...sentToOf(result) };
|
|
84
84
|
}
|
|
85
85
|
async requestArtifactAccess(artifactId, input, opts) {
|
|
86
86
|
const res = await fetch(`${this.baseUrl}/context/artifacts/${encodeURIComponent(artifactId)}/access-requests`, {
|
|
@@ -98,7 +98,7 @@ export class ContextGrantsClient {
|
|
|
98
98
|
const result = JSON.parse(body);
|
|
99
99
|
if (result.status !== 'pending_approval' || !result.agreementId)
|
|
100
100
|
throw new Error('Expected a pending artifact access request');
|
|
101
|
-
return { status: 'pending_approval', agreementId: result.agreementId };
|
|
101
|
+
return { status: 'pending_approval', agreementId: result.agreementId, ...sentToOf(result) };
|
|
102
102
|
}
|
|
103
103
|
async delegateGrant(parentGrantId, input, opts) {
|
|
104
104
|
const res = await fetch(`${this.baseUrl}/context/grants/${encodeURIComponent(parentGrantId)}/delegate`, {
|
|
@@ -185,3 +185,10 @@ export class ContextGrantsClient {
|
|
|
185
185
|
return JSON.parse(body);
|
|
186
186
|
}
|
|
187
187
|
}
|
|
188
|
+
function sentToOf(result) {
|
|
189
|
+
return {
|
|
190
|
+
...(result.sentTo && typeof result.sentTo === 'object' ? { sentTo: result.sentTo } : {}),
|
|
191
|
+
...(result.approveIn && typeof result.approveIn === 'object' ? { approveIn: result.approveIn } : {}),
|
|
192
|
+
...(typeof result.message === 'string' ? { message: result.message } : {}),
|
|
193
|
+
};
|
|
194
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { decodeOperatorKeyClaims } from '../shared/operatorKey.js';
|
|
1
2
|
// one shaping rule for every agreement this package parses.
|
|
2
3
|
import { shapeAgreement } from './AgreementClient.js';
|
|
3
4
|
import { getBackendUrl } from '../utils/urlUtils.js';
|
|
@@ -9,7 +10,7 @@ const buildHeaders = buildCredsHeaders;
|
|
|
9
10
|
function assertCreds(creds, op) {
|
|
10
11
|
if (!creds?.operatorKey)
|
|
11
12
|
throw new Error(`operatorKey is required for ${op}`);
|
|
12
|
-
if (!creds?.agentId)
|
|
13
|
+
if (!creds?.agentId && !decodeOperatorKeyClaims(creds.operatorKey)?.connectionClientId)
|
|
13
14
|
throw new Error(`agentId is required for ${op}`);
|
|
14
15
|
}
|
|
15
16
|
export async function publishOffer(payload, creds) {
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { decodeOperatorKeyClaims } from '../shared/operatorKey.js';
|
|
2
|
+
import { buildOperatorHeaders } from './operatorHeaders.js';
|
|
1
3
|
import { getBackendUrl } from '../utils/urlUtils.js';
|
|
2
4
|
import { throwApiError } from '../shared/apiError.js';
|
|
3
5
|
/**
|
|
@@ -14,7 +16,7 @@ export class MessagesClient {
|
|
|
14
16
|
constructor(operatorKey, agentId) {
|
|
15
17
|
if (!operatorKey)
|
|
16
18
|
throw new Error('MessagesClient: operatorKey is required');
|
|
17
|
-
if (!agentId)
|
|
19
|
+
if (!agentId && !decodeOperatorKeyClaims(operatorKey)?.connectionClientId)
|
|
18
20
|
throw new Error('MessagesClient: agentId is required (operator-token impersonation)');
|
|
19
21
|
this.operatorKey = operatorKey;
|
|
20
22
|
this.agentId = agentId;
|
|
@@ -43,8 +45,7 @@ export class MessagesClient {
|
|
|
43
45
|
_headers() {
|
|
44
46
|
return {
|
|
45
47
|
'content-type': 'application/json',
|
|
46
|
-
|
|
47
|
-
'X-Agent-Id': this.agentId,
|
|
48
|
+
...buildOperatorHeaders(this.operatorKey, this.agentId),
|
|
48
49
|
};
|
|
49
50
|
}
|
|
50
51
|
}
|
package/dist/http/OrgsClient.js
CHANGED
package/dist/http/TaskClient.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { decodeOperatorKeyClaims } from '../shared/operatorKey.js';
|
|
1
2
|
import { getBackendUrl } from '../utils/urlUtils.js';
|
|
2
3
|
import { partyActorIds } from '../types.js';
|
|
3
4
|
import { throwApiError } from '../shared/apiError.js';
|
|
@@ -8,7 +9,7 @@ const buildHeaders = buildCredsHeaders;
|
|
|
8
9
|
function assertCreds(creds, op) {
|
|
9
10
|
if (!creds?.operatorKey)
|
|
10
11
|
throw new Error(`operatorKey is required for ${op}`);
|
|
11
|
-
if (!creds?.agentId)
|
|
12
|
+
if (!creds?.agentId && !decodeOperatorKeyClaims(creds.operatorKey)?.connectionClientId)
|
|
12
13
|
throw new Error(`agentId is required for ${op}`);
|
|
13
14
|
}
|
|
14
15
|
function extractTask(data) {
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Creds } from '../types.js';
|
|
2
|
+
/** One catalog row as the picker reads it. */
|
|
3
|
+
export interface ToolPickOption {
|
|
4
|
+
name: string;
|
|
5
|
+
line: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* - `pick` one tool does the need: `tool` names it.
|
|
9
|
+
* - `nothing_fits` no tool does it.
|
|
10
|
+
* - `unsure` / `off` no answer worth giving: show the list.
|
|
11
|
+
*/
|
|
12
|
+
export interface ToolPickAnswer {
|
|
13
|
+
how: 'pick' | 'nothing_fits' | 'unsure' | 'off';
|
|
14
|
+
tool?: string;
|
|
15
|
+
confidence?: number;
|
|
16
|
+
}
|
|
17
|
+
/** Null means "no answer": the surface shows the list it always has. */
|
|
18
|
+
export type PickTool = (need: string, options: ToolPickOption[]) => Promise<ToolPickAnswer | null>;
|
|
19
|
+
/**
|
|
20
|
+
* The backend picker: `POST /internal/tool-pick` with this connection's key.
|
|
21
|
+
* The model call and its key live on the backend, since this server also runs
|
|
22
|
+
* on a caller's own machine. Any failure is null, including the 404 from a
|
|
23
|
+
* backend that predates the route.
|
|
24
|
+
*/
|
|
25
|
+
export declare function backendToolPick(creds: Creds): PickTool;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { buildCredsHeaders } from './operatorHeaders.js';
|
|
2
|
+
import { getBackendUrl } from '../utils/urlUtils.js';
|
|
3
|
+
/** A listing is always the fallback, so waiting long for a pick buys nothing. */
|
|
4
|
+
const TIMEOUT_MS = 6000;
|
|
5
|
+
/**
|
|
6
|
+
* The backend picker: `POST /internal/tool-pick` with this connection's key.
|
|
7
|
+
* The model call and its key live on the backend, since this server also runs
|
|
8
|
+
* on a caller's own machine. Any failure is null, including the 404 from a
|
|
9
|
+
* backend that predates the route.
|
|
10
|
+
*/
|
|
11
|
+
export function backendToolPick(creds) {
|
|
12
|
+
return async (need, options) => {
|
|
13
|
+
if (!creds.operatorKey)
|
|
14
|
+
return null;
|
|
15
|
+
try {
|
|
16
|
+
const res = await fetch(`${getBackendUrl()}/internal/tool-pick`, {
|
|
17
|
+
method: 'POST',
|
|
18
|
+
headers: buildCredsHeaders(creds),
|
|
19
|
+
body: JSON.stringify({ need, options }),
|
|
20
|
+
signal: AbortSignal.timeout(TIMEOUT_MS),
|
|
21
|
+
});
|
|
22
|
+
if (!res.ok) {
|
|
23
|
+
await res.body?.cancel().catch(() => undefined);
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
const data = (await res.json().catch(() => null));
|
|
27
|
+
if (!data || typeof data.how !== 'string')
|
|
28
|
+
return null;
|
|
29
|
+
return data;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
}
|
package/dist/http/index.d.ts
CHANGED
|
@@ -33,3 +33,4 @@ export { InboxClient } from './InboxClient.js';
|
|
|
33
33
|
export { planInboxAck, planPartialInboxAck, inboxAckAllowed, inboxAckHandledIds, } from '../inboxAck.js';
|
|
34
34
|
export type { InboxAckDecision, InboxAckStep, InboxAckEnvelope, PlanInboxAckOptions, } from '../inboxAck.js';
|
|
35
35
|
export { WAKE_LANE_HEADER, laneHeader, buildOperatorHeaders, buildCredsHeaders, } from './operatorHeaders.js';
|
|
36
|
+
export { backendToolPick, type ToolPickOption, type ToolPickAnswer, type PickTool } from './ToolPickClient.js';
|
package/dist/http/index.js
CHANGED
|
@@ -25,3 +25,4 @@ export { planInboxAck, planPartialInboxAck, inboxAckAllowed, inboxAckHandledIds,
|
|
|
25
25
|
// because the MCP transports in agent-sdk and ziggs-mcp send it too, and a
|
|
26
26
|
// copy of a header nobody notices missing goes stale unseen.
|
|
27
27
|
export { WAKE_LANE_HEADER, laneHeader, buildOperatorHeaders, buildCredsHeaders, } from './operatorHeaders.js';
|
|
28
|
+
export { backendToolPick } from './ToolPickClient.js';
|
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
type AssistantHints = {
|
|
2
|
+
orgId?: string;
|
|
3
|
+
resource?: string;
|
|
4
|
+
};
|
|
5
|
+
/** Node-only, opt-in request context. Ordinary HTTP clients remain browser-safe. */
|
|
6
|
+
export declare function withAssistantRequest<T>(hints: AssistantHints, run: () => T): Promise<Awaited<T>>;
|
|
7
|
+
export declare function assistantRequestHeaders(): Record<string, string>;
|
|
1
8
|
/**
|
|
2
9
|
* Auth headers for operator-key HTTP clients.
|
|
3
10
|
*
|
|
@@ -29,3 +36,4 @@ export declare function buildCredsHeaders(creds: {
|
|
|
29
36
|
agentId: string;
|
|
30
37
|
laneId?: string;
|
|
31
38
|
}): Record<string, string>;
|
|
39
|
+
export {};
|
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
let assistantRequest;
|
|
2
|
+
let assistantContextLoading;
|
|
3
|
+
/** Node-only, opt-in request context. Ordinary HTTP clients remain browser-safe. */
|
|
4
|
+
export async function withAssistantRequest(hints, run) {
|
|
5
|
+
assistantContextLoading ??= import('node:async_hooks').then(({ AsyncLocalStorage }) => {
|
|
6
|
+
assistantRequest = new AsyncLocalStorage();
|
|
7
|
+
return assistantRequest;
|
|
8
|
+
});
|
|
9
|
+
return await (await assistantContextLoading).run(hints, run);
|
|
10
|
+
}
|
|
11
|
+
export function assistantRequestHeaders() {
|
|
12
|
+
const hints = assistantRequest?.getStore();
|
|
13
|
+
return { ...(hints?.orgId ? { 'X-Workspace-Id': hints.orgId } : {}), ...(hints?.resource ? { 'X-Ziggs-Resource': hints.resource } : {}) };
|
|
14
|
+
}
|
|
1
15
|
/**
|
|
2
16
|
* Auth headers for operator-key HTTP clients.
|
|
3
17
|
*
|
|
@@ -27,6 +41,7 @@ export function buildOperatorHeaders(operatorKey, agentId, extra,
|
|
|
27
41
|
laneId) {
|
|
28
42
|
return {
|
|
29
43
|
Authorization: `Bearer ${operatorKey}`,
|
|
44
|
+
...assistantRequestHeaders(),
|
|
30
45
|
// `X-Agent-Id` is only sent when an agentId is present — agent-scoped keys
|
|
31
46
|
// identify the agent themselves; fleet keys must pass one.
|
|
32
47
|
...(agentId ? { 'X-Agent-Id': agentId } : {}),
|
package/dist/inboxAck.d.ts
CHANGED
|
@@ -34,12 +34,13 @@ export interface PlanInboxAckOptions {
|
|
|
34
34
|
* delivery counts (the MCP tool with no configured self id).
|
|
35
35
|
*/
|
|
36
36
|
ownAgentId?: string;
|
|
37
|
+
ownAgentIds?: readonly string[];
|
|
37
38
|
/** Failed handovers hold the watermark unless `forceAck`. */
|
|
38
39
|
failures?: number;
|
|
39
40
|
forceAck?: boolean;
|
|
40
41
|
}
|
|
41
42
|
/** Resource ids this envelope's ack must list. */
|
|
42
|
-
export declare function inboxAckHandledIds(envelope: InboxAckEnvelope, ownAgentId?: string): string[];
|
|
43
|
+
export declare function inboxAckHandledIds(envelope: InboxAckEnvelope, ownAgentId?: string, ownAgentIds?: readonly string[]): string[];
|
|
43
44
|
/** Whether an ack is allowed for this envelope, given handover state. */
|
|
44
45
|
export declare function inboxAckAllowed(envelope: InboxAckEnvelope, opts?: PlanInboxAckOptions): boolean;
|
|
45
46
|
export declare function planInboxAck(envelope: InboxAckEnvelope, opts?: PlanInboxAckOptions): InboxAckDecision;
|
package/dist/inboxAck.js
CHANGED
|
@@ -15,8 +15,8 @@ function isOutOfReach(note) {
|
|
|
15
15
|
typeof n.remedy === 'string');
|
|
16
16
|
}
|
|
17
17
|
/** Resource ids this envelope's ack must list. */
|
|
18
|
-
export function inboxAckHandledIds(envelope, ownAgentId) {
|
|
19
|
-
const mine = (assigneeId) => !ownAgentId || assigneeId === ownAgentId;
|
|
18
|
+
export function inboxAckHandledIds(envelope, ownAgentId, ownAgentIds) {
|
|
19
|
+
const mine = (assigneeId) => ownAgentIds ? ownAgentIds.includes(assigneeId ?? '') : !ownAgentId || assigneeId === ownAgentId;
|
|
20
20
|
return [
|
|
21
21
|
...new Set([
|
|
22
22
|
...(envelope.deliveries ?? [])
|
|
@@ -41,12 +41,12 @@ export function inboxAckAllowed(envelope, opts = {}) {
|
|
|
41
41
|
export function planInboxAck(envelope, opts = {}) {
|
|
42
42
|
return {
|
|
43
43
|
allowed: inboxAckAllowed(envelope, opts),
|
|
44
|
-
handledResourceIds: inboxAckHandledIds(envelope, opts.ownAgentId),
|
|
44
|
+
handledResourceIds: inboxAckHandledIds(envelope, opts.ownAgentId, opts.ownAgentIds),
|
|
45
45
|
};
|
|
46
46
|
}
|
|
47
47
|
/** Every row whose instant the server's bury-guard will ask about. */
|
|
48
|
-
function gatingRows(envelope, ownAgentId) {
|
|
49
|
-
const mine = (assigneeId) => !ownAgentId || assigneeId === ownAgentId;
|
|
48
|
+
function gatingRows(envelope, ownAgentId, ownAgentIds) {
|
|
49
|
+
const mine = (assigneeId) => ownAgentIds ? ownAgentIds.includes(assigneeId ?? '') : !ownAgentId || assigneeId === ownAgentId;
|
|
50
50
|
return [
|
|
51
51
|
...(envelope.deliveries ?? [])
|
|
52
52
|
.filter((d) => mine(d.assigneeId))
|
|
@@ -92,7 +92,7 @@ export function planPartialInboxAck(envelope, handled, opts = {}) {
|
|
|
92
92
|
}
|
|
93
93
|
if (tokenByInstant.size === 0)
|
|
94
94
|
return null;
|
|
95
|
-
const gating = gatingRows(envelope, opts.ownAgentId);
|
|
95
|
+
const gating = gatingRows(envelope, opts.ownAgentId, opts.ownAgentIds);
|
|
96
96
|
// ISO-8601 sorts lexicographically in time order, which is what every other
|
|
97
97
|
// ts comparison on this rail already relies on.
|
|
98
98
|
const instants = [...tokenByInstant.keys()].sort();
|
package/dist/index.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ export { parseErrorMessage, parseErrorCode, throwApiError, } from './shared/apiE
|
|
|
12
12
|
export { ApiError } from './types.js';
|
|
13
13
|
export type { Creds, Task, TaskState, PlanStep, PlanStepStatus, Agreement, AgreementParties, AgreementPartySide, EngagementKind, BroadcastAudience, EntryType, ContentType, MessageMetadata, MessageHandler, InboxDeliveryKind, InboxDeliveryRef, InboxChatNews, InboxProposalRef, InboxTaskRef, InboxRequestRef, InboxConnectionRequestRef, InboxHumanAttention, InboxEnvelope, InboxAckResult, InboxPeek, InboxReadOptions, } from './types.js';
|
|
14
14
|
export type { ProposeTerms, ProposeDirectInput, ProposeBroadcastInput, ProposeAgreementData, ClaimOptions, ClaimRoom, } from './http/AgreementClient.js';
|
|
15
|
-
export { decisionWords } from './decisionWords.js';
|
|
15
|
+
export { decisionWords, speakableActor } from './decisionWords.js';
|
|
16
16
|
export type { DecisionOutcome, DecisionWords, DecisionWordsInput, } from './decisionWords.js';
|
|
17
17
|
export { planInboxAck, planPartialInboxAck, inboxAckAllowed, inboxAckHandledIds, } from './inboxAck.js';
|
|
18
18
|
export type { InboxAckDecision, InboxAckStep, InboxAckEnvelope, PlanInboxAckOptions, } from './inboxAck.js';
|
|
@@ -24,3 +24,6 @@ export { sessionOrientation, } from './sessionOrientation.js';
|
|
|
24
24
|
export type { ContinuationKind, InboxAckDriver, InboxHandling, RepresentedPerson, SessionContinuation, SessionOrientation, SessionOrientationInput, } from './sessionOrientation.js';
|
|
25
25
|
export { inboxEngagement, isEchoOrDuplicate, isHireWorkOrder, contactPath, formationGuide, marketplaceFormation, liveHireChangedTerms, liveHireConflictFromStatus, withdrawHeldGraph, reminderHonesty, graphReleaseHonesty, hintsFromTasks, } from './engagementGuide.js';
|
|
26
26
|
export type { EngagementGuide, EngagementGuideKind, EngagementOutcome, EngagementTaskHint, ContactPathInput, FormationInput, GraphHonesty, HireTermsHonesty, WaitingOn, } from './engagementGuide.js';
|
|
27
|
+
export { isContextAccessRequest } from './shared/contextAccessRequest.js';
|
|
28
|
+
export { AssistantConnectionClient, type AssistantWorkspace, } from './http/AssistantConnectionClient.js';
|
|
29
|
+
export { withAssistantRequest, assistantRequestHeaders } from './http/operatorHeaders.js';
|
package/dist/index.js
CHANGED
|
@@ -15,7 +15,7 @@ export { parseErrorMessage, parseErrorCode, throwApiError, } from './shared/apiE
|
|
|
15
15
|
export { ApiError } from './types.js';
|
|
16
16
|
// One decision, one sentence, on every rail: the reason a delivery row carries
|
|
17
17
|
// when somebody answered a request this agent made, rendered as words.
|
|
18
|
-
export { decisionWords } from './decisionWords.js';
|
|
18
|
+
export { decisionWords, speakableActor } from './decisionWords.js';
|
|
19
19
|
export { planInboxAck, planPartialInboxAck, inboxAckAllowed, inboxAckHandledIds, } from './inboxAck.js';
|
|
20
20
|
export { agreementLaneId, agreementIdFromLane, isAgreementLaneId, isAgreementId } from '@ziggs-ai/contracts';
|
|
21
21
|
export * from './shared/operatorKey.js';
|
|
@@ -23,3 +23,6 @@ export { INBOX_HOST_CLAIMANT_HEADER, INSTANCE_IDENTITY_MAX_LENGTH, cliHostIdenti
|
|
|
23
23
|
export * from './utils/appUrls.js';
|
|
24
24
|
export { sessionOrientation, } from './sessionOrientation.js';
|
|
25
25
|
export { inboxEngagement, isEchoOrDuplicate, isHireWorkOrder, contactPath, formationGuide, marketplaceFormation, liveHireChangedTerms, liveHireConflictFromStatus, withdrawHeldGraph, reminderHonesty, graphReleaseHonesty, hintsFromTasks, } from './engagementGuide.js';
|
|
26
|
+
export { isContextAccessRequest } from './shared/contextAccessRequest.js';
|
|
27
|
+
export { AssistantConnectionClient, } from './http/AssistantConnectionClient.js';
|
|
28
|
+
export { withAssistantRequest, assistantRequestHeaders } from './http/operatorHeaders.js';
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** The common marker carried by access-request approvals and notifications. */
|
|
2
|
+
export interface ContextAccessRequest {
|
|
3
|
+
primitive: 'context';
|
|
4
|
+
kind: 'access-request';
|
|
5
|
+
holderId?: string;
|
|
6
|
+
scope?: {
|
|
7
|
+
kind?: string;
|
|
8
|
+
id?: string;
|
|
9
|
+
};
|
|
10
|
+
resumeAgreementId?: string;
|
|
11
|
+
}
|
|
12
|
+
export declare function isContextAccessRequest(value: unknown): value is ContextAccessRequest;
|
|
@@ -5,6 +5,7 @@ export interface OperatorKeyClaims {
|
|
|
5
5
|
keyId?: string;
|
|
6
6
|
ownerId?: string;
|
|
7
7
|
boundAgentId?: string | null;
|
|
8
|
+
connectionClientId?: string | null;
|
|
8
9
|
/**
|
|
9
10
|
* How the key was authorized. MCP consent uses `mcp_oauth` or `device_code`;
|
|
10
11
|
* provisioning and other mint paths carry their own provenance.
|
|
@@ -14,6 +14,7 @@ export function decodeOperatorKeyClaims(token) {
|
|
|
14
14
|
keyId: payload.keyId,
|
|
15
15
|
ownerId: payload.ownerId,
|
|
16
16
|
boundAgentId: typeof payload.boundAgentId === 'string' ? payload.boundAgentId : null,
|
|
17
|
+
connectionClientId: typeof payload.connectionClientId === 'string' ? payload.connectionClientId : null,
|
|
17
18
|
issuedVia: typeof payload.issuedVia === 'string' ? payload.issuedVia : undefined,
|
|
18
19
|
exp: payload.exp,
|
|
19
20
|
};
|