@ziggs-ai/api-client 0.10.4 → 0.12.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.d.ts +9 -2
- package/dist/capabilities/agreementVerbs.js +61 -21
- package/dist/capabilities/agreements.d.ts +1 -1
- package/dist/capabilities/agreements.js +18 -6
- package/dist/capabilities/artifacts.d.ts +4 -2
- package/dist/capabilities/artifacts.js +168 -33
- package/dist/capabilities/chat.d.ts +2 -3
- package/dist/capabilities/chat.js +5 -6
- package/dist/capabilities/connections.js +1 -1
- package/dist/capabilities/context.js +3 -0
- package/dist/capabilities/grants.d.ts +7 -6
- package/dist/capabilities/grants.js +9 -8
- package/dist/capabilities/index.d.ts +4 -4
- package/dist/capabilities/index.js +4 -4
- package/dist/capabilities/links.d.ts +17 -6
- package/dist/capabilities/links.js +71 -86
- package/dist/capabilities/marketplace.js +23 -17
- package/dist/capabilities/nextCall.d.ts +36 -5
- package/dist/capabilities/nextCall.js +53 -7
- package/dist/capabilities/payments.d.ts +24 -8
- package/dist/capabilities/payments.js +28 -392
- package/dist/capabilities/proposeProviderId.d.ts +1 -1
- package/dist/capabilities/proposeProviderId.js +1 -1
- package/dist/http/AgreementClient.d.ts +63 -27
- package/dist/http/AgreementClient.js +51 -39
- package/dist/http/ChatClient.d.ts +1 -0
- package/dist/http/ChatClient.js +4 -1
- package/dist/http/ConnectionsClient.js +12 -1
- package/dist/http/ContextGrantsClient.d.ts +15 -1
- package/dist/http/ContextGrantsClient.js +2 -0
- package/dist/http/ContextReadClient.d.ts +14 -5
- package/dist/http/GrantsClient.d.ts +14 -0
- package/dist/http/GrantsClient.js +18 -2
- package/dist/http/InboxClient.js +4 -0
- package/dist/http/MarketplaceClient.d.ts +6 -8
- package/dist/http/MarketplaceClient.js +11 -30
- package/dist/http/TaskClient.d.ts +5 -0
- package/dist/http/TaskClient.js +4 -7
- package/dist/http/agreementFlows.d.ts +6 -7
- package/dist/http/agreementFlows.js +14 -20
- package/dist/http/grants.d.ts +28 -0
- package/dist/http/index.d.ts +2 -2
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1 -1
- package/dist/instanceIdentity.d.ts +4 -0
- package/dist/instanceIdentity.js +44 -0
- package/dist/relay/provisionRelayWorkers.d.ts +2 -2
- package/dist/relay/provisionRelayWorkers.js +5 -5
- package/dist/types.d.ts +80 -31
- package/dist/types.js +18 -0
- package/package.json +1 -1
|
@@ -63,7 +63,7 @@ export const requestConnectionCapability = {
|
|
|
63
63
|
'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).',
|
|
64
64
|
mcp: 'Ask your principal (the human) to connect a remote MCP server and grant you the listed tools. ' +
|
|
65
65
|
'Opens a connection-consent agreement as an approvable card in the chat you pass — the human approves it there like any other agreement (there is no MCP tool to approve it, so tell them to approve it in the chat). ' +
|
|
66
|
-
'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
|
|
66
|
+
'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.',
|
|
67
67
|
},
|
|
68
68
|
annotation: 'write',
|
|
69
69
|
params: {
|
|
@@ -214,6 +214,9 @@ export const contextDelegateCapability = {
|
|
|
214
214
|
const client = new ContextGrantsClient(creds.operatorKey, creds.agentId);
|
|
215
215
|
const result = await client.delegateGrant(args['parentGrantId'], {
|
|
216
216
|
holderId: args['holderId'],
|
|
217
|
+
// An agent passing part of its own grant onward hands it to another
|
|
218
|
+
// agent; a person is granted through their own surface.
|
|
219
|
+
holderKind: 'agent',
|
|
217
220
|
scope: { kind: scopeKind, id: scopeId },
|
|
218
221
|
temporal: temporal,
|
|
219
222
|
expiresAt: args['expiresAt'] ?? undefined,
|
|
@@ -5,12 +5,13 @@ import { type CapabilityDefinition } from './types.js';
|
|
|
5
5
|
* cross-session. `unreadableRails` comes from the backend so a short
|
|
6
6
|
* list is never presented as complete when the key can't read a rail.
|
|
7
7
|
*
|
|
8
|
-
* HOLD, not reach. `GET /grants` lists
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
8
|
+
* HOLD, not reach. `GET /grants` lists rows this holder is named on, and taking
|
|
9
|
+
* part in a room or being party to a live agreement now IS such a row. Two kinds
|
|
10
|
+
* of access still leave none: authoring an artifact, and the org memberships
|
|
11
|
+
* that make an ORG-held grant cover you (the row names the org, not you). So a
|
|
12
|
+
* reader can still be entitled to something this list will never mention, and
|
|
13
|
+
* the description says so, because the old "the single answer" wording was read
|
|
14
|
+
* as completeness and an empty list as "no access".
|
|
14
15
|
*/
|
|
15
16
|
export declare const listGrantsCapability: CapabilityDefinition;
|
|
16
17
|
export declare const GRANTS_CAPABILITIES: CapabilityDefinition[];
|
|
@@ -27,20 +27,21 @@ function parseScopeKinds(raw) {
|
|
|
27
27
|
* cross-session. `unreadableRails` comes from the backend so a short
|
|
28
28
|
* list is never presented as complete when the key can't read a rail.
|
|
29
29
|
*
|
|
30
|
-
* HOLD, not reach. `GET /grants` lists
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
30
|
+
* HOLD, not reach. `GET /grants` lists rows this holder is named on, and taking
|
|
31
|
+
* part in a room or being party to a live agreement now IS such a row. Two kinds
|
|
32
|
+
* of access still leave none: authoring an artifact, and the org memberships
|
|
33
|
+
* that make an ORG-held grant cover you (the row names the org, not you). So a
|
|
34
|
+
* reader can still be entitled to something this list will never mention, and
|
|
35
|
+
* the description says so, because the old "the single answer" wording was read
|
|
36
|
+
* as completeness and an empty list as "no access".
|
|
36
37
|
*/
|
|
37
38
|
export const listGrantsCapability = {
|
|
38
39
|
key: 'grant_list',
|
|
39
40
|
names: { sdk: 'grant_list', mcp: 'ziggs_grant_list' },
|
|
40
41
|
title: 'List grants you hold',
|
|
41
42
|
descriptions: {
|
|
42
|
-
sdk: 'List grants this agent holds — or, with role=issuer, grants this agent caused (author-shares and delegate children). Context (chat/agreement/org/artifact), connection, and wallet — canonical grants (grantId, scope, caveats, expiresAt, health; no content or credentials). Default role=holder answers "what grants do I HOLD?", which is narrower than "what can I reach?": authorship,
|
|
43
|
-
mcp: 'List grants this delegate holds — or, with role=issuer, grants this delegate caused (author-shares and delegate children). Context (chat/agreement/org/artifact), connection, and wallet — canonical grants (grantId, scope, caveats, expiresAt, health; no content or credentials). Default role=holder answers "what grants do I HOLD?", which is narrower than "what can I reach?": authorship,
|
|
43
|
+
sdk: 'List grants this agent holds — or, with role=issuer, grants this agent caused (author-shares and delegate children). Context (chat/agreement/org/artifact), connection, and wallet — canonical grants (grantId, scope, caveats, expiresAt, health; no content or credentials). Default role=holder answers "what grants do I HOLD?", which is narrower than "what can I reach?": your own authorship, and grants held by an org you belong to rather than by you, leave nothing under your own id — an empty holder list means "no grants of your own", never "no access". role=issuer answers "who did I share with / what did I mint?" after artifact_share. Filter by scopeKind, scopeId, and health (defaults to active). Rails you lack the operator-key read scope for are named in unreadableRails. Cursor-paginated.',
|
|
44
|
+
mcp: 'List grants this delegate holds — or, with role=issuer, grants this delegate caused (author-shares and delegate children). Context (chat/agreement/org/artifact), connection, and wallet — canonical grants (grantId, scope, caveats, expiresAt, health; no content or credentials). Default role=holder answers "what grants do I HOLD?", which is narrower than "what can I reach?": your own authorship, and grants held by an org you belong to rather than by you, leave nothing under your own id — an empty holder list means "no grants of your own", never "no access". After ziggs_artifact_share, pass role=issuer (and usually scopeKind=["artifact"]) to recover grantIds and revoke with ziggs_context_revoke_grant. Filter by scopeKind, scopeId, and health (defaults to active). Rails you lack the operator-key read scope for are named in unreadableRails. Cursor-paginated.',
|
|
44
45
|
},
|
|
45
46
|
annotation: 'read-only',
|
|
46
47
|
params: {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
export { type CapabilitySurface, type CapabilityAnnotation, type CapabilityParam, type CapabilityEnv, type CapabilityDefinition, fullCreds, rethrowWithContext, } from './types.js';
|
|
2
|
-
export { nextCall,
|
|
3
|
-
export { AGREEMENT_VERB_CAPABILITIES,
|
|
4
|
-
export { PAYMENT_CAPABILITIES, paymentBalanceCapability
|
|
5
|
-
export { LINK_CAPABILITIES,
|
|
2
|
+
export { nextCall, peerPrincipalId, peerPrincipalForCourier, type NextCall, } from './nextCall.js';
|
|
3
|
+
export { AGREEMENT_VERB_CAPABILITIES, agreementBuyCapability, agreementBidCapability, agreementBrokerCapability, agreementRequestCapability, agreementOfferCapability, agreementHandoffCapability, } from './agreementVerbs.js';
|
|
4
|
+
export { PAYMENT_CAPABILITIES, paymentBalanceCapability } from './payments.js';
|
|
5
|
+
export { LINK_CAPABILITIES, listLinksCapability, proposeLinkCapability, linkIsReachOnly, } from './links.js';
|
|
6
6
|
export { AGREEMENT_CAPABILITIES, agreementClaimCapability } from './agreements.js';
|
|
7
7
|
export { AGREEMENT_PROPOSE_PROVIDER_ID_DESCRIPTION } from './proposeProviderId.js';
|
|
8
8
|
export { MARKETPLACE_CAPABILITIES, marketplaceViewCapability } from './marketplace.js';
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
export { fullCreds, rethrowWithContext, } from './types.js';
|
|
2
|
-
export { nextCall,
|
|
3
|
-
export { AGREEMENT_VERB_CAPABILITIES,
|
|
4
|
-
export { PAYMENT_CAPABILITIES, paymentBalanceCapability
|
|
5
|
-
export { LINK_CAPABILITIES,
|
|
2
|
+
export { nextCall, peerPrincipalId, peerPrincipalForCourier, } from './nextCall.js';
|
|
3
|
+
export { AGREEMENT_VERB_CAPABILITIES, agreementBuyCapability, agreementBidCapability, agreementBrokerCapability, agreementRequestCapability, agreementOfferCapability, agreementHandoffCapability, } from './agreementVerbs.js';
|
|
4
|
+
export { PAYMENT_CAPABILITIES, paymentBalanceCapability } from './payments.js';
|
|
5
|
+
export { LINK_CAPABILITIES, listLinksCapability, proposeLinkCapability, linkIsReachOnly, } from './links.js';
|
|
6
6
|
export { AGREEMENT_CAPABILITIES, agreementClaimCapability } from './agreements.js';
|
|
7
7
|
export { AGREEMENT_PROPOSE_PROVIDER_ID_DESCRIPTION } from './proposeProviderId.js';
|
|
8
8
|
export { MARKETPLACE_CAPABILITIES, marketplaceViewCapability } from './marketplace.js';
|
|
@@ -3,23 +3,34 @@ import { type CapabilityDefinition, type CapabilityEnv } from './types.js';
|
|
|
3
3
|
* A link is reach-only — the follow-up move differs by surface tool names.
|
|
4
4
|
*
|
|
5
5
|
* Opening a chat is itself the admission: the backend issues a standing from-now
|
|
6
|
-
*
|
|
6
|
+
* write grant to each side as it creates the room, so both delegates can
|
|
7
7
|
* read and post immediately. Do NOT tell agents to follow chat_open with an
|
|
8
8
|
* issue_grant on that same new chat — that only mints a duplicate grant carrying
|
|
9
9
|
* the default 30-day expiry. issue_grant / context_delegate are for scopes that
|
|
10
10
|
* already exist (an older chat, an agreement, an org).
|
|
11
11
|
*/
|
|
12
12
|
export declare function linkIsReachOnly(env: CapabilityEnv): string;
|
|
13
|
-
export declare const createLinkInviteCapability: CapabilityDefinition;
|
|
14
13
|
export declare const listLinksCapability: CapabilityDefinition;
|
|
15
14
|
/**
|
|
16
|
-
*
|
|
15
|
+
* The one way to connect with someone.
|
|
17
16
|
*
|
|
18
17
|
* This used to ride the propose grammar as `engagementKind: "link"`, which put
|
|
19
18
|
* bilateral trust on the same verb as commercial terms it has none of: no
|
|
20
|
-
* price, no chat, no work. It
|
|
21
|
-
*
|
|
22
|
-
*
|
|
19
|
+
* price, no chat, no work. It moved here, and then absorbed the other two ways
|
|
20
|
+
* of doing the same thing: naming an agent id, naming an email — a form the
|
|
21
|
+
* backend had but no tool ever sent, so an assistant could not name a person at
|
|
22
|
+
* all — and minting a share link.
|
|
23
|
+
*
|
|
24
|
+
* `to` is an email or an agent id, and the link is with the PERSON either way.
|
|
25
|
+
* An id is only a way to find its owner: naming one specific assistant is
|
|
26
|
+
* fragile, because people connect more than one, swap them, and someone who has
|
|
27
|
+
* connected none has no agent to name. An agent that answers to an ORG is
|
|
28
|
+
* refused rather than resolved, since that would link the caller to whoever
|
|
29
|
+
* happens to own the org, who consented to nothing.
|
|
30
|
+
*
|
|
31
|
+
* The answer is constant. It never says whether the address belonged to anyone,
|
|
32
|
+
* whether the two were already connected, or whether this was a repeat — a
|
|
33
|
+
* response that distinguished those would be a way to check who has an account.
|
|
23
34
|
*/
|
|
24
35
|
export declare const proposeLinkCapability: CapabilityDefinition;
|
|
25
36
|
export declare const LINK_CAPABILITIES: CapabilityDefinition[];
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { nextCall,
|
|
1
|
+
import { createLink, listAgreements } from '../http/AgreementClient.js';
|
|
2
|
+
import { nextCall, peerPrincipalForCourier, } from './nextCall.js';
|
|
3
3
|
import { fullCreds } from './types.js';
|
|
4
4
|
const DEFAULT_WEB_URL = 'https://ziggsai.com';
|
|
5
5
|
function webAppOrigin(env) {
|
|
@@ -23,7 +23,7 @@ function inviteShareUrl(env, agreementId) {
|
|
|
23
23
|
* A link is reach-only — the follow-up move differs by surface tool names.
|
|
24
24
|
*
|
|
25
25
|
* Opening a chat is itself the admission: the backend issues a standing from-now
|
|
26
|
-
*
|
|
26
|
+
* write grant to each side as it creates the room, so both delegates can
|
|
27
27
|
* read and post immediately. Do NOT tell agents to follow chat_open with an
|
|
28
28
|
* issue_grant on that same new chat — that only mints a duplicate grant carrying
|
|
29
29
|
* the default 30-day expiry. issue_grant / context_delegate are for scopes that
|
|
@@ -31,8 +31,8 @@ function inviteShareUrl(env, agreementId) {
|
|
|
31
31
|
*/
|
|
32
32
|
export function linkIsReachOnly(env) {
|
|
33
33
|
return env.surface === 'mcp'
|
|
34
|
-
? 'A link is reach-only — it shares no context on its own. Open a chat with the
|
|
35
|
-
: 'A link is reach-only — it shares no context on its own.
|
|
34
|
+
? 'A link is reach-only — it shares no context on its own. Open a chat with the PERSON on the other side (ziggs_chat_open, participantId = the peer principal): delivery lands in their mailbox and whoever runs that side picks it up, which is not yours to choose. Opening the room admits both sides to read and post from then on. To share context that already exists, issue a grant on it with ziggs_context_issue_grant or share a slice of one you hold with ziggs_context_delegate.'
|
|
35
|
+
: 'A link is reach-only — it shares no context on its own. Open a chat with the PERSON on the other side; delivery lands in their mailbox and whoever runs that side picks it up, which is not yours to choose. Opening the room admits both sides to it from then on. To share context that already exists, share a slice of a grant you hold with context_delegate, or ask the peer owner to issue one.';
|
|
36
36
|
}
|
|
37
37
|
const LINK_STATUSES = ['active', 'open', 'cancelled', 'all'];
|
|
38
38
|
/**
|
|
@@ -41,63 +41,20 @@ const LINK_STATUSES = ['active', 'open', 'cancelled', 'all'];
|
|
|
41
41
|
* the limit instead of letting an agent discover it by getting a 400.
|
|
42
42
|
*/
|
|
43
43
|
const MAX_LINK_INVITE_CLAIMS = 25;
|
|
44
|
-
|
|
45
|
-
// the agreement verbs carry the rest: request a direct link with
|
|
46
|
-
// link_propose (counterparty = the agent id), claim
|
|
44
|
+
// Two tools. Links are agreements, so the agreement verbs carry the rest: claim
|
|
47
45
|
// an invite with agreement_claim, end a link with agreement_revoke.
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
sdk: "Create a shareable OPEN link invite (bilateral agent-to-agent trust) when you do NOT have the counterparty's agent id (e.g. connecting across orgs). Creates an open link agreement proposed to everyone and returns shareUrl — one public page that is the entire invite, for a person or for their assistant. Set maxClaims to let several people claim the same link — each gets their own separate connection. When you DO have the agent id, propose the link directly instead: link_propose with counterparty = that id.",
|
|
54
|
-
mcp: 'Create a shareable OPEN link invite (bilateral agent-to-agent trust, NOT a third-party service connection — see ziggs_connection_list for that) when you do NOT have the counterparty\'s agent id (e.g. connecting across orgs). Creates an open link agreement (POST /agreements {engagementKind:"link"}, proposedTo:"everyone") and returns shareUrl: one public page that is the entire invite — the recipient accepts from it with no account, and their assistant can read the connect instructions off the same URL. Give the human that link and nothing else. Set maxClaims to share ONE link with several people; each claimer gets their own separate connection, not a group. Revoke via ziggs_agreement_revoke to disable. When you DO have the agent id, propose the link directly instead: ziggs_link_propose with counterparty = that id.',
|
|
55
|
-
},
|
|
56
|
-
annotation: 'write',
|
|
57
|
-
params: {
|
|
58
|
-
message: {
|
|
59
|
-
type: 'string',
|
|
60
|
-
description: 'Optional note shown to whoever opens the invite (agreement description)',
|
|
61
|
-
},
|
|
62
|
-
maxClaims: {
|
|
63
|
-
type: 'number',
|
|
64
|
-
description: `How many people may claim this one link (default 1, max ${MAX_LINK_INVITE_CLAIMS}). Each claimer forms their own separate connection with you — this does not create a group.`,
|
|
65
|
-
},
|
|
66
|
-
},
|
|
67
|
-
needsAgentId: true,
|
|
68
|
-
handler: async (args, env) => {
|
|
69
|
-
const maxClaims = args['maxClaims'];
|
|
70
|
-
const { agreement } = await createAgreement({
|
|
71
|
-
engagementKind: 'link',
|
|
72
|
-
description: args['message'],
|
|
73
|
-
...(maxClaims == null ? {} : { maxClaims }),
|
|
74
|
-
}, fullCreds(env));
|
|
75
|
-
const shareUrl = inviteShareUrl(env, agreement.agreementId);
|
|
76
|
-
const seats = agreement.linkInvite?.maxClaims ?? 1;
|
|
77
|
-
const seatNote = seats > 1
|
|
78
|
-
? `valid 7 days and claimable by up to ${seats} people (each gets their own separate connection — not a group)`
|
|
79
|
-
: 'single-use and valid 7 days';
|
|
80
|
-
return {
|
|
81
|
-
status: 'open',
|
|
82
|
-
inviteId: agreement.agreementId,
|
|
83
|
-
shareUrl,
|
|
84
|
-
maxClaims: seats,
|
|
85
|
-
seatsRemaining: seats - (agreement.linkInvite?.claimsUsed ?? 0),
|
|
86
|
-
message: env.surface === 'mcp'
|
|
87
|
-
? `Open link invite created, ${seatNote}. Give the human shareUrl and nothing else — it is the whole invite. A recipient with no Ziggs account signs up straight from that page, no beta code needed, and accepting the link is part of the same step; a recipient who would rather their own assistant do the wiring can hand it the same URL, because the page carries the MCP server address and the claim instructions in its markup. Either way the recipient's side of the link is one of their agents — their assistant by default — and it must be running before the link carries anything.`
|
|
88
|
-
: `Open link invite created, ${seatNote}. shareUrl is the whole invite: a recipient with no Ziggs account signs up straight from that page and accepts the link in the same step, and an assistant handed the same URL reads the connect instructions off it. Their side of the link is one of their agents (their assistant by default), and it must be running before the link carries anything. No agent id needed on either side. Revoke with agreement_revoke to disable.`,
|
|
89
|
-
agreement,
|
|
90
|
-
};
|
|
91
|
-
},
|
|
92
|
-
sdkOptions: { isAgreementCreation: true },
|
|
93
|
-
};
|
|
46
|
+
//
|
|
47
|
+
// There were three. `link_create_invite` was the same act as `link_propose` with
|
|
48
|
+
// a different way of naming the other side, and an assistant had to choose
|
|
49
|
+
// between them before knowing which applied. One verb takes an email, an agent
|
|
50
|
+
// id, or nothing.
|
|
94
51
|
export const listLinksCapability = {
|
|
95
52
|
key: 'link_list',
|
|
96
53
|
names: { sdk: 'link_list', mcp: 'ziggs_link_list' },
|
|
97
54
|
title: 'List your links',
|
|
98
55
|
descriptions: {
|
|
99
|
-
sdk: 'List link agreements for this agent — bilateral agent-to-agent trust relationships (GET /agreements?engagementKind=link). Any agent can link with any other agent. Defaults to ACTIVE links only; pass status to see pending proposals ("open") or revoked ones ("cancelled"). Each item is a link summary: agreementId, status, proposalStatus, parties.
|
|
100
|
-
mcp: 'List link agreements for this agent — bilateral agent-to-agent trust relationships, NOT third-party service connections (see ziggs_connection_list for those) (GET /agreements?engagementKind=link). Any agent can link with any other agent. Defaults to ACTIVE links only; pass status to see pending proposals ("open") or revoked ones ("cancelled"). Each item is a link summary: agreementId, status, proposalStatus, parties
|
|
56
|
+
sdk: 'List link agreements for this agent — bilateral agent-to-agent trust relationships (GET /agreements?engagementKind=link). Any agent can link with any other agent. Defaults to ACTIVE links only; pass status to see pending proposals ("open") or revoked ones ("cancelled"). Each item is a link summary: agreementId, status, proposalStatus, parties. The two principals on a link are the two PEOPLE it connects; the actor slots record which agent carried the paperwork and are not addresses. Connect with someone new using link_propose; end a link with agreement_revoke.',
|
|
57
|
+
mcp: 'List link agreements for this agent — bilateral agent-to-agent trust relationships, NOT third-party service connections (see ziggs_connection_list for those) (GET /agreements?engagementKind=link). Any agent can link with any other agent. Defaults to ACTIVE links only; pass status to see pending proposals ("open") or revoked ones ("cancelled"). Each item is a link summary: agreementId, status, proposalStatus, and parties — where parties.creator.principal and parties.provider.principal are the two PEOPLE the link connects. The actor slots record which agent carried the paperwork and are not addresses: message the person. Approve pending links via ziggs_agreement_respond; connect with someone new using ziggs_link_propose; end one with ziggs_agreement_revoke.',
|
|
101
58
|
},
|
|
102
59
|
annotation: 'read-only',
|
|
103
60
|
params: {
|
|
@@ -120,15 +77,28 @@ export const listLinksCapability = {
|
|
|
120
77
|
const active = links.filter((a) => a.status === 'active');
|
|
121
78
|
// A link grants reach and nothing else, so the move after seeing one is
|
|
122
79
|
// always the same: open a room with that peer, or share context explicitly.
|
|
123
|
-
// Both were named in prose, and the peer id had to be dug out of
|
|
124
|
-
//
|
|
80
|
+
// Both were named in prose, and the peer id had to be dug out of `parties`
|
|
81
|
+
// by whoever read it — while being right here.
|
|
82
|
+
//
|
|
83
|
+
// The peer is a PERSON. This used to pre-fill the peer's `actor`, which is
|
|
84
|
+
// courier info and null on most links, so the suggestion either named an
|
|
85
|
+
// agent that is no longer a door or silently disappeared. It fills in only
|
|
86
|
+
// when this agent's own stamp identifies which side is ours; otherwise the
|
|
87
|
+
// peer is named in prose and read off the rows below, because a pre-filled
|
|
88
|
+
// call naming the wrong party would get run.
|
|
125
89
|
const readPlan = [];
|
|
90
|
+
let unnamedPeers = 0;
|
|
126
91
|
for (const link of active.slice(0, 3)) {
|
|
127
|
-
const peer =
|
|
128
|
-
if (!peer)
|
|
92
|
+
const peer = peerPrincipalForCourier(link.parties, env.creds.agentId);
|
|
93
|
+
if (!peer) {
|
|
94
|
+
unnamedPeers += 1;
|
|
129
95
|
continue;
|
|
96
|
+
}
|
|
130
97
|
readPlan.push(nextCall(env, 'chat_open', { participantId: peer }, 'open a room with this linked peer — a link alone carries no context'));
|
|
131
98
|
}
|
|
99
|
+
if (unnamedPeers > 0) {
|
|
100
|
+
readPlan.push(nextCall(env, 'chat_open', undefined, 'open a room with a linked peer: participantId is the other principal on the link (parties.creator / parties.provider), which is a person'));
|
|
101
|
+
}
|
|
132
102
|
if (active.length) {
|
|
133
103
|
readPlan.push(nextCall(env, 'context_issue_grant', undefined, 'share context that already exists: a link does not share any on its own'));
|
|
134
104
|
}
|
|
@@ -143,54 +113,70 @@ export const listLinksCapability = {
|
|
|
143
113
|
sdkOptions: { isGenericFallback: true },
|
|
144
114
|
};
|
|
145
115
|
/**
|
|
146
|
-
*
|
|
116
|
+
* The one way to connect with someone.
|
|
147
117
|
*
|
|
148
118
|
* This used to ride the propose grammar as `engagementKind: "link"`, which put
|
|
149
119
|
* bilateral trust on the same verb as commercial terms it has none of: no
|
|
150
|
-
* price, no chat, no work. It
|
|
151
|
-
*
|
|
152
|
-
*
|
|
120
|
+
* price, no chat, no work. It moved here, and then absorbed the other two ways
|
|
121
|
+
* of doing the same thing: naming an agent id, naming an email — a form the
|
|
122
|
+
* backend had but no tool ever sent, so an assistant could not name a person at
|
|
123
|
+
* all — and minting a share link.
|
|
124
|
+
*
|
|
125
|
+
* `to` is an email or an agent id, and the link is with the PERSON either way.
|
|
126
|
+
* An id is only a way to find its owner: naming one specific assistant is
|
|
127
|
+
* fragile, because people connect more than one, swap them, and someone who has
|
|
128
|
+
* connected none has no agent to name. An agent that answers to an ORG is
|
|
129
|
+
* refused rather than resolved, since that would link the caller to whoever
|
|
130
|
+
* happens to own the org, who consented to nothing.
|
|
131
|
+
*
|
|
132
|
+
* The answer is constant. It never says whether the address belonged to anyone,
|
|
133
|
+
* whether the two were already connected, or whether this was a repeat — a
|
|
134
|
+
* response that distinguished those would be a way to check who has an account.
|
|
153
135
|
*/
|
|
154
136
|
export const proposeLinkCapability = {
|
|
155
137
|
key: 'link_propose',
|
|
156
138
|
names: { sdk: 'link_propose', mcp: 'ziggs_link_propose' },
|
|
157
|
-
title: '
|
|
139
|
+
title: 'Connect with someone',
|
|
158
140
|
descriptions: {
|
|
159
|
-
sdk: "
|
|
160
|
-
mcp: "
|
|
141
|
+
sdk: "Connect with a person: pass their email, or the id of an agent that answers to them, and Ziggs delivers the invitation. The link is with the PERSON either way — an agent id is only a way to find its owner, and an agent that answers to an organisation is refused, because a link connects two people. Leave `to` out to get a share link you hand over yourself. A link is reach and nothing else: no chat, no money, no work. The answer is the same every time, so it never tells you whether that address has an account.",
|
|
142
|
+
mcp: "Connect with a person (NOT a third-party service connection — see ziggs_connection_list for that): pass their email, or the id of an agent that answers to them, and Ziggs delivers the invitation. The link is with the PERSON either way — an agent id is only a way to find its owner, and an agent answering to an organisation is refused, because a link connects two people. Leave `to` out and you get shareUrl: one public page that is the whole invite, which you hand to your human to paste wherever they like. Someone with no Ziggs account signs up from that page and accepts in the same step, and their assistant handed the same URL reads the connect instructions off it. A link is reach and nothing else: no chat, no money, no work. The answer is identical every time, so it never reveals whether an address has an account. Approve links proposed to you with ziggs_agreement_respond; end one with ziggs_agreement_revoke.",
|
|
161
143
|
},
|
|
162
144
|
annotation: 'write',
|
|
163
145
|
params: {
|
|
164
|
-
|
|
146
|
+
to: {
|
|
165
147
|
type: 'string',
|
|
166
|
-
|
|
167
|
-
description: 'Agent id to link with.',
|
|
148
|
+
description: "The person to connect with: their email address, or the id of an agent that answers to them. Leave it out for a share link you pass on yourself.",
|
|
168
149
|
},
|
|
169
150
|
message: {
|
|
170
151
|
type: 'string',
|
|
171
|
-
description: 'Optional note shown to whoever
|
|
152
|
+
description: 'Optional note shown to whoever is asked to accept it.',
|
|
153
|
+
},
|
|
154
|
+
maxClaims: {
|
|
155
|
+
type: 'number',
|
|
156
|
+
description: `Share links only: how many people may claim this one link (default 1, max ${MAX_LINK_INVITE_CLAIMS}). Each claimer forms their own separate connection with you — this does not create a group.`,
|
|
172
157
|
},
|
|
173
158
|
},
|
|
174
159
|
needsAgentId: true,
|
|
175
160
|
handler: async (args, env) => {
|
|
176
|
-
const
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
161
|
+
const to = args['to']?.trim();
|
|
162
|
+
const maxClaims = args['maxClaims'];
|
|
163
|
+
const { shareUrl } = await createLink({
|
|
164
|
+
...(to ? { to } : {}),
|
|
180
165
|
...(args['message'] ? { description: args['message'] } : {}),
|
|
166
|
+
...(maxClaims == null || to ? {} : { maxClaims }),
|
|
181
167
|
}, fullCreds(env));
|
|
182
|
-
//
|
|
183
|
-
//
|
|
184
|
-
//
|
|
185
|
-
|
|
186
|
-
const note = routedTo && routedTo !== counterparty
|
|
187
|
-
? `Approval routed to the target agent's owner (${routedTo}): a person decides who their delegate trusts. You passed ${counterparty}.`
|
|
188
|
-
: undefined;
|
|
168
|
+
// Two messages, chosen by what the CALLER passed rather than by what came
|
|
169
|
+
// back. That is the whole discipline: the caller knows whether it named
|
|
170
|
+
// somebody, so branching on it discloses nothing, while branching on
|
|
171
|
+
// anything the server learned about the target would.
|
|
189
172
|
return {
|
|
190
|
-
|
|
191
|
-
|
|
173
|
+
status: 'sent',
|
|
174
|
+
shareUrl,
|
|
175
|
+
message: to
|
|
176
|
+
? `Invitation sent to ${to}. You will not be told whether they already had an account, whether you were already connected, or whether this repeated an earlier invitation — the answer is the same in every case, on purpose. It becomes a live connection when they accept. ${linkIsReachOnly(env)}`
|
|
177
|
+
: `Share link created, valid 7 days. Give your human shareUrl and nothing else: it is the whole invite. ${linkIsReachOnly(env)}`,
|
|
192
178
|
readPlan: [
|
|
193
|
-
nextCall(env, 'link_list', { status: 'open' }, 'check whether it has been
|
|
179
|
+
nextCall(env, 'link_list', { status: 'open' }, 'check whether it has been accepted yet'),
|
|
194
180
|
],
|
|
195
181
|
};
|
|
196
182
|
},
|
|
@@ -198,6 +184,5 @@ export const proposeLinkCapability = {
|
|
|
198
184
|
};
|
|
199
185
|
export const LINK_CAPABILITIES = [
|
|
200
186
|
proposeLinkCapability,
|
|
201
|
-
createLinkInviteCapability,
|
|
202
187
|
listLinksCapability,
|
|
203
188
|
];
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { pullOffers,
|
|
1
|
+
import { pullOffers, pullRequests } from '../http/MarketplaceClient.js';
|
|
2
2
|
import { fullCreds } from './types.js';
|
|
3
3
|
import { nextCall } from './nextCall.js';
|
|
4
|
-
const VIEW_KINDS = ['all', '
|
|
4
|
+
const VIEW_KINDS = ['all', 'requests', 'offers'];
|
|
5
5
|
function publishHint(env) {
|
|
6
|
-
const propose = env.surface === 'mcp' ? '
|
|
6
|
+
const propose = env.surface === 'mcp' ? 'ziggs_agreement_request' : 'agreement_request';
|
|
7
7
|
const claim = env.surface === 'mcp' ? 'ziggs_agreement_claim' : 'agreement_claim';
|
|
8
8
|
return (`Claim any row with ${claim} (agreementId). Publish your own with ${propose}: ` +
|
|
9
|
-
`proposedTo "everyone" or "org" with no providerId broadcasts a
|
|
9
|
+
`proposedTo "everyone" or "org" with no providerId broadcasts a request (claimer works, you pay); ` +
|
|
10
10
|
`the same with providerId = your own id publishes a standing offer (you work, claimer pays).`);
|
|
11
11
|
}
|
|
12
12
|
/** A broadcast sentinel in a party slot means "open", not a counterparty. */
|
|
@@ -35,15 +35,21 @@ function toListingRow(a, kind) {
|
|
|
35
35
|
kind,
|
|
36
36
|
description: terms.description ?? '',
|
|
37
37
|
price: a?.money?.price ?? 0,
|
|
38
|
+
// What that price MEANS: a whole-engagement total, or a rate charged per
|
|
39
|
+
// completed task. Omitting it left a metered standing hire indistinguishable
|
|
40
|
+
// from a flat-priced one at the moment a caller decides to claim, and
|
|
41
|
+
// `per_task` is the default for a hire. Absent on rows written before the
|
|
42
|
+
// field existed, which read as `total`.
|
|
43
|
+
...(terms.billing ? { billing: terms.billing } : {}),
|
|
38
44
|
engagementKind: a?.engagementKind,
|
|
39
45
|
lifecycle: terms.lifecycle,
|
|
40
46
|
...(terms.expiresAt ? { expiresAt: terms.expiresAt } : {}),
|
|
41
47
|
...(terms.maxExecutions != null ? { maxExecutions: terms.maxExecutions } : {}),
|
|
42
|
-
// Who does the work on an offer, who is paying on a
|
|
48
|
+
// Who does the work on an offer, who is paying on a request. The other slot is
|
|
43
49
|
// the open one you would be filling by claiming, so it carries no name yet.
|
|
44
|
-
...(named(parties.
|
|
45
|
-
...(named(parties.provider) ? { provider: parties.provider } : {}),
|
|
46
|
-
...(named(parties.payer) ? { payer: parties.payer } : {}),
|
|
50
|
+
...(named(parties.provider?.actor) ? { providerAgent: parties.provider.actor } : {}),
|
|
51
|
+
...(named(parties.provider?.principal) ? { provider: parties.provider.principal } : {}),
|
|
52
|
+
...(named(parties.payer?.principal) ? { payer: parties.payer.principal } : {}),
|
|
47
53
|
// Access the job cannot be done without — worth knowing before claiming it.
|
|
48
54
|
...(requiredConnections.length ? { requiredConnections } : {}),
|
|
49
55
|
createdAt: a?.createdAt,
|
|
@@ -59,15 +65,15 @@ export const marketplaceViewCapability = {
|
|
|
59
65
|
names: { sdk: 'marketplace_view', mcp: 'ziggs_marketplace_view' },
|
|
60
66
|
title: 'Browse the marketplace',
|
|
61
67
|
descriptions: {
|
|
62
|
-
sdk: 'Browse the open marketplace — where every engagement STARTS (posted-first: reuse an active agreement, else claim a listing here, before ever proposing).
|
|
63
|
-
mcp: 'Browse the open marketplace — where every engagement STARTS (posted-first: reuse an active agreement, else claim a listing here, before ever proposing).
|
|
68
|
+
sdk: 'Browse the open marketplace — where every engagement STARTS (posted-first: reuse an active agreement, else claim a listing here, before ever proposing). Requests are work buyers broadcast (you would do the work); standing offers are services sellers broadcast (you would buy). Returns public rows plus org-scoped rows from your org, filtered server-side. Claim a row with agreement_claim — listings are take-it-or-leave-it, never counter one; publish your own via agreement_propose with proposedTo "everyone"/"org" (providerId = your id for an offer, omitted for a request).',
|
|
69
|
+
mcp: 'Browse the open marketplace — where every engagement STARTS (posted-first: reuse an active agreement, else claim a listing here, before ever proposing). Requests are work buyers broadcast (you would do the work); standing offers are services sellers broadcast (you would buy). Returns public rows plus org-scoped rows from your org, filtered server-side. Claim a row with ziggs_agreement_claim — listings are take-it-or-leave-it, never counter one; publish your own with ziggs_agreement_offer (you do the work, the claimer pays) or ziggs_agreement_request (the claimer does the work, you pay).',
|
|
64
70
|
},
|
|
65
71
|
annotation: 'read-only',
|
|
66
72
|
params: {
|
|
67
73
|
kind: {
|
|
68
74
|
type: 'string',
|
|
69
75
|
enum: VIEW_KINDS,
|
|
70
|
-
description: 'all (default) |
|
|
76
|
+
description: 'all (default) | requests | offers',
|
|
71
77
|
},
|
|
72
78
|
limit: { type: 'number', description: 'Max rows per kind (default 20)' },
|
|
73
79
|
since: { type: 'string', description: 'ISO timestamp — only rows published after this' },
|
|
@@ -83,22 +89,22 @@ export const marketplaceViewCapability = {
|
|
|
83
89
|
limit: typeof args['limit'] === 'number' ? args['limit'] : 20,
|
|
84
90
|
...(args['since'] ? { since: args['since'] } : {}),
|
|
85
91
|
};
|
|
86
|
-
const [
|
|
87
|
-
kind === 'offers' ? Promise.resolve([]) :
|
|
88
|
-
kind === '
|
|
92
|
+
const [requests, offers] = await Promise.all([
|
|
93
|
+
kind === 'offers' ? Promise.resolve([]) : pullRequests(options, creds),
|
|
94
|
+
kind === 'requests' ? Promise.resolve([]) : pullOffers(options, creds),
|
|
89
95
|
]);
|
|
90
96
|
return {
|
|
91
97
|
...(kind !== 'offers'
|
|
92
|
-
? {
|
|
98
|
+
? { requests: requests.map((q) => toListingRow(q, 'request')), requestCount: requests.length }
|
|
93
99
|
: {}),
|
|
94
|
-
...(kind !== '
|
|
100
|
+
...(kind !== 'requests'
|
|
95
101
|
? { offers: offers.map((o) => toListingRow(o, 'offer')), offerCount: offers.length }
|
|
96
102
|
: {}),
|
|
97
103
|
// Claiming is the move after browsing, and the id is in the row the
|
|
98
104
|
// caller just received. The prose hint stays for the publish side, which
|
|
99
105
|
// is a choice rather than a call.
|
|
100
106
|
readPlan: [
|
|
101
|
-
...(
|
|
107
|
+
...(requests.length || offers.length
|
|
102
108
|
? [
|
|
103
109
|
nextCall(env, 'agreement_claim', undefined, 'claim a listing from this view by passing its agreementId — listings are take-it-or-leave-it, never countered'),
|
|
104
110
|
]
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { AgreementParties } from '../types.js';
|
|
1
2
|
import type { CapabilityEnv } from './types.js';
|
|
2
3
|
/**
|
|
3
4
|
* One runnable next step: a tool name, pre-filled arguments, and why.
|
|
@@ -32,13 +33,43 @@ export interface NextCall {
|
|
|
32
33
|
*/
|
|
33
34
|
export declare function nextCall(env: CapabilityEnv, capabilityKey: string, args: Record<string, unknown> | undefined, why: string): NextCall;
|
|
34
35
|
/**
|
|
35
|
-
* The other
|
|
36
|
+
* The other PRINCIPAL in a two-party agreement, from the perspective of
|
|
37
|
+
* `selfId`.
|
|
38
|
+
*
|
|
39
|
+
* This was `peerAgentId`, and it read the `actor` columns — the agents that
|
|
40
|
+
* carried the paperwork. Those became courier info when a link was re-keyed to
|
|
41
|
+
* the two people it belongs to, and on most links they are null, so the hint it
|
|
42
|
+
* fed either pre-filled a call with an agent that is no longer a door or
|
|
43
|
+
* silently vanished because the helper returned nothing.
|
|
44
|
+
*
|
|
45
|
+
* Named for the SLOT, not for what a link happens to put in it. On a link both
|
|
46
|
+
* principals are guaranteed to be people, because links are person-only and
|
|
47
|
+
* both sides are userIds by construction — but the same slots hold an org or an
|
|
48
|
+
* agent on other engagement kinds, so a caller who read "person" here and
|
|
49
|
+
* trusted it on a hire would be wrong. The person guarantee is a link-only
|
|
50
|
+
* property; ask the kind before relying on it.
|
|
36
51
|
*
|
|
37
52
|
* Returns null rather than guessing when the row does not identify one, because
|
|
38
53
|
* a pre-filled call naming the wrong counterparty is worse than no pre-filled
|
|
39
54
|
* call: the caller would run it, and it would do something they did not ask for.
|
|
40
55
|
*/
|
|
41
|
-
export declare function
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
56
|
+
export declare function peerPrincipalId(parties: AgreementParties | undefined, selfId: string | undefined): string | null;
|
|
57
|
+
/**
|
|
58
|
+
* The peer's principal, located by finding MY side rather than by knowing my
|
|
59
|
+
* own principal id.
|
|
60
|
+
*
|
|
61
|
+
* An agent knows its own agent id and not the id of the person it answers for,
|
|
62
|
+
* so it cannot ask {@link peerPrincipalId} which of two people it is. What it
|
|
63
|
+
* CAN recognise is its own courier stamp: if my agent id is in one side's
|
|
64
|
+
* `actor`, that side is mine and the other side's principal is the peer.
|
|
65
|
+
*
|
|
66
|
+
* Note the difference from the bug this replaced. Reading the peer's `actor` as
|
|
67
|
+
* the peer's address was wrong — those slots are courier info, null on most
|
|
68
|
+
* links, and never a door. Reading MY OWN `actor` to work out which side I am on
|
|
69
|
+
* is sound, because I am comparing against an id I hold.
|
|
70
|
+
*
|
|
71
|
+
* Returns null when neither side carries my stamp, which is the common case for
|
|
72
|
+
* a link two people formed from the web. No pre-filled call is the right answer
|
|
73
|
+
* there: naming the wrong counterparty would get run.
|
|
74
|
+
*/
|
|
75
|
+
export declare function peerPrincipalForCourier(parties: AgreementParties | undefined, myAgentId: string | undefined): string | null;
|