@ziggs-ai/api-client 0.8.0 → 0.9.1

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.
Files changed (49) hide show
  1. package/README.md +10 -0
  2. package/dist/ConnectionManager.d.ts +21 -57
  3. package/dist/ConnectionManager.js +34 -163
  4. package/dist/capabilities/agreements.js +2 -2
  5. package/dist/capabilities/artifacts.js +11 -10
  6. package/dist/capabilities/connections.js +1 -1
  7. package/dist/capabilities/context.js +29 -11
  8. package/dist/capabilities/grants.d.ts +7 -0
  9. package/dist/capabilities/grants.js +9 -2
  10. package/dist/capabilities/index.d.ts +1 -1
  11. package/dist/capabilities/index.js +1 -1
  12. package/dist/capabilities/links.d.ts +0 -8
  13. package/dist/capabilities/links.js +3 -25
  14. package/dist/capabilities/types.d.ts +1 -0
  15. package/dist/capabilities/types.js +7 -2
  16. package/dist/http/AgreementClient.d.ts +67 -16
  17. package/dist/http/AgreementClient.js +161 -29
  18. package/dist/http/ArtifactsClient.d.ts +5 -1
  19. package/dist/http/ArtifactsClient.js +17 -5
  20. package/dist/http/ChatClient.js +5 -2
  21. package/dist/http/ConnectionsClient.js +4 -4
  22. package/dist/http/ContextDiscoveryClient.js +2 -1
  23. package/dist/http/ContextReadClient.d.ts +30 -3
  24. package/dist/http/ContextReadClient.js +63 -17
  25. package/dist/http/GrantsClient.js +2 -1
  26. package/dist/http/InboxClient.d.ts +32 -50
  27. package/dist/http/InboxClient.js +0 -39
  28. package/dist/http/MarketplaceClient.d.ts +0 -1
  29. package/dist/http/MarketplaceClient.js +8 -3
  30. package/dist/http/MessagesClient.js +3 -5
  31. package/dist/http/OrgsClient.js +3 -2
  32. package/dist/http/PaymentsClient.js +3 -5
  33. package/dist/http/TaskClient.d.ts +8 -0
  34. package/dist/http/TaskClient.js +3 -0
  35. package/dist/http/agreementFlows.d.ts +13 -5
  36. package/dist/http/agreementFlows.js +18 -24
  37. package/dist/http/index.d.ts +3 -3
  38. package/dist/http/index.js +1 -1
  39. package/dist/http/operatorHeaders.d.ts +7 -1
  40. package/dist/http/operatorHeaders.js +8 -1
  41. package/dist/index.d.ts +5 -4
  42. package/dist/index.js +4 -3
  43. package/dist/shared/apiError.d.ts +22 -1
  44. package/dist/shared/apiError.js +60 -3
  45. package/dist/shared/rateLimit.d.ts +12 -22
  46. package/dist/shared/rateLimit.js +18 -51
  47. package/dist/types.d.ts +70 -1
  48. package/dist/types.js +39 -1
  49. package/package.json +1 -1
@@ -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) {
@@ -101,7 +104,7 @@ export async function listMyChats(creds) {
101
104
  assertCreds(creds, 'list my chats');
102
105
  // ZIG-699 — empty array only for a genuine empty 200; any failure (non-2xx /
103
106
  // network) throws so ziggs_chat_list reports a real error instead of "you
104
- // have no chats". Status stays whitespace-delimited for toolError classification.
107
+ // have no chats". ZIG-1124 — HTTP failures are ApiError (status/body/code).
105
108
  let res;
106
109
  try {
107
110
  res = await fetch(`${getBackendUrl()}/chats/mine`, {
@@ -116,7 +119,7 @@ export async function listMyChats(creds) {
116
119
  if (!res.ok) {
117
120
  const body = await res.text().catch(() => '');
118
121
  runtimeLog.warn('ChatClient', `⚠️ listMyChats failed: ${res.status} ${res.statusText} ${body?.slice(0, 200)}`);
119
- throw new Error(`GET /chats/mine ${res.status} ${body?.slice(0, 200)}`);
122
+ throwApiError(res, body, `GET /chats/mine failed: ${res.status}`);
120
123
  }
121
124
  const data = await res.json().catch(() => null);
122
125
  return Array.isArray(data?.['chats']) ? data['chats'] : [];
@@ -1,5 +1,6 @@
1
1
  import 'dotenv/config';
2
2
  import { getBackendUrl } from '../utils/urlUtils.js';
3
+ import { throwApiError } from '../shared/apiError.js';
3
4
  import { buildOperatorHeaders } from './operatorHeaders.js';
4
5
  import { GrantsClient } from './GrantsClient.js';
5
6
  // ZIG-569 — defense-in-depth mirror of the backend leak-guard
@@ -169,10 +170,9 @@ export class ConnectionsClient {
169
170
  const response = await fetch(`${this.baseUrl}${path}`, init);
170
171
  const text = await response.text().catch(() => '');
171
172
  if (!response.ok) {
172
- const err = new Error(`${method} ${path} ${response.status} ${text.slice(0, 200)}`);
173
- err.status = response.status;
174
- err.body = text;
175
- throw err;
173
+ // ZIG-1124 — ApiError (status/body/code); ConnectionsError remains the
174
+ // documented duck type for callers that branch on `.status`.
175
+ throwApiError(response, text, `${method} ${path} failed: ${response.status}`);
176
176
  }
177
177
  return text;
178
178
  }
@@ -1,5 +1,6 @@
1
1
  import 'dotenv/config';
2
2
  import { getBackendUrl } from '../utils/urlUtils.js';
3
+ import { throwApiError } from '../shared/apiError.js';
3
4
  import { buildOperatorHeaders } from './operatorHeaders.js';
4
5
  /**
5
6
  * P4 discovery — labels-only pointers to context the agent could REQUEST but
@@ -34,7 +35,7 @@ export class ContextDiscoveryClient {
34
35
  });
35
36
  const body = await res.text().catch(() => '');
36
37
  if (!res.ok) {
37
- throw new Error(`ContextDiscoveryClient.discoverGrantable ${res.status} ${body.slice(0, 200)}`);
38
+ throwApiError(res, body, `ContextDiscoveryClient.discoverGrantable failed: ${res.status}`);
38
39
  }
39
40
  const parsed = JSON.parse(body);
40
41
  return (parsed.items ?? []).map((i) => ({
@@ -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,24 +97,19 @@ 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
  }
50
105
  const res = await fetch(url.toString(), { headers });
51
106
  const body = await res.text().catch(() => '');
52
107
  if (!res.ok) {
53
- // Carry the status like `snapshot()` does, so callers can branch on it.
54
108
  // A 403 here is a legitimate outcome, not a transport failure: addressing
55
109
  // and authorisation are separate, so an agent can be told about mail it
56
- // is not (or is no longer) allowed to open.
57
- // ZIG-1019: a 429 carries the server's own wait; everything else keeps
58
- // the plain status-tagged error callers already branch on.
59
- if (res.status === 429) {
60
- throw pollSurfaceError(`ContextReadClient.read ${type}`, res, body);
61
- }
62
- const err = new Error(`ContextReadClient.read ${type} ${res.status} ${body.slice(0, 200)}`);
63
- err.status = res.status;
64
- throw err;
110
+ // is not (or is no longer) allowed to open. ZIG-1124 — one ApiError shape
111
+ // (429 → RateLimitedError with Retry-After).
112
+ throw pollSurfaceError(`ContextReadClient.read ${type}`, res, body);
65
113
  }
66
114
  return JSON.parse(body);
67
115
  }
@@ -88,18 +136,16 @@ export class ContextReadClient {
88
136
  };
89
137
  if (this.agentId)
90
138
  headers['X-Agent-Id'] = this.agentId;
139
+ if (this.laneId)
140
+ headers['X-Ziggs-Lane'] = this.laneId;
91
141
  if (opts.contextGrantId) {
92
142
  headers['X-Context-Grant-Id'] = opts.contextGrantId;
93
143
  }
94
144
  const res = await fetch(url.toString(), { headers });
95
145
  const body = await res.text().catch(() => '');
96
146
  if (!res.ok) {
97
- if (res.status === 429) {
98
- throw pollSurfaceError('ContextReadClient.snapshot', res, body);
99
- }
100
- const err = new Error(`ContextReadClient.snapshot ${res.status} ${body.slice(0, 200)}`);
101
- err.status = res.status;
102
- throw err;
147
+ // ZIG-1124 — ApiError for every non-OK (429 → RateLimitedError).
148
+ throw pollSurfaceError('ContextReadClient.snapshot', res, body);
103
149
  }
104
150
  return JSON.parse(body);
105
151
  }
@@ -1,5 +1,6 @@
1
1
  import 'dotenv/config';
2
2
  import { getBackendUrl } from '../utils/urlUtils.js';
3
+ import { throwApiError } from '../shared/apiError.js';
3
4
  import { buildOperatorHeaders } from './operatorHeaders.js';
4
5
  /**
5
6
  * ZIG-648 — unified grant listing across every rail. `GET /grants` returns the
@@ -41,7 +42,7 @@ export class GrantsClient {
41
42
  });
42
43
  const body = await res.text().catch(() => '');
43
44
  if (!res.ok) {
44
- throw new Error(`GrantsClient.listGrants ${res.status} ${body.slice(0, 200)}`);
45
+ throwApiError(res, body, `GrantsClient.listGrants failed: ${res.status}`);
45
46
  }
46
47
  const parsed = JSON.parse(body);
47
48
  return {
@@ -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 {
@@ -98,45 +127,6 @@ export interface InboxAckResult {
98
127
  */
99
128
  export interface InboxReadOptions {
100
129
  waitSeconds?: number;
101
- /**
102
- * Operator read only (ZIG-965): restrict the sweep to this roster. The
103
- * server intersects it with the agents the key owner runs — it narrows,
104
- * never widens. A launcher hosting a subset should always pass the agents
105
- * it actually registered, or it pays for a sweep of the owner's whole
106
- * seeded fleet.
107
- */
108
- agents?: string[];
109
- }
110
- /**
111
- * One agent's line in the operator sweep: enough to decide whether to start a
112
- * host, and nothing more.
113
- *
114
- * Deliberately NOT that agent's envelope. The launcher only chooses who to
115
- * run; the host it starts reads its own inbox on its own credentials a moment
116
- * later, so building N envelopes here was work thrown away.
117
- */
118
- export interface InboxOperatorAgentEntry {
119
- agentId: string;
120
- /** Newest delivery addressed to this agent, or null if it never had one. */
121
- deliveredUpTo: string | null;
122
- /** How far it has acked. Mail exists when `deliveredUpTo > ackedUpTo`. */
123
- ackedUpTo: string | null;
124
- /**
125
- * True when this agent holds open assigned work. Independent of the
126
- * watermark: a host that died mid-task already acked past the task's
127
- * delivery, and the open task is the only durable trace it needs restarting.
128
- */
129
- hasOpenTasks: boolean;
130
- }
131
- /** GET /inbox/operator: which of the key owner's agents have mail or open work. */
132
- export interface InboxOperatorEnvelope {
133
- asOf: string;
134
- /** Agents with mail or open work. */
135
- agents: InboxOperatorAgentEntry[];
136
- /** Agents examined that had neither. */
137
- idleAgents: number;
138
- /** Owned agents beyond the server's per-pass cap — not examined. */
139
- truncatedAgents: number;
140
130
  }
141
131
  /**
142
132
  * The doorbell, not the door (ZIG-434): references addressed to this agent
@@ -155,14 +145,6 @@ export declare class InboxClient {
155
145
  private headers;
156
146
  private inboxUrl;
157
147
  getInbox(opts?: InboxReadOptions): Promise<InboxEnvelope>;
158
- /**
159
- * Operator-level multiplexed read: which of this key owner's agents have
160
- * mail or open work (an agent-scoped key collapses to its one agent).
161
- *
162
- * Returns who to start, never what they were sent — the host you start
163
- * reads its own inbox on its own identity. Ack stays per agent.
164
- */
165
- getOperatorInbox(opts?: InboxReadOptions): Promise<InboxOperatorEnvelope>;
166
148
  /**
167
149
  * Advance this agent's watermark — pass the envelope's `ackTo`. Monotonic
168
150
  * server-side: an older value is a no-op, so a replayed ack can never
@@ -35,9 +35,6 @@ export class InboxClient {
35
35
  if (opts.waitSeconds != null && opts.waitSeconds > 0) {
36
36
  url.searchParams.set('wait', String(opts.waitSeconds));
37
37
  }
38
- if (opts.agents?.length) {
39
- url.searchParams.set('agents', opts.agents.join(','));
40
- }
41
38
  return url.toString();
42
39
  }
43
40
  async getInbox(opts = {}) {
@@ -50,42 +47,6 @@ export class InboxClient {
50
47
  }
51
48
  return JSON.parse(body);
52
49
  }
53
- /**
54
- * Operator-level multiplexed read: which of this key owner's agents have
55
- * mail or open work (an agent-scoped key collapses to its one agent).
56
- *
57
- * Returns who to start, never what they were sent — the host you start
58
- * reads its own inbox on its own identity. Ack stays per agent.
59
- */
60
- async getOperatorInbox(opts = {}) {
61
- // Bounded wait (ZIG-965): the server holds at most `waitSeconds` (+ sweep
62
- // time); a poll that outlives that by a wide margin is a dead sweep, and
63
- // without a timeout it blocked the launcher's whole poll loop — lazy wake
64
- // simply stopped. Abort and let the caller's retry loop take over.
65
- const timeoutMs = ((opts.waitSeconds ?? 0) + 30) * 1000;
66
- const ac = new AbortController();
67
- const timer = setTimeout(() => ac.abort(), timeoutMs);
68
- let res;
69
- try {
70
- res = await fetch(this.inboxUrl('/inbox/operator', opts), {
71
- headers: this.headers(),
72
- signal: ac.signal,
73
- });
74
- }
75
- catch (err) {
76
- throw ac.signal.aborted
77
- ? new Error(`InboxClient.getOperatorInbox timed out after ${timeoutMs}ms`)
78
- : err;
79
- }
80
- finally {
81
- clearTimeout(timer);
82
- }
83
- const body = await res.text().catch(() => '');
84
- if (!res.ok) {
85
- throw pollSurfaceError('InboxClient.getOperatorInbox', res, body);
86
- }
87
- return JSON.parse(body);
88
- }
89
50
  /**
90
51
  * Advance this agent's watermark — pass the envelope's `ackTo`. Monotonic
91
52
  * server-side: an older value is a no-op, so a replayed ack can never
@@ -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;
@@ -1,4 +1,6 @@
1
1
  import 'dotenv/config';
2
+ // ZIG-1111: one shaping rule for every agreement this package parses.
3
+ import { shapeAgreement } from './AgreementClient.js';
2
4
  import { getBackendUrl } from '../utils/urlUtils.js';
3
5
  import { throwApiError } from '../shared/apiError.js';
4
6
  function getMarketplaceBaseUrl() { return `${getBackendUrl()}/marketplace`; }
@@ -7,6 +9,9 @@ function buildHeaders(creds) {
7
9
  'content-type': 'application/json',
8
10
  Authorization: `Bearer ${creds.operatorKey}`,
9
11
  'X-Agent-Id': creds.agentId,
12
+ // ZIG-1092 — the wake's lane, so the backend can fence this call to the
13
+ // engagement it belongs to rather than the agent's whole authority.
14
+ ...(creds.laneId ? { 'X-Ziggs-Lane': creds.laneId } : {}),
10
15
  };
11
16
  }
12
17
  function assertCreds(creds, op) {
@@ -27,7 +32,7 @@ export async function publishOffer(payload, creds) {
27
32
  throwApiError(res, body, `Marketplace offer publish failed: ${res.status}`);
28
33
  }
29
34
  const data = await res.json().catch(() => null);
30
- return (data?.['offer'] ?? data);
35
+ return shapeAgreement((data?.['offer'] ?? data));
31
36
  }
32
37
  export async function pullOffers(options, creds) {
33
38
  assertCreds(creds, 'marketplace offers pull');
@@ -64,7 +69,7 @@ export async function claimOffer(agreementId, creds) {
64
69
  const data = await res.json().catch(() => null);
65
70
  if (!data?.['ok'])
66
71
  throw new Error(data?.['error'] || 'Claim failed');
67
- return data['offer'];
72
+ return shapeAgreement(data['offer']);
68
73
  }
69
74
  export async function publishQuest(payload, creds) {
70
75
  assertCreds(creds, 'quest publish');
@@ -80,7 +85,7 @@ export async function publishQuest(payload, creds) {
80
85
  const data = await res.json().catch(() => null);
81
86
  if (!data?.['agreement'])
82
87
  throw new Error('Quest publish returned no agreement');
83
- return data['agreement'];
88
+ return shapeAgreement(data['agreement']);
84
89
  }
85
90
  export async function pullQuests(options, creds) {
86
91
  assertCreds(creds, 'quest pull');
@@ -1,5 +1,6 @@
1
1
  import 'dotenv/config';
2
2
  import { getBackendUrl } from '../utils/urlUtils.js';
3
+ import { throwApiError } from '../shared/apiError.js';
3
4
  /**
4
5
  * Read-side client for forward-delta message reads.
5
6
  *
@@ -35,11 +36,8 @@ export class MessagesClient {
35
36
  const res = await fetch(url.toString(), { headers: this._headers() });
36
37
  if (!res.ok) {
37
38
  const body = await res.text().catch(() => '');
38
- const err = new Error(`MessagesClient.list ${res.status} ${res.statusText} ${body.slice(0, 200)}`);
39
- // Callers branch on the HTTP status (404 = chat deleted/not visible)
40
- // without parsing the message string.
41
- err.status = res.status;
42
- throw err;
39
+ // Callers branch on ApiError.status (404 = chat deleted/not visible).
40
+ throwApiError(res, body, `MessagesClient.list failed: ${res.status}`);
43
41
  }
44
42
  return (await res.json());
45
43
  }
@@ -1,5 +1,6 @@
1
1
  import 'dotenv/config';
2
2
  import { getBackendUrl } from '../utils/urlUtils.js';
3
+ import { throwApiError } from '../shared/apiError.js';
3
4
  import { buildOperatorHeaders } from './operatorHeaders.js';
4
5
  /**
5
6
  * ZIG-739 / ZIG-956 — the operator's full org membership (not just granted
@@ -15,7 +16,7 @@ export async function fetchMyOrgs(creds, baseUrl) {
15
16
  });
16
17
  const body = await res.text().catch(() => '');
17
18
  if (!res.ok) {
18
- throw new Error(`GET /orgs/me ${res.status} ${body.slice(0, 200)}`);
19
+ throwApiError(res, body, `GET /orgs/me failed: ${res.status}`);
19
20
  }
20
21
  const parsed = body ? JSON.parse(body) : {};
21
22
  return (parsed.orgs ?? []).map((o) => ({
@@ -55,7 +56,7 @@ export async function fetchDelegateAccess(creds, baseUrl) {
55
56
  });
56
57
  const body = await res.text().catch(() => '');
57
58
  if (!res.ok) {
58
- throw new Error(`GET /agents/claude-delegate/access ${res.status} ${body.slice(0, 200)}`);
59
+ throwApiError(res, body, `GET /agents/claude-delegate/access failed: ${res.status}`);
59
60
  }
60
61
  return body ? JSON.parse(body) : {};
61
62
  }
@@ -2,7 +2,7 @@ import 'dotenv/config';
2
2
  import { getBackendUrl } from '../utils/urlUtils.js';
3
3
  import { buildOperatorHeaders } from './operatorHeaders.js';
4
4
  import { GrantsClient } from './GrantsClient.js';
5
- import { parseErrorMessage } from '../shared/apiError.js';
5
+ import { throwApiError } from '../shared/apiError.js';
6
6
  function randomIdempotencyKey(prefix = 'op') {
7
7
  return `${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
8
8
  }
@@ -217,10 +217,8 @@ export class PaymentsClient {
217
217
  const response = await fetch(`${this.baseUrl}${path}`, init);
218
218
  const text = await response.text();
219
219
  if (!response.ok) {
220
- const err = new Error(parseErrorMessage(text, `HTTP ${response.status}`));
221
- err.status = response.status;
222
- err.body = text;
223
- throw err;
220
+ // ZIG-1124 — ApiError; PaymentsError remains the duck type for `.status`.
221
+ throwApiError(response, text, `HTTP ${response.status}`);
224
222
  }
225
223
  return text ? JSON.parse(text) : null;
226
224
  }
@@ -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) {
@@ -1,4 +1,4 @@
1
- import { type ProposeTerms } from './AgreementClient.js';
1
+ import { type ClaimedKind, type ProposeTerms } from './AgreementClient.js';
2
2
  import { type Agreement, type Creds, type EngagementKind } from '../types.js';
3
3
  /**
4
4
  * ZIG-1022 — one propose grammar. Direct, broadcast (quest and standing
@@ -22,11 +22,19 @@ export declare function proposeUnified(input: UnifiedProposeInput, creds: Creds)
22
22
  agreement: Agreement;
23
23
  shape: ProposeShape;
24
24
  }>;
25
- export type ClaimedKind = 'link' | 'offer' | 'quest' | 'hand-off';
26
25
  /**
27
- * ZIG-1021 — one claim verb for any open broadcast. Fetches the agreement to
28
- * route: link invites and quests claim through POST /agreements/:id/claim;
29
- * standing offers (open payer side) through POST /marketplace/offers/claim.
26
+ * ZIG-1021 — one claim verb for any open broadcast: link invite, quest,
27
+ * hand-off, or standing offer.
28
+ *
29
+ * One request. This used to read the agreement first to decide which endpoint to
30
+ * post to, and `GET /agreements/:id` is party-scoped — a claimer is by definition
31
+ * not yet a party to the broadcast it is claiming, so the routing read 404'd and
32
+ * every standing offer in the store failed with "Agreement not found" before
33
+ * either claim endpoint was called (ZIG-1155). The backend routes it now, where
34
+ * the row is readable without being a party to it.
35
+ *
36
+ * `kind` arrives on the claim response — the route that did the routing reports
37
+ * which broadcast kind this turned out to be.
30
38
  */
31
39
  export declare function claimOpenAgreement(agreementId: string, creds: Creds): Promise<{
32
40
  agreement: Agreement;