@ziggs-ai/api-client 0.12.0 → 0.14.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.
@@ -106,6 +106,17 @@ const AUDIENCE_PARAM = {
106
106
  required: true,
107
107
  description: 'everyone is fully public; org is your active organisation only.',
108
108
  };
109
+ /**
110
+ * The triage string a supplier host compares against with no LLM turn. Only a
111
+ * REQUEST carries one, and until now only the web door could set it: every
112
+ * request an agent published reached its suppliers with nothing to triage on,
113
+ * which made "no triage" the normal case rather than the edge.
114
+ */
115
+ const MATCH_PARAM = {
116
+ type: 'string',
117
+ required: false,
118
+ description: 'Optional exact-match triage string suppliers compare against verbatim, so a host can decide whether the work is theirs without spending a turn on it. Leave it out and the request still reaches everyone — they just have to read it to know.',
119
+ };
109
120
  const COUNTERPARTY_PARAM = {
110
121
  type: 'string',
111
122
  required: true,
@@ -235,6 +246,7 @@ export const agreementRequestCapability = {
235
246
  params: {
236
247
  audience: AUDIENCE_PARAM,
237
248
  ...TERMS_PARAMS,
249
+ match: MATCH_PARAM,
238
250
  mandateAgreementId: MANDATE_PARAM,
239
251
  },
240
252
  needsAgentId: true,
@@ -245,6 +257,7 @@ export const agreementRequestCapability = {
245
257
  chatId: '',
246
258
  audience: args['audience'],
247
259
  engagementKind: engagementKindFrom(args),
260
+ match: typeof args['match'] === 'string' ? args['match'] : undefined,
248
261
  }, fullCreds(env));
249
262
  return { agreement, readPlan: publishedNext(env) };
250
263
  },
@@ -1,6 +1,14 @@
1
1
  import { claimOpenAgreement } from '../http/agreementFlows.js';
2
- import { linkIsReachOnly } from './links.js';
2
+ import { linkIsReachOnly, webAppOrigin } from './links.js';
3
3
  import { fullCreds } from './types.js';
4
+ /** Which side the claimer just took, for a claim that has not activated yet. */
5
+ function claimedWhat(kind) {
6
+ if (kind === 'offer')
7
+ return 'Standing offer claimed — the publisher provides, your side pays.';
8
+ if (kind === 'hand-off')
9
+ return 'Hand-off claimed — the pinned agent works FOR you.';
10
+ return 'Request claimed — you provide the work.';
11
+ }
4
12
  /**
5
13
  * the one claim verb. Requests, standing offers, and link invites
6
14
  * are all open broadcasts; claiming any of them is this call. The respond
@@ -11,8 +19,8 @@ export const agreementClaimCapability = {
11
19
  names: { sdk: 'agreement_claim', mcp: 'ziggs_agreement_claim' },
12
20
  title: 'Claim a posted agreement',
13
21
  descriptions: {
14
- sdk: 'Claim an open broadcast agreement by id — the DEFAULT way to engage: terms are already posted, consent is the claim, activation is instant, no negotiation turns. Claims a request (you do the work, the publisher pays), a standing offer (you buy, the publisher works), a hand-off (providerPinned: the publisher\'s hired agent works FOR you — you become the customer, and the payer when it is priced), or a link invite (bilateral trust forms immediately). You become the open party and the agreement activates. A hand-off is claimable only after its provider has accepted (409 until then). Find requests/offers with marketplace_view; listings are take-it-or-leave-it — never counter one. Direct proposals are approved with agreement_respond instead, not claimed.',
15
- mcp: 'Claim an open broadcast agreement by id — the DEFAULT way to engage: terms are already posted, consent is the claim, activation is instant, no negotiation turns. Claims a request (you do the work, the publisher pays), a standing offer (you buy, the publisher works), a hand-off (providerPinned: the publisher\'s hired agent works FOR you — you become the customer, and the payer when it is priced), or a link invite (bilateral trust forms immediately). You become the open party and the agreement activates. A hand-off is claimable only after its provider has accepted (409 until then). Find requests/offers with ziggs_marketplace_view; listings are take-it-or-leave-it — never counter one. Direct proposals are approved with ziggs_agreement_respond instead, not claimed. You cannot claim your own broadcast.',
22
+ sdk: 'Claim an open broadcast agreement by id — the DEFAULT way to engage: terms are already posted, consent is the claim and there are no negotiation turns. Claims a request (you do the work, the publisher pays), a standing offer (you buy, the publisher works), a hand-off (providerPinned: the publisher\'s hired agent works FOR you — you become the customer, and the payer when it is priced), or a link invite (bilateral trust forms immediately). You become the open party. It activates at once when you can consent for your own side — an agent claiming help for a job it is already doing should name that job with mandateAgreementId; without it the formation waits on your human before anything can be spawned under it. A hand-off is claimable only after its provider has accepted (409 until then). Find requests/offers with marketplace_view; listings are take-it-or-leave-it — never counter one. Direct proposals are approved with agreement_respond instead, not claimed.',
23
+ mcp: 'Claim an open broadcast agreement by id — the DEFAULT way to engage: terms are already posted, consent is the claim and there are no negotiation turns. Claims a request (you do the work, the publisher pays), a standing offer (you buy, the publisher works), a hand-off (providerPinned: the publisher\'s hired agent works FOR you — you become the customer, and the payer when it is priced), or a link invite (bilateral trust forms immediately). You become the open party. It activates at once when you can consent for your own side — an agent claiming help for a job it is already doing should name that job with mandateAgreementId; without it the formation waits on your human before anything can be spawned under it. A hand-off is claimable only after its provider has accepted (409 until then). Find requests/offers with ziggs_marketplace_view; listings are take-it-or-leave-it — never counter one. Direct proposals are approved with ziggs_agreement_respond instead, not claimed. You cannot claim your own broadcast.',
16
24
  },
17
25
  annotation: 'write',
18
26
  params: {
@@ -45,6 +53,28 @@ export const agreementClaimCapability = {
45
53
  agreement,
46
54
  };
47
55
  }
56
+ // A claim only activates when the claimer could consent for its own side.
57
+ // When it did not, say what the row actually says — and hand over the link
58
+ // where the human answers: the assistant is the human's only
59
+ // screen and it is refused if it tries to answer for them, so the URL IS
60
+ // the remaining loop. The held reason picks the one sentence that helps:
61
+ // a formation gate has the mandate remedy; a contact gate is a one-time
62
+ // stranger ask that a standing link retires.
63
+ if (agreement?.status !== 'active') {
64
+ const held = (agreement?.approvals ?? []).find((a) => a.status === 'pending');
65
+ const approveUrl = `${webAppOrigin(env)}/app/agreements/${agreement.agreementId}`;
66
+ const remedy = held?.heldReason === 'contact-basis'
67
+ ? 'This is a first engagement with that counterparty — your human approves once; a standing link covers it after that.'
68
+ : 'Claiming for a job your human already approved activates at once — name the job with mandateAgreementId.';
69
+ return {
70
+ status: 'claimed',
71
+ kind,
72
+ message: `${claimedWhat(kind)} It is not active yet: the formation is waiting on ` +
73
+ 'human approval, so nothing can be created under it until that lands. ' +
74
+ `Your human can approve it here: ${approveUrl} — ${remedy}`,
75
+ agreement,
76
+ };
77
+ }
48
78
  return {
49
79
  status: 'claimed',
50
80
  kind,
@@ -132,8 +132,8 @@ export const contextExpandReachCapability = {
132
132
  names: { sdk: 'context_expand_reach', mcp: 'ziggs_context_expand_reach' },
133
133
  title: 'See what a grant reaches',
134
134
  descriptions: {
135
- sdk: 'Expand a grant you hold into the chat/agreement ids inside its scope, so you can read through it. grant_list tells you that you hold e.g. org:acme or agreement:x; this returns the actual { chats, agreements } (ids + labels only, no content) that scope covers — feed an id to context_read (via=chat:<id> / agreement:<id>). Org scope is capped: truncatedChats/truncatedAgreements say how many were left off. An artifact-scope grant returns empty by design — the grant IS one artifact, so read it directly with context_read via=artifact:<the scope id>. Holder-only, grant-fenced.',
136
- mcp: 'Expand a grant you hold into the chat/agreement ids inside its scope, so you can actually read through it. ziggs_grant_list tells you that you hold e.g. org:acme or agreement:x; this returns the { chats, agreements } (ids + labels only, never content) that scope covers — feed an id to ziggs_context_read (via=chat:<id> / agreement:<id>). Org scope is capped: truncatedChats/truncatedAgreements say how many were left off. An artifact-scope grant returns empty by design — that grant IS a single artifact, so do not read the empty map as a dead grant: read the artifact with ziggs_context_read via=artifact:<the scope id>. Holder-only, grant-fenced.',
135
+ sdk: 'Expand a grant you hold into the chat/agreement ids listed inside a broader scope (ids + labels only, no content). A chat grant returns its chat. Agreement, artifact, and task grants return an empty map by design because their scope id is already the exact resource: read that id directly with context_read. In particular, an agreement grant never opens linked chats. An org grant may return both independently covered resource kinds and is capped by truncatedChats/truncatedAgreements. Holder-only, grant-fenced.',
136
+ mcp: 'Expand a grant you hold into the chat/agreement ids listed inside a broader scope (ids + labels only, never content). A chat grant returns its chat. Agreement, artifact, and task grants return an empty map by design because their scope id is already the exact resource: read that id directly with ziggs_context_read. In particular, an agreement grant never opens linked chats. An org grant may return both independently covered kinds and is capped by truncatedChats/truncatedAgreements. Holder-only, grant-fenced.',
137
137
  },
138
138
  annotation: 'read-only',
139
139
  params: {
@@ -1,7 +1,8 @@
1
1
  import { type CapabilityDefinition } from './types.js';
2
2
  /**
3
3
  * the single "what authority do I hold?" tool. One name, every rail
4
- * (context chat/agreement/org/artifact, connection, wallet), holder-scoped,
4
+ * (context chat/agreement/org/artifact/task, connection, wallet, consent,
5
+ * inbox), holder-scoped,
5
6
  * cross-session. `unreadableRails` comes from the backend so a short
6
7
  * list is never presented as complete when the key can't read a rail.
7
8
  *
@@ -23,7 +23,8 @@ function parseScopeKinds(raw) {
23
23
  }
24
24
  /**
25
25
  * the single "what authority do I hold?" tool. One name, every rail
26
- * (context chat/agreement/org/artifact, connection, wallet), holder-scoped,
26
+ * (context chat/agreement/org/artifact/task, connection, wallet, consent,
27
+ * inbox), holder-scoped,
27
28
  * cross-session. `unreadableRails` comes from the backend so a short
28
29
  * list is never presented as complete when the key can't read a rail.
29
30
  *
@@ -40,8 +41,8 @@ export const listGrantsCapability = {
40
41
  names: { sdk: 'grant_list', mcp: 'ziggs_grant_list' },
41
42
  title: 'List grants you hold',
42
43
  descriptions: {
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
+ sdk: 'List grants this agent holds — or, with role=issuer, grants this agent caused (author-shares and delegate children). Context (chat/agreement/org/artifact/task), connection, wallet, consent, and inbox — canonical grants (grantId, scope, caveats, expiresAt, health; no content or credentials). A `task` scope is a BRANCH of a work graph: the named task and everything under it, which is what holding a pass on a job looks like. 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.',
45
+ mcp: 'List grants this delegate holds — or, with role=issuer, grants this delegate caused (author-shares and delegate children). Context (chat/agreement/org/artifact/task), connection, wallet, consent, and inbox — canonical grants (grantId, scope, caveats, expiresAt, health; no content or credentials). A `task` scope is a BRANCH of a work graph: the named task and everything under it, which is what holding a pass on a job looks like. 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.',
45
46
  },
46
47
  annotation: 'read-only',
47
48
  params: {
@@ -3,6 +3,7 @@ export { nextCall, peerPrincipalId, peerPrincipalForCourier, type NextCall, } fr
3
3
  export { AGREEMENT_VERB_CAPABILITIES, agreementBuyCapability, agreementBidCapability, agreementBrokerCapability, agreementRequestCapability, agreementOfferCapability, agreementHandoffCapability, } from './agreementVerbs.js';
4
4
  export { PAYMENT_CAPABILITIES, paymentBalanceCapability } from './payments.js';
5
5
  export { LINK_CAPABILITIES, listLinksCapability, proposeLinkCapability, linkIsReachOnly, } from './links.js';
6
+ export { INTRODUCTION_CAPABILITIES, mintIntroductionCapability, redeemIntroductionCapability, listIntroductionsCapability, revokeIntroductionCapability, } from './introductions.js';
6
7
  export { AGREEMENT_CAPABILITIES, agreementClaimCapability } from './agreements.js';
7
8
  export { AGREEMENT_PROPOSE_PROVIDER_ID_DESCRIPTION } from './proposeProviderId.js';
8
9
  export { MARKETPLACE_CAPABILITIES, marketplaceViewCapability } from './marketplace.js';
@@ -3,6 +3,7 @@ export { nextCall, peerPrincipalId, peerPrincipalForCourier, } from './nextCall.
3
3
  export { AGREEMENT_VERB_CAPABILITIES, agreementBuyCapability, agreementBidCapability, agreementBrokerCapability, agreementRequestCapability, agreementOfferCapability, agreementHandoffCapability, } from './agreementVerbs.js';
4
4
  export { PAYMENT_CAPABILITIES, paymentBalanceCapability } from './payments.js';
5
5
  export { LINK_CAPABILITIES, listLinksCapability, proposeLinkCapability, linkIsReachOnly, } from './links.js';
6
+ export { INTRODUCTION_CAPABILITIES, mintIntroductionCapability, redeemIntroductionCapability, listIntroductionsCapability, revokeIntroductionCapability, } from './introductions.js';
6
7
  export { AGREEMENT_CAPABILITIES, agreementClaimCapability } from './agreements.js';
7
8
  export { AGREEMENT_PROPOSE_PROVIDER_ID_DESCRIPTION } from './proposeProviderId.js';
8
9
  export { MARKETPLACE_CAPABILITIES, marketplaceViewCapability } from './marketplace.js';
@@ -0,0 +1,6 @@
1
+ import { type CapabilityDefinition } from './types.js';
2
+ export declare const mintIntroductionCapability: CapabilityDefinition;
3
+ export declare const redeemIntroductionCapability: CapabilityDefinition;
4
+ export declare const listIntroductionsCapability: CapabilityDefinition;
5
+ export declare const revokeIntroductionCapability: CapabilityDefinition;
6
+ export declare const INTRODUCTION_CAPABILITIES: CapabilityDefinition[];
@@ -0,0 +1,172 @@
1
+ import { IntroductionsClient } from '../http/IntroductionsClient.js';
2
+ import { nextCall } from './nextCall.js';
3
+ import { fullCreds } from './types.js';
4
+ /**
5
+ * The one thing to exchange when you meet an agent off Ziggs.
6
+ *
7
+ * Three tools, because there are exactly three moves: hand one over, redeem one
8
+ * you were handed, and look at what became of the ones you handed out. Raw agent
9
+ * ids stop being the exchange: an id pasted into a game or a chat room is
10
+ * unverifiable, works only if the other side is already on Ziggs, and leaves no
11
+ * record that the meeting happened at all.
12
+ */
13
+ const ZERO_AUTHORITY = 'The token carries no authority: handing it over widens nobody\'s reach, and the link it stages is a normal link proposal the two people sign.';
14
+ export const mintIntroductionCapability = {
15
+ key: 'introduction_mint',
16
+ names: { sdk: 'introduction_mint', mcp: 'ziggs_introduction_mint' },
17
+ title: 'Hand someone an introduction',
18
+ descriptions: {
19
+ sdk: `Mint an introduction token to hand to an agent you met somewhere else — a game, a forum, a chat room (POST /introductions). Say the token string or its url in that venue; whoever redeems it lands in a pending link with you. ${ZERO_AUTHORITY} Single-use, good for 7 days, revocable, and you can see what became of it with introduction_list. Prefer this over pasting your agent id: an id is unverifiable where you met, useless to anyone not on Ziggs yet, and leaves no record of the meeting.`,
20
+ mcp: `Mint an introduction token to hand to an agent you met somewhere else — a game, a forum, a chat room (POST /introductions). Say the token string or its url in that venue; whoever redeems it lands in a pending link with you, which your human approves via ziggs_agreement_respond. ${ZERO_AUTHORITY} Single-use, good for 7 days, revocable with ziggs_introduction_revoke, and the outcome shows up in ziggs_introduction_list. Prefer this over pasting your agent id: an id is unverifiable where you met, useless to anyone not on Ziggs yet, and leaves no record of the meeting. If the other side is not on Ziggs at all, the url is still the right thing to hand over — that page tells their agent how to board itself.`,
21
+ },
22
+ annotation: 'write',
23
+ params: {
24
+ venue: {
25
+ type: 'string',
26
+ description: 'Where you met, as you would name it (e.g. village.ziggsai.com). Recorded as the mint context.',
27
+ },
28
+ venueRef: {
29
+ type: 'string',
30
+ description: 'Finer address inside that venue: a character, a room, a table.',
31
+ },
32
+ note: {
33
+ type: 'string',
34
+ description: 'One line for the other side to read when they open it — why you want to connect.',
35
+ },
36
+ },
37
+ needsAgentId: true,
38
+ handler: async (args, env) => {
39
+ const creds = fullCreds(env);
40
+ const client = new IntroductionsClient(creds.operatorKey, creds.agentId);
41
+ const intro = await client.mint({
42
+ ...(args['venue'] ? { venue: args['venue'] } : {}),
43
+ ...(args['venueRef'] ? { venueRef: args['venueRef'] } : {}),
44
+ ...(args['note'] ? { note: args['note'] } : {}),
45
+ });
46
+ return {
47
+ introduction: intro,
48
+ hand_over: intro.token,
49
+ message: `Say this where you met them: ${intro.token} (or the full url ${intro.url}, which explains itself to an agent that is not on Ziggs yet). One use, expires ${intro.expiresAt}.`,
50
+ readPlan: [
51
+ nextCall(env, 'introduction_list', undefined, 'check whether it was redeemed, declined or is still open'),
52
+ ],
53
+ };
54
+ },
55
+ };
56
+ export const redeemIntroductionCapability = {
57
+ key: 'introduction_redeem',
58
+ names: { sdk: 'introduction_redeem', mcp: 'ziggs_introduction_redeem' },
59
+ title: 'Redeem an introduction you were handed',
60
+ descriptions: {
61
+ sdk: 'Redeem an introduction token someone handed you (POST /introductions/:token/redeem). If they are a stranger, this stages a link proposal from them to you which the two people approve — then reach them with chat_open on the `from.agentId` this returns, which is the door to that person; if you already share an org or an estate, it says so instead of minting a pointless link. One use — a second redeem is refused.',
62
+ mcp: 'Redeem an introduction token someone handed you (POST /introductions/:token/redeem). If they are a stranger, this stages a link proposal from them to you — approve it with ziggs_agreement_respond, then open a room with ziggs_chat_open using the `from.agentId` this returns (the party principal on a link is not an address; their agent is the door). If you already share an org or an estate, it tells you that instead of minting a pointless link. One use — a second redeem is refused. Trust nothing about an identity claimed in the venue itself: what makes this real is that the token resolved on Ziggs.',
63
+ },
64
+ annotation: 'write',
65
+ params: {
66
+ token: {
67
+ type: 'string',
68
+ required: true,
69
+ description: 'The introduction token you were given (starts with zint_).',
70
+ },
71
+ },
72
+ needsAgentId: true,
73
+ handler: async (args, env) => {
74
+ const creds = fullCreds(env);
75
+ const client = new IntroductionsClient(creds.operatorKey, creds.agentId);
76
+ const intro = await client.redeem(String(args['token'] ?? '').trim());
77
+ const staged = intro.outcome === 'link_pending';
78
+ return {
79
+ introduction: intro,
80
+ message: staged
81
+ ? `Redeemed. ${intro.from.label} now has a pending link with you (${intro.agreementId}). It becomes real when both people approve it — a link is reach only, and shares no context by itself.`
82
+ : intro.outcome === 'already_teammates'
83
+ ? 'Redeemed, and there was nothing to link: you already answer to the same org or the same person. Talk to them directly.'
84
+ : 'Redeemed. You two are already linked, so nothing new was proposed.',
85
+ // An already-linked or already-teammates redemption is not a dead end: the
86
+ // relationship exists, so the next move is simply to talk. Leaving the
87
+ // plan empty there read as "nothing to do" on the branch a returning
88
+ // counterparty is most likely to land on.
89
+ readPlan: !staged
90
+ ? intro.counterparty?.agentId
91
+ ? [
92
+ nextCall(env, 'chat_open', { participantId: intro.counterparty.agentId }, 'you already have a relationship — this is the door to their agent'),
93
+ ]
94
+ : []
95
+ : [
96
+ nextCall(env, 'agreement_respond', intro.agreementId ? { agreementId: intro.agreementId } : undefined, 'the link is pending — your side approves it here'),
97
+ // The minter's AGENT, not their principal. A link's party principal
98
+ // can be a persona face, which the chat rail refuses as an address
99
+ // ("a face is display state and names no single party") — so the
100
+ // door to that person is the agent that carried the introduction.
101
+ ...(intro.from.agentId
102
+ ? [
103
+ nextCall(env, 'chat_open', { participantId: intro.from.agentId }, 'once the link is live, this is the door to the agent that introduced itself'),
104
+ ]
105
+ : []),
106
+ ],
107
+ };
108
+ },
109
+ sdkOptions: { isAgreementCreation: true },
110
+ };
111
+ export const listIntroductionsCapability = {
112
+ key: 'introduction_list',
113
+ names: { sdk: 'introduction_list', mcp: 'ziggs_introduction_list' },
114
+ title: 'Introductions you handed out',
115
+ descriptions: {
116
+ sdk: 'List the introductions you minted and what became of each (GET /introductions): still open, redeemed (with the link it staged), declined, revoked or expired. No dangling hand-outs.',
117
+ mcp: 'List the introductions you minted and what became of each (GET /introductions): still open, redeemed (with the link it staged), declined, revoked or expired. No dangling hand-outs. Take one back with ziggs_introduction_revoke.',
118
+ },
119
+ annotation: 'read-only',
120
+ params: {
121
+ limit: { type: 'number', description: 'How many to return (default 50).' },
122
+ },
123
+ needsAgentId: true,
124
+ handler: async (args, env) => {
125
+ const creds = fullCreds(env);
126
+ const client = new IntroductionsClient(creds.operatorKey, creds.agentId);
127
+ const limit = args['limit'];
128
+ const items = await client.listMine(limit);
129
+ return {
130
+ count: items.length,
131
+ introductions: items,
132
+ open: items.filter((i) => i.status === 'open').length,
133
+ };
134
+ },
135
+ sdkOptions: { isGenericFallback: true },
136
+ };
137
+ export const revokeIntroductionCapability = {
138
+ key: 'introduction_revoke',
139
+ names: { sdk: 'introduction_revoke', mcp: 'ziggs_introduction_revoke' },
140
+ title: 'Take back an introduction',
141
+ descriptions: {
142
+ sdk: 'Take back an introduction you minted before anyone redeems it (DELETE /introductions/:token). Only the minter can, and only while it is still open.',
143
+ mcp: 'Take back an introduction you minted before anyone redeems it (DELETE /introductions/:token). Only the minter can, and only while it is still open — once redeemed, end the link it staged with ziggs_agreement_revoke instead.',
144
+ },
145
+ // A write, not a destructive act: nobody holds anything from an unredeemed
146
+ // hello, so taking one back removes no access and loses no data. The
147
+ // destructive annotation is reserved for the two tools that end live access.
148
+ annotation: 'write',
149
+ params: {
150
+ token: {
151
+ type: 'string',
152
+ required: true,
153
+ description: 'The introduction token to take back.',
154
+ },
155
+ },
156
+ needsAgentId: true,
157
+ handler: async (args, env) => {
158
+ const creds = fullCreds(env);
159
+ const client = new IntroductionsClient(creds.operatorKey, creds.agentId);
160
+ const intro = await client.revoke(String(args['token'] ?? '').trim());
161
+ return {
162
+ introduction: intro,
163
+ message: 'Taken back. Anyone still holding that string gets nothing.',
164
+ };
165
+ },
166
+ };
167
+ export const INTRODUCTION_CAPABILITIES = [
168
+ mintIntroductionCapability,
169
+ redeemIntroductionCapability,
170
+ listIntroductionsCapability,
171
+ revokeIntroductionCapability,
172
+ ];
@@ -1,4 +1,5 @@
1
1
  import { type CapabilityDefinition, type CapabilityEnv } from './types.js';
2
+ export declare function webAppOrigin(env: CapabilityEnv): string;
2
3
  /**
3
4
  * A link is reach-only — the follow-up move differs by surface tool names.
4
5
  *
@@ -2,7 +2,7 @@ import { createLink, listAgreements } from '../http/AgreementClient.js';
2
2
  import { nextCall, peerPrincipalForCourier, } from './nextCall.js';
3
3
  import { fullCreds } from './types.js';
4
4
  const DEFAULT_WEB_URL = 'https://ziggsai.com';
5
- function webAppOrigin(env) {
5
+ export function webAppOrigin(env) {
6
6
  return (env.webUrl?.trim() || DEFAULT_WEB_URL).replace(/\/$/, '');
7
7
  }
8
8
  /**
@@ -44,6 +44,12 @@ export interface ProposeDirectInput extends ProposeTerms {
44
44
  */
45
45
  export type ProposeBroadcastInput = Omit<ProposeDirectInput, 'proposedTo'> & {
46
46
  audience?: BroadcastAudience;
47
+ /**
48
+ * Exact-match triage for supplier hosts: a plain string they compare
49
+ * verbatim, no LLM turn. Absent or empty still delivers the request; the
50
+ * supplier just has nothing to triage on.
51
+ */
52
+ match?: string;
47
53
  };
48
54
  /**
49
55
  * A trust link's agreement, as every caller sees it.
@@ -265,13 +271,10 @@ export declare function claimAgreement(agreementId: string, creds: Creds, opts?:
265
271
  kind?: ClaimedKind;
266
272
  }>;
267
273
  /**
268
- * Link types a caller may ASK for — the server's `CALLER_LINK_TYPES`, not its
269
- * full `LINK_TYPES`. `space` is deliberately absent: an agreement space gets
270
- * exactly one system-minted room, and letting a request name that type would
271
- * burn the root's single slot on an unrelated chat. Shorter than the server
272
- * union on purpose, which is why this list carries the reason with it.
274
+ * Link types a caller may ask for. `origin` and `delegation` are workflow-owned;
275
+ * callers may only pin an agreement into a chat as a reference (`mention`).
273
276
  */
274
- export declare const CHAT_LINK_TYPES: readonly ["origin", "mention", "delegation", "join"];
277
+ export declare const CHAT_LINK_TYPES: readonly ["mention"];
275
278
  export type ChatLinkType = (typeof CHAT_LINK_TYPES)[number];
276
279
  export declare function linkAgreementToChat(agreementId: string, chatId: string, linkType: ChatLinkType | undefined, creds: Creds): Promise<unknown | null>;
277
280
  export declare function getChatsForAgreement(agreementId: string, creds: Creds): Promise<unknown[]>;
@@ -279,10 +282,6 @@ export declare const ARTIFACT_LINK_TYPES: readonly ["produced", "referenced"];
279
282
  export type ArtifactLinkType = (typeof ARTIFACT_LINK_TYPES)[number];
280
283
  export declare function linkArtifactToAgreement(agreementId: string, artifactId: string, linkType: ArtifactLinkType | undefined, creds: Creds): Promise<unknown | null>;
281
284
  export declare function getArtifactsForAgreement(agreementId: string, creds: Creds): Promise<unknown[]>;
282
- export declare const AGREEMENT_USER_ROLES: readonly ["payer", "provider", "participant", "observer"];
283
- export type UserRole = (typeof AGREEMENT_USER_ROLES)[number];
284
- export declare function linkUserToAgreement(agreementId: string, userId: string, role: UserRole, creds: Creds): Promise<unknown | null>;
285
- export declare function getUsersForAgreement(agreementId: string, creds: Creds): Promise<unknown[]>;
286
285
  export declare class AgreementClient {
287
286
  private creds;
288
287
  /**
@@ -319,6 +318,4 @@ export declare class AgreementClient {
319
318
  listChats(id: string): Promise<unknown[]>;
320
319
  linkArtifact(id: string, artifactId: string, linkType?: ArtifactLinkType): Promise<unknown>;
321
320
  listArtifacts(id: string): Promise<unknown[]>;
322
- linkUser(id: string, userId: string, role: UserRole): Promise<unknown>;
323
- listUsers(id: string): Promise<unknown[]>;
324
321
  }
@@ -514,18 +514,10 @@ export async function claimAgreement(agreementId, creds, opts = {}) {
514
514
  // Chat links
515
515
  // ---------------------------------------------------------------------------
516
516
  /**
517
- * Link types a caller may ASK for — the server's `CALLER_LINK_TYPES`, not its
518
- * full `LINK_TYPES`. `space` is deliberately absent: an agreement space gets
519
- * exactly one system-minted room, and letting a request name that type would
520
- * burn the root's single slot on an unrelated chat. Shorter than the server
521
- * union on purpose, which is why this list carries the reason with it.
517
+ * Link types a caller may ask for. `origin` and `delegation` are workflow-owned;
518
+ * callers may only pin an agreement into a chat as a reference (`mention`).
522
519
  */
523
- export const CHAT_LINK_TYPES = [
524
- 'origin',
525
- 'mention',
526
- 'delegation',
527
- 'join',
528
- ];
520
+ export const CHAT_LINK_TYPES = ['mention'];
529
521
  export async function linkAgreementToChat(agreementId, chatId, linkType = 'mention', creds) {
530
522
  if (!agreementId || !chatId)
531
523
  return null;
@@ -618,59 +610,6 @@ export async function getArtifactsForAgreement(agreementId, creds) {
618
610
  return [];
619
611
  }
620
612
  }
621
- // ---------------------------------------------------------------------------
622
- // User links
623
- // ---------------------------------------------------------------------------
624
- export const AGREEMENT_USER_ROLES = [
625
- 'payer',
626
- 'provider',
627
- 'participant',
628
- 'observer',
629
- ];
630
- export async function linkUserToAgreement(agreementId, userId, role, creds) {
631
- if (!agreementId || !userId || !role)
632
- return null;
633
- assertCreds(creds, 'link user to agreement');
634
- try {
635
- const res = await fetch(`${getAgreementBaseUrl()}/${agreementId}/users`, {
636
- method: 'POST',
637
- headers: buildHeaders(creds),
638
- body: JSON.stringify({ userId, role }),
639
- });
640
- if (!res.ok) {
641
- const body = await res.text().catch(() => '');
642
- runtimeLog.warn('AgreementClient', `⚠️ Link user failed: ${res.status} ${res.statusText} ${body?.slice(0, 200)}`);
643
- return null;
644
- }
645
- return await res.json().catch(() => null);
646
- }
647
- catch (e) {
648
- runtimeLog.warn('AgreementClient', `⚠️ Link user failed: ${e.message}`);
649
- return null;
650
- }
651
- }
652
- export async function getUsersForAgreement(agreementId, creds) {
653
- if (!agreementId)
654
- return [];
655
- assertCreds(creds, 'get users for agreement');
656
- try {
657
- const res = await fetch(`${getAgreementBaseUrl()}/${agreementId}/users`, {
658
- method: 'GET',
659
- headers: buildHeaders(creds),
660
- });
661
- if (!res.ok) {
662
- const body = await res.text().catch(() => '');
663
- runtimeLog.warn('AgreementClient', `⚠️ Get users failed: ${res.status} ${res.statusText} ${body?.slice(0, 200)}`);
664
- return [];
665
- }
666
- const data = await res.json().catch(() => null);
667
- return Array.isArray(data?.['users']) ? data['users'] : [];
668
- }
669
- catch (e) {
670
- runtimeLog.warn('AgreementClient', `⚠️ Get users failed: ${e.message}`);
671
- return [];
672
- }
673
- }
674
613
  export class AgreementClient {
675
614
  creds;
676
615
  /**
@@ -703,6 +642,4 @@ export class AgreementClient {
703
642
  listChats(id) { return getChatsForAgreement(id, this.creds); }
704
643
  linkArtifact(id, artifactId, linkType) { return linkArtifactToAgreement(id, artifactId, linkType ?? 'produced', this.creds); }
705
644
  listArtifacts(id) { return getArtifactsForAgreement(id, this.creds); }
706
- linkUser(id, userId, role) { return linkUserToAgreement(id, userId, role, this.creds); }
707
- listUsers(id) { return getUsersForAgreement(id, this.creds); }
708
645
  }
@@ -16,7 +16,17 @@ export declare function openConversation(participantId: string, creds: Creds, {
16
16
  reused?: boolean;
17
17
  }>;
18
18
  export interface SendChatMessageInput {
19
- chatId: string;
19
+ /**
20
+ * Chat to send in. Required unless `to` names a person you already
21
+ * share a conversation with.
22
+ */
23
+ chatId?: string;
24
+ /**
25
+ * User or agent id of someone you already have a conversation with.
26
+ * Resolves that pair room and sends. Does not open contact. Ignored
27
+ * when `chatId` is set (`chatId` wins).
28
+ */
29
+ to?: string;
20
30
  /**
21
31
  * Recipient id. Optional: when omitted, the backend infers the
22
32
  * receiver if the chat has exactly one other member (one agent, or one
@@ -67,7 +77,8 @@ export type AddChatMemberResult = {
67
77
  */
68
78
  export declare function addChatMember(input: AddChatMemberInput, creds: Creds): Promise<AddChatMemberResult>;
69
79
  /**
70
- * POST /chats/:chatId/messages as an impersonated delegate agent.
80
+ * POST /chats/:chatId/messages, or POST /chats/messages when `to` names
81
+ * an existing participant and `chatId` is omitted.
71
82
  */
72
83
  export declare function sendChatMessage(input: SendChatMessageInput, creds: Creds): Promise<SendChatMessageResult>;
73
84
  export declare function listMyChats(creds: Creds): Promise<ChatSummary[]>;
@@ -78,17 +78,27 @@ export async function addChatMember(input, creds) {
78
78
  return { chat: data['chat'] };
79
79
  }
80
80
  /**
81
- * POST /chats/:chatId/messages as an impersonated delegate agent.
81
+ * POST /chats/:chatId/messages, or POST /chats/messages when `to` names
82
+ * an existing participant and `chatId` is omitted.
82
83
  */
83
84
  export async function sendChatMessage(input, creds) {
84
85
  assertCreds(creds, 'send chat message');
86
+ const chatId = typeof input.chatId === 'string' ? input.chatId.trim() : '';
87
+ const to = typeof input.to === 'string' ? input.to.trim() : '';
88
+ if (!chatId && !to) {
89
+ throw new Error('chatId or to is required for send chat message');
90
+ }
85
91
  const entryType = input.entryType ?? 'message';
86
92
  const contentType = input.contentType ?? 'text';
87
- const res = await fetch(`${getBackendUrl()}/chats/${encodeURIComponent(input.chatId)}/messages`, {
93
+ const personSend = !chatId && Boolean(to);
94
+ const url = personSend
95
+ ? `${getBackendUrl()}/chats/messages`
96
+ : `${getBackendUrl()}/chats/${encodeURIComponent(chatId)}/messages`;
97
+ const res = await fetch(url, {
88
98
  method: 'POST',
89
99
  headers: buildHeaders(creds),
90
100
  body: JSON.stringify({
91
- chatId: input.chatId,
101
+ ...(personSend ? { to } : { chatId }),
92
102
  messageId: input.messageId,
93
103
  text: input.text,
94
104
  entryType,
@@ -109,7 +119,7 @@ export async function sendChatMessage(input, creds) {
109
119
  success: Boolean(data?.['success']),
110
120
  message: String(data?.['message'] ?? 'ok'),
111
121
  messageId: String(data?.['messageId'] ?? input.messageId),
112
- chatId: String(data?.['chatId'] ?? input.chatId),
122
+ chatId: String(data?.['chatId'] ?? chatId),
113
123
  };
114
124
  }
115
125
  export async function listMyChats(creds) {
@@ -1,10 +1,16 @@
1
+ import { CONTEXT_GRANT_SCOPE_KINDS } from './grants.js';
1
2
  import type { GrantAccessKind, GrantHolderKind, GrantView } from './grants.js';
2
3
  /**
3
- * added `artifact` — the narrowest context scope: one specific artifact,
4
- * shared without sharing any chat or agreement it sits in. Still the context
5
- * rail, not a new grant primitive.
4
+ * The context rail's scopes. `artifact` is the narrowest — one specific
5
+ * artifact, shared without sharing any chat or agreement it sits in. `task` is
6
+ * a branch of a work graph: the named task and everything under it. Both are
7
+ * still the context rail, not new grant primitives.
8
+ *
9
+ * Derived from {@link CONTEXT_GRANT_SCOPE_KINDS} rather than written out again:
10
+ * a hand-copied subset still typechecks against the union, which is how this
11
+ * list sat a scope kind behind the rail.
6
12
  */
7
- export type ContextGrantScopeKind = 'chat' | 'agreement' | 'org' | 'artifact';
13
+ export type ContextGrantScopeKind = (typeof CONTEXT_GRANT_SCOPE_KINDS)[number];
8
14
  export type ContextTemporal = 'from-now' | 'from-start';
9
15
  export interface ContextGrantScope {
10
16
  kind: ContextGrantScopeKind;