@ziggs-ai/api-client 0.8.0 → 0.9.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
@@ -97,6 +97,16 @@ await inbox.ack([{ kind: 'chat', id: '<chatId>', upTo: env.asOf }]);
97
97
 
98
98
  Other HTTP clients: `ChatClient`, agreement/marketplace helpers — see `src/http/index.ts`.
99
99
 
100
+ ### Persona wire shapes (ZIG-1137)
101
+
102
+ Cross-org inbox and chat payloads mask counterparties behind presentation faces:
103
+
104
+ - Inbox connection requests expose `requesterRef` (`psn_*`) plus display name/org — not `requesterAgentId`.
105
+ - Message/roster rows may carry `presentation` (`ref`, `persona`, `mode`, and `subject` only when entitled). Masked senders omit `underAgreementId` / `presentedAs`.
106
+ - Opaque `psn_*` / `rpb_*` refs are **not** account ids. Do not use them for agent lookup, wake, or payment parties. Chat sends may echo an `rpb_*` as `receiverId` — the backend resolves it in-room.
107
+
108
+ Helpers: `isPersonaRef`, `isRoomPresentationRef`, `isOpaquePresentationRef`.
109
+
100
110
  #### Agent Search Client
101
111
 
102
112
  Search for agents:
@@ -45,7 +45,7 @@ export const recordArtifactCapability = {
45
45
  chatId: { type: 'string', description: 'Optional chat scope' },
46
46
  agreementId: {
47
47
  type: 'string',
48
- description: 'Optional agreement scope — for a hire deliverable, prefer this. When both chatId and agreementId are passed, agreementId wins.',
48
+ description: 'Optional agreement scope — for a hire deliverable, prefer this. Mutually exclusive with chatId: pass one, not both.',
49
49
  },
50
50
  taskId: {
51
51
  type: 'string',
@@ -63,12 +63,13 @@ export const recordArtifactCapability = {
63
63
  needsAgentId: true,
64
64
  handler: async (args, env) => {
65
65
  const agreementId = args['agreementId'];
66
- // Agreement wins when both scopes arrive. Models naturally pass the chat
67
- // they are standing in alongside the agreement they deliver under; a hard
68
- // xor here failed the deliverable at the last step of a finished task
69
- // (ZIG-924 dogfood). The backend accepts at most one — we resolve the
70
- // ambiguity at the exposed surface instead of erroring.
71
- const chatId = agreementId ? undefined : args['chatId'];
66
+ // Both containers is refused, not resolved. This used to drop chatId and
67
+ // call agreement the winner — two answers to one call, and the silent one
68
+ // was worse: the artifact landed somewhere the caller had just been told it
69
+ // would also appear. The refusal itself lives in ArtifactsClient, the one
70
+ // gate every writer goes through, so this surface cannot drift from the
71
+ // others by wording its own verdict (ZIG-1075).
72
+ const chatId = args['chatId'];
72
73
  // ZIG-1037: no scope is legal. The throw that used to live here ("Pass
73
74
  // chatId or agreementId") is the exact failure this ticket removed — it cost
74
75
  // a live agent a turn mid-delivery for naming no container, when the record
@@ -80,7 +81,7 @@ export const recordArtifactCapability = {
80
81
  const contentType = args['content_type'];
81
82
  const taskId = args['taskId'];
82
83
  const creds = fullCreds(env);
83
- const { artifactId } = await new ArtifactsClient(creds.operatorKey, creds.agentId).writeStrict({
84
+ const { artifactId } = await new ArtifactsClient(creds.operatorKey, creds.agentId, creds.laneId).writeStrict({
84
85
  text: args['text'],
85
86
  visibility,
86
87
  chatId,
@@ -128,7 +129,7 @@ export const listArtifactsCapability = {
128
129
  needsAgentId: true,
129
130
  handler: async (args, env) => {
130
131
  const creds = fullCreds(env);
131
- return new ArtifactsClient(creds.operatorKey, creds.agentId).list({ authoredBy: 'me' }, {
132
+ return new ArtifactsClient(creds.operatorKey, creds.agentId, creds.laneId).list({ authoredBy: 'me' }, {
132
133
  after: args['after'],
133
134
  limit: typeof args['limit'] === 'number' ? args['limit'] : undefined,
134
135
  });
@@ -248,7 +249,7 @@ export const attachArtifactCapability = {
248
249
  throw new Error('role must be input or output');
249
250
  }
250
251
  const creds = fullCreds(env);
251
- const client = new ArtifactsClient(creds.operatorKey, creds.agentId);
252
+ const client = new ArtifactsClient(creds.operatorKey, creds.agentId, creds.laneId);
252
253
  if (chatId) {
253
254
  await client.attachToChat(artifactId, chatId);
254
255
  return {
@@ -1,15 +1,9 @@
1
- import { ContextReadClient } from '../http/ContextReadClient.js';
1
+ import { ContextReadClient, CONTEXT_READ_TYPES, CONTEXT_READ_VIA, parseVia, viaHint, } from '../http/ContextReadClient.js';
2
2
  import { ContextGrantsClient, } from '../http/ContextGrantsClient.js';
3
3
  import { ContextDiscoveryClient } from '../http/ContextDiscoveryClient.js';
4
4
  import { grantCaveat, CONTEXT_GRANT_SCOPE_KINDS, } from '../http/grants.js';
5
5
  import { fetchMyOrgs, resolveOrgSelector } from '../http/OrgsClient.js';
6
6
  import { fullCreds } from './types.js';
7
- const CONTEXT_READ_TYPES = [
8
- 'messages',
9
- 'artifacts',
10
- 'agreements',
11
- 'tasks',
12
- ];
13
7
  // ZIG-1037: `artifact` joined the context rail. Delegating an artifact grant
14
8
  // onward works (same kind, same id — an exact re-grant); narrowing a container
15
9
  // grant DOWN to an artifact inside it is deliberately not supported yet.
@@ -49,12 +43,26 @@ export async function resolveOrgScopeId(env, scopeId) {
49
43
  const lister = env.surface === 'mcp' ? 'ziggs_org_list' : 'your org list';
50
44
  throw new Error(`No org named "${scopeId}" in your memberships — use ${lister} to see them, or pass the org id.`);
51
45
  }
46
+ /**
47
+ * Which `via` each read type accepts, rendered for the tool text. Generated from
48
+ * CONTEXT_READ_VIA so the pairing an agent is told about and the pairing the
49
+ * call actually accepts are one statement — the prose that used to enumerate
50
+ * these by hand had already drifted from the server's list.
51
+ */
52
+ const VIA_BY_TYPE = CONTEXT_READ_TYPES.map((t) => `${t} via ${viaHint(t)}`).join('; ');
53
+ /**
54
+ * The one fact about `artifact:<id>` that both surfaces must state: it is how you
55
+ * reach an artifact no container can return. Shared for the same reason the
56
+ * pairings above are generated — hand-copied prose is what drifted.
57
+ */
58
+ const ARTIFACT_VIA_NOTE = 'via=artifact:<id> is a point read of one named artifact and the ONLY way to ' +
59
+ 'read one attached to no chat, agreement or task';
52
60
  export const contextReadCapability = {
53
61
  key: 'context_read',
54
62
  names: { sdk: 'context_read', mcp: 'ziggs_context_read' },
55
63
  descriptions: {
56
- sdk: 'Read the contents of a scope you already hold a grant for: messages | artifacts | agreements | tasks. Use grant_list first to see which scopes your grants cover, then read through any of them. For artifacts you can also name one directly with via=artifact:<id> — the only way to read an artifact attached to no chat, agreement or task. Cursored; all access is grant-fenced server-side.',
57
- mcp: 'Read the contents of a scope you already hold: messages | artifacts | agreements | tasks (the type param), under via=chat:<id>, agreement:<id>, or task:<id> — plus via=artifact:<id> for artifacts, a point read of one named artifact and the ONLY way to read one that is attached to no container (e.g. an artifact someone shared with you; ziggs_grant_list scopeKind=artifact shows those). Forward-delta with after+direction=forward; cursor pagination; contextGrantId pins a grant. The response carries a `readPlan` with the next page and/or forward-delta call pre-filled (after=this page\'s latestSequence), so you can keep reading without rebuilding args. This is the single read path for all four types — to discover which scopes exist, use the listers: ziggs_chat_list, ziggs_task_list, ziggs_agreement_list, ziggs_grant_list, ziggs_link_list.',
64
+ sdk: `Read the contents of a scope you already hold a grant for: ${CONTEXT_READ_TYPES.join(' | ')}. Each type reads through its own entries — ${VIA_BY_TYPE}. Use grant_list first to see which scopes your grants cover, then read through any of them. ${ARTIFACT_VIA_NOTE}. Cursored; all access is grant-fenced server-side.`,
65
+ mcp: `Read the contents of a scope you already hold: ${CONTEXT_READ_TYPES.join(' | ')} (the type param). Each type accepts its own via entries — ${VIA_BY_TYPE} — and any other pairing is refused. ${ARTIFACT_VIA_NOTE} (e.g. an artifact someone shared with you; ziggs_grant_list scopeKind=artifact shows those). Forward-delta with after+direction=forward; cursor pagination; contextGrantId pins a grant. The response carries a \`readPlan\` with the next page and/or forward-delta call pre-filled (after=this page's latestSequence), so you can keep reading without rebuilding args. This is the single read path for all four types — to discover which scopes exist, use the listers: ziggs_chat_list, ziggs_task_list, ziggs_agreement_list, ziggs_grant_list, ziggs_link_list.`,
58
66
  },
59
67
  annotation: 'read-only',
60
68
  params: {
@@ -67,7 +75,7 @@ export const contextReadCapability = {
67
75
  via: {
68
76
  type: 'string',
69
77
  required: true,
70
- description: 'Scope entry you hold, e.g. chat:<id>, agreement:<id>, task:<id>. For type=artifacts also artifact:<id> to read that one artifact.',
78
+ description: `Scope entry you hold, as <kind>:<id>. Accepted per type — ${VIA_BY_TYPE}.`,
71
79
  },
72
80
  cursor: { type: 'string', description: 'Opaque cursor from a prior nextCursor to page' },
73
81
  after: { type: 'string', description: 'ISO timestamp for forward-delta (messages/artifacts)' },
@@ -93,11 +101,21 @@ export const contextReadCapability = {
93
101
  const via = args['via'];
94
102
  if (!via)
95
103
  throw new Error('via is required');
104
+ // Same verdict the server gives, given at the call site so the reason
105
+ // arrives with the mistake. Never a softer one: a client that "fixes" an
106
+ // argument the server would reject is a second answer to one call.
107
+ const parsed = parseVia(via);
108
+ if (!parsed) {
109
+ throw new Error(`via must be <kind>:<id> — one of ${viaHint(type)} for type=${type}`);
110
+ }
111
+ if (!CONTEXT_READ_VIA[type].includes(parsed.kind)) {
112
+ throw new Error(`${type} reads accept via=${viaHint(type)} — not ${parsed.kind}:<id>`);
113
+ }
96
114
  const direction = args['direction'];
97
115
  if (direction !== undefined && direction !== 'forward') {
98
116
  throw new Error('direction must be "forward"');
99
117
  }
100
- return new ContextReadClient(creds.operatorKey, creds.agentId).read(type, {
118
+ return new ContextReadClient(creds.operatorKey, creds.agentId, undefined, creds.laneId).read(type, {
101
119
  via,
102
120
  cursor: args['cursor'],
103
121
  after: args['after'],
@@ -4,6 +4,13 @@ import { type CapabilityDefinition } from './types.js';
4
4
  * (context chat/agreement/org/artifact, connection, wallet), holder-scoped,
5
5
  * cross-session. `unreadableRails` comes from the backend (ZIG-956) so a short
6
6
  * list is never presented as complete when the key can't read a rail.
7
+ *
8
+ * ZIG-1088 — HOLD, not reach. `GET /grants` queries three row collections and
9
+ * nothing else; the implicit arms in AccessService (authorship, chat
10
+ * membership, agreement party, org membership) leave no row behind, so a reader
11
+ * can be entitled to something this list will never mention. The description
12
+ * says so, because the old "the single answer" wording was read as completeness
13
+ * and an empty list as "no access".
7
14
  */
8
15
  export declare const listGrantsCapability: CapabilityDefinition;
9
16
  export declare const GRANTS_CAPABILITIES: CapabilityDefinition[];
@@ -26,13 +26,20 @@ function parseScopeKinds(raw) {
26
26
  * (context chat/agreement/org/artifact, connection, wallet), holder-scoped,
27
27
  * cross-session. `unreadableRails` comes from the backend (ZIG-956) so a short
28
28
  * list is never presented as complete when the key can't read a rail.
29
+ *
30
+ * ZIG-1088 — HOLD, not reach. `GET /grants` queries three row collections and
31
+ * nothing else; the implicit arms in AccessService (authorship, chat
32
+ * membership, agreement party, org membership) leave no row behind, so a reader
33
+ * can be entitled to something this list will never mention. The description
34
+ * says so, because the old "the single answer" wording was read as completeness
35
+ * and an empty list as "no access".
29
36
  */
30
37
  export const listGrantsCapability = {
31
38
  key: 'grant_list',
32
39
  names: { sdk: 'grant_list', mcp: 'ziggs_grant_list' },
33
40
  descriptions: {
34
- sdk: 'List every grant this agent holds across all rails in one call — context (chat/agreement/org/artifact), connection, and wallet — as canonical grants (grantId, scope, caveats, expiresAt, health; no message/artifact content or credentials). The single answer to "what grants of mine do you hold?", holder-scoped and cross-session. Filter by scopeKind (rail) and health (defaults to active). Rails you lack the operator-key read scope for are named in unreadableRails, not silently dropped. Cursor-paginated: pass cursor from a prior nextCursor. Pair with context_read to read through a context grant, or context_expand_reach to enumerate a scope.',
35
- mcp: 'List every grant this delegate holds across all rails in one call — context (chat/agreement/org/artifact), connection, and wallet — as canonical grants (grantId, scope, caveats, expiresAt, health; no content or credentials). The single answer to "what grants of mine do you hold?", holder-scoped and cross-session. Filter by scopeKind (rail) and health (defaults to active). Rails you lack the operator-key read scope for are named in unreadableRails, not silently dropped. Cursor-paginated: pass cursor from a prior nextCursor to page. Pass a grantId to ziggs_context_read to pin a specific grant, or ziggs_context_expand_reach to enumerate a scope.',
41
+ sdk: 'List every grant this agent holds across all rails in one call — context (chat/agreement/org/artifact), connection, and wallet — as canonical grants (grantId, scope, caveats, expiresAt, health; no message/artifact content or credentials). Holder-scoped and cross-session. It answers "what grants do I HOLD?", which is narrower than "what can I reach?": reach you have by authoring something, by sitting in a chat, by being a party to an agreement, or through your org is not a grant row and never appears here — so an empty list means "no grants", never "no access". Filter by scopeKind (rail) and health (defaults to active). Rails you lack the operator-key read scope for are named in unreadableRails, not silently dropped. Cursor-paginated: pass cursor from a prior nextCursor. To answer "what can I reach?" instead, use context_expand_reach to enumerate a scope, or context_read to read through a grant.',
42
+ mcp: 'List every grant this delegate holds across all rails in one call — context (chat/agreement/org/artifact), connection, and wallet — as canonical grants (grantId, scope, caveats, expiresAt, health; no content or credentials). Holder-scoped and cross-session. It answers "what grants do I HOLD?", which is narrower than "what can I reach?": reach you have by authoring something, by sitting in a chat, by being a party to an agreement, or through your org is not a grant row and never appears here — so an empty list means "no grants", never "no access". Filter by scopeKind (rail) and health (defaults to active). Rails you lack the operator-key read scope for are named in unreadableRails, not silently dropped. Cursor-paginated: pass cursor from a prior nextCursor to page. To answer "what can I reach?" instead, use ziggs_context_expand_reach to enumerate a scope, or pass a grantId to ziggs_context_read to pin a specific grant.',
36
43
  },
37
44
  annotation: 'read-only',
38
45
  params: {
@@ -43,6 +43,7 @@ export interface CapabilityEnv {
43
43
  creds: {
44
44
  operatorKey: string;
45
45
  agentId?: string;
46
+ laneId?: string;
46
47
  };
47
48
  /** SDK runner base-URL override (BACKEND_URL / ZIGGS_BACKEND_URL). */
48
49
  baseUrl?: string;
@@ -1,11 +1,13 @@
1
1
  /** Creds for impersonated calls; adapters guarantee agentId when needsAgentId. */
2
2
  export function fullCreds(env) {
3
- const { operatorKey, agentId } = env.creds;
3
+ const { operatorKey, agentId, laneId } = env.creds;
4
4
  if (!operatorKey)
5
5
  throw new Error('operatorKey missing from tool context');
6
6
  if (!agentId)
7
7
  throw new Error('agentId missing from tool context');
8
- return { operatorKey, agentId };
8
+ // ZIG-1092 — the lane rides along so every Creds-based client sends
9
+ // X-Ziggs-Lane without each capability having to remember to.
10
+ return { operatorKey, agentId, ...(laneId ? { laneId } : {}) };
9
11
  }
10
12
  /**
11
13
  * Re-throw a client error with a capability-level prefix, preserving the HTTP
@@ -1,6 +1,5 @@
1
1
  import 'dotenv/config';
2
2
  import { type Creds, type Agreement, type EngagementKind, type BroadcastAudience } from '../types.js';
3
- export type PlanReviewTiming = 'with_proposal' | 'before_execution';
4
3
  /**
5
4
  * Shared proposal terms. When `engagementKind` is omitted the server defaults to `service`.
6
5
  * For an open buyer-broadcast quest, set `proposedTo` to a broadcast sentinel —
@@ -22,7 +21,6 @@ export interface ProposeTerms {
22
21
  billing?: 'total' | 'per_task';
23
22
  agreementDescription?: string;
24
23
  parentAgreementId?: string;
25
- parentTaskId?: string;
26
24
  /**
27
25
  * Who does the work. Required on direct proposals (your own id = you offer;
28
26
  * the proposedTo id = you commission the recipient); forbidden on
@@ -30,9 +28,6 @@ export interface ProposeTerms {
30
28
  * side — there is no payer input.
31
29
  */
32
30
  providerId?: string;
33
- plan?: unknown;
34
- planReviewTiming?: PlanReviewTiming;
35
- requireMidWorkPlanAck?: boolean;
36
31
  /** Defaults to `service` on the server when omitted. Set `hire` for representation contracts. */
37
32
  engagementKind?: EngagementKind;
38
33
  idempotencyKey?: string;
@@ -67,16 +62,12 @@ export interface DelegateAgreementData {
67
62
  executorId: string;
68
63
  chatId: string;
69
64
  parentAgreementId: string;
70
- parentTaskId?: string;
71
65
  price?: number;
72
66
  lifecycle?: string;
73
67
  expiresAt?: string;
74
68
  maxExecutions?: number;
75
69
  agreementDescription?: string;
76
70
  payerId?: string;
77
- plan?: unknown;
78
- planReviewTiming?: PlanReviewTiming;
79
- requireMidWorkPlanAck?: boolean;
80
71
  idempotencyKey?: string;
81
72
  }
82
73
  export declare function delegateAgreement(proposalData: DelegateAgreementData, creds: Creds): Promise<Agreement>;
@@ -91,7 +82,29 @@ export declare function delegateAgreement(proposalData: DelegateAgreementData, c
91
82
  export declare function respondToAgreement(agreementId: string, action: 'approve' | 'reject', creds: Creds, opts?: {
92
83
  ownerUserId?: string | null;
93
84
  agreement?: Agreement | null;
85
+ /** Where to send the human when the decision is theirs (ZIG-1087). */
86
+ appUrl?: string;
94
87
  }): Promise<Agreement>;
88
+ /**
89
+ * The approval facts a "who decides this?" question is answered from: the party
90
+ * ids still owing a decision, plus the named responder slot.
91
+ *
92
+ * Split out (ZIG-1087) so the inbox card and the respond call answer that
93
+ * question with ONE rule. The card had no rule at all — it offered the respond
94
+ * tool for every pending proposal, including the ones only the human can
95
+ * answer, and the agent found out by 403.
96
+ */
97
+ export interface PendingApprovalFacts {
98
+ /** Party ids with a pending entry on the approvals ledger. */
99
+ pendingPartyIds: readonly string[];
100
+ /** The direct responder slot, when the proposal names one. */
101
+ proposedTo?: string | null;
102
+ }
103
+ /**
104
+ * Which of `candidateIds` holds the pending approval slot, first match wins —
105
+ * so pass them in preference order (agent id before owner principal).
106
+ */
107
+ export declare function resolvePendingApprovalPartyId(facts: PendingApprovalFacts, candidateIds: ReadonlyArray<string | null | undefined>): string | null;
95
108
  /**
96
109
  * ZIG-524 — resolve which approvals.partyId the current operator may submit.
97
110
  * Checks pending ledger entries against impersonated agent id and owner principal.
@@ -116,10 +129,6 @@ export interface CounterAgreementData {
116
129
  lifecycle?: string;
117
130
  maxExecutions?: number;
118
131
  description?: string;
119
- /** Override plan `{ steps: [...] }`; omit to copy from original proposal task */
120
- plan?: unknown;
121
- planReviewTiming?: PlanReviewTiming;
122
- requireMidWorkPlanAck?: boolean;
123
132
  }
124
133
  export declare function counterAgreement(agreementId: string, counter: CounterAgreementData, creds: Creds): Promise<Agreement>;
125
134
  export declare function getAgreementStatus(agreementId: string, creds: Creds): Promise<unknown | null>;
@@ -185,7 +194,15 @@ export declare function claimAgreement(agreementId: string, creds: Creds): Promi
185
194
  ok: boolean;
186
195
  agreement: Agreement;
187
196
  }>;
188
- export type ChatLinkType = 'origin' | 'mention' | 'delegation' | 'join';
197
+ /**
198
+ * Link types a caller may ASK for — the server's `CALLER_LINK_TYPES`, not its
199
+ * full `LINK_TYPES`. `space` is deliberately absent: an agreement space gets
200
+ * exactly one system-minted room, and letting a request name that type would
201
+ * burn the root's single slot on an unrelated chat. Shorter than the server
202
+ * union on purpose, which is why this list carries the reason with it.
203
+ */
204
+ export declare const CHAT_LINK_TYPES: readonly ["origin", "mention", "delegation", "join"];
205
+ export type ChatLinkType = (typeof CHAT_LINK_TYPES)[number];
189
206
  export declare function linkAgreementToChat(agreementId: string, chatId: string, linkType: ChatLinkType | undefined, creds: Creds): Promise<unknown | null>;
190
207
  export declare function getChatsForAgreement(agreementId: string, creds: Creds): Promise<unknown[]>;
191
208
  export declare function joinAgreement(agreementId: string, creds: Creds): Promise<{
@@ -193,10 +210,12 @@ export declare function joinAgreement(agreementId: string, creds: Creds): Promis
193
210
  agentId: string | null;
194
211
  isNew: boolean;
195
212
  }>;
196
- export type ArtifactLinkType = 'produced' | 'referenced';
213
+ export declare const ARTIFACT_LINK_TYPES: readonly ["produced", "referenced"];
214
+ export type ArtifactLinkType = (typeof ARTIFACT_LINK_TYPES)[number];
197
215
  export declare function linkArtifactToAgreement(agreementId: string, artifactId: string, linkType: ArtifactLinkType | undefined, creds: Creds): Promise<unknown | null>;
198
216
  export declare function getArtifactsForAgreement(agreementId: string, creds: Creds): Promise<unknown[]>;
199
- export type UserRole = 'payer' | 'provider' | 'participant' | 'observer';
217
+ export declare const AGREEMENT_USER_ROLES: readonly ["payer", "provider", "participant", "observer"];
218
+ export type UserRole = (typeof AGREEMENT_USER_ROLES)[number];
200
219
  export declare function linkUserToAgreement(agreementId: string, userId: string, role: UserRole, creds: Creds): Promise<unknown | null>;
201
220
  export declare function getUsersForAgreement(agreementId: string, creds: Creds): Promise<unknown[]>;
202
221
  export declare class AgreementClient {
@@ -14,6 +14,9 @@ function buildHeaders(creds) {
14
14
  'content-type': 'application/json',
15
15
  Authorization: `Bearer ${creds.operatorKey}`,
16
16
  'X-Agent-Id': creds.agentId,
17
+ // ZIG-1092 — the wake's lane, so the backend can fence this call to the
18
+ // engagement it belongs to rather than the agent's whole authority.
19
+ ...(creds.laneId ? { 'X-Ziggs-Lane': creds.laneId } : {}),
17
20
  };
18
21
  }
19
22
  function assertCreds(creds, op) {
@@ -135,28 +138,54 @@ export async function respondToAgreement(agreementId, action, creds, opts = {})
135
138
  else if (!partyId) {
136
139
  throw new Error(`No pending approval entry for this operator on agreement ${agreementId}`);
137
140
  }
141
+ // ZIG-1087 — the slot resolved to the principal, not to us. Every call from
142
+ // this client impersonates `creds.agentId` (X-Agent-Id on every request), and
143
+ // `PUT /approvals/:partyId` takes a decision only from the party itself, so
144
+ // sending this would 403 with "You can only submit your own approval" —
145
+ // a condition, from which the caller cannot tell that no retry will ever
146
+ // work. Withholding consent from delegates is deliberate; being silent about
147
+ // it is not.
148
+ if (partyId && partyId !== creds.agentId) {
149
+ throw new Error(`Agreement ${agreementId} is waiting on ${partyId} — your principal, not you. ` +
150
+ `Consent is the human's to give: a delegate can submit its own approval slot and never its principal's, ` +
151
+ `so there is nothing to retry here and no tool that changes it. ` +
152
+ `Ask your human to approve or reject it${opts.appUrl ? ` at ${opts.appUrl}` : ' in the Ziggs app, under Agreements'}, ` +
153
+ `then read the agreement again to see the outcome.`);
154
+ }
138
155
  return approveAgreementAsParty(agreementId, partyId, action === 'approve' ? 'approved' : 'rejected', creds);
139
156
  }
140
157
  /**
141
- * ZIG-524 — resolve which approvals.partyId the current operator may submit.
142
- * Checks pending ledger entries against impersonated agent id and owner principal.
158
+ * Which of `candidateIds` holds the pending approval slot, first match wins —
159
+ * so pass them in preference order (agent id before owner principal).
143
160
  */
144
- export function resolveMyPendingApprovalPartyId(agreement, opts) {
145
- const actorIds = [opts.agentId, opts.ownerUserId].filter((id) => typeof id === 'string' && id.length > 0);
146
- const pending = (agreement.approvals ?? []).filter((a) => a.status === 'pending' || a.status === 'PENDING');
161
+ export function resolvePendingApprovalPartyId(facts, candidateIds) {
162
+ const actorIds = candidateIds.filter((id) => typeof id === 'string' && id.length > 0);
147
163
  for (const id of actorIds) {
148
- const hit = pending.find((a) => a.partyId === id);
149
- if (hit)
150
- return hit.partyId;
164
+ if (facts.pendingPartyIds.includes(id))
165
+ return id;
151
166
  }
152
- const proposedTo = agreement.parties?.proposedTo;
167
+ // A row with no pending ledger entries is a legacy one (approvals[] predates
168
+ // ZIG-244), and there the named responder slot IS the decision.
169
+ const proposedTo = facts.proposedTo ?? null;
153
170
  if (proposedTo &&
154
171
  actorIds.includes(proposedTo) &&
155
- (pending.length === 0 || pending.some((a) => a.partyId === proposedTo))) {
172
+ facts.pendingPartyIds.length === 0) {
156
173
  return proposedTo;
157
174
  }
158
175
  return null;
159
176
  }
177
+ /**
178
+ * ZIG-524 — resolve which approvals.partyId the current operator may submit.
179
+ * Checks pending ledger entries against impersonated agent id and owner principal.
180
+ */
181
+ export function resolveMyPendingApprovalPartyId(agreement, opts) {
182
+ return resolvePendingApprovalPartyId({
183
+ pendingPartyIds: (agreement.approvals ?? [])
184
+ .filter((a) => a.status === 'pending' || a.status === 'PENDING')
185
+ .map((a) => a.partyId),
186
+ proposedTo: agreement.parties?.proposedTo ?? null,
187
+ }, [opts.agentId, opts.ownerUserId]);
188
+ }
160
189
  /**
161
190
  * Record this party's approval decision on an agreement
162
191
  * (`PUT /agreements/:agreementId/approvals/:partyId`).
@@ -406,6 +435,22 @@ export async function claimAgreement(agreementId, creds) {
406
435
  }
407
436
  return data;
408
437
  }
438
+ // ---------------------------------------------------------------------------
439
+ // Chat links
440
+ // ---------------------------------------------------------------------------
441
+ /**
442
+ * Link types a caller may ASK for — the server's `CALLER_LINK_TYPES`, not its
443
+ * full `LINK_TYPES`. `space` is deliberately absent: an agreement space gets
444
+ * exactly one system-minted room, and letting a request name that type would
445
+ * burn the root's single slot on an unrelated chat. Shorter than the server
446
+ * union on purpose, which is why this list carries the reason with it.
447
+ */
448
+ export const CHAT_LINK_TYPES = [
449
+ 'origin',
450
+ 'mention',
451
+ 'delegation',
452
+ 'join',
453
+ ];
409
454
  export async function linkAgreementToChat(agreementId, chatId, linkType = 'mention', creds) {
410
455
  if (!agreementId || !chatId)
411
456
  return null;
@@ -468,6 +513,10 @@ export async function joinAgreement(agreementId, creds) {
468
513
  }
469
514
  return data;
470
515
  }
516
+ // ---------------------------------------------------------------------------
517
+ // Artifact links
518
+ // ---------------------------------------------------------------------------
519
+ export const ARTIFACT_LINK_TYPES = ['produced', 'referenced'];
471
520
  export async function linkArtifactToAgreement(agreementId, artifactId, linkType = 'produced', creds) {
472
521
  if (!agreementId || !artifactId)
473
522
  return null;
@@ -512,6 +561,15 @@ export async function getArtifactsForAgreement(agreementId, creds) {
512
561
  return [];
513
562
  }
514
563
  }
564
+ // ---------------------------------------------------------------------------
565
+ // User links
566
+ // ---------------------------------------------------------------------------
567
+ export const AGREEMENT_USER_ROLES = [
568
+ 'payer',
569
+ 'provider',
570
+ 'participant',
571
+ 'observer',
572
+ ];
515
573
  export async function linkUserToAgreement(agreementId, userId, role, creds) {
516
574
  if (!agreementId || !userId || !role)
517
575
  return null;
@@ -62,11 +62,15 @@ export declare function artifactScopeForSession(sessionId: string): {
62
62
  export declare class ArtifactsClient {
63
63
  private readonly operatorKey;
64
64
  private readonly agentId?;
65
+ private readonly laneId?;
65
66
  /**
66
67
  * @param operatorKey Agent-scoped or fleet operator key.
67
68
  * @param agentId Required for fleet keys (sent as X-Agent-Id). Omit for agent-scoped keys.
69
+ * @param laneId ZIG-1092 — the wake's lane (chat id, or `agrn-<agreementId>`),
70
+ * sent as X-Ziggs-Lane so `authoredBy=me` is fenced to this engagement
71
+ * instead of returning every customer's deliverables in one call.
68
72
  */
69
- constructor(operatorKey: string, agentId?: string);
73
+ constructor(operatorKey: string, agentId?: string, laneId?: string);
70
74
  list(q: ListArtifactsQuery, opts?: ListArtifactsOptions): Promise<ListArtifactsResult>;
71
75
  /**
72
76
  * Record an agent thought as an `agent-private` artifact. Replaces the
@@ -28,15 +28,20 @@ export function artifactScopeForSession(sessionId) {
28
28
  export class ArtifactsClient {
29
29
  operatorKey;
30
30
  agentId;
31
+ laneId;
31
32
  /**
32
33
  * @param operatorKey Agent-scoped or fleet operator key.
33
34
  * @param agentId Required for fleet keys (sent as X-Agent-Id). Omit for agent-scoped keys.
35
+ * @param laneId ZIG-1092 — the wake's lane (chat id, or `agrn-<agreementId>`),
36
+ * sent as X-Ziggs-Lane so `authoredBy=me` is fenced to this engagement
37
+ * instead of returning every customer's deliverables in one call.
34
38
  */
35
- constructor(operatorKey, agentId) {
39
+ constructor(operatorKey, agentId, laneId) {
36
40
  if (!operatorKey)
37
41
  throw new Error('ArtifactsClient: operatorKey is required');
38
42
  this.operatorKey = operatorKey;
39
43
  this.agentId = agentId;
44
+ this.laneId = laneId;
40
45
  }
41
46
  async list(q, opts = {}) {
42
47
  const selectors = [q.chatId, q.agreementId, q.taskId, q.authoredBy].filter(Boolean);
@@ -178,12 +183,19 @@ export class ArtifactsClient {
178
183
  */
179
184
  _assertScopeXor(input) {
180
185
  if (input.chatId && input.agreementId) {
181
- throw new Error('ArtifactsClient.write: pass at most one of chatId or agreementId');
186
+ // ZIG-1075: the refusal names the recovery, because this is the one gate
187
+ // for every caller — the exposed tool surfaces inherit it rather than each
188
+ // wording their own, and a caller that used to have chatId silently
189
+ // dropped here now learns what to do instead. Wording matters: a bare
190
+ // "pick one" at the last step of a finished task is what the drop was
191
+ // added to avoid (ZIG-924 dogfood).
192
+ throw new Error('pass at most one of chatId or agreementId — a deliverable under an ' +
193
+ 'agreement wants agreementId alone (its parties see it); use chatId ' +
194
+ 'only for a chat-scoped note. To put it in both places, record it ' +
195
+ 'with agreementId, then attach it to the chat.');
182
196
  }
183
197
  }
184
198
  _headers() {
185
- return buildOperatorHeaders(this.operatorKey, this.agentId, {
186
- 'content-type': 'application/json',
187
- });
199
+ return buildOperatorHeaders(this.operatorKey, this.agentId, { 'content-type': 'application/json' }, this.laneId);
188
200
  }
189
201
  }
@@ -7,6 +7,9 @@ function buildHeaders(creds) {
7
7
  'content-type': 'application/json',
8
8
  Authorization: `Bearer ${creds.operatorKey}`,
9
9
  'X-Agent-Id': creds.agentId,
10
+ // ZIG-1092 — the wake's lane, so the backend can fence this call to the
11
+ // engagement it belongs to rather than the agent's whole authority.
12
+ ...(creds.laneId ? { 'X-Ziggs-Lane': creds.laneId } : {}),
10
13
  };
11
14
  }
12
15
  function assertCreds(creds, op) {
@@ -1,5 +1,28 @@
1
1
  import 'dotenv/config';
2
- export type ContextReadType = 'messages' | 'artifacts' | 'agreements' | 'tasks';
2
+ export declare const CONTEXT_READ_TYPES: readonly ["messages", "artifacts", "agreements", "tasks"];
3
+ export type ContextReadType = (typeof CONTEXT_READ_TYPES)[number];
4
+ /**
5
+ * Entry points a read can name as `via=<kind>:<id>` — the server's `parseVia`
6
+ * grammar. `counterparty` parses but reads nothing: it resolves a scope graph,
7
+ * not content, which is why {@link CONTEXT_READ_VIA} admits it for no type.
8
+ */
9
+ export declare const VIA_KINDS: readonly ["chat", "agreement", "task", "counterparty", "artifact"];
10
+ export type ViaKind = (typeof VIA_KINDS)[number];
11
+ /**
12
+ * Which entry points each read type actually accepts, mirroring the server's
13
+ * `CONTEXT_READ_VIA`. Stated once here so the tool descriptions, the argument
14
+ * check, and this type all read off the same list instead of three prose
15
+ * copies that drift — and so a wrong pairing is refused with the reason rather
16
+ * than as a bare 400 from a round-trip away.
17
+ */
18
+ export declare const CONTEXT_READ_VIA: Record<ContextReadType, readonly ViaKind[]>;
19
+ /** `chat:<id>, agreement:<id>` — the accepted entries for one read type, for humans. */
20
+ export declare function viaHint(type: ContextReadType): string;
21
+ /** Split `chat:abc` into its parts, or null when it is not a `via` at all. */
22
+ export declare function parseVia(via: string): {
23
+ kind: ViaKind;
24
+ id: string;
25
+ } | null;
3
26
  export interface ContextReadQuery {
4
27
  via: string;
5
28
  cursor?: string;
@@ -12,7 +35,7 @@ export interface ContextReadQuery {
12
35
  export interface ContextReadEnvelope<T = unknown> {
13
36
  type: ContextReadType;
14
37
  via: {
15
- kind: string;
38
+ kind: ViaKind;
16
39
  id: string;
17
40
  };
18
41
  items: T[];
@@ -44,11 +67,15 @@ export declare class ContextReadClient {
44
67
  private readonly operatorKey;
45
68
  private readonly agentId?;
46
69
  private readonly baseUrl;
70
+ private readonly laneId?;
47
71
  /**
48
72
  * @param operatorKey Agent-scoped or fleet operator key.
49
73
  * @param agentId Required for fleet keys (sent as X-Agent-Id). Omit for agent-scoped keys.
74
+ * @param laneId ZIG-1092 — the wake's lane, sent as X-Ziggs-Lane. This is the
75
+ * path the dogfood leak ran through: `via=artifact:<other customer's spec>`
76
+ * was authorised purely because the same agent had authored it.
50
77
  */
51
- constructor(operatorKey: string, agentId?: string, baseUrl?: string);
78
+ constructor(operatorKey: string, agentId?: string, baseUrl?: string, laneId?: string);
52
79
  read<T = unknown>(type: ContextReadType, query: ContextReadQuery): Promise<ContextReadEnvelope<T>>;
53
80
  /**
54
81
  * Aggregated chat snapshot — `GET /context/snapshot?via=chat:<id>`. The
@@ -1,6 +1,54 @@
1
1
  import 'dotenv/config';
2
2
  import { getBackendUrl } from '../utils/urlUtils.js';
3
3
  import { pollSurfaceError } from '../shared/rateLimit.js';
4
+ export const CONTEXT_READ_TYPES = [
5
+ 'messages',
6
+ 'artifacts',
7
+ 'agreements',
8
+ 'tasks',
9
+ ];
10
+ /**
11
+ * Entry points a read can name as `via=<kind>:<id>` — the server's `parseVia`
12
+ * grammar. `counterparty` parses but reads nothing: it resolves a scope graph,
13
+ * not content, which is why {@link CONTEXT_READ_VIA} admits it for no type.
14
+ */
15
+ export const VIA_KINDS = [
16
+ 'chat',
17
+ 'agreement',
18
+ 'task',
19
+ 'counterparty',
20
+ 'artifact',
21
+ ];
22
+ /**
23
+ * Which entry points each read type actually accepts, mirroring the server's
24
+ * `CONTEXT_READ_VIA`. Stated once here so the tool descriptions, the argument
25
+ * check, and this type all read off the same list instead of three prose
26
+ * copies that drift — and so a wrong pairing is refused with the reason rather
27
+ * than as a bare 400 from a round-trip away.
28
+ */
29
+ export const CONTEXT_READ_VIA = {
30
+ messages: ['chat'],
31
+ artifacts: ['chat', 'agreement', 'task', 'artifact'],
32
+ agreements: ['chat', 'agreement'],
33
+ tasks: ['agreement', 'task'],
34
+ };
35
+ /** `chat:<id>, agreement:<id>` — the accepted entries for one read type, for humans. */
36
+ export function viaHint(type) {
37
+ return CONTEXT_READ_VIA[type].map((k) => `${k}:<id>`).join(', ');
38
+ }
39
+ /** Split `chat:abc` into its parts, or null when it is not a `via` at all. */
40
+ export function parseVia(via) {
41
+ const at = via.indexOf(':');
42
+ if (at <= 0)
43
+ return null;
44
+ const kind = via.slice(0, at);
45
+ const id = via.slice(at + 1);
46
+ if (!id)
47
+ return null;
48
+ return VIA_KINDS.includes(kind)
49
+ ? { kind: kind, id }
50
+ : null;
51
+ }
4
52
  /**
5
53
  * Protocol-first uniform context reads (ZIG-427).
6
54
  * Wraps `GET /context/read/:type` — one client, one envelope, four types.
@@ -9,16 +57,21 @@ export class ContextReadClient {
9
57
  operatorKey;
10
58
  agentId;
11
59
  baseUrl;
60
+ laneId;
12
61
  /**
13
62
  * @param operatorKey Agent-scoped or fleet operator key.
14
63
  * @param agentId Required for fleet keys (sent as X-Agent-Id). Omit for agent-scoped keys.
64
+ * @param laneId ZIG-1092 — the wake's lane, sent as X-Ziggs-Lane. This is the
65
+ * path the dogfood leak ran through: `via=artifact:<other customer's spec>`
66
+ * was authorised purely because the same agent had authored it.
15
67
  */
16
- constructor(operatorKey, agentId, baseUrl) {
68
+ constructor(operatorKey, agentId, baseUrl, laneId) {
17
69
  if (!operatorKey)
18
70
  throw new Error('ContextReadClient: operatorKey is required');
19
71
  this.operatorKey = operatorKey;
20
72
  this.agentId = agentId;
21
73
  this.baseUrl = baseUrl || getBackendUrl();
74
+ this.laneId = laneId;
22
75
  }
23
76
  async read(type, query) {
24
77
  if (!query.via?.trim()) {
@@ -44,6 +97,8 @@ export class ContextReadClient {
44
97
  };
45
98
  if (this.agentId)
46
99
  headers['X-Agent-Id'] = this.agentId;
100
+ if (this.laneId)
101
+ headers['X-Ziggs-Lane'] = this.laneId;
47
102
  if (query.contextGrantId) {
48
103
  headers['X-Context-Grant-Id'] = query.contextGrantId;
49
104
  }
@@ -88,6 +143,8 @@ export class ContextReadClient {
88
143
  };
89
144
  if (this.agentId)
90
145
  headers['X-Agent-Id'] = this.agentId;
146
+ if (this.laneId)
147
+ headers['X-Ziggs-Lane'] = this.laneId;
91
148
  if (opts.contextGrantId) {
92
149
  headers['X-Context-Grant-Id'] = opts.contextGrantId;
93
150
  }
@@ -1,11 +1,21 @@
1
1
  import 'dotenv/config';
2
+ /**
3
+ * What a delivery can be about. A closed union, not a comment: a consumer that
4
+ * dispatches on `kind` (the MCP read plan) is only safe if the compiler can tell
5
+ * it a case is missing. The task-only deliverable that was acked unread got
6
+ * through precisely because this was `string`.
7
+ *
8
+ * A type and not a value list, unlike the backend's `RESOURCE_KINDS` — nothing
9
+ * on this side validates a delivery kind at runtime (the server does that on the
10
+ * way in), and exhaustiveness checking is purely type-level.
11
+ */
12
+ export type InboxDeliveryKind = 'message' | 'artifact' | 'task-state' | 'agreement';
2
13
  /**
3
14
  * One thing addressed to this agent. A reference, never content — following it
4
15
  * (a chat read, a task read) is where this agent's grants are enforced.
5
16
  */
6
17
  export interface InboxDeliveryRef {
7
- /** 'message' | 'artifact' | 'task-state' | 'agreement'. */
8
- kind: string;
18
+ kind: InboxDeliveryKind;
9
19
  resourceId: string;
10
20
  chatId: string | null;
11
21
  agreementId: string | null;
@@ -24,16 +34,35 @@ export interface InboxProposalRef {
24
34
  agreementId: string;
25
35
  title: string;
26
36
  proposedAt: string | null;
37
+ /**
38
+ * ZIG-1087 — party ids still owing a decision, and the named responder slot.
39
+ * The inbox lists proposals awaiting the agent OR its human, and only the
40
+ * agent's own slot is one it can submit; these say which is which.
41
+ *
42
+ * Optional because a backend deployed before ZIG-1087 omits them, and this
43
+ * client is installed independently of the server it talks to. Absent reads
44
+ * as "no slot of mine", which routes the decision to the human — the safe
45
+ * direction: it withholds a call, it never invents authority.
46
+ */
47
+ pendingApprovalPartyIds?: string[];
48
+ proposedTo?: string | null;
27
49
  }
28
50
  export interface InboxConnectionRequestRef {
29
51
  requestId: string;
30
- requesterAgentId: string;
52
+ /**
53
+ * Non-addressable persona reference for the requester (`psn_*`).
54
+ * Never use as an account id for lookup / wake / pay (ZIG-1137).
55
+ */
56
+ requesterRef: string;
31
57
  /** ZIG-1039 — human-readable name for consent cards. */
32
58
  requesterDisplayName?: string | null;
33
59
  /** ZIG-1039 — org label for consent cards. */
34
60
  requesterOrgName?: string | null;
35
61
  message: string | null;
36
62
  requestedAt: string | null;
63
+ /** ZIG-1087 — see InboxProposalRef; a link request uses the same gate. */
64
+ pendingApprovalPartyIds?: string[];
65
+ proposedTo?: string | null;
37
66
  }
38
67
  /** Pull-only MCP: prompt the human when proposals need a decision (ZIG-482 / ZIG-481). */
39
68
  export interface InboxHumanAttention {
@@ -31,7 +31,6 @@ export interface PublishQuestPayload {
31
31
  description: string;
32
32
  chatId?: string;
33
33
  payerId?: string;
34
- parentTaskId?: string;
35
34
  price?: number;
36
35
  lifecycle?: string;
37
36
  expiresAt?: string;
@@ -7,6 +7,9 @@ function buildHeaders(creds) {
7
7
  'content-type': 'application/json',
8
8
  Authorization: `Bearer ${creds.operatorKey}`,
9
9
  'X-Agent-Id': creds.agentId,
10
+ // ZIG-1092 — the wake's lane, so the backend can fence this call to the
11
+ // engagement it belongs to rather than the agent's whole authority.
12
+ ...(creds.laneId ? { 'X-Ziggs-Lane': creds.laneId } : {}),
10
13
  };
11
14
  }
12
15
  function assertCreds(creds, op) {
@@ -1,10 +1,18 @@
1
1
  import 'dotenv/config';
2
2
  import { type Creds, type Task, type TaskState } from '../types.js';
3
+ /**
4
+ * When the buyer reviews a task's plan. Task-rail only — an agreement has no
5
+ * plan to review, which is why ZIG-1095 cut this from the propose/counter/
6
+ * subcontract inputs rather than teaching those routes to keep one.
7
+ */
8
+ export type PlanReviewTiming = 'with_proposal' | 'before_execution';
3
9
  export interface CreateTaskData {
4
10
  description: string;
5
11
  agreementId: string;
6
12
  parentTaskId?: string;
7
13
  plan?: unknown;
14
+ planReviewTiming?: PlanReviewTiming;
15
+ requireMidWorkPlanAck?: boolean;
8
16
  idempotencyKey?: string;
9
17
  /** Explicit delegation target (ZIG-586) — must be a party to the agreement; validated server-side. */
10
18
  assigneeId?: string;
@@ -7,6 +7,9 @@ function buildHeaders(creds) {
7
7
  'content-type': 'application/json',
8
8
  Authorization: `Bearer ${creds.operatorKey}`,
9
9
  'X-Agent-Id': creds.agentId,
10
+ // ZIG-1092 — the wake's lane, so the backend can fence this call to the
11
+ // engagement it belongs to rather than the agent's whole authority.
12
+ ...(creds.laneId ? { 'X-Ziggs-Lane': creds.laneId } : {}),
10
13
  };
11
14
  }
12
15
  function assertCreds(creds, op) {
@@ -7,8 +7,8 @@ export { MessagesClient } from './MessagesClient.js';
7
7
  export type { ListMessagesOptions, ListMessagesResult } from './MessagesClient.js';
8
8
  export { ArtifactsClient, artifactScopeForSession, AGREEMENT_LANE_PREFIX, } from './ArtifactsClient.js';
9
9
  export type { ArtifactVisibility, ListArtifactsOptions, ListArtifactsQuery, ListArtifactsResult, WriteArtifactInput, } from './ArtifactsClient.js';
10
- export { ContextReadClient } from './ContextReadClient.js';
11
- export type { ContextReadType, ContextReadQuery, ContextReadEnvelope, ContextSnapshotResult, } from './ContextReadClient.js';
10
+ export { ContextReadClient, CONTEXT_READ_TYPES, CONTEXT_READ_VIA, VIA_KINDS, parseVia, viaHint, } from './ContextReadClient.js';
11
+ export type { ContextReadType, ContextReadQuery, ContextReadEnvelope, ContextSnapshotResult, ViaKind, } from './ContextReadClient.js';
12
12
  export { ContextDiscoveryClient } from './ContextDiscoveryClient.js';
13
13
  export type { DiscoverableItem } from './ContextDiscoveryClient.js';
14
14
  export { GrantsClient } from './GrantsClient.js';
@@ -26,4 +26,4 @@ export type { MyOrg, OrgResolution } from './OrgsClient.js';
26
26
  export { AgentSearchClient } from './AgentSearchClient.js';
27
27
  export { TelemetryClient } from './TelemetryClient.js';
28
28
  export { InboxClient } from './InboxClient.js';
29
- export type { InboxDeliveryRef, InboxChatNews, InboxProposalRef, InboxTaskRef, InboxConnectionRequestRef, InboxHumanAttention, InboxEnvelope, InboxAckResult, InboxOperatorAgentEntry, InboxOperatorEnvelope, } from './InboxClient.js';
29
+ export type { InboxDeliveryKind, InboxDeliveryRef, InboxChatNews, InboxProposalRef, InboxTaskRef, InboxConnectionRequestRef, InboxHumanAttention, InboxEnvelope, InboxAckResult, InboxOperatorAgentEntry, InboxOperatorEnvelope, } from './InboxClient.js';
@@ -7,7 +7,7 @@ export { MessagesClient } from './MessagesClient.js';
7
7
  export { ArtifactsClient,
8
8
  // ZIG-1032: agreement lanes are not chats — callers scope artifact writes with this.
9
9
  artifactScopeForSession, AGREEMENT_LANE_PREFIX, } from './ArtifactsClient.js';
10
- export { ContextReadClient } from './ContextReadClient.js';
10
+ export { ContextReadClient, CONTEXT_READ_TYPES, CONTEXT_READ_VIA, VIA_KINDS, parseVia, viaHint, } from './ContextReadClient.js';
11
11
  export { ContextDiscoveryClient } from './ContextDiscoveryClient.js';
12
12
  export { GrantsClient } from './GrantsClient.js';
13
13
  export { ContextGrantsClient } from './ContextGrantsClient.js';
@@ -3,4 +3,10 @@
3
3
  * an agentId is present — agent-scoped keys identify the agent themselves;
4
4
  * fleet keys must pass one (ZIG-642).
5
5
  */
6
- export declare function buildOperatorHeaders(operatorKey: string, agentId?: string, extra?: Record<string, string>): Record<string, string>;
6
+ export declare function buildOperatorHeaders(operatorKey: string, agentId?: string, extra?: Record<string, string>,
7
+ /**
8
+ * ZIG-1092 — the lane this call belongs to, sent as `X-Ziggs-Lane`. The
9
+ * backend narrows the wake's reach to the engagement's orgs; omitting it
10
+ * narrows to the agent's own org, so it can never widen reach.
11
+ */
12
+ laneId?: string): Record<string, string>;
@@ -3,10 +3,17 @@
3
3
  * an agentId is present — agent-scoped keys identify the agent themselves;
4
4
  * fleet keys must pass one (ZIG-642).
5
5
  */
6
- export function buildOperatorHeaders(operatorKey, agentId, extra) {
6
+ export function buildOperatorHeaders(operatorKey, agentId, extra,
7
+ /**
8
+ * ZIG-1092 — the lane this call belongs to, sent as `X-Ziggs-Lane`. The
9
+ * backend narrows the wake's reach to the engagement's orgs; omitting it
10
+ * narrows to the agent's own org, so it can never widen reach.
11
+ */
12
+ laneId) {
7
13
  return {
8
14
  Authorization: `Bearer ${operatorKey}`,
9
15
  ...(agentId ? { 'X-Agent-Id': agentId } : {}),
16
+ ...(laneId ? { 'X-Ziggs-Lane': laneId } : {}),
10
17
  ...extra,
11
18
  };
12
19
  }
package/dist/index.d.ts CHANGED
@@ -3,7 +3,8 @@ export * from './capabilities/index.js';
3
3
  export * from './relay/provisionRelayWorkers.js';
4
4
  export { ConnectionManager } from './ConnectionManager.js';
5
5
  export type { StartAgentOptions } from './ConnectionManager.js';
6
- export { EntryTypes, ContentTypes, OPEN_AGREEMENT_TARGET, ORG_AGREEMENT_TARGET, BROADCAST_TARGETS, isBroadcastTarget, AGREEMENT_ENGAGEMENT_KIND, isValidContentType } from './types.js';
6
+ export { EntryTypes, ContentTypes, OPEN_AGREEMENT_TARGET, ORG_AGREEMENT_TARGET, BROADCAST_TARGETS, isBroadcastTarget, isPersonaRef, isRoomPresentationRef, isOpaquePresentationRef, AGREEMENT_ENGAGEMENT_KIND, isValidContentType, } from './types.js';
7
+ export type { PrincipalPresentation } from './types.js';
7
8
  export { getBackendUrl, getWebSocketUrl } from './utils/urlUtils.js';
8
9
  export { runtimeLog, resetRuntimeLogLevelCache } from './shared/runtimeLog.js';
9
10
  export { RateLimitedError, isRateLimited, parseRetryAfterMs, pollSurfaceError, } from './shared/rateLimit.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, 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, } from './types.js';
6
6
  export { getBackendUrl, getWebSocketUrl } from './utils/urlUtils.js';
7
7
  export { runtimeLog, resetRuntimeLogLevelCache } from './shared/runtimeLog.js';
8
8
  // ZIG-1019: retry loops need the server's own wait, not a guess.
package/dist/types.d.ts CHANGED
@@ -6,6 +6,21 @@ export declare class ApiError extends Error {
6
6
  export interface Creds {
7
7
  operatorKey: string;
8
8
  agentId: string;
9
+ /**
10
+ * ZIG-1092 — the lane (chat id, or `agrn-<agreementId>`) this call is being
11
+ * made from. Sent as `X-Ziggs-Lane` so the backend can fence the wake to the
12
+ * engagement the agent is actually acting inside.
13
+ *
14
+ * The operator key says WHO is calling; this says ON WHOSE BEHALF, RIGHT NOW.
15
+ * Without it an agent serving several customers carries its full authority
16
+ * into every call, and one tool call reaches another customer's work.
17
+ *
18
+ * Optional, and omitting it can only narrow what comes back (the backend
19
+ * falls back to the agent's own org) — never widen it. Nothing here is
20
+ * trusted: the server re-derives the party orgs itself and refuses a lane the
21
+ * agent is not in.
22
+ */
23
+ laneId?: string;
9
24
  }
10
25
  export type TaskState = 'active' | 'proposal' | 'completed' | 'failed' | 'cancelled' | 'ledger_open';
11
26
  export type PlanStepStatus = 'pending' | 'in_progress' | 'completed' | 'skipped';
@@ -159,6 +174,36 @@ export type BroadcastAudience = typeof OPEN_AGREEMENT_TARGET | typeof ORG_AGREEM
159
174
  export declare const BROADCAST_TARGETS: readonly ["everyone", "org"];
160
175
  /** True when `id` is a broadcast sentinel ('everyone' | 'org') rather than a concrete principal id. */
161
176
  export declare function isBroadcastTarget(id: string | null | undefined): boolean;
177
+ /**
178
+ * Persona face id (`psn_*`). Non-addressable — never use for agent lookup,
179
+ * wake, or payment parties (ZIG-1137).
180
+ */
181
+ export declare function isPersonaRef(id: string | null | undefined): boolean;
182
+ /**
183
+ * Room presentation binding id (`rpb_*`). Opaque to account lookup / wake /
184
+ * pay. Chat sends may echo it as `receiverId` — the backend resolves it in-room
185
+ * (ZIG-1137).
186
+ */
187
+ export declare function isRoomPresentationRef(id: string | null | undefined): boolean;
188
+ /** Either opaque presentation ref (`psn_*` | `rpb_*`). */
189
+ export declare function isOpaquePresentationRef(id: string | null | undefined): boolean;
190
+ /** Display + entitlement face for a principal on the message/roster wire. */
191
+ export interface PrincipalPresentation {
192
+ /** Public, non-addressable reference (`psn_*` or `rpb_*`). */
193
+ ref?: string;
194
+ persona: {
195
+ id: string;
196
+ name: string;
197
+ image?: string | null;
198
+ revision?: number;
199
+ };
200
+ mode: 'persona' | 'chain';
201
+ /** Present only when the viewer is entitled to resolve the subject. */
202
+ subject?: {
203
+ id: string;
204
+ type: 'user' | 'agent';
205
+ };
206
+ }
162
207
  export interface MessageMetadata {
163
208
  chatId: string;
164
209
  /** Stable id for dedup across push + inbox catch-up (ZIG-454). */
@@ -167,13 +212,23 @@ export interface MessageMetadata {
167
212
  sender: {
168
213
  id: string;
169
214
  type?: string;
215
+ presentation?: PrincipalPresentation | null;
216
+ /** Absent when the viewer is masked (persona layer). */
217
+ underAgreementId?: string | null;
218
+ presentedAs?: string | null;
170
219
  };
171
220
  senderId: string;
172
221
  senderType?: string;
173
222
  receiver?: {
174
223
  id: string;
224
+ presentation?: PrincipalPresentation | null;
175
225
  } | null;
176
226
  receiverId?: string | null;
227
+ /**
228
+ * Message-level presentation stamp when the backend attaches one
229
+ * (same shape as sender.presentation). Prefer sender.presentation.
230
+ */
231
+ presentation?: PrincipalPresentation | null;
177
232
  entryType?: string;
178
233
  content_type?: string;
179
234
  taskId?: string | null;
package/dist/types.js CHANGED
@@ -58,3 +58,22 @@ export const BROADCAST_TARGETS = [OPEN_AGREEMENT_TARGET, ORG_AGREEMENT_TARGET];
58
58
  export function isBroadcastTarget(id) {
59
59
  return id === OPEN_AGREEMENT_TARGET || id === ORG_AGREEMENT_TARGET;
60
60
  }
61
+ /**
62
+ * Persona face id (`psn_*`). Non-addressable — never use for agent lookup,
63
+ * wake, or payment parties (ZIG-1137).
64
+ */
65
+ export function isPersonaRef(id) {
66
+ return typeof id === 'string' && id.startsWith('psn_');
67
+ }
68
+ /**
69
+ * Room presentation binding id (`rpb_*`). Opaque to account lookup / wake /
70
+ * pay. Chat sends may echo it as `receiverId` — the backend resolves it in-room
71
+ * (ZIG-1137).
72
+ */
73
+ export function isRoomPresentationRef(id) {
74
+ return typeof id === 'string' && id.startsWith('rpb_');
75
+ }
76
+ /** Either opaque presentation ref (`psn_*` | `rpb_*`). */
77
+ export function isOpaquePresentationRef(id) {
78
+ return isPersonaRef(id) || isRoomPresentationRef(id);
79
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ziggs-ai/api-client",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "HTTP and WebSocket client for the Ziggs backend API",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",