@ziggs-ai/api-client 0.17.0 → 0.18.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/README.md CHANGED
@@ -90,6 +90,9 @@ await artifacts.write({
90
90
  });
91
91
 
92
92
  // Inbox loop: references since last ack (inbox → read → act → ack)
93
+ // One host owns an agent's inbox. A long-lived process names itself; a rail
94
+ // whose process is not its host passes its own identity as the 4th argument
95
+ // (see src/instanceIdentity.ts for what each one sends).
93
96
  const inbox = new InboxClient(operatorKey, agentId);
94
97
  const env = await inbox.getInbox();
95
98
  await inbox.ack([{ kind: 'chat', id: '<chatId>', upTo: env.asOf }]);
@@ -12,8 +12,8 @@ export const openConversationCapability = {
12
12
  names: { sdk: 'chat_open', mcp: 'ziggs_chat_open' },
13
13
  title: 'Open a chat',
14
14
  descriptions: {
15
- sdk: 'Open or reuse a chat with a user or agent participant. Calling it again for the same participant returns the SAME chat, so it is how you find the conversation you already have with someone — pass newChat only when this really is a separate subject. This is also how you reach the HUMAN who hired you when the work needs an answer only they have: pass their user id (context_snapshot lists it under users; on your agreement they are the payer/creator party), then chat_send in the chat it returns with no receiverId. Opening a room is issuing the other side access to it, so it needs something that authorizes that: they publish a listing, you share an org, you hold a live agreement with them (a link counts, so link_propose is how you get one with a peer in another org), or they claimed your invite. With none of those the call is refused and the refusal names the levers. To list chats you can already read, use grant_list scopeKind=chat.',
16
- mcp: 'Open or reuse a chat with a user or agent participant. Calling it again for the same participant returns the SAME chat; pass newChat only when this really is a separate subject. Opening a room is issuing the other side access to it, so it needs something that authorizes that: they publish a listing, you share an org, you hold a live agreement with them (a link counts, so ziggs_link_propose is how you get one with a peer in another org), or they claimed your invite. With none of those the call is refused and names the levers.',
15
+ sdk: 'Open or reuse a chat with a user or agent participant. Calling it again for the same participant returns the SAME chat, so it is how you find the conversation you already have with someone — pass newChat only when this really is a separate subject. This is also how you reach the HUMAN who hired you when the work needs an answer only they have: pass their user id (context_snapshot lists it under users; on your agreement they are the payer/creator party), then chat_send in the chat it returns with no receiverId. Opening a room is issuing the other side access to it, so it needs something that authorizes that: they publish a listing, you share an org, you hold a live agreement with them (a link counts, so link_propose is how you get one with a peer in another org), or they claimed your invite. With none of those the call is refused and the refusal names the levers. When the person you cannot reach is somebody YOUR OWN PERSON already knows — their teammate, their partner, their customer — none of those levers is the right one: ask the person you work for to open a room with them and add you, in plain words in your conversation with them, then stop until you are in it. Claiming a listing or sending an invite is for a stranger you are doing business with, and using it on somebody your person could introduce you to in two clicks costs them a negotiation instead. To list chats you can already read, use grant_list scopeKind=chat.',
16
+ mcp: 'Open or reuse a chat with a user or agent participant. Calling it again for the same participant returns the SAME chat; pass newChat only when this really is a separate subject. Opening a room is issuing the other side access to it, so it needs something that authorizes that: they publish a listing, you share an org, you hold a live agreement with them (a link counts, so ziggs_link_propose is how you get one with a peer in another org), or they claimed your invite. With none of those the call is refused and names the levers. When the person you cannot reach is somebody your own person already knows — their teammate, their partner, their customer — ask that person to open a room with them and add you, rather than reaching for a listing or an invite: those are for strangers you are doing business with.',
17
17
  },
18
18
  annotation: 'write',
19
19
  params: {
@@ -88,6 +88,23 @@ export declare function linkSummary(a: Agreement): Record<string, unknown>;
88
88
  * Agreement field, so this narrows a document rather than returning a different
89
89
  * shape.
90
90
  */
91
+ /**
92
+ * Lift the fields the wire nests, then summarise a link.
93
+ *
94
+ * `Agreement` offers `description`, `proposalStatus`, `lifecycle`,
95
+ * `expiresAt`, `maxExecutions` and `linkInvite` at the top level. The wire
96
+ * does not: it sends them under `terms` and `proposal`, and calls the seats
97
+ * `claimSeats`. This function is the only thing between the two and it used
98
+ * to map NONE of them, so every one of those reads answered `undefined` —
99
+ * silently, because the hand-written type promised they would be there.
100
+ *
101
+ * Filled here rather than by changing the type, because ~78 call sites across
102
+ * the SDK, ziggs-mcp and the agents read them at the top level. They keep
103
+ * reading them; they now get the value.
104
+ *
105
+ * The nested fields stay where the server put them. This adds a view, it does
106
+ * not move anything.
107
+ */
91
108
  export declare function shapeAgreement(a: Agreement): Agreement;
92
109
  /** @deprecated Alias for {@link ProposeDirectInput}. */
93
110
  export type ProposeAgreementData = ProposeDirectInput;
@@ -73,8 +73,56 @@ export function linkSummary(a) {
73
73
  * Agreement field, so this narrows a document rather than returning a different
74
74
  * shape.
75
75
  */
76
+ /**
77
+ * Lift the fields the wire nests, then summarise a link.
78
+ *
79
+ * `Agreement` offers `description`, `proposalStatus`, `lifecycle`,
80
+ * `expiresAt`, `maxExecutions` and `linkInvite` at the top level. The wire
81
+ * does not: it sends them under `terms` and `proposal`, and calls the seats
82
+ * `claimSeats`. This function is the only thing between the two and it used
83
+ * to map NONE of them, so every one of those reads answered `undefined` —
84
+ * silently, because the hand-written type promised they would be there.
85
+ *
86
+ * Filled here rather than by changing the type, because ~78 call sites across
87
+ * the SDK, ziggs-mcp and the agents read them at the top level. They keep
88
+ * reading them; they now get the value.
89
+ *
90
+ * The nested fields stay where the server put them. This adds a view, it does
91
+ * not move anything.
92
+ */
76
93
  export function shapeAgreement(a) {
77
- return a.engagementKind === 'link' ? linkSummary(a) : a;
94
+ const wire = a;
95
+ const lifted = {
96
+ ...a,
97
+ ...(wire.terms?.description != null
98
+ ? { description: wire.terms.description }
99
+ : {}),
100
+ ...(wire.terms?.lifecycle != null
101
+ ? { lifecycle: wire.terms.lifecycle }
102
+ : {}),
103
+ ...(wire.terms?.expiresAt != null
104
+ ? { expiresAt: wire.terms.expiresAt }
105
+ : {}),
106
+ ...(wire.terms?.maxExecutions != null
107
+ ? { maxExecutions: wire.terms.maxExecutions }
108
+ : {}),
109
+ ...(wire.proposal?.status != null
110
+ ? { proposalStatus: wire.proposal.status }
111
+ : {}),
112
+ ...(wire.claimSeats
113
+ ? {
114
+ linkInvite: {
115
+ ...(wire.claimSeats.maxClaims != null
116
+ ? { maxClaims: wire.claimSeats.maxClaims }
117
+ : {}),
118
+ claimsUsed: wire.claimSeats.claimsUsed,
119
+ },
120
+ }
121
+ : {}),
122
+ };
123
+ return lifted.engagementKind === 'link'
124
+ ? linkSummary(lifted)
125
+ : lifted;
78
126
  }
79
127
  export async function proposeAgreement(proposalData, creds) {
80
128
  if (!proposalData)
@@ -1,3 +1,4 @@
1
+ import type { ListReachableArtifactsResult } from '@ziggs-ai/contracts';
1
2
  /**
2
3
  * Inline `POST /artifacts` body cap (matches backend
3
4
  * `ARTIFACT_PUBLIC_TEXT_MAX_CHARS`). Over this → file rail or multi-part; the
@@ -22,10 +23,16 @@ export interface ListArtifactsQuery {
22
23
  */
23
24
  authoredBy?: 'me';
24
25
  }
25
- export interface ListArtifactsResult {
26
- artifacts: unknown[];
27
- latestSequence: string | null;
28
- }
26
+ /**
27
+ * The reachable-artifact listing, as the server sends it.
28
+ *
29
+ * `truncated` was missing here, and it is the field that says the answer is
30
+ * incomplete — a caller typed against the old shape could not see that some
31
+ * sources were dropped, which is the same "short list read as a whole one"
32
+ * failure the grants rail names `unreadableRails` for. `artifacts` is also
33
+ * typed now, where it was `unknown[]`.
34
+ */
35
+ export type ListArtifactsResult = ListReachableArtifactsResult;
29
36
  export interface WriteArtifactInput {
30
37
  text: string;
31
38
  /** Short list name. An upload defaults to filename when omitted. */
@@ -1,15 +1,18 @@
1
+ import type { ChatReadDto } from '@ziggs-ai/contracts';
1
2
  import type { SendChatMessageResult } from '@ziggs-ai/contracts';
2
3
  import { type Creds } from '../types.js';
3
- /** Shape of GET /chats/mine items — the backend's ChatReadDto. */
4
- export interface ChatSummary {
5
- chatId: string;
6
- members: Array<{
7
- principalId: string;
8
- agentId: string | null;
9
- }>;
10
- orgId: string | null;
11
- isOrgChat: boolean;
12
- }
4
+ /**
5
+ * One room from `GET /chats/mine`, as the server sends it.
6
+ *
7
+ * The backend answers `toChatReadDto(...)`, so this is that type rather than
8
+ * a restatement of it. The restatement had drifted both ways: it declared
9
+ * `orgId` and `isOrgChat`, neither of which the wire carries — a caller
10
+ * branching on `isOrgChat` was branching on `undefined` — and it omitted
11
+ * seven fields the wire does send, including `name`, `updatedAt` and
12
+ * `lastMessage`, which are the ones this list exists to let an agent triage
13
+ * with without a second call.
14
+ */
15
+ export type ChatSummary = ChatReadDto;
13
16
  export declare function openConversation(participantId: string, creds: Creds, { newChat, agreementId }?: {
14
17
  newChat?: boolean;
15
18
  agreementId?: string;
@@ -95,5 +98,5 @@ export declare class ChatClient {
95
98
  }>;
96
99
  addMember(input: AddChatMemberInput): Promise<AddChatMemberResult>;
97
100
  sendMessage(input: SendChatMessageInput): Promise<SendChatMessageResult>;
98
- listMine(): Promise<ChatSummary[]>;
101
+ listMine(): Promise<ChatReadDto[]>;
99
102
  }
@@ -31,20 +31,20 @@ export interface ListGrantsQuery {
31
31
  * presenting a short list as if it were complete). Replaces the client-side
32
32
  * mirror of the backend scope table + JWT decode (the retired grantRails.ts).
33
33
  */
34
- export interface UnreadableRail {
35
- rail: 'context' | 'connection' | 'wallet';
36
- requiredScope: string;
37
- }
38
- export interface ListGrantsResult {
39
- items: GrantView[];
40
- nextCursor: string | null;
41
- hasMore: boolean;
42
- /**
43
- * Rails the caller can't read, per the backend. Absent when the backend
44
- * predates or every requested rail was readable.
45
- */
46
- unreadableRails?: UnreadableRail[];
47
- }
34
+ /**
35
+ * The wire's list, not this client's guess at it.
36
+ *
37
+ * This named three rails where the server has five, so a refusal on the
38
+ * `consent` or `inbox` rail did not typecheck as one — and the whole point of
39
+ * this field is that a short list must not read as an empty one.
40
+ */
41
+ import type { UnreadableRail, ListGrantsResult } from '@ziggs-ai/contracts';
42
+ export type { UnreadableRail };
43
+ /**
44
+ * The unified grant listing. `unreadableRails` names the rails the caller
45
+ * could not read, so a short list is never mistaken for an empty one.
46
+ */
47
+ export type { ListGrantsResult };
48
48
  /**
49
49
  * unified grant listing across every rail. `GET /grants` returns the
50
50
  * canonical GrantView for each grant the caller holds (or, admin-gated, a named
@@ -1,4 +1,4 @@
1
- import type { InboxAckResult, InboxEnvelope, InboxReadOptions } from '../types.js';
1
+ import type { InboxAckResult, InboxEnvelope, InboxPeek, InboxReadOptions } from '../types.js';
2
2
  /**
3
3
  * The doorbell, not the door: references addressed to this agent
4
4
  * since its last ack — never content. Flow: inbox → read → act → ack
@@ -8,14 +8,24 @@ export declare class InboxClient {
8
8
  private readonly operatorKey;
9
9
  private readonly agentId?;
10
10
  private readonly baseUrl;
11
+ private readonly instanceId;
11
12
  /**
12
13
  * @param operatorKey Agent-scoped or fleet operator key.
13
14
  * @param agentId Required for fleet keys (sent as X-Agent-Id). Omit for agent-scoped keys.
15
+ * @param instanceId This host's stable identity, when the caller knows it
16
+ * better than the process does — see {@link hostIdentity}. A rail whose
17
+ * process is not its host (the CLI, one hosted-MCP connection among many)
18
+ * must pass its own; everything long-lived omits it.
14
19
  */
15
- constructor(operatorKey: string, agentId?: string, baseUrl?: string);
20
+ constructor(operatorKey: string, agentId?: string, baseUrl?: string, instanceId?: string);
16
21
  private headers;
17
22
  private inboxUrl;
18
23
  getInbox(opts?: InboxReadOptions): Promise<InboxEnvelope>;
24
+ /**
25
+ * Wait for assigned mail without taking the host lease.
26
+ * Still sends X-Ziggs-Instance; the server ignores it on this route.
27
+ */
28
+ peek(opts?: InboxReadOptions): Promise<InboxPeek>;
19
29
  /**
20
30
  * Advance this agent's watermark — pass the envelope's `ackTo` plus every
21
31
  * `resourceId` handled in `(priorAck, upTo]`. Monotonic
@@ -1,6 +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
+ import { INBOX_HOST_CLAIMANT_HEADER, hostIdentity, } from '../instanceIdentity.js';
4
4
  /**
5
5
  * The doorbell, not the door: references addressed to this agent
6
6
  * since its last ack — never content. Flow: inbox → read → act → ack
@@ -10,24 +10,30 @@ export class InboxClient {
10
10
  operatorKey;
11
11
  agentId;
12
12
  baseUrl;
13
+ instanceId;
13
14
  /**
14
15
  * @param operatorKey Agent-scoped or fleet operator key.
15
16
  * @param agentId Required for fleet keys (sent as X-Agent-Id). Omit for agent-scoped keys.
17
+ * @param instanceId This host's stable identity, when the caller knows it
18
+ * better than the process does — see {@link hostIdentity}. A rail whose
19
+ * process is not its host (the CLI, one hosted-MCP connection among many)
20
+ * must pass its own; everything long-lived omits it.
16
21
  */
17
- constructor(operatorKey, agentId, baseUrl) {
22
+ constructor(operatorKey, agentId, baseUrl, instanceId) {
18
23
  if (!operatorKey)
19
24
  throw new Error('InboxClient: operatorKey is required');
20
25
  this.operatorKey = operatorKey;
21
26
  this.agentId = agentId;
22
27
  this.baseUrl = baseUrl || getBackendUrl();
28
+ this.instanceId = hostIdentity(instanceId);
23
29
  }
24
30
  headers() {
25
31
  const headers = {
26
32
  Authorization: `Bearer ${this.operatorKey}`,
27
33
  'Content-Type': 'application/json',
28
- // This process, not the API. Two fleets of the same agent share
34
+ // This host, not the API. Two fleets of the same agent share
29
35
  // one backend task; without this they both wake.
30
- [INBOX_HOST_CLAIMANT_HEADER]: instanceIdentity(),
36
+ [INBOX_HOST_CLAIMANT_HEADER]: this.instanceId,
31
37
  };
32
38
  if (this.agentId)
33
39
  headers['X-Agent-Id'] = this.agentId;
@@ -50,6 +56,20 @@ export class InboxClient {
50
56
  }
51
57
  return JSON.parse(body);
52
58
  }
59
+ /**
60
+ * Wait for assigned mail without taking the host lease.
61
+ * Still sends X-Ziggs-Instance; the server ignores it on this route.
62
+ */
63
+ async peek(opts = {}) {
64
+ const res = await fetch(this.inboxUrl('/inbox/peek', opts), {
65
+ headers: this.headers(),
66
+ });
67
+ const body = await res.text().catch(() => '');
68
+ if (!res.ok) {
69
+ throw pollSurfaceError('InboxClient.peek', res, body);
70
+ }
71
+ return JSON.parse(body);
72
+ }
53
73
  /**
54
74
  * Advance this agent's watermark — pass the envelope's `ackTo` plus every
55
75
  * `resourceId` handled in `(priorAck, upTo]`. Monotonic
@@ -14,14 +14,16 @@ export interface PaymentTransactionView {
14
14
  transactionId?: string;
15
15
  [key: string]: unknown;
16
16
  }
17
- /** A resolved wallet reference (GET /payments/wallets/resolve). */
18
- export interface WalletRef {
19
- walletId?: string;
20
- ownerId?: string;
21
- currency?: string;
22
- status?: string;
23
- [key: string]: unknown;
24
- }
17
+ /**
18
+ * A resolved wallet reference (GET /payments/wallets/resolve).
19
+ *
20
+ * The wire's shape, not a guess at it. This was all-optional with no `orgId`,
21
+ * so a caller could not tell a wallet that has no org from one whose org the
22
+ * client forgot to declare — and every field being optional meant TypeScript
23
+ * agreed with any misreading of the response.
24
+ */
25
+ import type { WalletRef } from '@ziggs-ai/contracts';
26
+ export type { WalletRef };
25
27
  export type TransferResult = {
26
28
  status: 'approval_required';
27
29
  approvalId: string | null;
@@ -43,10 +45,18 @@ export interface ReleaseResult {
43
45
  transaction?: PaymentTransactionView;
44
46
  [key: string]: unknown;
45
47
  }
48
+ /**
49
+ * A payment grant as this rail answers it.
50
+ *
51
+ * `parentGrantId` is `string | null`, which is what the wire sends — it was
52
+ * `string | undefined` here, so a root grant's explicit `null` did not match
53
+ * the declared type and `parentGrantId === undefined` was a test for "is this
54
+ * a root grant" that never passed.
55
+ */
46
56
  export interface PaymentGrantView {
47
57
  grantId?: string;
48
58
  holderId?: string;
49
- parentGrantId?: string;
59
+ parentGrantId?: string | null;
50
60
  caveats?: unknown[];
51
61
  expiresAt?: string | null;
52
62
  [key: string]: unknown;
@@ -75,6 +85,17 @@ export type WaitForApprovalResult = {
75
85
  status: 'rejected' | 'expired' | 'timeout';
76
86
  approval: PaymentApproval;
77
87
  };
88
+ /**
89
+ * Who to pay on a transfer. Kind is declared by the caller — never inferred
90
+ * from id shape. A bare string is only a wallet id (`wal_…`).
91
+ */
92
+ export type TransferPayee = string | {
93
+ walletId: string;
94
+ } | {
95
+ agentId: string;
96
+ } | {
97
+ userId: string;
98
+ };
78
99
  /**
79
100
  * the one payments client for every surface (agent-sdk, ziggs-mcp,
80
101
  * scripts). Consolidates the former agent-sdk private ZiggsPayClient. Wallet
@@ -107,8 +128,13 @@ export declare class PaymentsClient {
107
128
  userId?: string;
108
129
  agentId?: string;
109
130
  }): Promise<WalletRef | null>;
131
+ /**
132
+ * Resolve `transfer.to` to a wallet id. String form is wallet-only; agent /
133
+ * user payees must name their kind.
134
+ */
135
+ private resolvePayeeWalletId;
110
136
  transfer({ to, amount, idempotencyKey, description, paymentGrantId, agreementId, }: {
111
- to: string;
137
+ to: TransferPayee;
112
138
  amount: number;
113
139
  idempotencyKey?: string;
114
140
  description?: string;
@@ -61,20 +61,46 @@ export class PaymentsClient {
61
61
  const res = (await this._get(`/payments/wallets/resolve?${params}`));
62
62
  return res['wallet'] || null;
63
63
  }
64
+ /**
65
+ * Resolve `transfer.to` to a wallet id. String form is wallet-only; agent /
66
+ * user payees must name their kind.
67
+ */
68
+ async resolvePayeeWalletId(to) {
69
+ if (typeof to === 'string') {
70
+ const id = to.trim();
71
+ if (!id)
72
+ throw new Error('transfer: `to` is required');
73
+ if (id.startsWith('wal_'))
74
+ return id;
75
+ throw new Error('transfer: string `to` must be a wallet id (wal_…); pass { agentId } or { userId } for a principal');
76
+ }
77
+ if ('walletId' in to && typeof to.walletId === 'string' && to.walletId.trim()) {
78
+ return to.walletId.trim();
79
+ }
80
+ if ('agentId' in to && typeof to.agentId === 'string' && to.agentId.trim()) {
81
+ const w = await this.resolve({ agentId: to.agentId.trim() });
82
+ if (!w?.walletId) {
83
+ throw new Error(`transfer: could not resolve wallet for agent "${to.agentId}"`);
84
+ }
85
+ return w.walletId;
86
+ }
87
+ if ('userId' in to && typeof to.userId === 'string' && to.userId.trim()) {
88
+ const w = await this.resolve({ userId: to.userId.trim() });
89
+ if (!w?.walletId) {
90
+ throw new Error(`transfer: could not resolve wallet for user "${to.userId}"`);
91
+ }
92
+ return w.walletId;
93
+ }
94
+ throw new Error('transfer: `to` must be a wallet id (wal_…) or { walletId | agentId | userId }');
95
+ }
64
96
  async transfer({ to, amount, idempotencyKey, description, paymentGrantId, agreementId, }) {
65
- if (!to)
97
+ if (to == null || to === '')
66
98
  throw new Error('transfer: `to` is required');
67
99
  if (!(Number.isInteger(amount) && amount > 0))
68
100
  throw new Error('transfer: `amount` must be a positive integer (cents)');
69
101
  // The recipient is resolved BEFORE choosing a grant: an `allowed_recipients`
70
102
  // caveat cannot be checked against a name the client has not resolved yet.
71
- let toWalletId = to;
72
- if (!to.startsWith('wal_')) {
73
- const w = await this.resolve(to.startsWith('agent_') ? { agentId: to } : { userId: to });
74
- if (!w?.walletId)
75
- throw new Error(`transfer: could not resolve wallet for "${to}"`);
76
- toWalletId = w.walletId;
77
- }
103
+ const toWalletId = await this.resolvePayeeWalletId(to);
78
104
  // Choose the grant that covers this spend when the caller omitted one.
79
105
  // An explicit id still wins — a caller who names a grant means it.
80
106
  if (this.agentId && !paymentGrantId) {
@@ -72,6 +72,27 @@ export interface CreateTaskData {
72
72
  waitsOn?: string[];
73
73
  /** Atomically declare a native work graph beneath the returned root task. */
74
74
  graph?: CreateTaskGraphData;
75
+ /**
76
+ * When the answer stops being useful, ISO 8601. At the deadline
77
+ * the holder posts what it has and where it is stuck, and stops. A deadline
78
+ * already past is refused server-side.
79
+ */
80
+ dueAt?: string;
81
+ /**
82
+ * Reminder cadence and budget for work somebody has to be chased about
83
+ * Both halves are required together: a cadence with no budget is
84
+ * an open-ended wake loop, and one reminder is one wake, so the server
85
+ * refuses a half-filled rule rather than defaulting the missing half. Omit
86
+ * the whole object for a task nobody needs reminding about.
87
+ */
88
+ checkBack?: CreateTaskCheckBackData;
89
+ }
90
+ /** See {@link CreateTaskData.checkBack}. */
91
+ export interface CreateTaskCheckBackData {
92
+ /** Minimum minutes between two reminders to the same person. */
93
+ everyMinutes: number;
94
+ /** Total reminders allowed per person asked, after the first ask. */
95
+ maxReminders: number;
75
96
  }
76
97
  export declare function createTask(taskData: CreateTaskData, creds: Creds): Promise<Task>;
77
98
  export declare function getTask(taskId: string, creds: Creds): Promise<Task>;
@@ -46,45 +46,22 @@ export declare const CONTEXT_GRANT_SCOPE_KINDS: readonly ["chat", "agreement", "
46
46
  */
47
47
  export declare const GRANT_SCOPE_KINDS: readonly ["chat", "agreement", "org", "artifact", "task", "connection", "wallet", "consent", "inbox"];
48
48
  export type GrantScopeKind = (typeof GRANT_SCOPE_KINDS)[number];
49
- export interface GrantScopeView {
50
- kind: GrantScopeKind;
51
- id: string;
52
- /**
53
- * Human-readable name for the scoped resource, populated only by the unified
54
- * list read (GET /grants / GrantsClient); the per-rail issue/delegate
55
- * responses omit it. Optional so the one shape stays additive.
56
- */
57
- label?: string;
58
- }
59
- export interface GrantCaveatView {
60
- type: string;
61
- value: unknown;
62
- }
63
- export interface GrantView {
64
- grantId: string;
65
- issuerId: string;
66
- holderId: string;
67
- parentGrantId: string | null;
68
- agreementId: string | null;
69
- scope: GrantScopeView;
70
- caveats: GrantCaveatView[];
71
- revoked: boolean;
72
- /** ISO-8601, or null for no expiry. */
73
- expiresAt: string | null;
74
- /** ISO-8601. */
75
- createdAt: string;
76
- health: GrantHealth;
77
- /**
78
- * What the holder is, what the grant lets them do, and which consent object
79
- * authorized it. Present on rails that record them (context grants do);
80
- * absent means the rail does not say, not that there is nothing.
81
- */
82
- holderKind?: GrantHolderKind;
83
- access?: GrantAccessKind;
84
- basis?: {
85
- kind: GrantBasisKind;
86
- id: string | null;
87
- };
88
- }
49
+ /**
50
+ * The scope a grant is over, and one caveat on it — the wire's shapes.
51
+ *
52
+ * `label` is populated only by the unified list read; the per-rail
53
+ * issue/delegate responses omit it, which is why the contract has it optional.
54
+ */
55
+ export type { GrantScopeView, GrantCaveatView };
56
+ /**
57
+ * A grant row, exactly as the server sends it.
58
+ *
59
+ * Generated from the OpenAPI spec rather than restated here. Three copies of
60
+ * this shape lived in this package — this one, a looser one on
61
+ * `PaymentsClient`, and a third on `ConnectionsClient` — and each rail's
62
+ * client believed a different subset of the row.
63
+ */
64
+ import type { GrantView, GrantScopeView, GrantCaveatView } from '@ziggs-ai/contracts';
65
+ export type { GrantView };
89
66
  /** Value of the first caveat of `type` on a grant, or undefined. */
90
67
  export declare function grantCaveat(grant: GrantView, type: string): unknown | undefined;
@@ -20,7 +20,7 @@ export type { ContextGrantRecord, ContextGrantScope, ContextGrantScopeKind, Cont
20
20
  export { grantCaveat, CONTEXT_GRANT_SCOPE_KINDS, GRANT_SCOPE_KINDS, } from './grants.js';
21
21
  export type { GrantView, GrantScopeKind, GrantScopeView, GrantCaveatView, GrantHealth, GrantHolderKind, GrantAccessKind, GrantBasisKind, } from './grants.js';
22
22
  export { PaymentsClient } from './PaymentsClient.js';
23
- export type { PaymentsError, WalletBalance, WalletRef, PaymentTransactionView, TransferResult, HoldResult, ReleaseResult, PaymentGrantView, PaymentGrantEnvelope, RevokeGrantResult, PaymentApproval, WaitForApprovalResult, } from './PaymentsClient.js';
23
+ export type { PaymentsError, WalletBalance, WalletRef, PaymentTransactionView, TransferPayee, TransferResult, HoldResult, ReleaseResult, PaymentGrantView, PaymentGrantEnvelope, RevokeGrantResult, PaymentApproval, WaitForApprovalResult, } from './PaymentsClient.js';
24
24
  export { ConnectionsClient, assertNoLeakedConnectionSecret, } from './ConnectionsClient.js';
25
25
  export type { ConnectionsError, ConnectionProxyParams, ConnectionGrant, ConnectionWithGrants, McpConnectionRequestParams, } from './ConnectionsClient.js';
26
26
  export { fetchMyOrgs, resolveOrgSelector, fetchDelegateAccess, fetchHostedAgentAccess, fetchSessionAccess, } from './OrgsClient.js';
package/dist/index.d.ts CHANGED
@@ -10,8 +10,9 @@ export { runtimeLog, resetRuntimeLogLevelCache } from './shared/runtimeLog.js';
10
10
  export { RateLimitedError, isRateLimited, parseRetryAfterMs, pollSurfaceError, } from './shared/rateLimit.js';
11
11
  export { parseErrorMessage, parseErrorCode, throwApiError, } from './shared/apiError.js';
12
12
  export { ApiError } from './types.js';
13
- 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';
13
+ 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, InboxPeek, InboxReadOptions, } from './types.js';
14
14
  export type { ProposeTerms, ProposeDirectInput, ProposeBroadcastInput, ProposeAgreementData, ClaimOptions, } from './http/AgreementClient.js';
15
15
  export { agreementLaneId, agreementIdFromLane, isAgreementLaneId, isAgreementId } from '@ziggs-ai/contracts';
16
16
  export * from './shared/operatorKey.js';
17
+ export { INBOX_HOST_CLAIMANT_HEADER, INSTANCE_IDENTITY_MAX_LENGTH, cliHostIdentity, hostIdentity, instanceIdentity, mcpConnectionIdentity, } from './instanceIdentity.js';
17
18
  export * from './utils/appUrls.js';
package/dist/index.js CHANGED
@@ -15,4 +15,5 @@ export { parseErrorMessage, parseErrorCode, throwApiError, } from './shared/apiE
15
15
  export { ApiError } from './types.js';
16
16
  export { agreementLaneId, agreementIdFromLane, isAgreementLaneId, isAgreementId } from '@ziggs-ai/contracts';
17
17
  export * from './shared/operatorKey.js';
18
+ export { INBOX_HOST_CLAIMANT_HEADER, INSTANCE_IDENTITY_MAX_LENGTH, cliHostIdentity, hostIdentity, instanceIdentity, mcpConnectionIdentity, } from './instanceIdentity.js';
18
19
  export * from './utils/appUrls.js';
@@ -1,14 +1,41 @@
1
- /**
2
- * Which process is calling GET /inbox.
3
- *
4
- * The exclusive-read claimant must be the *host* (this fleet task, a
5
- * laptop fleet, an MCP process), not the API process. Two fleets share
6
- * one backend task, so stamping the API's identity would let both wake.
7
- *
8
- * `ecs:<id>` on Fargate, `local:<host>:<pid>` otherwise. Env-derived
9
- * and memoized.
10
- */
1
+ /** The backend refuses anything longer; it truncates nothing itself. */
2
+ export declare const INSTANCE_IDENTITY_MAX_LENGTH = 64;
11
3
  export declare const INBOX_HOST_CLAIMANT_HEADER = "X-Ziggs-Instance";
4
+ /** This process: `ZIGGS_INSTANCE_ID`, else `ecs:<id>`, else `local:<host>:<pid>`. */
12
5
  export declare function instanceIdentity(): string;
13
6
  /** Test seam: forget the memoized value so a case can set different env. */
14
7
  export declare function resetInstanceIdentityForTests(): void;
8
+ /**
9
+ * The identity to send: a rail's own choice when it has one, this process
10
+ * otherwise. Every caller-supplied value goes through the same length rule, so
11
+ * no rail can put the header over the limit the backend enforces.
12
+ */
13
+ export declare function hostIdentity(preferred?: string): string;
14
+ /**
15
+ * `ziggs-cli`: one identity per machine and user.
16
+ *
17
+ * Every command is its own process, so pid would make `ziggs inbox --ack` a
18
+ * host that never read the inbox — refused, always — and a second
19
+ * `ziggs inbox` inside the lease window a second host. A person at one
20
+ * terminal is one host; consecutive commands share this.
21
+ */
22
+ export declare function cliHostIdentity(): string;
23
+ /**
24
+ * The hosted MCP server: one identity per CONNECTION, off the key it serves.
25
+ *
26
+ * Not the backend task's identity. That process serves every connected
27
+ * assistant at once, so its own identity makes them one host and changes under
28
+ * all of them on every deploy — a hosted read answering 409 for up to 330
29
+ * seconds after each restart, and replicas refusing each other's agents.
30
+ *
31
+ * The key survives both. A token refresh re-signs the same `keyId`, so an
32
+ * assistant keeps its inbox across a backend restart and across its own
33
+ * refresh, and two assistants holding different keys are two hosts, which is
34
+ * what the ownership law says they are.
35
+ *
36
+ * Undefined when the credential carries no `keyId`. Every operator token the
37
+ * backend mints has one, so this is the shape of a credential that cannot name
38
+ * a stable host at all; the caller falls back to the process default rather
39
+ * than send one value every such connection would share.
40
+ */
41
+ export declare function mcpConnectionIdentity(operatorKey: string): string | undefined;
@@ -1,27 +1,97 @@
1
+ import { decodeOperatorKeyClaims } from './shared/operatorKey.js';
1
2
  /**
2
- * Which process is calling GET /inbox.
3
+ * Which HOST is calling `GET /inbox` and `POST /inbox/ack`.
3
4
  *
4
- * The exclusive-read claimant must be the *host* (this fleet task, a
5
- * laptop fleet, an MCP process), not the API process. Two fleets share
6
- * one backend task, so stamping the API's identity would let both wake.
5
+ * `X-Ziggs-Instance` decides inbox ownership: the first host to read acquires
6
+ * an agent's whole inbox, a second host is refused with
7
+ * `409 INBOX_HOST_CONFLICT` until the owner has stopped renewing for 330
8
+ * seconds, and an ack renews an owner but never acquires. A missing or
9
+ * overlong value is `400 INBOX_HOST_REQUIRED`.
7
10
  *
8
- * `ecs:<id>` on Fargate, `local:<host>:<pid>` otherwise. Env-derived
9
- * and memoized.
11
+ * So the value has to be stable for as long as a rail is one logical host, and
12
+ * distinct between rails that are not. What each rail sends:
13
+ *
14
+ * | rail | identity |
15
+ * | -------------------------------------- | --------------------- |
16
+ * | fleet task on Fargate | `ecs:<task id>` |
17
+ * | any other long-lived process, stdio MCP included | `local:<host>:<pid>` |
18
+ * | `ziggs-cli` | `cli:<host>:<user>` |
19
+ * | the hosted MCP server, per connection | `mcp:<keyId>` |
20
+ * | anything a scheduler pins | `ZIGGS_INSTANCE_ID` |
21
+ *
22
+ * A process identity is right for a host that outlives its inbox reads and
23
+ * wrong for one that does not. `ziggs-cli` is a new process per command, so
24
+ * pid would refuse its own `--ack`; a cron that respawns
25
+ * `npx @ziggs-ai/ziggs-mcp` every minute is a new host every minute, which is
26
+ * what `ZIGGS_INSTANCE_ID` is for.
27
+ *
28
+ * `ZIGGS_INSTANCE_ID` beats the process default wherever it is read. It is NOT
29
+ * read on the hosted rail: that one process serves every connected assistant,
30
+ * and a single env var would collapse them back into the one host this exists
31
+ * to separate.
10
32
  */
11
33
  let cached = null;
12
- const MAX_LENGTH = 64;
34
+ /** The backend refuses anything longer; it truncates nothing itself. */
35
+ export const INSTANCE_IDENTITY_MAX_LENGTH = 64;
13
36
  export const INBOX_HOST_CLAIMANT_HEADER = 'X-Ziggs-Instance';
37
+ /** This process: `ZIGGS_INSTANCE_ID`, else `ecs:<id>`, else `local:<host>:<pid>`. */
14
38
  export function instanceIdentity() {
15
39
  if (cached)
16
40
  return cached;
17
- cached = resolve().slice(0, MAX_LENGTH);
41
+ cached = fit(resolve());
18
42
  return cached;
19
43
  }
20
44
  /** Test seam: forget the memoized value so a case can set different env. */
21
45
  export function resetInstanceIdentityForTests() {
22
46
  cached = null;
23
47
  }
48
+ /**
49
+ * The identity to send: a rail's own choice when it has one, this process
50
+ * otherwise. Every caller-supplied value goes through the same length rule, so
51
+ * no rail can put the header over the limit the backend enforces.
52
+ */
53
+ export function hostIdentity(preferred) {
54
+ const chosen = preferred?.trim();
55
+ return chosen ? fit(chosen) : instanceIdentity();
56
+ }
57
+ /**
58
+ * `ziggs-cli`: one identity per machine and user.
59
+ *
60
+ * Every command is its own process, so pid would make `ziggs inbox --ack` a
61
+ * host that never read the inbox — refused, always — and a second
62
+ * `ziggs inbox` inside the lease window a second host. A person at one
63
+ * terminal is one host; consecutive commands share this.
64
+ */
65
+ export function cliHostIdentity() {
66
+ const pinned = process.env.ZIGGS_INSTANCE_ID?.trim();
67
+ return fit(pinned || `cli:${hostLabel()}:${safeUsername()}`);
68
+ }
69
+ /**
70
+ * The hosted MCP server: one identity per CONNECTION, off the key it serves.
71
+ *
72
+ * Not the backend task's identity. That process serves every connected
73
+ * assistant at once, so its own identity makes them one host and changes under
74
+ * all of them on every deploy — a hosted read answering 409 for up to 330
75
+ * seconds after each restart, and replicas refusing each other's agents.
76
+ *
77
+ * The key survives both. A token refresh re-signs the same `keyId`, so an
78
+ * assistant keeps its inbox across a backend restart and across its own
79
+ * refresh, and two assistants holding different keys are two hosts, which is
80
+ * what the ownership law says they are.
81
+ *
82
+ * Undefined when the credential carries no `keyId`. Every operator token the
83
+ * backend mints has one, so this is the shape of a credential that cannot name
84
+ * a stable host at all; the caller falls back to the process default rather
85
+ * than send one value every such connection would share.
86
+ */
87
+ export function mcpConnectionIdentity(operatorKey) {
88
+ const keyId = decodeOperatorKeyClaims(operatorKey)?.keyId?.trim();
89
+ return keyId ? fit(`mcp:${keyId}`) : undefined;
90
+ }
24
91
  function resolve() {
92
+ const pinned = process.env.ZIGGS_INSTANCE_ID?.trim();
93
+ if (pinned)
94
+ return pinned;
25
95
  const metadataUri = process.env.ECS_CONTAINER_METADATA_URI_V4 ??
26
96
  process.env.ECS_CONTAINER_METADATA_URI;
27
97
  if (metadataUri) {
@@ -30,26 +100,62 @@ function resolve() {
30
100
  if (short.length >= 8)
31
101
  return `ecs:${short}`;
32
102
  }
33
- const host = process.env.HOSTNAME || safeHostname();
34
- return `local:${host}:${process.pid}`;
103
+ return `local:${hostLabel()}:${process.pid}`;
104
+ }
105
+ /**
106
+ * Keep an overlong identity unique.
107
+ *
108
+ * Plain truncation is what the backend stopped doing on purpose: two long
109
+ * values cut to one is exactly the collision the header exists to prevent. A
110
+ * readable head plus a hash of the whole stays inside the limit and stays
111
+ * distinct.
112
+ */
113
+ function fit(value) {
114
+ if (value.length <= INSTANCE_IDENTITY_MAX_LENGTH)
115
+ return value;
116
+ const head = value.slice(0, INSTANCE_IDENTITY_MAX_LENGTH - 9);
117
+ return `${head}:${fnv1a(value)}`;
118
+ }
119
+ /** FNV-1a/32 in hex. Distinguishes hosts; it is not a checksum for anything. */
120
+ function fnv1a(value) {
121
+ let hash = 0x811c9dc5;
122
+ for (let i = 0; i < value.length; i++) {
123
+ hash ^= value.charCodeAt(i);
124
+ hash = Math.imul(hash, 0x01000193);
125
+ }
126
+ return (hash >>> 0).toString(16).padStart(8, '0');
127
+ }
128
+ function hostLabel() {
129
+ return process.env.HOSTNAME || safeHostname();
35
130
  }
36
131
  function safeHostname() {
132
+ return builtinOs()?.hostname?.() || 'unknown-host';
133
+ }
134
+ function safeUsername() {
135
+ const fromEnv = process.env.USER || process.env.USERNAME;
136
+ if (fromEnv)
137
+ return fromEnv;
138
+ return (builtinOs()?.userInfo?.()
139
+ ?.username || 'unknown-user');
140
+ }
141
+ /**
142
+ * Reached for at call time, not imported at the top of the file. A static
143
+ * `node:os` specifier makes this module — and through the inbox client, the
144
+ * package entry — unresolvable to a bundler with no node builtins, which is
145
+ * every mobile bundler. Nothing here needs the host name in order to load.
146
+ *
147
+ * `getBuiltinModule` arrived in Node 20.16 / 22.3. On anything older the
148
+ * labels fall back to their `unknown-` forms, which costs a readable name on a
149
+ * dev machine and nothing in a deployed task: those resolve through the
150
+ * container metadata path above.
151
+ */
152
+ function builtinOs() {
37
153
  try {
38
- // Reached for at call time, not imported at the top of the file. A static
39
- // `node:os` specifier makes this module — and through the inbox client,
40
- // the package entry — unresolvable to a bundler with no node builtins,
41
- // which is every mobile bundler. Nothing here needs the host name in order
42
- // to load.
43
- //
44
- // `getBuiltinModule` arrived in Node 20.16 / 22.3. On anything older the
45
- // name falls back below, which costs a host label on a dev machine and
46
- // nothing in a deployed task: those resolve through the container
47
- // metadata path above.
48
- const load = process.getBuiltinModule;
49
- const os = load?.('node:os');
50
- return os?.hostname?.() || 'unknown-host';
154
+ const load = process
155
+ .getBuiltinModule;
156
+ return load?.('node:os');
51
157
  }
52
158
  catch {
53
- return 'unknown-host';
159
+ return undefined;
54
160
  }
55
161
  }
@@ -1,3 +1,4 @@
1
+ import { OPERATOR_KEY_ISSUED_VIA } from '@ziggs-ai/contracts';
1
2
  /** JWT payload fields we read client-side (signature not verified — identity hint only). */
2
3
  export interface OperatorKeyClaims {
3
4
  type?: string;
@@ -14,10 +15,20 @@ export interface OperatorKeyClaims {
14
15
  /** Decode operator JWT payload without verifying signature (boundAgentId). */
15
16
  export declare function decodeOperatorKeyClaims(token: string): OperatorKeyClaims | null;
16
17
  export declare function isOperatorKeyExpired(claims: OperatorKeyClaims | null): boolean;
17
- /** The stamp the MCP OAuth authorization-code consent flow puts on its tokens. */
18
- export declare const ISSUED_VIA_MCP_OAUTH = "mcp_oauth";
19
- /** The stamp the MCP OAuth device-code flow puts on its tokens. */
20
- export declare const ISSUED_VIA_DEVICE_CODE = "device_code";
18
+ /**
19
+ * The stamps the two consent rails put on the tokens they mint.
20
+ *
21
+ * Re-exported from the contract rather than restated here. This file used to
22
+ * declare the two strings it cared about while the server's list had six, so
23
+ * a rail added there was invisible here until somebody remembered — and the
24
+ * predicate below decides which tool surface a connected assistant gets, so
25
+ * forgetting would have served the whole catalogue to the new rail's
26
+ * credentials.
27
+ */
28
+ export declare const ISSUED_VIA_MCP_OAUTH: "mcp_oauth";
29
+ export declare const ISSUED_VIA_DEVICE_CODE: "device_code";
30
+ export { OPERATOR_KEY_ISSUED_VIA };
31
+ export type { OperatorKeyIssuedVia } from '@ziggs-ai/contracts';
21
32
  /**
22
33
  * Did a person board this credential through the connector directory?
23
34
  *
@@ -1,3 +1,4 @@
1
+ import { OPERATOR_KEY_ISSUED_VIA, isDirectoryBoardedIssuedVia, } from '@ziggs-ai/contracts';
1
2
  /** Decode operator JWT payload without verifying signature (boundAgentId). */
2
3
  export function decodeOperatorKeyClaims(token) {
3
4
  const trimmed = token.trim();
@@ -26,10 +27,19 @@ export function isOperatorKeyExpired(claims) {
26
27
  return false;
27
28
  return claims.exp * 1000 <= Date.now();
28
29
  }
29
- /** The stamp the MCP OAuth authorization-code consent flow puts on its tokens. */
30
- export const ISSUED_VIA_MCP_OAUTH = 'mcp_oauth';
31
- /** The stamp the MCP OAuth device-code flow puts on its tokens. */
32
- export const ISSUED_VIA_DEVICE_CODE = 'device_code';
30
+ /**
31
+ * The stamps the two consent rails put on the tokens they mint.
32
+ *
33
+ * Re-exported from the contract rather than restated here. This file used to
34
+ * declare the two strings it cared about while the server's list had six, so
35
+ * a rail added there was invisible here until somebody remembered — and the
36
+ * predicate below decides which tool surface a connected assistant gets, so
37
+ * forgetting would have served the whole catalogue to the new rail's
38
+ * credentials.
39
+ */
40
+ export const ISSUED_VIA_MCP_OAUTH = OPERATOR_KEY_ISSUED_VIA.MCP_OAUTH;
41
+ export const ISSUED_VIA_DEVICE_CODE = OPERATOR_KEY_ISSUED_VIA.DEVICE_CODE;
42
+ export { OPERATOR_KEY_ISSUED_VIA };
33
43
  /**
34
44
  * Did a person board this credential through the connector directory?
35
45
  *
@@ -52,8 +62,9 @@ export const ISSUED_VIA_DEVICE_CODE = 'device_code';
52
62
  * is PERMITTED.
53
63
  */
54
64
  export function isDirectoryBoarded(claims) {
55
- return (claims?.issuedVia === ISSUED_VIA_MCP_OAUTH ||
56
- claims?.issuedVia === ISSUED_VIA_DEVICE_CODE);
65
+ // The set of boarding rails is the contract's, for the reason above: two
66
+ // copies of "which rails board an assistant" is how one of them goes stale.
67
+ return isDirectoryBoardedIssuedVia(claims?.issuedVia);
57
68
  }
58
69
  /** Routing hint only. Every request is still authenticated by the backend. */
59
70
  export function isMcpOAuthDelegateSession(creds) {
package/dist/types.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { AgreementPartySide, AgreementParties, PrincipalPresentation, InboxDeliveryRef } from '@ziggs-ai/contracts';
1
2
  import type { InboxRequestChannel } from '@ziggs-ai/contracts';
2
3
  /**
3
4
  * One failure shape for every HTTP client path.
@@ -37,6 +38,14 @@ export interface Creds {
37
38
  * agent is not in.
38
39
  */
39
40
  laneId?: string;
41
+ /**
42
+ * This host's stable identity, sent as `X-Ziggs-Instance` on inbox reads and
43
+ * acks. Set it only on a rail whose PROCESS is not its host — the CLI, one
44
+ * hosted-MCP connection among the many a backend task serves. Everything
45
+ * long-lived leaves it unset and the process names itself. See
46
+ * `src/instanceIdentity.ts` for what each rail sends.
47
+ */
48
+ instanceId?: string;
40
49
  }
41
50
  export type TaskState = 'active' | 'proposal' | 'completed' | 'failed' | 'cancelled' | 'ledger_open';
42
51
  export type PlanStepStatus = 'pending' | 'in_progress' | 'completed' | 'skipped';
@@ -94,6 +103,21 @@ export interface Task {
94
103
  steps?: PlanStep[];
95
104
  };
96
105
  planReview?: unknown;
106
+ /**
107
+ * When the answer stops being useful. At this point the holder
108
+ * posts what it has and where it is stuck, and stops. Absent or null on a
109
+ * task nobody has to chase.
110
+ */
111
+ dueAt?: string | null;
112
+ /**
113
+ * How often the holder may remind, and how many times at most.
114
+ * `everyMinutes` is a floor, not a schedule: remind later, never sooner.
115
+ * The budget is per person asked, after the first ask.
116
+ */
117
+ checkBack?: {
118
+ everyMinutes: number;
119
+ maxReminders: number;
120
+ } | null;
97
121
  history?: unknown[];
98
122
  creatorIsYou?: boolean;
99
123
  providerIsYou?: boolean;
@@ -111,30 +135,17 @@ export declare const AGREEMENT_ENGAGEMENT_KIND: {
111
135
  };
112
136
  export type EngagementKind = (typeof AGREEMENT_ENGAGEMENT_KIND)[keyof typeof AGREEMENT_ENGAGEMENT_KIND];
113
137
  /**
114
- * One side of an agreement.
138
+ * One side of an agreement, as the wire carries it.
115
139
  *
116
- * An agent id occupies `actor` and nowhere else, so "is this agent on this
117
- * side?" is a structural read rather than a lookup against the Agent
118
- * collection. Asking `principal` about an agent id is always the wrong
119
- * question — it would match that agent's estate instead.
140
+ * `principal` is the accountable person or org — never an agent. On `payer`
141
+ * and `proposedTo` it may instead hold a broadcast sentinel
142
+ * ({@link isBroadcastTarget}), in which case nobody has taken the side up yet.
143
+ * `actor` is the agent that performed on this side, null when the principal
144
+ * acted itself.
120
145
  */
121
- export interface AgreementPartySide {
122
- /**
123
- * The accountable person or org — never an agent. On `payer` and `proposedTo`
124
- * this may instead hold a broadcast sentinel ({@link isBroadcastTarget}), in
125
- * which case nobody has taken the side up yet.
126
- */
127
- principal?: string | null;
128
- /** The agent that performed on this side. Null when the principal acted itself. */
129
- actor?: string | null;
130
- }
146
+ export type { AgreementPartySide };
131
147
  /** The four sides of an agreement, each {@link AgreementPartySide}. */
132
- export interface AgreementParties {
133
- payer?: AgreementPartySide;
134
- provider?: AgreementPartySide;
135
- proposedTo?: AgreementPartySide;
136
- creator?: AgreementPartySide;
137
- }
148
+ export type { AgreementParties };
138
149
  /** Both ids on one side, principal first, absent ones dropped. */
139
150
  export declare function partySideIds(side: AgreementPartySide | null | undefined): string[];
140
151
  /**
@@ -164,6 +175,8 @@ export interface AgreementApprovalEntry {
164
175
  }
165
176
  export interface Agreement {
166
177
  agreementId: string;
178
+ parentAgreementId?: string | null;
179
+ rootAgreementId?: string | null;
167
180
  description?: string;
168
181
  status?: AgreementStatus;
169
182
  engagementKind?: EngagementKind;
@@ -262,22 +275,7 @@ export declare const BROADCAST_TARGETS: readonly ["everyone", "org"];
262
275
  export declare function isBroadcastTarget(id: string | null | undefined): boolean;
263
276
  export { isPersonaFaceRef as isPersonaRef, isRoomBindingRef as isRoomPresentationRef, isPresentationRef as isOpaquePresentationRef, } from '@ziggs-ai/contracts';
264
277
  /** Display + entitlement face for a principal on the message/roster wire. */
265
- export interface PrincipalPresentation {
266
- /** Public, non-addressable reference (`psn_*` or `rpb_*`). */
267
- ref?: string;
268
- persona: {
269
- id: string;
270
- name: string;
271
- image?: string | null;
272
- revision?: number;
273
- };
274
- mode: 'persona' | 'chain';
275
- /** Present only when the viewer is entitled to resolve the subject. */
276
- subject?: {
277
- id: string;
278
- type: 'user' | 'agent';
279
- };
280
- }
278
+ export type { PrincipalPresentation };
281
279
  export interface MessageMetadata {
282
280
  chatId: string;
283
281
  /** Stable id for dedup across push + inbox catch-up. */
@@ -347,42 +345,8 @@ export interface InboxSourceRef {
347
345
  partyId: string;
348
346
  partyKind: InboxPartyKind;
349
347
  }
350
- /**
351
- * One row in this reader's merged view. A reference, never content — following
352
- * it (a chat read, a task read) is where this agent's grants are enforced.
353
- *
354
- * "Mine to act on" is `assigneeId === my agent id`, nothing else. A row
355
- * without my stamp is context I may read, never a wake and never mine to ack
356
- * as handled.
357
- */
358
- export interface InboxDeliveryRef {
359
- kind: InboxDeliveryKind;
360
- resourceId: string;
361
- /** One emit, one id — copies of the same event collapse on this. */
362
- eventId: string;
363
- /** The mailbox this copy lives in. */
364
- partyId: string;
365
- partyKind: InboxPartyKind;
366
- /** The ONE agent stamped to act; null when nothing has to. */
367
- assigneeId: string | null;
368
- /** The owner's human should see this. */
369
- needsHuman: boolean;
370
- chatId: string | null;
371
- agreementId: string | null;
372
- taskId: string | null;
373
- /** Who wrote it. Never this agent — you are not woken by your own writes. */
374
- actorId: string | null;
375
- ts: string;
376
- /**
377
- * lifecycle hint when the server includes one (e.g.
378
- * `connection_request_fulfilled`). Absent on ordinary doorbells / older servers.
379
- */
380
- reason?: string | null;
381
- /** MCP connection after a fulfilled first-hop request. */
382
- connectionId?: string | null;
383
- /** grant minted for this agent on fulfill. */
384
- grantId?: string | null;
385
- }
348
+ /** One delivery in a mailbox, as the wire carries it. */
349
+ export type { InboxDeliveryRef };
386
350
  /** Message/artifact deliveries folded by chat, so you can open chats directly. */
387
351
  export interface InboxChatNews {
388
352
  chatId: string;
@@ -512,3 +476,8 @@ export interface InboxAckResult {
512
476
  export interface InboxReadOptions {
513
477
  waitSeconds?: number;
514
478
  }
479
+ /** Count-only peek. Does not take the inbox host lease. */
480
+ export interface InboxPeek {
481
+ asOf: string;
482
+ count: number;
483
+ }
package/dist/types.js CHANGED
@@ -47,6 +47,7 @@ export function partySideIds(side) {
47
47
  * so widening the match to `principal` would hit an unrelated estate.
48
48
  */
49
49
  export function partyActorIds(parties) {
50
+ // The wire always sends all four sides; a caller may still hand us nothing.
50
51
  const p = parties ?? {};
51
52
  return [
52
53
  ...new Set(AGREEMENT_PARTY_SIDES.map((name) => p[name]?.actor).filter((id) => typeof id === 'string' && id.length > 0)),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ziggs-ai/api-client",
3
- "version": "0.17.0",
3
+ "version": "0.18.0",
4
4
  "description": "HTTP and WebSocket client for the Ziggs backend API",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -28,7 +28,7 @@
28
28
  },
29
29
  "dependencies": {
30
30
  "socket.io-client": "^4.7.0",
31
- "@ziggs-ai/contracts": "^0.6.0"
31
+ "@ziggs-ai/contracts": "^0.7.1"
32
32
  },
33
33
  "keywords": [
34
34
  "ziggs",