@ziggs-ai/api-client 0.11.0 → 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 +7 -0
- package/dist/capabilities/agreementVerbs.js +42 -2
- package/dist/capabilities/agreements.js +13 -1
- package/dist/capabilities/index.d.ts +2 -2
- package/dist/capabilities/index.js +2 -2
- package/dist/capabilities/links.d.ts +16 -5
- package/dist/capabilities/links.js +69 -82
- package/dist/capabilities/marketplace.js +3 -3
- package/dist/capabilities/nextCall.d.ts +36 -5
- package/dist/capabilities/nextCall.js +53 -7
- package/dist/http/AgreementClient.d.ts +47 -16
- package/dist/http/AgreementClient.js +33 -29
- package/dist/http/InboxClient.js +4 -0
- package/dist/http/MarketplaceClient.d.ts +0 -2
- package/dist/http/MarketplaceClient.js +0 -19
- package/dist/http/TaskClient.js +2 -7
- package/dist/http/agreementFlows.d.ts +3 -4
- package/dist/http/agreementFlows.js +8 -13
- 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 +36 -16
- package/dist/types.js +18 -0
- package/package.json +1 -1
|
@@ -3,6 +3,13 @@ export declare const agreementBuyCapability: CapabilityDefinition;
|
|
|
3
3
|
export declare const agreementBidCapability: CapabilityDefinition;
|
|
4
4
|
export declare const agreementBrokerCapability: CapabilityDefinition;
|
|
5
5
|
export declare const agreementRequestCapability: CapabilityDefinition;
|
|
6
|
+
/**
|
|
7
|
+
* No mandate param here, deliberately: a listing is formed inside no job. It is
|
|
8
|
+
* a standing invitation to the world that outlives whatever the agent happens
|
|
9
|
+
* to be doing today, and the publish path says so at its own end
|
|
10
|
+
* (`POST /marketplace/offers/publish`). Hanging one under a job would kill the
|
|
11
|
+
* listing when the job ended.
|
|
12
|
+
*/
|
|
6
13
|
export declare const agreementOfferCapability: CapabilityDefinition;
|
|
7
14
|
export declare const agreementHandoffCapability: CapabilityDefinition;
|
|
8
15
|
export declare const AGREEMENT_VERB_CAPABILITIES: CapabilityDefinition[];
|
|
@@ -77,6 +77,28 @@ function termsFrom(args) {
|
|
|
77
77
|
function engagementKindFrom(args) {
|
|
78
78
|
return args['engagementKind'] ?? 'service';
|
|
79
79
|
}
|
|
80
|
+
/**
|
|
81
|
+
* The job an agent declares it is acting inside.
|
|
82
|
+
*
|
|
83
|
+
* The consent gate excuses a formation that sits inside a job a human already
|
|
84
|
+
* approved, and it verifies the claim against the acting agent. What did not
|
|
85
|
+
* exist was a way to MAKE the claim: `parentAgreementId` rode only on internal
|
|
86
|
+
* service calls, so every agreement an agent formed arrived at the gate as an
|
|
87
|
+
* outsider and waited on a human — including the ordinary case of buying the
|
|
88
|
+
* help the job it was hired for needs.
|
|
89
|
+
*
|
|
90
|
+
* Declared by the caller, never guessed by the server. The SDK fills it in from
|
|
91
|
+
* the task it is executing, so an agent doing assigned work pays nothing for it.
|
|
92
|
+
*/
|
|
93
|
+
const MANDATE_PARAM = {
|
|
94
|
+
type: 'string',
|
|
95
|
+
description: 'The active agreement whose work this is part of — the job you are doing. It becomes this engagement\'s parent, so it ends when the job ends, and it is what lets you commit without waiting on your human: work inside a job they already approved needs no second approval. Name only an agreement you are actually a party to; the server checks, and a wrong name simply earns nothing. Leave it out for work that belongs to no job.',
|
|
96
|
+
};
|
|
97
|
+
/** Pull the declared mandate off a validated arg bag, as the wire field. */
|
|
98
|
+
function mandateFrom(args) {
|
|
99
|
+
const declared = args['mandateAgreementId'];
|
|
100
|
+
return typeof declared === 'string' && declared ? { parentAgreementId: declared } : {};
|
|
101
|
+
}
|
|
80
102
|
/** The audience a broadcast reaches. */
|
|
81
103
|
const AUDIENCE_PARAM = {
|
|
82
104
|
type: 'string',
|
|
@@ -114,12 +136,14 @@ export const agreementBuyCapability = {
|
|
|
114
136
|
counterparty: COUNTERPARTY_PARAM,
|
|
115
137
|
chatId: CHAT_PARAM,
|
|
116
138
|
...TERMS_PARAMS,
|
|
139
|
+
mandateAgreementId: MANDATE_PARAM,
|
|
117
140
|
},
|
|
118
141
|
needsAgentId: true,
|
|
119
142
|
handler: async (args, env) => {
|
|
120
143
|
const counterparty = args['counterparty'];
|
|
121
144
|
const agreement = await proposeDirectTo({
|
|
122
145
|
...termsFrom(args),
|
|
146
|
+
...mandateFrom(args),
|
|
123
147
|
proposedTo: counterparty,
|
|
124
148
|
chatId: args['chatId'],
|
|
125
149
|
// They provide. The payer is derived server-side as the other side.
|
|
@@ -143,12 +167,14 @@ export const agreementBidCapability = {
|
|
|
143
167
|
counterparty: COUNTERPARTY_PARAM,
|
|
144
168
|
chatId: CHAT_PARAM,
|
|
145
169
|
...TERMS_PARAMS,
|
|
170
|
+
mandateAgreementId: MANDATE_PARAM,
|
|
146
171
|
},
|
|
147
172
|
needsAgentId: true,
|
|
148
173
|
handler: async (args, env) => {
|
|
149
174
|
const creds = fullCreds(env);
|
|
150
175
|
const agreement = await proposeDirectTo({
|
|
151
176
|
...termsFrom(args),
|
|
177
|
+
...mandateFrom(args),
|
|
152
178
|
proposedTo: args['counterparty'],
|
|
153
179
|
chatId: args['chatId'],
|
|
154
180
|
// You provide, so the counterparty pays.
|
|
@@ -180,11 +206,13 @@ export const agreementBrokerCapability = {
|
|
|
180
206
|
},
|
|
181
207
|
chatId: CHAT_PARAM,
|
|
182
208
|
...TERMS_PARAMS,
|
|
209
|
+
mandateAgreementId: MANDATE_PARAM,
|
|
183
210
|
},
|
|
184
211
|
needsAgentId: true,
|
|
185
212
|
handler: async (args, env) => {
|
|
186
213
|
const agreement = await proposeDirectTo({
|
|
187
214
|
...termsFrom(args),
|
|
215
|
+
...mandateFrom(args),
|
|
188
216
|
proposedTo: args['counterparty'],
|
|
189
217
|
chatId: args['chatId'],
|
|
190
218
|
providerId: args['provider'],
|
|
@@ -204,11 +232,16 @@ export const agreementRequestCapability = {
|
|
|
204
232
|
mcp: 'Post work you want done and will pay for: whoever claims it does the work. Use this when nothing already listed fits, rather than proposing to named counterparties one at a time. To offer work you would do, use ziggs_agreement_offer. Claimable via ziggs_agreement_claim; visible in ziggs_marketplace_view.',
|
|
205
233
|
},
|
|
206
234
|
annotation: 'write',
|
|
207
|
-
params: {
|
|
235
|
+
params: {
|
|
236
|
+
audience: AUDIENCE_PARAM,
|
|
237
|
+
...TERMS_PARAMS,
|
|
238
|
+
mandateAgreementId: MANDATE_PARAM,
|
|
239
|
+
},
|
|
208
240
|
needsAgentId: true,
|
|
209
241
|
handler: async (args, env) => {
|
|
210
242
|
const agreement = await proposeBroadcast({
|
|
211
243
|
...termsFrom(args),
|
|
244
|
+
...mandateFrom(args),
|
|
212
245
|
chatId: '',
|
|
213
246
|
audience: args['audience'],
|
|
214
247
|
engagementKind: engagementKindFrom(args),
|
|
@@ -217,6 +250,13 @@ export const agreementRequestCapability = {
|
|
|
217
250
|
},
|
|
218
251
|
sdkOptions: { isAgreementCreation: true },
|
|
219
252
|
};
|
|
253
|
+
/**
|
|
254
|
+
* No mandate param here, deliberately: a listing is formed inside no job. It is
|
|
255
|
+
* a standing invitation to the world that outlives whatever the agent happens
|
|
256
|
+
* to be doing today, and the publish path says so at its own end
|
|
257
|
+
* (`POST /marketplace/offers/publish`). Hanging one under a job would kill the
|
|
258
|
+
* listing when the job ended.
|
|
259
|
+
*/
|
|
220
260
|
export const agreementOfferCapability = {
|
|
221
261
|
key: 'agreement_offer',
|
|
222
262
|
names: { sdk: 'agreement_offer', mcp: 'ziggs_agreement_offer' },
|
|
@@ -278,7 +318,7 @@ export const agreementHandoffCapability = {
|
|
|
278
318
|
// would have had to look it up to pass it, and a lookup whose answer is
|
|
279
319
|
// unique is not a decision worth handing to the caller.
|
|
280
320
|
const parent = await getAgreement(parentAgreementId, creds);
|
|
281
|
-
const providerId = parent?.parties?.
|
|
321
|
+
const providerId = parent?.parties?.provider?.actor;
|
|
282
322
|
if (!providerId) {
|
|
283
323
|
throw new Error(`Cannot hand off ${parentAgreementId}: it names no provider, so there is no hire to share. Check the id with agreement_get.`);
|
|
284
324
|
}
|
|
@@ -21,10 +21,22 @@ export const agreementClaimCapability = {
|
|
|
21
21
|
required: true,
|
|
22
22
|
description: 'The open agreement to claim (request / offer / link invite id)',
|
|
23
23
|
},
|
|
24
|
+
// The job this claim is part of. A claim is the DEFAULT way to engage, so
|
|
25
|
+
// without this the ordinary case — an agent claiming the help the job it is
|
|
26
|
+
// doing needs — waited on a human every time. It authorizes; it does not
|
|
27
|
+
// re-parent: the row belongs to whoever posted it, and a claimer cannot move
|
|
28
|
+
// somebody else's agreement into its own tree.
|
|
29
|
+
mandateAgreementId: {
|
|
30
|
+
type: 'string',
|
|
31
|
+
description: 'The active agreement whose work this claim is part of — the job you are doing. Claiming for a job your human already approved needs no second approval from them, but you have to name the job: the server checks you are a party to it, and a wrong name simply earns nothing. Leave it out when the claim belongs to no job.',
|
|
32
|
+
},
|
|
24
33
|
},
|
|
25
34
|
needsAgentId: true,
|
|
26
35
|
handler: async (args, env) => {
|
|
27
|
-
const
|
|
36
|
+
const declaredMandate = args['mandateAgreementId'];
|
|
37
|
+
const { agreement, kind } = await claimOpenAgreement(args['agreementId'], fullCreds(env), typeof declaredMandate === 'string' && declaredMandate
|
|
38
|
+
? { mandateAgreementId: declaredMandate }
|
|
39
|
+
: {});
|
|
28
40
|
if (kind === 'link') {
|
|
29
41
|
return {
|
|
30
42
|
status: 'linked',
|
|
@@ -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,
|
|
2
|
+
export { nextCall, peerPrincipalId, peerPrincipalForCourier, type NextCall, } from './nextCall.js';
|
|
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
|
-
export { LINK_CAPABILITIES,
|
|
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,
|
|
2
|
+
export { nextCall, peerPrincipalId, peerPrincipalForCourier, } from './nextCall.js';
|
|
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
|
-
export { LINK_CAPABILITIES,
|
|
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';
|
|
@@ -10,16 +10,27 @@ import { type CapabilityDefinition, type CapabilityEnv } from './types.js';
|
|
|
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
1
|
import { createLink, listAgreements } from '../http/AgreementClient.js';
|
|
2
|
-
import { nextCall,
|
|
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) {
|
|
@@ -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,62 +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 createLink({
|
|
71
|
-
description: args['message'],
|
|
72
|
-
...(maxClaims == null ? {} : { maxClaims }),
|
|
73
|
-
}, fullCreds(env));
|
|
74
|
-
const shareUrl = inviteShareUrl(env, agreement.agreementId);
|
|
75
|
-
const seats = agreement.linkInvite?.maxClaims ?? 1;
|
|
76
|
-
const seatNote = seats > 1
|
|
77
|
-
? `valid 7 days and claimable by up to ${seats} people (each gets their own separate connection — not a group)`
|
|
78
|
-
: 'single-use and valid 7 days';
|
|
79
|
-
return {
|
|
80
|
-
status: 'open',
|
|
81
|
-
inviteId: agreement.agreementId,
|
|
82
|
-
shareUrl,
|
|
83
|
-
maxClaims: seats,
|
|
84
|
-
seatsRemaining: seats - (agreement.linkInvite?.claimsUsed ?? 0),
|
|
85
|
-
message: env.surface === 'mcp'
|
|
86
|
-
? `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.`
|
|
87
|
-
: `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.`,
|
|
88
|
-
agreement,
|
|
89
|
-
};
|
|
90
|
-
},
|
|
91
|
-
sdkOptions: { isAgreementCreation: true },
|
|
92
|
-
};
|
|
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.
|
|
93
51
|
export const listLinksCapability = {
|
|
94
52
|
key: 'link_list',
|
|
95
53
|
names: { sdk: 'link_list', mcp: 'ziggs_link_list' },
|
|
96
54
|
title: 'List your links',
|
|
97
55
|
descriptions: {
|
|
98
|
-
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.
|
|
99
|
-
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.',
|
|
100
58
|
},
|
|
101
59
|
annotation: 'read-only',
|
|
102
60
|
params: {
|
|
@@ -119,15 +77,28 @@ export const listLinksCapability = {
|
|
|
119
77
|
const active = links.filter((a) => a.status === 'active');
|
|
120
78
|
// A link grants reach and nothing else, so the move after seeing one is
|
|
121
79
|
// always the same: open a room with that peer, or share context explicitly.
|
|
122
|
-
// Both were named in prose, and the peer id had to be dug out of
|
|
123
|
-
//
|
|
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.
|
|
124
89
|
const readPlan = [];
|
|
90
|
+
let unnamedPeers = 0;
|
|
125
91
|
for (const link of active.slice(0, 3)) {
|
|
126
|
-
const peer =
|
|
127
|
-
if (!peer)
|
|
92
|
+
const peer = peerPrincipalForCourier(link.parties, env.creds.agentId);
|
|
93
|
+
if (!peer) {
|
|
94
|
+
unnamedPeers += 1;
|
|
128
95
|
continue;
|
|
96
|
+
}
|
|
129
97
|
readPlan.push(nextCall(env, 'chat_open', { participantId: peer }, 'open a room with this linked peer — a link alone carries no context'));
|
|
130
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
|
+
}
|
|
131
102
|
if (active.length) {
|
|
132
103
|
readPlan.push(nextCall(env, 'context_issue_grant', undefined, 'share context that already exists: a link does not share any on its own'));
|
|
133
104
|
}
|
|
@@ -142,53 +113,70 @@ export const listLinksCapability = {
|
|
|
142
113
|
sdkOptions: { isGenericFallback: true },
|
|
143
114
|
};
|
|
144
115
|
/**
|
|
145
|
-
*
|
|
116
|
+
* The one way to connect with someone.
|
|
146
117
|
*
|
|
147
118
|
* This used to ride the propose grammar as `engagementKind: "link"`, which put
|
|
148
119
|
* bilateral trust on the same verb as commercial terms it has none of: no
|
|
149
|
-
* price, no chat, no work. It
|
|
150
|
-
*
|
|
151
|
-
*
|
|
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.
|
|
152
135
|
*/
|
|
153
136
|
export const proposeLinkCapability = {
|
|
154
137
|
key: 'link_propose',
|
|
155
138
|
names: { sdk: 'link_propose', mcp: 'ziggs_link_propose' },
|
|
156
|
-
title: '
|
|
139
|
+
title: 'Connect with someone',
|
|
157
140
|
descriptions: {
|
|
158
|
-
sdk: "
|
|
159
|
-
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.",
|
|
160
143
|
},
|
|
161
144
|
annotation: 'write',
|
|
162
145
|
params: {
|
|
163
|
-
|
|
146
|
+
to: {
|
|
164
147
|
type: 'string',
|
|
165
|
-
|
|
166
|
-
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.",
|
|
167
149
|
},
|
|
168
150
|
message: {
|
|
169
151
|
type: 'string',
|
|
170
|
-
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.`,
|
|
171
157
|
},
|
|
172
158
|
},
|
|
173
159
|
needsAgentId: true,
|
|
174
160
|
handler: async (args, env) => {
|
|
175
|
-
const
|
|
176
|
-
const
|
|
177
|
-
|
|
161
|
+
const to = args['to']?.trim();
|
|
162
|
+
const maxClaims = args['maxClaims'];
|
|
163
|
+
const { shareUrl } = await createLink({
|
|
164
|
+
...(to ? { to } : {}),
|
|
178
165
|
...(args['message'] ? { description: args['message'] } : {}),
|
|
166
|
+
...(maxClaims == null || to ? {} : { maxClaims }),
|
|
179
167
|
}, fullCreds(env));
|
|
180
|
-
//
|
|
181
|
-
//
|
|
182
|
-
//
|
|
183
|
-
|
|
184
|
-
const note = routedTo && routedTo !== counterparty
|
|
185
|
-
? `Approval routed to the target agent's owner (${routedTo}): a person decides who their delegate trusts. You passed ${counterparty}.`
|
|
186
|
-
: 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.
|
|
187
172
|
return {
|
|
188
|
-
|
|
189
|
-
|
|
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)}`,
|
|
190
178
|
readPlan: [
|
|
191
|
-
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'),
|
|
192
180
|
],
|
|
193
181
|
};
|
|
194
182
|
},
|
|
@@ -196,6 +184,5 @@ export const proposeLinkCapability = {
|
|
|
196
184
|
};
|
|
197
185
|
export const LINK_CAPABILITIES = [
|
|
198
186
|
proposeLinkCapability,
|
|
199
|
-
createLinkInviteCapability,
|
|
200
187
|
listLinksCapability,
|
|
201
188
|
];
|
|
@@ -47,9 +47,9 @@ function toListingRow(a, kind) {
|
|
|
47
47
|
...(terms.maxExecutions != null ? { maxExecutions: terms.maxExecutions } : {}),
|
|
48
48
|
// Who does the work on an offer, who is paying on a request. The other slot is
|
|
49
49
|
// the open one you would be filling by claiming, so it carries no name yet.
|
|
50
|
-
...(named(parties.
|
|
51
|
-
...(named(parties.provider) ? { provider: parties.provider } : {}),
|
|
52
|
-
...(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 } : {}),
|
|
53
53
|
// Access the job cannot be done without — worth knowing before claiming it.
|
|
54
54
|
...(requiredConnections.length ? { requiredConnections } : {}),
|
|
55
55
|
createdAt: a?.createdAt,
|
|
@@ -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;
|
|
@@ -14,19 +14,65 @@ export function nextCall(env, capabilityKey, args, why) {
|
|
|
14
14
|
};
|
|
15
15
|
}
|
|
16
16
|
/**
|
|
17
|
-
* The other
|
|
17
|
+
* The other PRINCIPAL in a two-party agreement, from the perspective of
|
|
18
|
+
* `selfId`.
|
|
19
|
+
*
|
|
20
|
+
* This was `peerAgentId`, and it read the `actor` columns — the agents that
|
|
21
|
+
* carried the paperwork. Those became courier info when a link was re-keyed to
|
|
22
|
+
* the two people it belongs to, and on most links they are null, so the hint it
|
|
23
|
+
* fed either pre-filled a call with an agent that is no longer a door or
|
|
24
|
+
* silently vanished because the helper returned nothing.
|
|
25
|
+
*
|
|
26
|
+
* Named for the SLOT, not for what a link happens to put in it. On a link both
|
|
27
|
+
* principals are guaranteed to be people, because links are person-only and
|
|
28
|
+
* both sides are userIds by construction — but the same slots hold an org or an
|
|
29
|
+
* agent on other engagement kinds, so a caller who read "person" here and
|
|
30
|
+
* trusted it on a hire would be wrong. The person guarantee is a link-only
|
|
31
|
+
* property; ask the kind before relying on it.
|
|
18
32
|
*
|
|
19
33
|
* Returns null rather than guessing when the row does not identify one, because
|
|
20
34
|
* a pre-filled call naming the wrong counterparty is worse than no pre-filled
|
|
21
35
|
* call: the caller would run it, and it would do something they did not ask for.
|
|
22
36
|
*/
|
|
23
|
-
export function
|
|
37
|
+
export function peerPrincipalId(parties, selfId) {
|
|
24
38
|
if (!parties || !selfId)
|
|
25
39
|
return null;
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
40
|
+
const creator = parties.creator?.principal;
|
|
41
|
+
const provider = parties.provider?.principal;
|
|
42
|
+
if (creator && creator !== selfId)
|
|
43
|
+
return creator;
|
|
44
|
+
if (provider && provider !== selfId)
|
|
45
|
+
return provider;
|
|
31
46
|
return null;
|
|
32
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* The peer's principal, located by finding MY side rather than by knowing my
|
|
50
|
+
* own principal id.
|
|
51
|
+
*
|
|
52
|
+
* An agent knows its own agent id and not the id of the person it answers for,
|
|
53
|
+
* so it cannot ask {@link peerPrincipalId} which of two people it is. What it
|
|
54
|
+
* CAN recognise is its own courier stamp: if my agent id is in one side's
|
|
55
|
+
* `actor`, that side is mine and the other side's principal is the peer.
|
|
56
|
+
*
|
|
57
|
+
* Note the difference from the bug this replaced. Reading the peer's `actor` as
|
|
58
|
+
* the peer's address was wrong — those slots are courier info, null on most
|
|
59
|
+
* links, and never a door. Reading MY OWN `actor` to work out which side I am on
|
|
60
|
+
* is sound, because I am comparing against an id I hold.
|
|
61
|
+
*
|
|
62
|
+
* Returns null when neither side carries my stamp, which is the common case for
|
|
63
|
+
* a link two people formed from the web. No pre-filled call is the right answer
|
|
64
|
+
* there: naming the wrong counterparty would get run.
|
|
65
|
+
*/
|
|
66
|
+
export function peerPrincipalForCourier(parties, myAgentId) {
|
|
67
|
+
if (!parties || !myAgentId)
|
|
68
|
+
return null;
|
|
69
|
+
const mineIsCreator = parties.creator?.actor === myAgentId;
|
|
70
|
+
const mineIsProvider = parties.provider?.actor === myAgentId;
|
|
71
|
+
// Both sides stamped with me is one estate on both ends, not a peer.
|
|
72
|
+
if (mineIsCreator === mineIsProvider)
|
|
73
|
+
return null;
|
|
74
|
+
const peer = mineIsCreator
|
|
75
|
+
? parties.provider?.principal
|
|
76
|
+
: parties.creator?.principal;
|
|
77
|
+
return peer && peer !== myAgentId ? peer : null;
|
|
78
|
+
}
|
|
@@ -124,8 +124,12 @@ export declare function respondToAgreement(agreementId: string, action: 'approve
|
|
|
124
124
|
export interface PendingApprovalFacts {
|
|
125
125
|
/** Party ids with a pending entry on the approvals ledger. */
|
|
126
126
|
pendingPartyIds: readonly string[];
|
|
127
|
-
/**
|
|
128
|
-
|
|
127
|
+
/**
|
|
128
|
+
* The direct responder slot, when the proposal names one. A side names up to
|
|
129
|
+
* two ids — the accountable principal and the delegate acting for it — and
|
|
130
|
+
* either may be the one holding the decision.
|
|
131
|
+
*/
|
|
132
|
+
proposedToIds?: readonly string[];
|
|
129
133
|
}
|
|
130
134
|
/**
|
|
131
135
|
* Which of `candidateIds` holds the pending approval slot, first match wins —
|
|
@@ -182,17 +186,33 @@ export declare function getMyAgreements(filters: GetMyAgreementsFilters | undefi
|
|
|
182
186
|
/** One agreement, shaped: a link comes back as its summary. */
|
|
183
187
|
export declare function getAgreement(agreementId: string, creds: Creds): Promise<Agreement | null>;
|
|
184
188
|
export interface CreateLinkBody {
|
|
185
|
-
/**
|
|
186
|
-
|
|
189
|
+
/**
|
|
190
|
+
* Who to connect with: an email address, or the id of an agent that answers
|
|
191
|
+
* to them. Either way the link is with the PERSON — an id is only a way to
|
|
192
|
+
* find its owner, and an agent that answers to an org is refused. Omit for an
|
|
193
|
+
* open invite you share yourself.
|
|
194
|
+
*/
|
|
195
|
+
to?: string;
|
|
187
196
|
/** Message shown to whoever is asked to approve or claim it. */
|
|
188
197
|
description?: string;
|
|
189
198
|
/** Open invites only: how many people may claim this one link. Default 1. */
|
|
190
199
|
maxClaims?: number;
|
|
191
|
-
/** Which of the caller's own agents stands on their side of the link. */
|
|
192
|
-
asAgentId?: string;
|
|
193
200
|
}
|
|
194
201
|
/**
|
|
195
|
-
*
|
|
202
|
+
* The route's one answer, whatever was in `to`.
|
|
203
|
+
*
|
|
204
|
+
* Deliberately carries no agreement. Returning the row for an id target and a
|
|
205
|
+
* bare "sent" for an address made one of the two an existence oracle over the
|
|
206
|
+
* user table, so both say the same thing now: it went. `shareUrl` is null
|
|
207
|
+
* exactly when `to` was named, which the caller already knows.
|
|
208
|
+
*/
|
|
209
|
+
export interface CreateLinkResult {
|
|
210
|
+
ok: boolean;
|
|
211
|
+
status: 'sent';
|
|
212
|
+
shareUrl: string | null;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Connect with someone — by email, by agent id, or with a shareable invite.
|
|
196
216
|
*
|
|
197
217
|
* This posts to `POST /agreements/links`. It used to post to `POST /agreements`,
|
|
198
218
|
* which also created work agreements inline and skipped the gates the proposal
|
|
@@ -200,10 +220,7 @@ export interface CreateLinkBody {
|
|
|
200
220
|
* (`POST /agreements/proposals`) and the marketplace claim routes; this rail
|
|
201
221
|
* carries links and nothing else.
|
|
202
222
|
*/
|
|
203
|
-
export declare function createLink(body: CreateLinkBody, creds: Creds): Promise<
|
|
204
|
-
ok: boolean;
|
|
205
|
-
agreement: Agreement;
|
|
206
|
-
}>;
|
|
223
|
+
export declare function createLink(body: CreateLinkBody, creds: Creds): Promise<CreateLinkResult>;
|
|
207
224
|
export declare function revokeAgreement(agreementId: string, creds: Creds): Promise<{
|
|
208
225
|
ok: boolean;
|
|
209
226
|
agreement: Agreement;
|
|
@@ -225,7 +242,24 @@ export type ClaimedKind = 'link' | 'offer' | 'request' | 'hand-off';
|
|
|
225
242
|
*
|
|
226
243
|
* POST /agreements/:id/claim — canonical; no partyId in body.
|
|
227
244
|
*/
|
|
228
|
-
|
|
245
|
+
/**
|
|
246
|
+
* What a claimer can say about WHY it is claiming.
|
|
247
|
+
*
|
|
248
|
+
* A claim binds a party to terms somebody else posted, so the only thing there
|
|
249
|
+
* is to declare is the authority behind it: the job the claiming agent is
|
|
250
|
+
* already doing. Nothing here changes the row — the lineage of a posted
|
|
251
|
+
* agreement belongs to whoever posted it.
|
|
252
|
+
*/
|
|
253
|
+
export interface ClaimOptions {
|
|
254
|
+
/**
|
|
255
|
+
* The active agreement whose work this claim is part of. An agent acting
|
|
256
|
+
* inside a job its human approved does not need a second consent for the
|
|
257
|
+
* engagements that job requires, but it has to NAME the job — the server
|
|
258
|
+
* verifies the claim against the acting agent and never guesses one.
|
|
259
|
+
*/
|
|
260
|
+
mandateAgreementId?: string;
|
|
261
|
+
}
|
|
262
|
+
export declare function claimAgreement(agreementId: string, creds: Creds, opts?: ClaimOptions): Promise<{
|
|
229
263
|
ok: boolean;
|
|
230
264
|
agreement: Agreement;
|
|
231
265
|
kind?: ClaimedKind;
|
|
@@ -267,10 +301,7 @@ export declare class AgreementClient {
|
|
|
267
301
|
list(filters?: ListAgreementsFilters): Promise<Agreement[]>;
|
|
268
302
|
listMine(filters?: GetMyAgreementsFilters): Promise<Agreement[]>;
|
|
269
303
|
get(id: string): Promise<Agreement | null>;
|
|
270
|
-
createLink(data: CreateLinkBody): Promise<
|
|
271
|
-
ok: boolean;
|
|
272
|
-
agreement: Agreement;
|
|
273
|
-
}>;
|
|
304
|
+
createLink(data: CreateLinkBody): Promise<CreateLinkResult>;
|
|
274
305
|
revoke(id: string): Promise<{
|
|
275
306
|
ok: boolean;
|
|
276
307
|
agreement: Agreement;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { runtimeLog } from '../shared/runtimeLog.js';
|
|
2
2
|
import { getBackendUrl } from '../utils/urlUtils.js';
|
|
3
|
-
import { OPEN_AGREEMENT_TARGET, ORG_AGREEMENT_TARGET, isBroadcastTarget } from '../types.js';
|
|
3
|
+
import { OPEN_AGREEMENT_TARGET, ORG_AGREEMENT_TARGET, isBroadcastTarget, partySideIds } from '../types.js';
|
|
4
4
|
import { throwApiError } from '../shared/apiError.js';
|
|
5
5
|
// Lazy: read at call time so a `configureApiClient` call that lands after this
|
|
6
6
|
// module is imported still takes effect. Baking it at module-load time would
|
|
@@ -37,6 +37,10 @@ function assertCreds(creds, op) {
|
|
|
37
37
|
* unchanged.
|
|
38
38
|
*/
|
|
39
39
|
export function linkSummary(a) {
|
|
40
|
+
const side = (s) => ({
|
|
41
|
+
principal: s?.principal ?? null,
|
|
42
|
+
actor: s?.actor ?? null,
|
|
43
|
+
});
|
|
40
44
|
return {
|
|
41
45
|
agreementId: a.agreementId,
|
|
42
46
|
// Kept deliberately: callers branch on this, and a summary that hides what
|
|
@@ -44,11 +48,11 @@ export function linkSummary(a) {
|
|
|
44
48
|
engagementKind: a.engagementKind,
|
|
45
49
|
status: a.status,
|
|
46
50
|
proposalStatus: a.proposalStatus,
|
|
51
|
+
// No `payer`: a link carries no money, so the paying side is never occupied.
|
|
47
52
|
parties: {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
proposedTo: a.parties?.proposedTo ?? null,
|
|
53
|
+
creator: side(a.parties?.creator),
|
|
54
|
+
provider: side(a.parties?.provider),
|
|
55
|
+
proposedTo: side(a.parties?.proposedTo),
|
|
52
56
|
},
|
|
53
57
|
...(a.description ? { description: a.description } : {}),
|
|
54
58
|
// Seat bookkeeping on an open invite — link state, not commerce.
|
|
@@ -169,8 +173,8 @@ export async function respondToAgreement(agreementId, action, creds, opts = {})
|
|
|
169
173
|
ownerUserId: opts.ownerUserId,
|
|
170
174
|
agentId: creds.agentId,
|
|
171
175
|
});
|
|
172
|
-
const proposedTo = agreement.parties?.proposedTo;
|
|
173
|
-
if (isBroadcastTarget(proposedTo) || isBroadcastTarget(agreement.parties?.payer)) {
|
|
176
|
+
const proposedTo = agreement.parties?.proposedTo?.principal;
|
|
177
|
+
if (isBroadcastTarget(proposedTo) || isBroadcastTarget(agreement.parties?.payer?.principal)) {
|
|
174
178
|
// respond is approve/reject on DIRECT proposals only. An open
|
|
175
179
|
// broadcast (request, standing offer, link invite) has no per-recipient
|
|
176
180
|
// approval slot — a BYSTANDER can neither approve nor reject it
|
|
@@ -206,11 +210,12 @@ export function resolvePendingApprovalPartyId(facts, candidateIds) {
|
|
|
206
210
|
}
|
|
207
211
|
// A row with no pending ledger entries is a legacy one (approvals[] predates
|
|
208
212
|
//), and there the named responder slot IS the decision.
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
actorIds
|
|
212
|
-
|
|
213
|
-
|
|
213
|
+
if (facts.pendingPartyIds.length === 0) {
|
|
214
|
+
const responderIds = facts.proposedToIds ?? [];
|
|
215
|
+
for (const id of actorIds) {
|
|
216
|
+
if (responderIds.includes(id))
|
|
217
|
+
return id;
|
|
218
|
+
}
|
|
214
219
|
}
|
|
215
220
|
return null;
|
|
216
221
|
}
|
|
@@ -223,7 +228,7 @@ export function resolveMyPendingApprovalPartyId(agreement, opts) {
|
|
|
223
228
|
pendingPartyIds: (agreement.approvals ?? [])
|
|
224
229
|
.filter((a) => a.status === 'pending' || a.status === 'PENDING')
|
|
225
230
|
.map((a) => a.partyId),
|
|
226
|
-
|
|
231
|
+
proposedToIds: partySideIds(agreement.parties?.proposedTo),
|
|
227
232
|
}, [opts.agentId, opts.ownerUserId]);
|
|
228
233
|
}
|
|
229
234
|
/**
|
|
@@ -407,7 +412,7 @@ export async function getAgreement(agreementId, creds) {
|
|
|
407
412
|
return agreement ? shapeAgreement(agreement) : null;
|
|
408
413
|
}
|
|
409
414
|
/**
|
|
410
|
-
*
|
|
415
|
+
* Connect with someone — by email, by agent id, or with a shareable invite.
|
|
411
416
|
*
|
|
412
417
|
* This posts to `POST /agreements/links`. It used to post to `POST /agreements`,
|
|
413
418
|
* which also created work agreements inline and skipped the gates the proposal
|
|
@@ -429,11 +434,15 @@ export async function createLink(body, creds) {
|
|
|
429
434
|
throwApiError(res, responseBody, `Link creation failed: ${res.status} ${res.statusText}`);
|
|
430
435
|
}
|
|
431
436
|
const data = await res.json().catch(() => null);
|
|
432
|
-
if (
|
|
433
|
-
throw new Error('Invalid response: expected { ok,
|
|
437
|
+
if (data?.['status'] !== 'sent') {
|
|
438
|
+
throw new Error('Invalid response: expected { ok, status: "sent", shareUrl } from POST /agreements/links');
|
|
434
439
|
}
|
|
435
|
-
const
|
|
436
|
-
return {
|
|
440
|
+
const shareUrl = data['shareUrl'];
|
|
441
|
+
return {
|
|
442
|
+
ok: data['ok'] === true,
|
|
443
|
+
status: 'sent',
|
|
444
|
+
shareUrl: typeof shareUrl === 'string' ? shareUrl : null,
|
|
445
|
+
};
|
|
437
446
|
}
|
|
438
447
|
export async function revokeAgreement(agreementId, creds) {
|
|
439
448
|
if (!agreementId)
|
|
@@ -477,20 +486,15 @@ export async function fulfillAgreement(agreementId, creds) {
|
|
|
477
486
|
const envelope = data;
|
|
478
487
|
return { ...envelope, agreement: shapeAgreement(envelope.agreement) };
|
|
479
488
|
}
|
|
480
|
-
|
|
481
|
-
* Claim an open agreement. Three shapes are claimable:
|
|
482
|
-
* - open link invite (`engagementKind: link`, proposedTo everyone)
|
|
483
|
-
* - open broadcast request (proposedTo 'everyone', status open)
|
|
484
|
-
* - org-scoped broadcast request (proposedTo 'org') — the server rejects the
|
|
485
|
-
* claim with 403 if the caller is not a member of the agreement's orgId
|
|
486
|
-
*
|
|
487
|
-
* POST /agreements/:id/claim — canonical; no partyId in body.
|
|
488
|
-
*/
|
|
489
|
-
export async function claimAgreement(agreementId, creds) {
|
|
489
|
+
export async function claimAgreement(agreementId, creds, opts = {}) {
|
|
490
490
|
if (!agreementId)
|
|
491
491
|
throw new Error('agreementId is required to claim an open agreement');
|
|
492
492
|
assertCreds(creds, 'open agreement claim');
|
|
493
|
-
const res = await fetch(`${getAgreementBaseUrl()}/${encodeURIComponent(agreementId)}/claim`, {
|
|
493
|
+
const res = await fetch(`${getAgreementBaseUrl()}/${encodeURIComponent(agreementId)}/claim`, {
|
|
494
|
+
method: 'POST',
|
|
495
|
+
headers: buildHeaders(creds),
|
|
496
|
+
body: JSON.stringify(opts.mandateAgreementId ? { mandateAgreementId: opts.mandateAgreementId } : {}),
|
|
497
|
+
});
|
|
494
498
|
if (!res.ok) {
|
|
495
499
|
const body = await res.text().catch(() => '');
|
|
496
500
|
throwApiError(res, body, `Open agreement claim failed: ${res.status} ${res.statusText}`);
|
package/dist/http/InboxClient.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { getBackendUrl } from '../utils/urlUtils.js';
|
|
2
2
|
import { pollSurfaceError } from '../shared/rateLimit.js';
|
|
3
|
+
import { INBOX_HOST_CLAIMANT_HEADER, instanceIdentity, } from '../instanceIdentity.js';
|
|
3
4
|
/**
|
|
4
5
|
* The doorbell, not the door: references addressed to this agent
|
|
5
6
|
* since its last ack — never content. Flow: inbox → read → act → ack
|
|
@@ -24,6 +25,9 @@ export class InboxClient {
|
|
|
24
25
|
const headers = {
|
|
25
26
|
Authorization: `Bearer ${this.operatorKey}`,
|
|
26
27
|
'Content-Type': 'application/json',
|
|
28
|
+
// This process, not the API. Two fleets of the same agent share
|
|
29
|
+
// one backend task; without this they both wake.
|
|
30
|
+
[INBOX_HOST_CLAIMANT_HEADER]: instanceIdentity(),
|
|
27
31
|
};
|
|
28
32
|
if (this.agentId)
|
|
29
33
|
headers['X-Agent-Id'] = this.agentId;
|
|
@@ -23,7 +23,6 @@ export interface PullOffersOptions {
|
|
|
23
23
|
since?: string;
|
|
24
24
|
}
|
|
25
25
|
export declare function pullOffers(options: PullOffersOptions | undefined, creds: Creds): Promise<Agreement[]>;
|
|
26
|
-
export declare function claimOffer(agreementId: string, creds: Creds): Promise<Agreement>;
|
|
27
26
|
export interface PublishRequestPayload {
|
|
28
27
|
/** See PublishOfferPayload.billing. */
|
|
29
28
|
billing?: 'total' | 'per_task';
|
|
@@ -53,7 +52,6 @@ export declare class MarketplaceClient {
|
|
|
53
52
|
constructor(operatorKey: string, agentId?: string);
|
|
54
53
|
publishOffer(payload: PublishOfferPayload): Promise<Agreement>;
|
|
55
54
|
pullOffers(options?: PullOffersOptions): Promise<Agreement[]>;
|
|
56
|
-
claimOffer(agreementId: string): Promise<Agreement>;
|
|
57
55
|
publishRequest(payload: PublishRequestPayload): Promise<Agreement>;
|
|
58
56
|
pullRequests(options?: PullRequestsOptions): Promise<Agreement[]>;
|
|
59
57
|
}
|
|
@@ -52,24 +52,6 @@ export async function pullOffers(options, creds) {
|
|
|
52
52
|
const data = await res.json().catch(() => null);
|
|
53
53
|
return data?.['offers'] ?? [];
|
|
54
54
|
}
|
|
55
|
-
export async function claimOffer(agreementId, creds) {
|
|
56
|
-
if (!agreementId)
|
|
57
|
-
throw new Error('agreementId is required');
|
|
58
|
-
assertCreds(creds, 'marketplace offer claim');
|
|
59
|
-
const res = await fetch(`${getMarketplaceBaseUrl()}/offers/claim`, {
|
|
60
|
-
method: 'POST',
|
|
61
|
-
headers: buildHeaders(creds),
|
|
62
|
-
body: JSON.stringify({ agreementId }),
|
|
63
|
-
});
|
|
64
|
-
if (!res.ok) {
|
|
65
|
-
const body = await res.text().catch(() => '');
|
|
66
|
-
throwApiError(res, body, `Marketplace offer claim failed: ${res.status}`);
|
|
67
|
-
}
|
|
68
|
-
const data = await res.json().catch(() => null);
|
|
69
|
-
if (!data?.['ok'])
|
|
70
|
-
throw new Error(data?.['error'] || 'Claim failed');
|
|
71
|
-
return shapeAgreement(data['offer']);
|
|
72
|
-
}
|
|
73
55
|
export async function publishRequest(payload, creds) {
|
|
74
56
|
assertCreds(creds, 'request publish');
|
|
75
57
|
const res = await fetch(`${getMarketplaceBaseUrl()}/requests/publish`, {
|
|
@@ -120,7 +102,6 @@ export class MarketplaceClient {
|
|
|
120
102
|
}
|
|
121
103
|
publishOffer(payload) { return publishOffer(payload, this.creds); }
|
|
122
104
|
pullOffers(options) { return pullOffers(options, this.creds); }
|
|
123
|
-
claimOffer(agreementId) { return claimOffer(agreementId, this.creds); }
|
|
124
105
|
publishRequest(payload) { return publishRequest(payload, this.creds); }
|
|
125
106
|
pullRequests(options) { return pullRequests(options, this.creds); }
|
|
126
107
|
}
|
package/dist/http/TaskClient.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getBackendUrl } from '../utils/urlUtils.js';
|
|
2
|
+
import { partyActorIds } from '../types.js';
|
|
2
3
|
import { throwApiError } from '../shared/apiError.js';
|
|
3
4
|
function getTaskBaseUrl() { return `${getBackendUrl()}/tasks`; }
|
|
4
5
|
function buildHeaders(creds) {
|
|
@@ -159,13 +160,7 @@ async function getActiveTasksForAgentViaPartyAgreements(agentId, creds) {
|
|
|
159
160
|
const { listAgreements } = await import('./AgreementClient.js');
|
|
160
161
|
const agreements = await listAgreements({ status: 'active' }, creds);
|
|
161
162
|
const partyIds = agreements
|
|
162
|
-
.filter((a) =>
|
|
163
|
-
const p = a.parties ?? {};
|
|
164
|
-
return (p.provider === agentId ||
|
|
165
|
-
p.providerAgent === agentId ||
|
|
166
|
-
p.creator === agentId ||
|
|
167
|
-
p.payer === agentId);
|
|
168
|
-
})
|
|
163
|
+
.filter((a) => partyActorIds(a.parties).includes(agentId))
|
|
169
164
|
.map((a) => a.agreementId)
|
|
170
165
|
.filter(Boolean);
|
|
171
166
|
if (partyIds.length === 0)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ClaimedKind, type ProposeTerms } from './AgreementClient.js';
|
|
1
|
+
import { type ClaimedKind, type ClaimOptions, type ProposeTerms } from './AgreementClient.js';
|
|
2
2
|
import { type Agreement, type Creds, type EngagementKind } from '../types.js';
|
|
3
3
|
/**
|
|
4
4
|
* one propose grammar. Direct, broadcast (request and standing
|
|
@@ -6,7 +6,6 @@ import { type Agreement, type Creds, type EngagementKind } from '../types.js';
|
|
|
6
6
|
* single propose tool instead of dedicated publish/request tools.
|
|
7
7
|
*
|
|
8
8
|
* Routing:
|
|
9
|
-
* - engagementKind 'link' → POST /agreements (link proposal / open invite)
|
|
10
9
|
* - proposedTo 'everyone' | 'org', providerId = self → seller-broadcast standing offer
|
|
11
10
|
* - proposedTo 'everyone' | 'org', no providerId → buyer-broadcast request
|
|
12
11
|
* - anything else → direct proposal
|
|
@@ -17,7 +16,7 @@ export interface UnifiedProposeInput extends ProposeTerms {
|
|
|
17
16
|
chatId?: string;
|
|
18
17
|
engagementKind?: EngagementKind;
|
|
19
18
|
}
|
|
20
|
-
export type ProposeShape = 'direct' | 'request' | 'offer'
|
|
19
|
+
export type ProposeShape = 'direct' | 'request' | 'offer';
|
|
21
20
|
export declare function proposeUnified(input: UnifiedProposeInput, creds: Creds): Promise<{
|
|
22
21
|
agreement: Agreement;
|
|
23
22
|
shape: ProposeShape;
|
|
@@ -36,7 +35,7 @@ export declare function proposeUnified(input: UnifiedProposeInput, creds: Creds)
|
|
|
36
35
|
* `kind` arrives on the claim response — the route that did the routing reports
|
|
37
36
|
* which broadcast kind this turned out to be.
|
|
38
37
|
*/
|
|
39
|
-
export declare function claimOpenAgreement(agreementId: string, creds: Creds): Promise<{
|
|
38
|
+
export declare function claimOpenAgreement(agreementId: string, creds: Creds, opts?: ClaimOptions): Promise<{
|
|
40
39
|
agreement: Agreement;
|
|
41
40
|
kind: ClaimedKind;
|
|
42
41
|
}>;
|
|
@@ -1,20 +1,15 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { claimAgreement, proposeBroadcast, proposeDirectTo, } from './AgreementClient.js';
|
|
2
2
|
import { publishOffer } from './MarketplaceClient.js';
|
|
3
3
|
import { isBroadcastTarget, } from '../types.js';
|
|
4
4
|
export async function proposeUnified(input, creds) {
|
|
5
5
|
const { proposedTo, chatId, engagementKind, providerId, ...terms } = input;
|
|
6
6
|
if (!proposedTo)
|
|
7
7
|
throw new Error('proposedTo is required (a user/agent id, or "everyone"/"org" to broadcast)');
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
...(target ? { targetAgentId: target } : {}),
|
|
14
|
-
...(terms.description ? { description: terms.description } : {}),
|
|
15
|
-
}, creds);
|
|
16
|
-
return { agreement, shape: 'link' };
|
|
17
|
-
}
|
|
8
|
+
// The 'link' arm is gone. A link is not a proposal shape: it has no
|
|
9
|
+
// price, no chat and no work, and it is formed by `link_propose`, which takes
|
|
10
|
+
// an email or an agent id and answers the same either way. Keeping a second
|
|
11
|
+
// door here would have meant a caller reaching a link through the propose
|
|
12
|
+
// grammar, where the constant answer and the org-root refusal do not apply.
|
|
18
13
|
if (isBroadcastTarget(proposedTo)) {
|
|
19
14
|
const audience = proposedTo;
|
|
20
15
|
if (providerId && creds.agentId && providerId === creds.agentId) {
|
|
@@ -68,10 +63,10 @@ export async function proposeUnified(input, creds) {
|
|
|
68
63
|
* `kind` arrives on the claim response — the route that did the routing reports
|
|
69
64
|
* which broadcast kind this turned out to be.
|
|
70
65
|
*/
|
|
71
|
-
export async function claimOpenAgreement(agreementId, creds) {
|
|
66
|
+
export async function claimOpenAgreement(agreementId, creds, opts = {}) {
|
|
72
67
|
if (!agreementId)
|
|
73
68
|
throw new Error('agreementId is required');
|
|
74
|
-
const { agreement, kind } = await claimAgreement(agreementId, creds);
|
|
69
|
+
const { agreement, kind } = await claimAgreement(agreementId, creds, opts);
|
|
75
70
|
// A server that has not shipped the `kind` field yet still claims correctly;
|
|
76
71
|
// 'request' is the shape the route has always handled.
|
|
77
72
|
return { agreement, kind: kind ?? 'request' };
|
package/dist/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ export * from './http/index.js';
|
|
|
2
2
|
export * from './capabilities/index.js';
|
|
3
3
|
export * from './relay/provisionRelayWorkers.js';
|
|
4
4
|
export { ConnectionManager } from './ConnectionManager.js';
|
|
5
|
-
export { EntryTypes, ContentTypes, OPEN_AGREEMENT_TARGET, ORG_AGREEMENT_TARGET, BROADCAST_TARGETS, isBroadcastTarget, isPersonaRef, isRoomPresentationRef, isOpaquePresentationRef, AGREEMENT_ENGAGEMENT_KIND, isValidContentType, } from './types.js';
|
|
5
|
+
export { EntryTypes, ContentTypes, OPEN_AGREEMENT_TARGET, ORG_AGREEMENT_TARGET, BROADCAST_TARGETS, isBroadcastTarget, isPersonaRef, isRoomPresentationRef, isOpaquePresentationRef, AGREEMENT_ENGAGEMENT_KIND, isValidContentType, partySideIds, partyActorIds, } from './types.js';
|
|
6
6
|
export type { PrincipalPresentation } from './types.js';
|
|
7
7
|
export { getBackendUrl } from './utils/urlUtils.js';
|
|
8
8
|
export { configureApiClient, apiClientConfig } from './config.js';
|
|
@@ -11,5 +11,5 @@ export { runtimeLog, resetRuntimeLogLevelCache } from './shared/runtimeLog.js';
|
|
|
11
11
|
export { RateLimitedError, isRateLimited, parseRetryAfterMs, pollSurfaceError, } from './shared/rateLimit.js';
|
|
12
12
|
export { parseErrorMessage, parseErrorCode, throwApiError, } from './shared/apiError.js';
|
|
13
13
|
export { ApiError } from './types.js';
|
|
14
|
-
export type { Creds, Task, TaskState, PlanStep, PlanStepStatus, Agreement, EngagementKind, BroadcastAudience, EntryType, ContentType, MessageMetadata, MessageHandler, InboxDeliveryKind, InboxDeliveryRef, InboxChatNews, InboxProposalRef, InboxTaskRef, InboxRequestRef, InboxConnectionRequestRef, InboxHumanAttention, InboxEnvelope, InboxAckResult, InboxReadOptions, } from './types.js';
|
|
15
|
-
export type { ProposeTerms, ProposeDirectInput, ProposeBroadcastInput, ProposeAgreementData, } from './http/AgreementClient.js';
|
|
14
|
+
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, InboxReadOptions, } from './types.js';
|
|
15
|
+
export type { ProposeTerms, ProposeDirectInput, ProposeBroadcastInput, ProposeAgreementData, ClaimOptions, } from './http/AgreementClient.js';
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@ export * from './http/index.js';
|
|
|
2
2
|
export * from './capabilities/index.js';
|
|
3
3
|
export * from './relay/provisionRelayWorkers.js';
|
|
4
4
|
export { ConnectionManager } from './ConnectionManager.js';
|
|
5
|
-
export { EntryTypes, ContentTypes, OPEN_AGREEMENT_TARGET, ORG_AGREEMENT_TARGET, BROADCAST_TARGETS, isBroadcastTarget, isPersonaRef, isRoomPresentationRef, isOpaquePresentationRef, AGREEMENT_ENGAGEMENT_KIND, isValidContentType, } from './types.js';
|
|
5
|
+
export { EntryTypes, ContentTypes, OPEN_AGREEMENT_TARGET, ORG_AGREEMENT_TARGET, BROADCAST_TARGETS, isBroadcastTarget, isPersonaRef, isRoomPresentationRef, isOpaquePresentationRef, AGREEMENT_ENGAGEMENT_KIND, isValidContentType, partySideIds, partyActorIds, } from './types.js';
|
|
6
6
|
export { getBackendUrl } from './utils/urlUtils.js';
|
|
7
7
|
// the host injects the environment; this package never reads it.
|
|
8
8
|
export { configureApiClient, apiClientConfig } from './config.js';
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { hostname } from 'node:os';
|
|
2
|
+
/**
|
|
3
|
+
* Which process is calling GET /inbox.
|
|
4
|
+
*
|
|
5
|
+
* The exclusive-read claimant must be the *host* (this fleet task, a
|
|
6
|
+
* laptop fleet, an MCP process), not the API process. Two fleets share
|
|
7
|
+
* one backend task, so stamping the API's identity would let both wake.
|
|
8
|
+
*
|
|
9
|
+
* `ecs:<id>` on Fargate, `local:<host>:<pid>` otherwise. Env-derived
|
|
10
|
+
* and memoized.
|
|
11
|
+
*/
|
|
12
|
+
let cached = null;
|
|
13
|
+
const MAX_LENGTH = 64;
|
|
14
|
+
export const INBOX_HOST_CLAIMANT_HEADER = 'X-Ziggs-Instance';
|
|
15
|
+
export function instanceIdentity() {
|
|
16
|
+
if (cached)
|
|
17
|
+
return cached;
|
|
18
|
+
cached = resolve().slice(0, MAX_LENGTH);
|
|
19
|
+
return cached;
|
|
20
|
+
}
|
|
21
|
+
/** Test seam: forget the memoized value so a case can set different env. */
|
|
22
|
+
export function resetInstanceIdentityForTests() {
|
|
23
|
+
cached = null;
|
|
24
|
+
}
|
|
25
|
+
function resolve() {
|
|
26
|
+
const metadataUri = process.env.ECS_CONTAINER_METADATA_URI_V4 ??
|
|
27
|
+
process.env.ECS_CONTAINER_METADATA_URI;
|
|
28
|
+
if (metadataUri) {
|
|
29
|
+
const id = metadataUri.split('/').filter(Boolean).pop() ?? '';
|
|
30
|
+
const short = id.replace(/[^A-Za-z0-9]/g, '').slice(0, 12);
|
|
31
|
+
if (short.length >= 8)
|
|
32
|
+
return `ecs:${short}`;
|
|
33
|
+
}
|
|
34
|
+
const host = process.env.HOSTNAME || safeHostname();
|
|
35
|
+
return `local:${host}:${process.pid}`;
|
|
36
|
+
}
|
|
37
|
+
function safeHostname() {
|
|
38
|
+
try {
|
|
39
|
+
return hostname() || 'unknown-host';
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return 'unknown-host';
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Agreement, Creds } from '../types.js';
|
|
2
|
-
import {
|
|
2
|
+
import { claimOpenAgreement, delegateAgreement, getMyAgreements, pullOffers } from '../http/index.js';
|
|
3
3
|
/** Minimal relay step shape — keep field names aligned with agents/coordinators/relayTypes.ts */
|
|
4
4
|
export interface RelayStepInput {
|
|
5
5
|
stepId: string;
|
|
@@ -28,7 +28,7 @@ export type ProvisionedRelayStep = RelayPayloadShape['steps'][number] & {
|
|
|
28
28
|
export interface RelayProvisionDeps {
|
|
29
29
|
getMyAgreements: typeof getMyAgreements;
|
|
30
30
|
pullOffers: typeof pullOffers;
|
|
31
|
-
|
|
31
|
+
claimOpenAgreement: typeof claimOpenAgreement;
|
|
32
32
|
delegateAgreement: typeof delegateAgreement;
|
|
33
33
|
}
|
|
34
34
|
export interface ProvisionRelayWorkersInput {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { claimOpenAgreement, delegateAgreement, getMyAgreements, pullOffers, } from '../http/index.js';
|
|
2
2
|
function providerAgentId(agreement) {
|
|
3
|
-
return agreement.parties?.
|
|
3
|
+
return agreement.parties?.provider?.actor ?? null;
|
|
4
4
|
}
|
|
5
5
|
function isActiveAgreement(agreement) {
|
|
6
6
|
return agreement.status === 'active';
|
|
@@ -30,7 +30,7 @@ export function findOpenOfferForAgent(assigneeId, offers) {
|
|
|
30
30
|
export async function provisionRelayWorkers(input, deps = {
|
|
31
31
|
getMyAgreements,
|
|
32
32
|
pullOffers,
|
|
33
|
-
|
|
33
|
+
claimOpenAgreement,
|
|
34
34
|
delegateAgreement,
|
|
35
35
|
}) {
|
|
36
36
|
const { creds, hireAgreementId, chatId, steps, inputArtifactIds } = input;
|
|
@@ -55,7 +55,7 @@ export async function provisionRelayWorkers(input, deps = {
|
|
|
55
55
|
continue;
|
|
56
56
|
}
|
|
57
57
|
if (step.offerAgreementId) {
|
|
58
|
-
const claimed = await deps.
|
|
58
|
+
const { agreement: claimed } = await deps.claimOpenAgreement(step.offerAgreementId, creds);
|
|
59
59
|
const agreementId = claimed.agreementId;
|
|
60
60
|
const status = isActiveAgreement(claimed)
|
|
61
61
|
? 'active'
|
|
@@ -78,7 +78,7 @@ export async function provisionRelayWorkers(input, deps = {
|
|
|
78
78
|
}
|
|
79
79
|
const openOffer = findOpenOfferForAgent(step.assigneeId, offersCache);
|
|
80
80
|
if (openOffer?.agreementId) {
|
|
81
|
-
const claimed = await deps.
|
|
81
|
+
const { agreement: claimed } = await deps.claimOpenAgreement(openOffer.agreementId, creds);
|
|
82
82
|
const agreementId = claimed.agreementId;
|
|
83
83
|
const status = isActiveAgreement(claimed)
|
|
84
84
|
? 'active'
|
package/dist/types.d.ts
CHANGED
|
@@ -91,19 +91,46 @@ export declare const AGREEMENT_ENGAGEMENT_KIND: {
|
|
|
91
91
|
readonly LINK: "link";
|
|
92
92
|
};
|
|
93
93
|
export type EngagementKind = (typeof AGREEMENT_ENGAGEMENT_KIND)[keyof typeof AGREEMENT_ENGAGEMENT_KIND];
|
|
94
|
+
/**
|
|
95
|
+
* One side of an agreement.
|
|
96
|
+
*
|
|
97
|
+
* An agent id occupies `actor` and nowhere else, so "is this agent on this
|
|
98
|
+
* side?" is a structural read rather than a lookup against the Agent
|
|
99
|
+
* collection. Asking `principal` about an agent id is always the wrong
|
|
100
|
+
* question — it would match that agent's estate instead.
|
|
101
|
+
*/
|
|
102
|
+
export interface AgreementPartySide {
|
|
103
|
+
/**
|
|
104
|
+
* The accountable person or org — never an agent. On `payer` and `proposedTo`
|
|
105
|
+
* this may instead hold a broadcast sentinel ({@link isBroadcastTarget}), in
|
|
106
|
+
* which case nobody has taken the side up yet.
|
|
107
|
+
*/
|
|
108
|
+
principal?: string | null;
|
|
109
|
+
/** The agent that performed on this side. Null when the principal acted itself. */
|
|
110
|
+
actor?: string | null;
|
|
111
|
+
}
|
|
112
|
+
/** The four sides of an agreement, each {@link AgreementPartySide}. */
|
|
113
|
+
export interface AgreementParties {
|
|
114
|
+
payer?: AgreementPartySide;
|
|
115
|
+
provider?: AgreementPartySide;
|
|
116
|
+
proposedTo?: AgreementPartySide;
|
|
117
|
+
creator?: AgreementPartySide;
|
|
118
|
+
}
|
|
119
|
+
/** Both ids on one side, principal first, absent ones dropped. */
|
|
120
|
+
export declare function partySideIds(side: AgreementPartySide | null | undefined): string[];
|
|
121
|
+
/**
|
|
122
|
+
* Every agent that acted on this agreement, deduped.
|
|
123
|
+
*
|
|
124
|
+
* The read for "is this agent a party?" — an agent id lives in `actor` alone,
|
|
125
|
+
* so widening the match to `principal` would hit an unrelated estate.
|
|
126
|
+
*/
|
|
127
|
+
export declare function partyActorIds(parties: AgreementParties | null | undefined): string[];
|
|
94
128
|
/** Compact agreement reference carried by task reads. */
|
|
95
129
|
export interface AgreementSummary {
|
|
96
130
|
agreementId: string;
|
|
97
131
|
description: string;
|
|
98
132
|
status: AgreementStatus;
|
|
99
|
-
parties?:
|
|
100
|
-
proposedTo?: string | null;
|
|
101
|
-
provider?: string | null;
|
|
102
|
-
payer?: string | null;
|
|
103
|
-
creator?: string | null;
|
|
104
|
-
creatorAgent?: string | null;
|
|
105
|
-
providerAgent?: string | null;
|
|
106
|
-
};
|
|
133
|
+
parties?: AgreementParties;
|
|
107
134
|
/** The opposite party relative to the requesting agent; null for observers,
|
|
108
135
|
* broadcast sentinels, and self-agreements. */
|
|
109
136
|
counterparty: string | null;
|
|
@@ -120,14 +147,7 @@ export interface Agreement {
|
|
|
120
147
|
status?: AgreementStatus;
|
|
121
148
|
engagementKind?: EngagementKind;
|
|
122
149
|
proposalStatus?: ProposalStatus;
|
|
123
|
-
parties?:
|
|
124
|
-
proposedTo?: string;
|
|
125
|
-
provider?: string;
|
|
126
|
-
payer?: string;
|
|
127
|
-
creator?: string;
|
|
128
|
-
creatorAgent?: string;
|
|
129
|
-
providerAgent?: string;
|
|
130
|
-
};
|
|
150
|
+
parties?: AgreementParties;
|
|
131
151
|
approvals?: AgreementApprovalEntry[];
|
|
132
152
|
/** Legacy root-level price — always null in practice. Use money.price instead. */
|
|
133
153
|
price?: number | null;
|
package/dist/types.js
CHANGED
|
@@ -34,6 +34,24 @@ export const AGREEMENT_ENGAGEMENT_KIND = {
|
|
|
34
34
|
/** a bilateral reach link between two agents (no money, no work). */
|
|
35
35
|
LINK: 'link',
|
|
36
36
|
};
|
|
37
|
+
/** The four sides, in the order every fan-out walks them. */
|
|
38
|
+
const AGREEMENT_PARTY_SIDES = ['payer', 'provider', 'proposedTo', 'creator'];
|
|
39
|
+
/** Both ids on one side, principal first, absent ones dropped. */
|
|
40
|
+
export function partySideIds(side) {
|
|
41
|
+
return [side?.principal, side?.actor].filter((id) => typeof id === 'string' && id.length > 0);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Every agent that acted on this agreement, deduped.
|
|
45
|
+
*
|
|
46
|
+
* The read for "is this agent a party?" — an agent id lives in `actor` alone,
|
|
47
|
+
* so widening the match to `principal` would hit an unrelated estate.
|
|
48
|
+
*/
|
|
49
|
+
export function partyActorIds(parties) {
|
|
50
|
+
const p = parties ?? {};
|
|
51
|
+
return [
|
|
52
|
+
...new Set(AGREEMENT_PARTY_SIDES.map((name) => p[name]?.actor).filter((id) => typeof id === 'string' && id.length > 0)),
|
|
53
|
+
];
|
|
54
|
+
}
|
|
37
55
|
/** ⚠️ Mirrors the server's entry-type constant; the server is authoritative. */
|
|
38
56
|
export const EntryTypes = {
|
|
39
57
|
MESSAGE: 'message',
|