@ziggs-ai/api-client 0.11.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.d.ts +7 -0
- package/dist/capabilities/agreementVerbs.js +55 -2
- package/dist/capabilities/agreements.js +13 -1
- package/dist/capabilities/context.js +2 -2
- package/dist/capabilities/grants.d.ts +2 -1
- package/dist/capabilities/grants.js +4 -3
- 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 +56 -28
- package/dist/http/AgreementClient.js +36 -95
- package/dist/http/ContextGrantsClient.d.ts +10 -4
- package/dist/http/InboxClient.js +4 -0
- package/dist/http/MarketplaceClient.d.ts +0 -18
- package/dist/http/MarketplaceClient.js +0 -36
- package/dist/http/TaskClient.d.ts +15 -1
- package/dist/http/TaskClient.js +2 -7
- package/dist/http/agreementFlows.d.ts +3 -4
- package/dist/http/agreementFlows.js +8 -13
- package/dist/http/grants.d.ts +9 -3
- package/dist/http/grants.js +14 -1
- 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 +68 -2
- package/dist/relay/provisionRelayWorkers.js +138 -18
- package/dist/types.d.ts +53 -16
- package/dist/types.js +18 -0
- package/package.json +1 -1
|
@@ -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/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) {
|
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, 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,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ContextGrantsClient, GrantsClient, 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';
|
|
@@ -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,
|
|
@@ -55,14 +155,14 @@ export async function provisionRelayWorkers(input, deps = {
|
|
|
55
155
|
continue;
|
|
56
156
|
}
|
|
57
157
|
if (step.offerAgreementId) {
|
|
58
|
-
const claimed = await deps.
|
|
158
|
+
const { agreement: claimed } = await deps.claimOpenAgreement(step.offerAgreementId, creds);
|
|
59
159
|
const agreementId = claimed.agreementId;
|
|
60
160
|
const status = isActiveAgreement(claimed)
|
|
61
161
|
? 'active'
|
|
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,
|
|
@@ -78,14 +178,14 @@ export async function provisionRelayWorkers(input, deps = {
|
|
|
78
178
|
}
|
|
79
179
|
const openOffer = findOpenOfferForAgent(step.assigneeId, offersCache);
|
|
80
180
|
if (openOffer?.agreementId) {
|
|
81
|
-
const claimed = await deps.
|
|
181
|
+
const { agreement: claimed } = await deps.claimOpenAgreement(openOffer.agreementId, creds);
|
|
82
182
|
const agreementId = claimed.agreementId;
|
|
83
183
|
const status = isActiveAgreement(claimed)
|
|
84
184
|
? 'active'
|
|
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
|
};
|
|
@@ -91,19 +99,46 @@ export declare const AGREEMENT_ENGAGEMENT_KIND: {
|
|
|
91
99
|
readonly LINK: "link";
|
|
92
100
|
};
|
|
93
101
|
export type EngagementKind = (typeof AGREEMENT_ENGAGEMENT_KIND)[keyof typeof AGREEMENT_ENGAGEMENT_KIND];
|
|
102
|
+
/**
|
|
103
|
+
* One side of an agreement.
|
|
104
|
+
*
|
|
105
|
+
* An agent id occupies `actor` and nowhere else, so "is this agent on this
|
|
106
|
+
* side?" is a structural read rather than a lookup against the Agent
|
|
107
|
+
* collection. Asking `principal` about an agent id is always the wrong
|
|
108
|
+
* question — it would match that agent's estate instead.
|
|
109
|
+
*/
|
|
110
|
+
export interface AgreementPartySide {
|
|
111
|
+
/**
|
|
112
|
+
* The accountable person or org — never an agent. On `payer` and `proposedTo`
|
|
113
|
+
* this may instead hold a broadcast sentinel ({@link isBroadcastTarget}), in
|
|
114
|
+
* which case nobody has taken the side up yet.
|
|
115
|
+
*/
|
|
116
|
+
principal?: string | null;
|
|
117
|
+
/** The agent that performed on this side. Null when the principal acted itself. */
|
|
118
|
+
actor?: string | null;
|
|
119
|
+
}
|
|
120
|
+
/** The four sides of an agreement, each {@link AgreementPartySide}. */
|
|
121
|
+
export interface AgreementParties {
|
|
122
|
+
payer?: AgreementPartySide;
|
|
123
|
+
provider?: AgreementPartySide;
|
|
124
|
+
proposedTo?: AgreementPartySide;
|
|
125
|
+
creator?: AgreementPartySide;
|
|
126
|
+
}
|
|
127
|
+
/** Both ids on one side, principal first, absent ones dropped. */
|
|
128
|
+
export declare function partySideIds(side: AgreementPartySide | null | undefined): string[];
|
|
129
|
+
/**
|
|
130
|
+
* Every agent that acted on this agreement, deduped.
|
|
131
|
+
*
|
|
132
|
+
* The read for "is this agent a party?" — an agent id lives in `actor` alone,
|
|
133
|
+
* so widening the match to `principal` would hit an unrelated estate.
|
|
134
|
+
*/
|
|
135
|
+
export declare function partyActorIds(parties: AgreementParties | null | undefined): string[];
|
|
94
136
|
/** Compact agreement reference carried by task reads. */
|
|
95
137
|
export interface AgreementSummary {
|
|
96
138
|
agreementId: string;
|
|
97
139
|
description: string;
|
|
98
140
|
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
|
-
};
|
|
141
|
+
parties?: AgreementParties;
|
|
107
142
|
/** The opposite party relative to the requesting agent; null for observers,
|
|
108
143
|
* broadcast sentinels, and self-agreements. */
|
|
109
144
|
counterparty: string | null;
|
|
@@ -120,14 +155,7 @@ export interface Agreement {
|
|
|
120
155
|
status?: AgreementStatus;
|
|
121
156
|
engagementKind?: EngagementKind;
|
|
122
157
|
proposalStatus?: ProposalStatus;
|
|
123
|
-
parties?:
|
|
124
|
-
proposedTo?: string;
|
|
125
|
-
provider?: string;
|
|
126
|
-
payer?: string;
|
|
127
|
-
creator?: string;
|
|
128
|
-
creatorAgent?: string;
|
|
129
|
-
providerAgent?: string;
|
|
130
|
-
};
|
|
158
|
+
parties?: AgreementParties;
|
|
131
159
|
approvals?: AgreementApprovalEntry[];
|
|
132
160
|
/** Legacy root-level price — always null in practice. Use money.price instead. */
|
|
133
161
|
price?: number | null;
|
|
@@ -341,6 +369,15 @@ export interface InboxProposalRef {
|
|
|
341
369
|
agreementId: string;
|
|
342
370
|
title: string;
|
|
343
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;
|
|
344
381
|
/**
|
|
345
382
|
* party ids still owing a decision, and the named responder slot.
|
|
346
383
|
* The inbox lists proposals awaiting the agent OR its human, and only the
|
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',
|