@ziggs-ai/api-client 0.12.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/capabilities/agreementVerbs.js +13 -0
- package/dist/capabilities/context.js +2 -2
- package/dist/capabilities/grants.d.ts +2 -1
- package/dist/capabilities/grants.js +4 -3
- package/dist/http/AgreementClient.d.ts +9 -12
- package/dist/http/AgreementClient.js +3 -66
- package/dist/http/ContextGrantsClient.d.ts +10 -4
- package/dist/http/MarketplaceClient.d.ts +0 -16
- package/dist/http/MarketplaceClient.js +0 -17
- package/dist/http/TaskClient.d.ts +15 -1
- package/dist/http/grants.d.ts +9 -3
- package/dist/http/grants.js +14 -1
- package/dist/relay/provisionRelayWorkers.d.ts +67 -1
- package/dist/relay/provisionRelayWorkers.js +135 -15
- package/dist/types.d.ts +17 -0
- package/package.json +1 -1
|
@@ -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
|
},
|
|
@@ -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
|
|
136
|
-
mcp: 'Expand a grant you hold into the chat/agreement ids inside
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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: {
|
|
@@ -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
|
|
269
|
-
*
|
|
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 ["
|
|
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
|
|
518
|
-
*
|
|
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
|
}
|
|
@@ -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
|
-
*
|
|
4
|
-
* shared without sharing any chat or agreement it sits in.
|
|
5
|
-
*
|
|
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 =
|
|
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;
|
|
@@ -23,21 +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 interface PublishRequestPayload {
|
|
27
|
-
/** See PublishOfferPayload.billing. */
|
|
28
|
-
billing?: 'total' | 'per_task';
|
|
29
|
-
description: string;
|
|
30
|
-
chatId?: string;
|
|
31
|
-
payerId?: string;
|
|
32
|
-
price?: number;
|
|
33
|
-
lifecycle?: string;
|
|
34
|
-
expiresAt?: string;
|
|
35
|
-
maxExecutions?: number;
|
|
36
|
-
agreementDescription?: string;
|
|
37
|
-
/** Broadcast audience: 'everyone' (default, fully public) or 'org' (members of your active org only). */
|
|
38
|
-
audience?: BroadcastAudience;
|
|
39
|
-
}
|
|
40
|
-
export declare function publishRequest(payload: PublishRequestPayload, creds: Creds): Promise<Agreement>;
|
|
41
26
|
export interface PullRequestsOptions {
|
|
42
27
|
limit?: number;
|
|
43
28
|
since?: string;
|
|
@@ -52,6 +37,5 @@ export declare class MarketplaceClient {
|
|
|
52
37
|
constructor(operatorKey: string, agentId?: string);
|
|
53
38
|
publishOffer(payload: PublishOfferPayload): Promise<Agreement>;
|
|
54
39
|
pullOffers(options?: PullOffersOptions): Promise<Agreement[]>;
|
|
55
|
-
publishRequest(payload: PublishRequestPayload): Promise<Agreement>;
|
|
56
40
|
pullRequests(options?: PullRequestsOptions): Promise<Agreement[]>;
|
|
57
41
|
}
|
|
@@ -52,22 +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 publishRequest(payload, creds) {
|
|
56
|
-
assertCreds(creds, 'request publish');
|
|
57
|
-
const res = await fetch(`${getMarketplaceBaseUrl()}/requests/publish`, {
|
|
58
|
-
method: 'POST',
|
|
59
|
-
headers: buildHeaders(creds),
|
|
60
|
-
body: JSON.stringify(payload || {}),
|
|
61
|
-
});
|
|
62
|
-
if (!res.ok) {
|
|
63
|
-
const body = await res.text().catch(() => '');
|
|
64
|
-
throwApiError(res, body, `Request publish failed: ${res.status}`);
|
|
65
|
-
}
|
|
66
|
-
const data = await res.json().catch(() => null);
|
|
67
|
-
if (!data?.['agreement'])
|
|
68
|
-
throw new Error('Request publish returned no agreement');
|
|
69
|
-
return shapeAgreement(data['agreement']);
|
|
70
|
-
}
|
|
71
55
|
export async function pullRequests(options, creds) {
|
|
72
56
|
assertCreds(creds, 'request pull');
|
|
73
57
|
const params = new URLSearchParams();
|
|
@@ -102,6 +86,5 @@ export class MarketplaceClient {
|
|
|
102
86
|
}
|
|
103
87
|
publishOffer(payload) { return publishOffer(payload, this.creds); }
|
|
104
88
|
pullOffers(options) { return pullOffers(options, this.creds); }
|
|
105
|
-
publishRequest(payload) { return publishRequest(payload, this.creds); }
|
|
106
89
|
pullRequests(options) { return pullRequests(options, this.creds); }
|
|
107
90
|
}
|
|
@@ -37,10 +37,24 @@ export interface CreateTaskData {
|
|
|
37
37
|
planReviewTiming?: PlanReviewTiming;
|
|
38
38
|
requireMidWorkPlanAck?: boolean;
|
|
39
39
|
idempotencyKey?: string;
|
|
40
|
-
/**
|
|
40
|
+
/**
|
|
41
|
+
* Explicit delegation target — must hold a grant on the agreement, so work
|
|
42
|
+
* can only be handed to someone that contract already reaches; validated
|
|
43
|
+
* server-side.
|
|
44
|
+
*/
|
|
41
45
|
assigneeId?: string;
|
|
42
46
|
/** prior-step artifact handles consumed as structured input links. */
|
|
43
47
|
inputArtifactIds?: string[];
|
|
48
|
+
/**
|
|
49
|
+
* Tasks this one waits for. The task is created immediately, but its assignee
|
|
50
|
+
* is not woken and cannot start it until every task named here is terminal.
|
|
51
|
+
* Declaring an edge needs read reach on the task you name.
|
|
52
|
+
*
|
|
53
|
+
* Declared rather than enacted: state the join up front and the platform
|
|
54
|
+
* wakes the joiner once, instead of a live agent polling for it and holding
|
|
55
|
+
* the shape in its own control flow where nothing else can see it.
|
|
56
|
+
*/
|
|
57
|
+
waitsOn?: string[];
|
|
44
58
|
}
|
|
45
59
|
export declare function createTask(taskData: CreateTaskData, creds: Creds): Promise<Task>;
|
|
46
60
|
export declare function getTask(taskId: string, creds: Creds): Promise<Task>;
|
package/dist/http/grants.d.ts
CHANGED
|
@@ -36,9 +36,15 @@ export type GrantBasisKind = 'creation' | 'publication' | 'membership' | 'accept
|
|
|
36
36
|
* stopped showing a whole scope kind after `artifact` was added; anything that
|
|
37
37
|
* means "every context scope" should read this.
|
|
38
38
|
*/
|
|
39
|
-
export declare const CONTEXT_GRANT_SCOPE_KINDS: readonly ["chat", "agreement", "org", "artifact"];
|
|
40
|
-
/**
|
|
41
|
-
|
|
39
|
+
export declare const CONTEXT_GRANT_SCOPE_KINDS: readonly ["chat", "agreement", "org", "artifact", "task"];
|
|
40
|
+
/**
|
|
41
|
+
* Every scope kind across every rail: context + connection + payment + consent
|
|
42
|
+
* + carrier. Mirrors the server's `GRANT_SCOPE_KINDS` (grants/grant-view.ts),
|
|
43
|
+
* which is the list `GET /grants?scopeKind=` validates against — a kind missing
|
|
44
|
+
* here cannot be asked for at all, which is the failure the comment above
|
|
45
|
+
* describes.
|
|
46
|
+
*/
|
|
47
|
+
export declare const GRANT_SCOPE_KINDS: readonly ["chat", "agreement", "org", "artifact", "task", "connection", "wallet", "consent", "inbox"];
|
|
42
48
|
export type GrantScopeKind = (typeof GRANT_SCOPE_KINDS)[number];
|
|
43
49
|
export interface GrantScopeView {
|
|
44
50
|
kind: GrantScopeKind;
|
package/dist/http/grants.js
CHANGED
|
@@ -12,12 +12,25 @@ export const CONTEXT_GRANT_SCOPE_KINDS = [
|
|
|
12
12
|
'org',
|
|
13
13
|
// narrowest context scope
|
|
14
14
|
'artifact',
|
|
15
|
+
// a branch of a work graph: the named task and everything under it, now and
|
|
16
|
+
// in future. Holding one is what being in a job means.
|
|
17
|
+
'task',
|
|
15
18
|
];
|
|
16
|
-
/**
|
|
19
|
+
/**
|
|
20
|
+
* Every scope kind across every rail: context + connection + payment + consent
|
|
21
|
+
* + carrier. Mirrors the server's `GRANT_SCOPE_KINDS` (grants/grant-view.ts),
|
|
22
|
+
* which is the list `GET /grants?scopeKind=` validates against — a kind missing
|
|
23
|
+
* here cannot be asked for at all, which is the failure the comment above
|
|
24
|
+
* describes.
|
|
25
|
+
*/
|
|
17
26
|
export const GRANT_SCOPE_KINDS = [
|
|
18
27
|
...CONTEXT_GRANT_SCOPE_KINDS,
|
|
19
28
|
'connection',
|
|
20
29
|
'wallet',
|
|
30
|
+
// approval authority — who may answer a slot
|
|
31
|
+
'consent',
|
|
32
|
+
// carrier grants — who reads whose mailbox
|
|
33
|
+
'inbox',
|
|
21
34
|
];
|
|
22
35
|
/** Value of the first caveat of `type` on a grant, or undefined. */
|
|
23
36
|
export function grantCaveat(grant, type) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Agreement, Creds } from '../types.js';
|
|
2
|
-
import { claimOpenAgreement, delegateAgreement, getMyAgreements, pullOffers } from '../http/index.js';
|
|
2
|
+
import { claimOpenAgreement, delegateAgreement, getMyAgreements, pullOffers, type DelegateContextGrantResult, type GrantView } 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;
|
|
@@ -21,19 +21,72 @@ export interface RelayPayloadShape {
|
|
|
21
21
|
}
|
|
22
22
|
export type ProvisionMethod = 'existing' | 'claim' | 'delegate';
|
|
23
23
|
export type StepProvisionStatus = 'active' | 'pending_approval';
|
|
24
|
+
/**
|
|
25
|
+
* What happened to the coordinator's pass on this worker contract.
|
|
26
|
+
*
|
|
27
|
+
* - `granted` — the coordinator may create work citing this contract.
|
|
28
|
+
* - `reused` — it already held one; nothing was minted.
|
|
29
|
+
* - `pending_approval` — the pass needs a person. An agent cannot MINT a root
|
|
30
|
+
* agreement grant at all: `POST /context/grants` requires the `context:grant`
|
|
31
|
+
* operator scope, which a delegate never holds ("no re-mint or grant will
|
|
32
|
+
* satisfy this"). So an initiator agent's only route is delegating the
|
|
33
|
+
* participation grant it holds — and handing a slice to a third party in
|
|
34
|
+
* another org opens a card that owner must approve.
|
|
35
|
+
* - `deferred` — the contract itself is not active yet, so there is no
|
|
36
|
+
* participation grant to delegate from. Provision again after it activates.
|
|
37
|
+
* - `failed` — named so the caller stops rather than kicking off a run whose
|
|
38
|
+
* branches will each be refused one at a time.
|
|
39
|
+
*/
|
|
40
|
+
export type GrantProvisionStatus = 'granted' | 'reused' | 'pending_approval' | 'deferred' | 'failed';
|
|
41
|
+
export interface StepGrantProvision {
|
|
42
|
+
status: GrantProvisionStatus;
|
|
43
|
+
grantId?: string;
|
|
44
|
+
/** The approval card's agreement id, when a person has to say yes. */
|
|
45
|
+
approvalAgreementId?: string;
|
|
46
|
+
error?: string;
|
|
47
|
+
}
|
|
24
48
|
export type ProvisionedRelayStep = RelayPayloadShape['steps'][number] & {
|
|
25
49
|
provisionMethod: ProvisionMethod;
|
|
26
50
|
status: StepProvisionStatus;
|
|
51
|
+
/**
|
|
52
|
+
* The coordinator's pass on this step's contract. Handing over an agreement
|
|
53
|
+
* id is not enough: membership in work is a grant, and the coordinator is not
|
|
54
|
+
* a party to this contract, so nothing about it reaches the coordinator
|
|
55
|
+
* otherwise.
|
|
56
|
+
*/
|
|
57
|
+
grant: StepGrantProvision;
|
|
27
58
|
};
|
|
28
59
|
export interface RelayProvisionDeps {
|
|
29
60
|
getMyAgreements: typeof getMyAgreements;
|
|
30
61
|
pullOffers: typeof pullOffers;
|
|
31
62
|
claimOpenAgreement: typeof claimOpenAgreement;
|
|
32
63
|
delegateAgreement: typeof delegateAgreement;
|
|
64
|
+
/**
|
|
65
|
+
* Grants on one agreement scope. `role` picks which side: `holder` is what
|
|
66
|
+
* this initiator holds (the participation grant to delegate FROM), `issuer` is
|
|
67
|
+
* what it has already caused (so a re-provision reuses rather than re-mints).
|
|
68
|
+
*/
|
|
69
|
+
listAgreementGrants: (args: {
|
|
70
|
+
agreementId: string;
|
|
71
|
+
role: 'holder' | 'issuer';
|
|
72
|
+
}, creds: Creds) => Promise<GrantView[]>;
|
|
73
|
+
delegateAgreementGrant: (args: {
|
|
74
|
+
parentGrantId: string;
|
|
75
|
+
holderId: string;
|
|
76
|
+
agreementId: string;
|
|
77
|
+
}, creds: Creds) => Promise<DelegateContextGrantResult>;
|
|
33
78
|
}
|
|
34
79
|
export interface ProvisionRelayWorkersInput {
|
|
35
80
|
creds: Creds;
|
|
36
81
|
hireAgreementId: string;
|
|
82
|
+
/**
|
|
83
|
+
* The coordinator that will create the work. Required, because provisioning a
|
|
84
|
+
* worker FOR a coordinator is two acts and this names who the second one is
|
|
85
|
+
* for: without an `agreement`-scope grant on each worker contract, the
|
|
86
|
+
* coordinator's very first `task_create` is refused with "holds no grant on
|
|
87
|
+
* agreement …" and every branch of the run fails the same way.
|
|
88
|
+
*/
|
|
89
|
+
coordinatorId: string;
|
|
37
90
|
chatId?: string;
|
|
38
91
|
steps: RelayStepInput[];
|
|
39
92
|
inputArtifactIds?: string[];
|
|
@@ -41,6 +94,11 @@ export interface ProvisionRelayWorkersInput {
|
|
|
41
94
|
export interface ProvisionRelayWorkersResult {
|
|
42
95
|
payload: RelayPayloadShape;
|
|
43
96
|
steps: ProvisionedRelayStep[];
|
|
97
|
+
/**
|
|
98
|
+
* Everything a person still has to approve before kickoff — agreement
|
|
99
|
+
* activations AND grant hand-overs, in one list. They gate the run
|
|
100
|
+
* identically, so splitting them would just make a caller check two.
|
|
101
|
+
*/
|
|
44
102
|
pendingApprovals: string[];
|
|
45
103
|
readyForKickoff: boolean;
|
|
46
104
|
}
|
|
@@ -49,5 +107,13 @@ type AgreementWithParent = Agreement & {
|
|
|
49
107
|
};
|
|
50
108
|
export declare function findExistingWorkerDelegation(hireAgreementId: string, assigneeId: string, agreements: AgreementWithParent[]): AgreementWithParent | null;
|
|
51
109
|
export declare function findOpenOfferForAgent(assigneeId: string, offers: Agreement[]): Agreement | null;
|
|
110
|
+
/**
|
|
111
|
+
* The default grant plumbing, built on the two clients that own these routes.
|
|
112
|
+
*
|
|
113
|
+
* A thin function seam rather than the clients themselves, so a test can drive
|
|
114
|
+
* provisioning without a backend — the same reason the agreement calls are
|
|
115
|
+
* injected.
|
|
116
|
+
*/
|
|
117
|
+
export declare function defaultRelayProvisionDeps(): RelayProvisionDeps;
|
|
52
118
|
export declare function provisionRelayWorkers(input: ProvisionRelayWorkersInput, deps?: RelayProvisionDeps): Promise<ProvisionRelayWorkersResult>;
|
|
53
119
|
export {};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { claimOpenAgreement, delegateAgreement, getMyAgreements, pullOffers, } from '../http/index.js';
|
|
1
|
+
import { ContextGrantsClient, GrantsClient, claimOpenAgreement, delegateAgreement, getMyAgreements, pullOffers, } from '../http/index.js';
|
|
2
2
|
function providerAgentId(agreement) {
|
|
3
3
|
return agreement.parties?.provider?.actor ?? null;
|
|
4
4
|
}
|
|
@@ -27,23 +27,123 @@ export function findOpenOfferForAgent(assigneeId, offers) {
|
|
|
27
27
|
return providerAgentId(o) === assigneeId;
|
|
28
28
|
}) ?? null);
|
|
29
29
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
30
|
+
/**
|
|
31
|
+
* The default grant plumbing, built on the two clients that own these routes.
|
|
32
|
+
*
|
|
33
|
+
* A thin function seam rather than the clients themselves, so a test can drive
|
|
34
|
+
* provisioning without a backend — the same reason the agreement calls are
|
|
35
|
+
* injected.
|
|
36
|
+
*/
|
|
37
|
+
export function defaultRelayProvisionDeps() {
|
|
38
|
+
return {
|
|
39
|
+
getMyAgreements,
|
|
40
|
+
pullOffers,
|
|
41
|
+
claimOpenAgreement,
|
|
42
|
+
delegateAgreement,
|
|
43
|
+
listAgreementGrants: async ({ agreementId, role }, creds) => {
|
|
44
|
+
const client = new GrantsClient(creds.operatorKey, creds.agentId);
|
|
45
|
+
return client.listAllGrants({
|
|
46
|
+
scopeKind: 'agreement',
|
|
47
|
+
scopeId: agreementId,
|
|
48
|
+
role,
|
|
49
|
+
health: 'active',
|
|
50
|
+
});
|
|
51
|
+
},
|
|
52
|
+
delegateAgreementGrant: async ({ parentGrantId, holderId, agreementId }, creds) => {
|
|
53
|
+
const client = new ContextGrantsClient(creds.operatorKey, creds.agentId);
|
|
54
|
+
return client.delegateGrant(parentGrantId, {
|
|
55
|
+
holderId,
|
|
56
|
+
holderKind: 'agent',
|
|
57
|
+
// `write` is exactly what `canActUnderAgreement` tests. Deliberately not
|
|
58
|
+
// `admit`: that would let a coordinator widen its own crew.
|
|
59
|
+
kind: 'write',
|
|
60
|
+
scope: { kind: 'agreement', id: agreementId },
|
|
61
|
+
// from-start, not from-now: the contract predates the grant, so a
|
|
62
|
+
// from-now watermark would exclude the very row the grant is about.
|
|
63
|
+
temporal: 'from-start',
|
|
64
|
+
});
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Hand the coordinator a pass on one worker contract.
|
|
70
|
+
*
|
|
71
|
+
* Membership in work is a grant, which is what lets a coordinator create work
|
|
72
|
+
* under a contract it is not a party to. Issuing that grant is the half that has
|
|
73
|
+
* to happen here: without it the recipe fails at the coordinator's first
|
|
74
|
+
* `task_create` with "Caller <coordinator> holds no grant on agreement <worker>,
|
|
75
|
+
* so it cannot create work citing it."
|
|
76
|
+
*
|
|
77
|
+
* Reused before minted, because provisioning is re-run: a second call after an
|
|
78
|
+
* agreement activates should not stack a second pass on a contract that already
|
|
79
|
+
* has one.
|
|
80
|
+
*/
|
|
81
|
+
async function grantCoordinatorAccess(args, deps) {
|
|
82
|
+
const { agreementId, coordinatorId, status, creds } = args;
|
|
83
|
+
// Participation grants are minted at ACTIVATION, so a contract still waiting
|
|
84
|
+
// on a signature has nothing to delegate from yet. Say so rather than
|
|
85
|
+
// reporting a failure the caller cannot act on.
|
|
86
|
+
if (status !== 'active') {
|
|
87
|
+
return {
|
|
88
|
+
status: 'deferred',
|
|
89
|
+
error: `${agreementId} is not active yet, so there is no participation grant to ` +
|
|
90
|
+
'delegate from — provision again once it activates',
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
try {
|
|
94
|
+
const alreadyIssued = await deps.listAgreementGrants({ agreementId, role: 'issuer' }, creds);
|
|
95
|
+
const existing = alreadyIssued.find((g) => g.holderId === coordinatorId);
|
|
96
|
+
if (existing?.grantId)
|
|
97
|
+
return { status: 'reused', grantId: existing.grantId };
|
|
98
|
+
const held = await deps.listAgreementGrants({ agreementId, role: 'holder' }, creds);
|
|
99
|
+
// Only a `write` or `admit` parent can beget the `write` child the act rule
|
|
100
|
+
// tests; a `read` participation grant cannot be widened by delegating it.
|
|
101
|
+
// `access` is optional on GrantView because some rails do not record it —
|
|
102
|
+
// context grants always do, so an absent value here is a malformed row and
|
|
103
|
+
// must not be treated as strong enough.
|
|
104
|
+
const parent = held.find((g) => g.access === 'write' || g.access === 'admit');
|
|
105
|
+
if (!parent?.grantId) {
|
|
106
|
+
return {
|
|
107
|
+
status: 'failed',
|
|
108
|
+
error: `no write-or-stronger grant on ${agreementId} to delegate from (found ` +
|
|
109
|
+
`${held.length ? held.map((g) => g.access ?? 'unstated').join(', ') : 'none'}) — ` +
|
|
110
|
+
'the initiator must be a party to the worker contract it is provisioning',
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
const result = await deps.delegateAgreementGrant({ parentGrantId: parent.grantId, holderId: coordinatorId, agreementId }, creds);
|
|
114
|
+
if (result.status === 'pending_approval') {
|
|
115
|
+
return { status: 'pending_approval', approvalAgreementId: result.agreementId };
|
|
116
|
+
}
|
|
117
|
+
return { status: 'granted', grantId: result.grant.grantId };
|
|
118
|
+
}
|
|
119
|
+
catch (err) {
|
|
120
|
+
return {
|
|
121
|
+
status: 'failed',
|
|
122
|
+
error: err instanceof Error ? err.message : String(err),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
export async function provisionRelayWorkers(input, deps = defaultRelayProvisionDeps()) {
|
|
127
|
+
const { creds, hireAgreementId, coordinatorId, chatId, steps, inputArtifactIds } = input;
|
|
37
128
|
if (!steps.length)
|
|
38
129
|
throw new Error('steps must not be empty');
|
|
130
|
+
if (!coordinatorId?.trim()) {
|
|
131
|
+
throw new Error('coordinatorId is required: provisioning a worker for a coordinator means both ' +
|
|
132
|
+
'the contract and an agreement-scope grant to that coordinator on it, and ' +
|
|
133
|
+
'without the grant every branch of the run is refused');
|
|
134
|
+
}
|
|
39
135
|
const myAgreements = (await deps.getMyAgreements({}, creds));
|
|
40
136
|
let offersCache = null;
|
|
41
|
-
|
|
137
|
+
// The contracts first, then the passes. Two passes rather than one because a
|
|
138
|
+
// grant can only be delegated from a participation grant, which exists only
|
|
139
|
+
// once the contract is active — so the grant decision needs the settled
|
|
140
|
+
// status of the row, not the intent that created it.
|
|
141
|
+
const contracted = [];
|
|
42
142
|
const pendingApprovals = [];
|
|
43
143
|
for (const step of steps) {
|
|
44
144
|
const existing = findExistingWorkerDelegation(hireAgreementId, step.assigneeId, myAgreements);
|
|
45
145
|
if (existing?.agreementId) {
|
|
46
|
-
|
|
146
|
+
contracted.push({
|
|
47
147
|
stepId: step.stepId,
|
|
48
148
|
order: step.order,
|
|
49
149
|
agreementId: existing.agreementId,
|
|
@@ -62,7 +162,7 @@ export async function provisionRelayWorkers(input, deps = {
|
|
|
62
162
|
: 'pending_approval';
|
|
63
163
|
if (status === 'pending_approval')
|
|
64
164
|
pendingApprovals.push(agreementId);
|
|
65
|
-
|
|
165
|
+
contracted.push({
|
|
66
166
|
stepId: step.stepId,
|
|
67
167
|
order: step.order,
|
|
68
168
|
agreementId,
|
|
@@ -85,7 +185,7 @@ export async function provisionRelayWorkers(input, deps = {
|
|
|
85
185
|
: 'pending_approval';
|
|
86
186
|
if (status === 'pending_approval')
|
|
87
187
|
pendingApprovals.push(agreementId);
|
|
88
|
-
|
|
188
|
+
contracted.push({
|
|
89
189
|
stepId: step.stepId,
|
|
90
190
|
order: step.order,
|
|
91
191
|
agreementId,
|
|
@@ -115,7 +215,7 @@ export async function provisionRelayWorkers(input, deps = {
|
|
|
115
215
|
: 'pending_approval';
|
|
116
216
|
if (status === 'pending_approval')
|
|
117
217
|
pendingApprovals.push(agreementId);
|
|
118
|
-
|
|
218
|
+
contracted.push({
|
|
119
219
|
stepId: step.stepId,
|
|
120
220
|
order: step.order,
|
|
121
221
|
agreementId,
|
|
@@ -125,7 +225,21 @@ export async function provisionRelayWorkers(input, deps = {
|
|
|
125
225
|
status,
|
|
126
226
|
});
|
|
127
227
|
}
|
|
128
|
-
|
|
228
|
+
contracted.sort((a, b) => a.order - b.order);
|
|
229
|
+
// ── the second act of provisioning: the coordinator's passes ─────────────
|
|
230
|
+
const provisioned = [];
|
|
231
|
+
for (const step of contracted) {
|
|
232
|
+
const grant = await grantCoordinatorAccess({
|
|
233
|
+
agreementId: step.agreementId,
|
|
234
|
+
coordinatorId,
|
|
235
|
+
status: step.status,
|
|
236
|
+
creds,
|
|
237
|
+
}, deps);
|
|
238
|
+
if (grant.status === 'pending_approval' && grant.approvalAgreementId) {
|
|
239
|
+
pendingApprovals.push(grant.approvalAgreementId);
|
|
240
|
+
}
|
|
241
|
+
provisioned.push({ ...step, grant });
|
|
242
|
+
}
|
|
129
243
|
const payload = {
|
|
130
244
|
inputArtifactIds,
|
|
131
245
|
steps: provisioned.map(({ stepId, order, agreementId, assigneeId, description }) => ({
|
|
@@ -136,8 +250,14 @@ export async function provisionRelayWorkers(input, deps = {
|
|
|
136
250
|
description,
|
|
137
251
|
})),
|
|
138
252
|
};
|
|
253
|
+
/**
|
|
254
|
+
* Ready means BOTH acts are done for every step. A run whose contracts are all
|
|
255
|
+
* active but whose coordinator holds no passes looks provisioned and has every
|
|
256
|
+
* branch refused, so the grant has to count here or this flag goes on lying.
|
|
257
|
+
*/
|
|
139
258
|
const readyForKickoff = pendingApprovals.length === 0 &&
|
|
140
|
-
provisioned.every((s) => s.status === 'active'
|
|
259
|
+
provisioned.every((s) => s.status === 'active' &&
|
|
260
|
+
(s.grant.status === 'granted' || s.grant.status === 'reused'));
|
|
141
261
|
return {
|
|
142
262
|
payload,
|
|
143
263
|
steps: provisioned,
|
package/dist/types.d.ts
CHANGED
|
@@ -71,6 +71,14 @@ export interface Task {
|
|
|
71
71
|
updatedAt?: string;
|
|
72
72
|
parentTaskId?: string;
|
|
73
73
|
rootTaskId?: string;
|
|
74
|
+
/** Tasks this one waits for before it may start. */
|
|
75
|
+
waitsOn?: string[];
|
|
76
|
+
/**
|
|
77
|
+
* True while {@link waitsOn} is unmet: the task exists but has not been
|
|
78
|
+
* delivered, and its assignee cannot take the processing lock yet. Distinct
|
|
79
|
+
* from "nobody has picked it up" — this one is waiting on purpose.
|
|
80
|
+
*/
|
|
81
|
+
heldByDeps?: boolean;
|
|
74
82
|
plan?: {
|
|
75
83
|
steps?: PlanStep[];
|
|
76
84
|
};
|
|
@@ -361,6 +369,15 @@ export interface InboxProposalRef {
|
|
|
361
369
|
agreementId: string;
|
|
362
370
|
title: string;
|
|
363
371
|
proposedAt: string | null;
|
|
372
|
+
/**
|
|
373
|
+
* The agent designated to wake for this proposal, or null when it is
|
|
374
|
+
* ambient/human work in this reader's merged inbox.
|
|
375
|
+
*
|
|
376
|
+
* Optional for rolling deploys. When an older server omits it, clients must
|
|
377
|
+
* only use an explicit agent-keyed approval/responder slot as a safe fallback;
|
|
378
|
+
* org visibility alone is never assignment.
|
|
379
|
+
*/
|
|
380
|
+
assignedAgentId?: string | null;
|
|
364
381
|
/**
|
|
365
382
|
* party ids still owing a decision, and the named responder slot.
|
|
366
383
|
* The inbox lists proposals awaiting the agent OR its human, and only the
|