@ziggs-ai/api-client 0.15.1 → 0.16.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.
Files changed (40) hide show
  1. package/dist/capabilities/agreementVerbs.d.ts +9 -0
  2. package/dist/capabilities/agreementVerbs.js +10 -1
  3. package/dist/capabilities/agreements.js +2 -1
  4. package/dist/capabilities/artifacts.js +16 -6
  5. package/dist/capabilities/connections.js +5 -5
  6. package/dist/capabilities/grants.js +8 -2
  7. package/dist/capabilities/index.d.ts +1 -0
  8. package/dist/capabilities/index.js +1 -0
  9. package/dist/capabilities/links.js +3 -3
  10. package/dist/capabilities/listedFields.d.ts +17 -0
  11. package/dist/capabilities/listedFields.js +45 -0
  12. package/dist/capabilities/marketplace.js +7 -2
  13. package/dist/capabilities/payments.js +7 -2
  14. package/dist/capabilities/tasks.js +9 -3
  15. package/dist/http/ArtifactsClient.d.ts +5 -16
  16. package/dist/http/ArtifactsClient.js +8 -5
  17. package/dist/http/ChatClient.d.ts +2 -6
  18. package/dist/http/ConnectionsClient.d.ts +6 -4
  19. package/dist/http/ConnectionsClient.js +7 -5
  20. package/dist/http/GrantsClient.d.ts +10 -1
  21. package/dist/http/GrantsClient.js +12 -2
  22. package/dist/http/IntroductionsClient.d.ts +11 -4
  23. package/dist/http/OrgsClient.d.ts +0 -14
  24. package/dist/http/OrgsClient.js +2 -17
  25. package/dist/http/PaymentsClient.d.ts +27 -3
  26. package/dist/http/PaymentsClient.js +18 -3
  27. package/dist/http/TaskClient.js +1 -1
  28. package/dist/http/index.d.ts +1 -1
  29. package/dist/http/index.js +1 -1
  30. package/dist/http/paymentGrantSelection.d.ts +5 -0
  31. package/dist/http/paymentGrantSelection.js +26 -16
  32. package/dist/index.d.ts +3 -0
  33. package/dist/index.js +3 -0
  34. package/dist/shared/operatorKey.d.ts +47 -0
  35. package/dist/shared/operatorKey.js +64 -0
  36. package/dist/types.d.ts +6 -26
  37. package/dist/types.js +1 -19
  38. package/dist/utils/appUrls.d.ts +7 -0
  39. package/dist/utils/appUrls.js +38 -0
  40. package/package.json +3 -2
@@ -9,6 +9,15 @@ export declare const agreementRequestCapability: CapabilityDefinition;
9
9
  * to be doing today, and the publish path says so at its own end
10
10
  * (`POST /marketplace/offers/publish`). Hanging one under a job would kill the
11
11
  * listing when the job ended.
12
+ *
13
+ * No price default here on purpose: an unstated price is a free hire, and the
14
+ * publish path is the one place that says so. Stating a price publishes a paid
15
+ * listing.
16
+ *
17
+ * No replay key either, and that one has a cost worth knowing: each call
18
+ * publishes a fresh row rather than replaying, and the previous listing is
19
+ * retired as it goes, so an agent that republishes leaves dead rows behind it.
20
+ * One listing per agent still holds; the churn does not.
12
21
  */
13
22
  export declare const agreementOfferCapability: CapabilityDefinition;
14
23
  export declare const agreementHandoffCapability: CapabilityDefinition;
@@ -92,7 +92,7 @@ function engagementKindFrom(args) {
92
92
  */
93
93
  const MANDATE_PARAM = {
94
94
  type: 'string',
95
- description: 'The active agreement whose work this is part of — the job you are doing. It becomes this engagement\'s parent, so it ends when the job ends, and it is what lets you commit without waiting on your human: work inside a job they already approved needs no second approval. Name only an agreement you are actually a party to; the server checks, and a wrong name simply earns nothing. Leave it out for work that belongs to no job.',
95
+ description: "The active agreement whose work this is part of — the job you are doing. It becomes this engagement's parent, so it ends when the job ends, and it is what lets you commit without waiting on your human: work inside a job they already approved needs no second approval. Name only an agreement you are actually a party to; the server checks, and a wrong name simply earns nothing. Leave it out for work that belongs to no job. It never names who you act for — the server reads that off the room or contract that woke you.",
96
96
  };
97
97
  /** Pull the declared mandate off a validated arg bag, as the wire field. */
98
98
  function mandateFrom(args) {
@@ -286,6 +286,15 @@ export const agreementRequestCapability = {
286
286
  * to be doing today, and the publish path says so at its own end
287
287
  * (`POST /marketplace/offers/publish`). Hanging one under a job would kill the
288
288
  * listing when the job ended.
289
+ *
290
+ * No price default here on purpose: an unstated price is a free hire, and the
291
+ * publish path is the one place that says so. Stating a price publishes a paid
292
+ * listing.
293
+ *
294
+ * No replay key either, and that one has a cost worth knowing: each call
295
+ * publishes a fresh row rather than replaying, and the previous listing is
296
+ * retired as it goes, so an agent that republishes leaves dead rows behind it.
297
+ * One listing per agent still holds; the churn does not.
289
298
  */
290
299
  export const agreementOfferCapability = {
291
300
  key: 'agreement_offer',
@@ -1,3 +1,4 @@
1
+ import { agreementAppUrl } from '../utils/appUrls.js';
1
2
  import { claimOpenAgreement } from '../http/agreementFlows.js';
2
3
  import { linkIsReachOnly, webAppOrigin } from './links.js';
3
4
  import { fullCreds } from './types.js';
@@ -25,7 +26,7 @@ export function presentClaimResult(agreement, kind, env) {
25
26
  }
26
27
  if (agreement?.status !== 'active') {
27
28
  const held = (agreement?.approvals ?? []).find((a) => a.status === 'pending');
28
- const approveUrl = `${webAppOrigin(env)}/app/agreements/${agreement.agreementId}`;
29
+ const approveUrl = agreementAppUrl(webAppOrigin(env), agreement.agreementId);
29
30
  const remedy = held?.heldReason === 'contact-basis'
30
31
  ? 'This is a first engagement with that counterparty — your human approves once; a standing link covers it after that.'
31
32
  : 'Claiming for a job your human already approved activates at once — name the job with mandateAgreementId.';
@@ -1,6 +1,7 @@
1
1
  import { ArtifactsClient } from '../http/ArtifactsClient.js';
2
2
  import { ContextGrantsClient } from '../http/ContextGrantsClient.js';
3
3
  import { fullCreds } from './types.js';
4
+ import { LIST_FIELDS_PARAM, parseListFields, pickListedRows } from './listedFields.js';
4
5
  /**
5
6
  * teaching: name the result slot on the success path so an agent finds
6
7
  * the right move unaided — worded in each surface's task grammar (SDK
@@ -90,6 +91,7 @@ async function recordFileArtifact(args, env, visibility) {
90
91
  const byteSize = bytes.byteLength;
91
92
  const uploaded = await client.uploadUrl({
92
93
  filename,
94
+ name: args['name']?.trim() || undefined,
93
95
  mime,
94
96
  // The byte length of what will be PUT, not of the base64 envelope — the
95
97
  // presign signs this number and S3 rejects a mismatch.
@@ -134,9 +136,8 @@ export const recordArtifactCapability = {
134
136
  'taskId to bind it to the task. A heavy deliverable belongs in an artifact rather than pasted into a message. ' +
135
137
  ARTIFACT_RECORD_INLINE_CAP,
136
138
  mcp:
137
- // Canonical for ziggs-mcp (tools.ts must not override). Last paragraph is
138
- // PROTOCOL.reporting copied verbatim — api-client cannot import ziggs-mcp;
139
- // record-artifact-teaching.test.ts gates the live tool against both.
139
+ // Canonical for ziggs-mcp (tools.ts must not override). Shared protocol
140
+ // (where finished work goes) lives on connect instructions.
140
141
  'Write an artifact — text (text) or a file (filename + mime + contentBase64; presign, upload ' +
141
142
  'and completion all happen inside this one call, so there is no separate upload dance). ' +
142
143
  'Scope is optional — pass agreementId or chatId to record it into that ' +
@@ -144,8 +145,7 @@ export const recordArtifactCapability = {
144
145
  'free-standing artifact that is yours until you attach or share it. Never guess a scope: ' +
145
146
  'recording with none always succeeds. Set visibility explicitly. ' +
146
147
  'For a finished deliverable, set contentType=result and pass taskId to bind it to the task. ' +
147
- ARTIFACT_RECORD_INLINE_CAP +
148
- ' Deliver finished work where the parties agreed it goes: in chat, as a task result, or as an artifact. When the work rides a task, close it with ziggs_task_set_result ({ taskId, state, result: { summary, status, links } }) too, because an agent picking the work up from its own inbox reads that result and not the conversation. Record heavy deliverables as artifacts (ziggs_artifact_record, contentType result, taskId to bind it) rather than pasting them into a message.',
148
+ ARTIFACT_RECORD_INLINE_CAP,
149
149
  },
150
150
  annotation: 'write',
151
151
  params: {
@@ -157,6 +157,10 @@ export const recordArtifactCapability = {
157
157
  type: 'string',
158
158
  description: 'Base64 file bytes for a file artifact — requires filename and mime. This call presigns, uploads and completes; you never compute a checksum. Bytes over ~1MB will not fit in a tool call: use artifact_upload_url for those.',
159
159
  },
160
+ name: {
161
+ type: 'string',
162
+ description: 'Short name shown in lists (room, agreement, artifacts page). Pass this for a text deliverable so the other party can tell what it is without opening it. A file defaults to filename when omitted.',
163
+ },
160
164
  filename: {
161
165
  type: 'string',
162
166
  description: 'Original filename — required with contentBase64',
@@ -232,6 +236,7 @@ export const recordArtifactCapability = {
232
236
  const creds = fullCreds(env);
233
237
  const { artifactId } = await new ArtifactsClient(creds.operatorKey, creds.agentId, creds.laneId).writeStrict({
234
238
  text,
239
+ name: args['name']?.trim() || undefined,
235
240
  visibility,
236
241
  chatId,
237
242
  agreementId,
@@ -275,14 +280,19 @@ export const listArtifactsCapability = {
275
280
  description: 'ISO timestamp — return only artifacts written strictly after this',
276
281
  },
277
282
  limit: { type: 'number', description: 'Page size (server default when omitted)' },
283
+ fields: LIST_FIELDS_PARAM,
278
284
  },
279
285
  needsAgentId: true,
280
286
  handler: async (args, env) => {
281
287
  const creds = fullCreds(env);
282
- return new ArtifactsClient(creds.operatorKey, creds.agentId, creds.laneId).list({ authoredBy: 'me' }, {
288
+ const listed = await new ArtifactsClient(creds.operatorKey, creds.agentId, creds.laneId).list({ authoredBy: 'me' }, {
283
289
  after: args['after'],
284
290
  limit: typeof args['limit'] === 'number' ? args['limit'] : undefined,
285
291
  });
292
+ const fields = parseListFields(args['fields']);
293
+ return fields
294
+ ? { ...listed, artifacts: pickListedRows(listed.artifacts, fields) }
295
+ : listed;
286
296
  },
287
297
  };
288
298
  /**
@@ -1,11 +1,11 @@
1
1
  import { ConnectionsClient } from "../http/ConnectionsClient.js";
2
2
  import { rethrowWithContext, } from "./types.js";
3
3
  function client(env) {
4
- // laneId, not just the key and the agent. The lane is which engagement this
5
- // wake is in, and the broker needs it to tell one hirer's grant from
6
- // another's: an agent that serves several hirers is the holder of every grant
7
- // it has been given, so the holder check alone cannot separate them. Every
8
- // other Creds-based client already carries it.
4
+ // laneId, not just the key and the agent. The lane is which hire this wake is
5
+ // in, and the broker reads off it who the agent is acting for — which is what
6
+ // tells one hirer's grant from another's: an agent that serves several hirers
7
+ // is the holder of every grant it has been given, so the holder check alone
8
+ // cannot separate them. Every other Creds-based client already carries it.
9
9
  const { operatorKey, agentId, laneId } = env.creds;
10
10
  if (!operatorKey)
11
11
  throw new Error("operatorKey missing from tool context");
@@ -1,6 +1,7 @@
1
1
  import { GrantsClient } from '../http/GrantsClient.js';
2
2
  import { GRANT_SCOPE_KINDS, } from '../http/grants.js';
3
3
  import { fullCreds } from './types.js';
4
+ import { LIST_FIELDS_PARAM, parseListFields, pickListedRows } from './listedFields.js';
4
5
  // BOTH the tool's param enum and its validator, so a missing kind makes the
5
6
  // filter the description advertises fail validation. Derived from the canonical
6
7
  // list next to the type rather than restated, because that is exactly how
@@ -67,6 +68,7 @@ export const listGrantsCapability = {
67
68
  },
68
69
  cursor: { type: 'string', description: 'Opaque cursor from a prior nextCursor to page' },
69
70
  limit: { type: 'number', description: 'Page size (server default when omitted)' },
71
+ fields: LIST_FIELDS_PARAM,
70
72
  },
71
73
  needsAgentId: true,
72
74
  handler: async (args, env) => {
@@ -80,7 +82,10 @@ export const listGrantsCapability = {
80
82
  if (roleArg !== undefined && roleArg !== 'holder' && roleArg !== 'issuer') {
81
83
  throw new Error('role must be holder or issuer');
82
84
  }
83
- const { items, nextCursor, unreadableRails } = await new GrantsClient(creds.operatorKey, creds.agentId, env.baseUrl).listGrants({
85
+ const { items, nextCursor, unreadableRails } = await new GrantsClient(creds.operatorKey, creds.agentId, env.baseUrl,
86
+ // The lane, so the server answers with the grants THIS wake may spend
87
+ // rather than everything the agent holds across every customer.
88
+ creds.laneId).listGrants({
84
89
  scopeKind,
85
90
  scopeId: typeof args['scopeId'] === 'string' ? args['scopeId'] : undefined,
86
91
  role: roleArg,
@@ -89,9 +94,10 @@ export const listGrantsCapability = {
89
94
  cursor: args['cursor'],
90
95
  limit: typeof args['limit'] === 'number' ? args['limit'] : undefined,
91
96
  });
97
+ const fields = parseListFields(args['fields']);
92
98
  return {
93
99
  count: items.length,
94
- grants: items,
100
+ grants: pickListedRows(items, fields),
95
101
  nextCursor,
96
102
  ...(unreadableRails?.length ? { unreadableRails } : {}),
97
103
  };
@@ -5,6 +5,7 @@ export { PAYMENT_CAPABILITIES, paymentBalanceCapability } from './payments.js';
5
5
  export { LINK_CAPABILITIES, listLinksCapability, proposeLinkCapability, linkIsReachOnly, } from './links.js';
6
6
  export { INTRODUCTION_CAPABILITIES, mintIntroductionCapability, redeemIntroductionCapability, listIntroductionsCapability, revokeIntroductionCapability, } from './introductions.js';
7
7
  export { AGREEMENT_CAPABILITIES, agreementClaimCapability, presentClaimResult, } from './agreements.js';
8
+ export { LIST_FIELDS_PARAM, parseListFields, pickListedRow, pickListedRows, } from './listedFields.js';
8
9
  export { TASK_CAPABILITIES, listTasksCapability } from './tasks.js';
9
10
  export { AGREEMENT_PROPOSE_PROVIDER_ID_DESCRIPTION } from './proposeProviderId.js';
10
11
  export { MARKETPLACE_CAPABILITIES, marketplaceViewCapability } from './marketplace.js';
@@ -5,6 +5,7 @@ export { PAYMENT_CAPABILITIES, paymentBalanceCapability } from './payments.js';
5
5
  export { LINK_CAPABILITIES, listLinksCapability, proposeLinkCapability, linkIsReachOnly, } from './links.js';
6
6
  export { INTRODUCTION_CAPABILITIES, mintIntroductionCapability, redeemIntroductionCapability, listIntroductionsCapability, revokeIntroductionCapability, } from './introductions.js';
7
7
  export { AGREEMENT_CAPABILITIES, agreementClaimCapability, presentClaimResult, } from './agreements.js';
8
+ export { LIST_FIELDS_PARAM, parseListFields, pickListedRow, pickListedRows, } from './listedFields.js';
8
9
  export { TASK_CAPABILITIES, listTasksCapability } from './tasks.js';
9
10
  export { AGREEMENT_PROPOSE_PROVIDER_ID_DESCRIPTION } from './proposeProviderId.js';
10
11
  export { MARKETPLACE_CAPABILITIES, marketplaceViewCapability } from './marketplace.js';
@@ -1,9 +1,9 @@
1
+ import { resolveWebAppOrigin, connectInviteAppUrl } from '../utils/appUrls.js';
1
2
  import { createLink, listAgreements } from '../http/AgreementClient.js';
2
3
  import { nextCall } from './nextCall.js';
3
4
  import { fullCreds } from './types.js';
4
- const DEFAULT_WEB_URL = 'https://ziggsai.com';
5
5
  export function webAppOrigin(env) {
6
- return (env.webUrl?.trim() || DEFAULT_WEB_URL).replace(/\/$/, '');
6
+ return resolveWebAppOrigin(env.webUrl);
7
7
  }
8
8
  /**
9
9
  * The one thing an invite travels as: `/connect/<inviteId>`.
@@ -17,7 +17,7 @@ export function webAppOrigin(env) {
17
17
  * across two repos.
18
18
  */
19
19
  function inviteShareUrl(env, agreementId) {
20
- return `${webAppOrigin(env)}/connect/${agreementId}`;
20
+ return connectInviteAppUrl(webAppOrigin(env), agreementId);
21
21
  }
22
22
  /**
23
23
  * A link is reach-only — the follow-up move differs by surface tool names.
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Opt-in row projection for list tools.
3
+ *
4
+ * List endpoints return the full row by default. Pass `fields` to keep only
5
+ * those keys — the same shape as Linear's `list_issues`. Envelope keys
6
+ * (count, nextCursor, hasMore) are never filtered.
7
+ */
8
+ export declare const LIST_FIELDS_PARAM: {
9
+ type: "array";
10
+ items: {
11
+ type: "string";
12
+ };
13
+ description: string;
14
+ };
15
+ export declare function parseListFields(raw: unknown): string[] | undefined;
16
+ export declare function pickListedRow(row: Record<string, unknown>, fields: string[] | undefined): Record<string, unknown>;
17
+ export declare function pickListedRows(rows: unknown[], fields: string[] | undefined): unknown[];
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Opt-in row projection for list tools.
3
+ *
4
+ * List endpoints return the full row by default. Pass `fields` to keep only
5
+ * those keys — the same shape as Linear's `list_issues`. Envelope keys
6
+ * (count, nextCursor, hasMore) are never filtered.
7
+ */
8
+ export const LIST_FIELDS_PARAM = {
9
+ type: 'array',
10
+ items: { type: 'string' },
11
+ description: 'Return only these keys on each row. Omit for the full row. Keep the id you will use next (agreementId, taskId, artifactId, grantId).',
12
+ };
13
+ export function parseListFields(raw) {
14
+ if (raw == null)
15
+ return undefined;
16
+ if (!Array.isArray(raw) || raw.length === 0) {
17
+ throw new Error('fields must be a non-empty array of row key names');
18
+ }
19
+ const out = [];
20
+ for (const item of raw) {
21
+ if (typeof item !== 'string' || !item.trim()) {
22
+ throw new Error('fields entries must be non-empty strings');
23
+ }
24
+ if (!out.includes(item))
25
+ out.push(item);
26
+ }
27
+ return out;
28
+ }
29
+ export function pickListedRow(row, fields) {
30
+ if (!fields)
31
+ return row;
32
+ const picked = {};
33
+ for (const key of fields) {
34
+ if (key in row)
35
+ picked[key] = row[key];
36
+ }
37
+ return picked;
38
+ }
39
+ export function pickListedRows(rows, fields) {
40
+ if (!fields)
41
+ return rows;
42
+ return rows.map((row) => row && typeof row === 'object' && !Array.isArray(row)
43
+ ? pickListedRow(row, fields)
44
+ : row);
45
+ }
@@ -1,6 +1,7 @@
1
1
  import { pullOffers, pullRequests } from '../http/MarketplaceClient.js';
2
2
  import { fullCreds } from './types.js';
3
3
  import { nextCall } from './nextCall.js';
4
+ import { LIST_FIELDS_PARAM, parseListFields, pickListedRows } from './listedFields.js';
4
5
  const VIEW_KINDS = ['all', 'requests', 'offers'];
5
6
  function publishHint(env) {
6
7
  const propose = env.surface === 'mcp' ? 'ziggs_agreement_request' : 'agreement_request';
@@ -77,6 +78,7 @@ export const marketplaceViewCapability = {
77
78
  },
78
79
  limit: { type: 'number', description: 'Max rows per kind (default 20)' },
79
80
  since: { type: 'string', description: 'ISO timestamp — only rows published after this' },
81
+ fields: LIST_FIELDS_PARAM,
80
82
  },
81
83
  needsAgentId: true,
82
84
  handler: async (args, env) => {
@@ -93,12 +95,15 @@ export const marketplaceViewCapability = {
93
95
  kind === 'offers' ? Promise.resolve([]) : pullRequests(options, creds),
94
96
  kind === 'requests' ? Promise.resolve([]) : pullOffers(options, creds),
95
97
  ]);
98
+ const fields = parseListFields(args['fields']);
99
+ const requestRows = pickListedRows(requests.map((q) => toListingRow(q, 'request')), fields);
100
+ const offerRows = pickListedRows(offers.map((o) => toListingRow(o, 'offer')), fields);
96
101
  return {
97
102
  ...(kind !== 'offers'
98
- ? { requests: requests.map((q) => toListingRow(q, 'request')), requestCount: requests.length }
103
+ ? { requests: requestRows, requestCount: requests.length }
99
104
  : {}),
100
105
  ...(kind !== 'requests'
101
- ? { offers: offers.map((o) => toListingRow(o, 'offer')), offerCount: offers.length }
106
+ ? { offers: offerRows, offerCount: offers.length }
102
107
  : {}),
103
108
  // Claiming is the move after browsing, and the id is in the row the
104
109
  // caller just received. The prose hint stays for the publish side, which
@@ -1,10 +1,15 @@
1
1
  import { PaymentsClient } from '../http/PaymentsClient.js';
2
2
  import { rethrowWithContext, } from './types.js';
3
3
  function client(env) {
4
- const { operatorKey, agentId } = env.creds;
4
+ // laneId too, like every other Creds-based client. Only the balance read is
5
+ // on this surface today, and a read does not spend — but the lane is what
6
+ // fences a spend to the job it was authorised in, so it is wired here rather
7
+ // than left for whoever brings a spending verb back to remember. Omitting it
8
+ // can only leave a spend unfenced; it can never widen one.
9
+ const { operatorKey, agentId, laneId } = env.creds;
5
10
  if (!operatorKey)
6
11
  throw new Error('operatorKey missing from tool context');
7
- return new PaymentsClient(operatorKey, agentId, env.baseUrl);
12
+ return new PaymentsClient(operatorKey, agentId, env.baseUrl, laneId);
8
13
  }
9
14
  /**
10
15
  * Reading the balance is the whole rail on an agent surface.
@@ -1,5 +1,6 @@
1
1
  import { listTasks } from '../http/TaskClient.js';
2
2
  import { fullCreds } from './types.js';
3
+ import { LIST_FIELDS_PARAM, parseListFields, pickListedRows } from './listedFields.js';
3
4
  /**
4
5
  * One list-tasks verb. MCP and the hosted SDK both used to re-declare the
5
6
  * same GET /tasks filters; assignedToMe vs assignedTo precedence then
@@ -11,8 +12,8 @@ export const listTasksCapability = {
11
12
  names: { sdk: 'task_list', mcp: 'ziggs_task_list' },
12
13
  title: 'List tasks',
13
14
  descriptions: {
14
- sdk: 'List tasks reachable by this agent (GET /tasks). Scope follows the operator key — same reach as chats and agreements. Optional state / assignee filters and cursor pagination. Use assignedToMe to list your own open work, and createdByMe to list the work you handed to someone else.',
15
- mcp: 'List tasks reachable by the acting agent (GET /tasks). Scope is determined by the operator key — same reach as chats and agreements. Optional state / assignee filters and cursor pagination. Use assignedToMe to list your own open work, and createdByMe to list the work you handed to someone else.',
15
+ sdk: 'List tasks reachable by this agent (GET /tasks). Scope follows the operator key — same reach as chats and agreements. Optional state / assignee filters and cursor pagination. Use assignedToMe to list your own open work, and createdByMe to list the work you handed to someone else. Pass fields to keep only those keys on each row.',
16
+ mcp: 'List tasks reachable by the acting agent (GET /tasks). Scope is determined by the operator key — same reach as chats and agreements. Optional state / assignee filters and cursor pagination. Use assignedToMe to list your own open work, and createdByMe to list the work you handed to someone else. Pass fields to keep only those keys on each row.',
16
17
  },
17
18
  annotation: 'read-only',
18
19
  params: {
@@ -40,6 +41,7 @@ export const listTasksCapability = {
40
41
  type: 'boolean',
41
42
  description: "Shorthand for assignedTo=<this agent's id>. Takes precedence over assignedTo when both are set.",
42
43
  },
44
+ fields: LIST_FIELDS_PARAM,
43
45
  },
44
46
  needsAgentId: true,
45
47
  handler: async (args, env) => {
@@ -48,13 +50,17 @@ export const listTasksCapability = {
48
50
  ? creds.agentId
49
51
  : args['assignedTo'];
50
52
  const createdBy = args['createdByMe'] ? creds.agentId : undefined;
51
- return listTasks({
53
+ const listed = await listTasks({
52
54
  state: args['state'],
53
55
  cursor: args['cursor'],
54
56
  limit: typeof args['limit'] === 'number' ? args['limit'] : undefined,
55
57
  assignedTo,
56
58
  ...(createdBy ? { createdBy } : {}),
57
59
  }, creds);
60
+ const fields = parseListFields(args['fields']);
61
+ return fields
62
+ ? { ...listed, tasks: pickListedRows(listed.tasks, fields) }
63
+ : listed;
58
64
  },
59
65
  };
60
66
  export const TASK_CAPABILITIES = [listTasksCapability];
@@ -28,6 +28,8 @@ export interface ListArtifactsResult {
28
28
  }
29
29
  export interface WriteArtifactInput {
30
30
  text: string;
31
+ /** Short list name. An upload defaults to filename when omitted. */
32
+ name?: string;
31
33
  contentType?: string;
32
34
  visibility?: ArtifactVisibility;
33
35
  /** When the artifact is associated with a chat. */
@@ -47,6 +49,8 @@ export interface WriteArtifactInput {
47
49
  }
48
50
  export type ArtifactFileFormat = 'pdf' | 'docx' | 'hwpx' | 'md' | 'txt' | 'html';
49
51
  export interface ArtifactUploadUrlInput {
52
+ /** Short list name. Defaults to filename on the server when omitted. */
53
+ name?: string;
50
54
  filename: string;
51
55
  mime: string;
52
56
  /** Exact UTF-8 / file byte length of the object that will be PUT. */
@@ -82,22 +86,7 @@ export type ArtifactFileView = Record<string, unknown> & {
82
86
  extractionError?: string | null;
83
87
  filename?: string | null;
84
88
  };
85
- /**
86
- * Replaces `ContextReader`'s artifact reads + `ContextWriter`'s breadcrumb
87
- * writes. `visibility: 'agent-private'` is the new home for agent thoughts —
88
- * they persist, are searchable, but are not visible to other chat parties.
89
- */
90
- /**
91
- * turn a runtime lane id into a scope the backend can accept.
92
- *
93
- * A task with no origin chat runs on the lane `agrn-<agreementId>` (see
94
- * AgentHost.laneSessionIdForTask). That is a routing key, not a chat: no such
95
- * chat row exists, so passing it as `chatId` made every breadcrumb and recorded
96
- * thought on an agreement lane come back 403 "not authorized for this scope" —
97
- * a fictional scope reading like an auth failure. The work is agreement-scoped,
98
- * so say so; same rule settled for deliverables.
99
- */
100
- export declare const AGREEMENT_LANE_PREFIX = "agrn-";
89
+ export { AGREEMENT_LANE_PREFIX } from '@ziggs-ai/contracts';
101
90
  export declare function artifactScopeForSession(sessionId: string): {
102
91
  chatId: string;
103
92
  } | {
@@ -33,17 +33,18 @@ function resolveUploadBytes(input) {
33
33
  * turn a runtime lane id into a scope the backend can accept.
34
34
  *
35
35
  * A task with no origin chat runs on the lane `agrn-<agreementId>` (see
36
- * AgentHost.laneSessionIdForTask). That is a routing key, not a chat: no such
36
+ * Agent.laneSessionIdForTask). That is a routing key, not a chat: no such
37
37
  * chat row exists, so passing it as `chatId` made every breadcrumb and recorded
38
38
  * thought on an agreement lane come back 403 "not authorized for this scope" —
39
39
  * a fictional scope reading like an auth failure. The work is agreement-scoped,
40
40
  * so say so; same rule settled for deliverables.
41
41
  */
42
- export const AGREEMENT_LANE_PREFIX = 'agrn-';
42
+ import { agreementIdFromLane } from '@ziggs-ai/contracts';
43
+ export { AGREEMENT_LANE_PREFIX } from '@ziggs-ai/contracts';
43
44
  export function artifactScopeForSession(sessionId) {
44
- if (sessionId.startsWith(AGREEMENT_LANE_PREFIX)) {
45
- return { agreementId: sessionId.slice(AGREEMENT_LANE_PREFIX.length) };
46
- }
45
+ const agreementId = agreementIdFromLane(sessionId);
46
+ if (agreementId)
47
+ return { agreementId };
47
48
  return { chatId: sessionId };
48
49
  }
49
50
  export class ArtifactsClient {
@@ -145,6 +146,7 @@ export class ArtifactsClient {
145
146
  headers: this._headers(),
146
147
  body: JSON.stringify({
147
148
  text,
149
+ ...(input.name?.trim() ? { name: input.name.trim() } : {}),
148
150
  contentType: input.contentType ?? 'text',
149
151
  visibility: input.visibility ?? 'chat',
150
152
  chatId: input.chatId,
@@ -214,6 +216,7 @@ export class ArtifactsClient {
214
216
  }
215
217
  const parsed = await this._post('/artifacts/upload-url', {
216
218
  filename: input.filename.trim(),
219
+ ...(input.name?.trim() ? { name: input.name.trim() } : {}),
217
220
  mime: input.mime.trim(),
218
221
  byteSize: input.byteSize,
219
222
  format: input.format,
@@ -1,3 +1,4 @@
1
+ import type { SendChatMessageResult } from '@ziggs-ai/contracts';
1
2
  import { type Creds } from '../types.js';
2
3
  /** Shape of GET /chats/mine items — the backend's ChatReadDto. */
3
4
  export interface ChatSummary {
@@ -41,12 +42,7 @@ export interface SendChatMessageInput {
41
42
  contentType?: string;
42
43
  underAgreementId?: string;
43
44
  }
44
- export interface SendChatMessageResult {
45
- success: boolean;
46
- message: string;
47
- messageId: string;
48
- chatId: string;
49
- }
45
+ export type { SendChatMessageResult } from '@ziggs-ai/contracts';
50
46
  export type ContextTemporal = 'from-now' | 'from-start';
51
47
  export interface AddChatMemberInput {
52
48
  chatId: string;
@@ -59,10 +59,12 @@ export declare class ConnectionsClient {
59
59
  private readonly laneId?;
60
60
  /**
61
61
  * @param laneId the wake's lane (a chat id, or `agrn-<agreementId>`), sent as
62
- * X-Ziggs-Lane. It tells the broker which engagement is spending a grant:
63
- * an agent that serves several hirers holds all of their grants at once, so
64
- * without a lane one hirer's grant is indistinguishable from another's, and
65
- * a grant issued for one job can be spent inside a different one.
62
+ * X-Ziggs-Lane. It tells the broker which hire this wake is in, and the
63
+ * broker reads off it WHO the agent is acting for: an agent that serves
64
+ * several hirers holds all of their grants at once and is the holder of
65
+ * every one, so without a lane one hirer's grant is indistinguishable from
66
+ * another's, and a grant one customer gave can be spent while working for
67
+ * somebody else.
66
68
  * ArtifactsClient sends the same header for the same reason.
67
69
  */
68
70
  constructor(operatorKey: string, agentId?: string, baseUrl?: string, laneId?: string);
@@ -37,10 +37,12 @@ export class ConnectionsClient {
37
37
  laneId;
38
38
  /**
39
39
  * @param laneId the wake's lane (a chat id, or `agrn-<agreementId>`), sent as
40
- * X-Ziggs-Lane. It tells the broker which engagement is spending a grant:
41
- * an agent that serves several hirers holds all of their grants at once, so
42
- * without a lane one hirer's grant is indistinguishable from another's, and
43
- * a grant issued for one job can be spent inside a different one.
40
+ * X-Ziggs-Lane. It tells the broker which hire this wake is in, and the
41
+ * broker reads off it WHO the agent is acting for: an agent that serves
42
+ * several hirers holds all of their grants at once and is the holder of
43
+ * every one, so without a lane one hirer's grant is indistinguishable from
44
+ * another's, and a grant one customer gave can be spent while working for
45
+ * somebody else.
44
46
  * ArtifactsClient sends the same header for the same reason.
45
47
  */
46
48
  constructor(operatorKey, agentId, baseUrl, laneId) {
@@ -101,7 +103,7 @@ export class ConnectionsClient {
101
103
  * defensively for leaked secrets, as `proxy` does.
102
104
  */
103
105
  async listForHolder() {
104
- const grantsClient = new GrantsClient(this.operatorKey, this.agentId, this.baseUrl);
106
+ const grantsClient = new GrantsClient(this.operatorKey, this.agentId, this.baseUrl, this.laneId);
105
107
  // All pages of the agent's live connection grants (not just the first page).
106
108
  const { items, unreadableRails } = await grantsClient.listAllGrantsWithRails({
107
109
  scopeKind: "connection",
@@ -56,7 +56,16 @@ export declare class GrantsClient {
56
56
  private readonly operatorKey;
57
57
  private readonly agentId?;
58
58
  private readonly baseUrl;
59
- constructor(operatorKey: string, agentId?: string, baseUrl?: string);
59
+ private readonly laneId?;
60
+ /**
61
+ * @param laneId the wake's lane (a chat id, or `agrn-<agreementId>`), sent as
62
+ * X-Ziggs-Lane. The server reads off it who the agent is acting for, and
63
+ * answers the spendable rails with only the grants this wake may spend —
64
+ * so an agent serving several customers is never handed a list it has to
65
+ * choose between. Without it the list is everything the agent holds, and
66
+ * the grant it picks may then be refused with nothing saying why.
67
+ */
68
+ constructor(operatorKey: string, agentId?: string, baseUrl?: string, laneId?: string);
60
69
  listGrants(query?: ListGrantsQuery): Promise<ListGrantsResult>;
61
70
  /**
62
71
  * Every grant matching `query`, following the cursor to completion. Use when a
@@ -12,12 +12,22 @@ export class GrantsClient {
12
12
  operatorKey;
13
13
  agentId;
14
14
  baseUrl;
15
- constructor(operatorKey, agentId, baseUrl) {
15
+ laneId;
16
+ /**
17
+ * @param laneId the wake's lane (a chat id, or `agrn-<agreementId>`), sent as
18
+ * X-Ziggs-Lane. The server reads off it who the agent is acting for, and
19
+ * answers the spendable rails with only the grants this wake may spend —
20
+ * so an agent serving several customers is never handed a list it has to
21
+ * choose between. Without it the list is everything the agent holds, and
22
+ * the grant it picks may then be refused with nothing saying why.
23
+ */
24
+ constructor(operatorKey, agentId, baseUrl, laneId) {
16
25
  if (!operatorKey)
17
26
  throw new Error('GrantsClient: operatorKey is required');
18
27
  this.operatorKey = operatorKey;
19
28
  this.agentId = agentId;
20
29
  this.baseUrl = baseUrl || getBackendUrl();
30
+ this.laneId = laneId;
21
31
  }
22
32
  async listGrants(query = {}) {
23
33
  const url = new URL(`${this.baseUrl}/grants`);
@@ -41,7 +51,7 @@ export class GrantsClient {
41
51
  if (query.limit != null)
42
52
  url.searchParams.set('limit', String(query.limit));
43
53
  const res = await fetch(url.toString(), {
44
- headers: buildOperatorHeaders(this.operatorKey, this.agentId),
54
+ headers: buildOperatorHeaders(this.operatorKey, this.agentId, undefined, this.laneId),
45
55
  });
46
56
  const body = await res.text().catch(() => '');
47
57
  if (!res.ok) {
@@ -5,15 +5,16 @@ export interface IntroductionFrom {
5
5
  orgId: string;
6
6
  agentId: string | null;
7
7
  agentCreatedAt?: string | null;
8
- claimedLabel?: {
8
+ claimedLabel: {
9
9
  text: string;
10
10
  selfChosen: boolean;
11
11
  verified: boolean;
12
12
  };
13
13
  /**
14
- * Same text as claimedLabel.text. Not identity — anyone can pick it.
14
+ * Same text as claimedLabel.text. Not identity. Remove after 2026-10-07;
15
+ * readers must use claimedLabel.
15
16
  */
16
- label: string;
17
+ label?: string;
17
18
  }
18
19
  export interface IntroductionView {
19
20
  token: string;
@@ -41,7 +42,13 @@ export interface IntroductionView {
41
42
  counterparty?: {
42
43
  principal: string;
43
44
  agentId: string | null;
44
- label: string;
45
+ claimedLabel: {
46
+ text: string;
47
+ selfChosen: boolean;
48
+ verified: boolean;
49
+ };
50
+ /** Same text as claimedLabel.text. Remove after 2026-10-07. */
51
+ label?: string;
45
52
  nextStep: string;
46
53
  };
47
54
  }
@@ -27,20 +27,6 @@ export type OrgResolution = {
27
27
  * match. Ambiguous names return the candidates rather than guessing.
28
28
  */
29
29
  export declare function resolveOrgSelector(orgs: MyOrg[], selector: string): OrgResolution;
30
- /**
31
- * MCP OAuth auto-provisioned delegate id prefix.
32
- *
33
- * The backend stopped classifying delegates by their id and now reads a stamp
34
- * on the agent row, which is what freed the prefix to drop the vendor name. A
35
- * client holds credentials, not rows, so this is still a shape test — but it is
36
- * a shape test against a NAME, and the only thing it decides is which access
37
- * endpoint to read. It said `delegate--` until the ids were renamed;
38
- * left alone, every delegate session would have fallen through to the hosted
39
- * reader and reported the wrong org and connection state.
40
- */
41
- export declare const MCP_OAUTH_DELEGATE_AGENT_ID_PREFIX = "delegate--";
42
- /** True when `agentId` is an inbound MCP OAuth auto-provisioned delegate. */
43
- export declare function isMcpOAuthDelegateAgentId(agentId: string): boolean;
44
30
  /**
45
31
  * runtime acting org from the server (self-hire / agent
46
32
  * row): GET /agents/delegate/access. Moved here from ziggs-mcp's inline
@@ -1,3 +1,4 @@
1
+ import { isMcpOAuthDelegateSession } from '../shared/operatorKey.js';
1
2
  import { getBackendUrl } from '../utils/urlUtils.js';
2
3
  import { throwApiError } from '../shared/apiError.js';
3
4
  import { buildOperatorHeaders } from './operatorHeaders.js';
@@ -42,22 +43,6 @@ export function resolveOrgSelector(orgs, selector) {
42
43
  return { status: 'ambiguous', matches: byName };
43
44
  return { status: 'not-found' };
44
45
  }
45
- /**
46
- * MCP OAuth auto-provisioned delegate id prefix.
47
- *
48
- * The backend stopped classifying delegates by their id and now reads a stamp
49
- * on the agent row, which is what freed the prefix to drop the vendor name. A
50
- * client holds credentials, not rows, so this is still a shape test — but it is
51
- * a shape test against a NAME, and the only thing it decides is which access
52
- * endpoint to read. It said `delegate--` until the ids were renamed;
53
- * left alone, every delegate session would have fallen through to the hosted
54
- * reader and reported the wrong org and connection state.
55
- */
56
- export const MCP_OAUTH_DELEGATE_AGENT_ID_PREFIX = 'delegate--';
57
- /** True when `agentId` is an inbound MCP OAuth auto-provisioned delegate. */
58
- export function isMcpOAuthDelegateAgentId(agentId) {
59
- return !!agentId && agentId.startsWith(MCP_OAUTH_DELEGATE_AGENT_ID_PREFIX);
60
- }
61
46
  /**
62
47
  * runtime acting org from the server (self-hire / agent
63
48
  * row): GET /agents/delegate/access. Moved here from ziggs-mcp's inline
@@ -125,7 +110,7 @@ export async function fetchHostedAgentAccess(creds, baseUrl) {
125
110
  * Claude OAuth delegates → self-hire status; everything else → hosted path.
126
111
  */
127
112
  export async function fetchSessionAccess(creds, baseUrl) {
128
- if (isMcpOAuthDelegateAgentId(creds.agentId)) {
113
+ if (isMcpOAuthDelegateSession(creds)) {
129
114
  return fetchDelegateAccess(creds, baseUrl);
130
115
  }
131
116
  return fetchHostedAgentAccess(creds, baseUrl);
@@ -87,7 +87,21 @@ export declare class PaymentsClient {
87
87
  private readonly operatorKey;
88
88
  private readonly agentId?;
89
89
  private readonly baseUrl;
90
- constructor(operatorKey: string, agentId?: string, baseUrl?: string);
90
+ private readonly laneId?;
91
+ /**
92
+ * @param laneId the wake's lane (a chat id, or `agrn-<agreementId>`), sent as
93
+ * X-Ziggs-Lane. It tells the backend which hire this wake is in, and the
94
+ * backend reads off it WHO the spend is for. An agent that serves several
95
+ * hirers holds all of their grants at once and the holder is the same agent
96
+ * in every one, so without a lane, spend authority one customer gave is
97
+ * spendable while the agent works for another. ConnectionsClient and
98
+ * ArtifactsClient send the same header for the same reason.
99
+ *
100
+ * Omitting it cannot widen anything — a stated lane can only cause a
101
+ * refusal — but it does leave the spend unfenced, so a caller that knows
102
+ * its lane should pass it.
103
+ */
104
+ constructor(operatorKey: string, agentId?: string, baseUrl?: string, laneId?: string);
91
105
  balance(): Promise<WalletBalance>;
92
106
  resolve({ userId, agentId }?: {
93
107
  userId?: string;
@@ -99,7 +113,12 @@ export declare class PaymentsClient {
99
113
  idempotencyKey?: string;
100
114
  description?: string;
101
115
  paymentGrantId?: string;
102
- /** The engagement this spend belongs to, so its own grant is preferred. */
116
+ /**
117
+ * The hire this spend belongs to. Used for attribution, and to prefer that
118
+ * hire's own grant over a standing one — a preference about which budget is
119
+ * billed, never about which grants may be spent. That answer is the
120
+ * server's, and it turns on who gave the grant.
121
+ */
103
122
  agreementId?: string;
104
123
  }): Promise<TransferResult>;
105
124
  hold({ amount, idempotencyKey, description, paymentGrantId, agreementId, }: {
@@ -107,7 +126,12 @@ export declare class PaymentsClient {
107
126
  idempotencyKey?: string;
108
127
  description?: string;
109
128
  paymentGrantId?: string;
110
- /** The engagement this spend belongs to, so its own grant is preferred. */
129
+ /**
130
+ * The hire this spend belongs to. Used for attribution, and to prefer that
131
+ * hire's own grant over a standing one — a preference about which budget is
132
+ * billed, never about which grants may be spent. That answer is the
133
+ * server's, and it turns on who gave the grant.
134
+ */
111
135
  agreementId?: string;
112
136
  }): Promise<HoldResult>;
113
137
  /**
@@ -18,12 +18,27 @@ export class PaymentsClient {
18
18
  operatorKey;
19
19
  agentId;
20
20
  baseUrl;
21
- constructor(operatorKey, agentId, baseUrl) {
21
+ laneId;
22
+ /**
23
+ * @param laneId the wake's lane (a chat id, or `agrn-<agreementId>`), sent as
24
+ * X-Ziggs-Lane. It tells the backend which hire this wake is in, and the
25
+ * backend reads off it WHO the spend is for. An agent that serves several
26
+ * hirers holds all of their grants at once and the holder is the same agent
27
+ * in every one, so without a lane, spend authority one customer gave is
28
+ * spendable while the agent works for another. ConnectionsClient and
29
+ * ArtifactsClient send the same header for the same reason.
30
+ *
31
+ * Omitting it cannot widen anything — a stated lane can only cause a
32
+ * refusal — but it does leave the spend unfenced, so a caller that knows
33
+ * its lane should pass it.
34
+ */
35
+ constructor(operatorKey, agentId, baseUrl, laneId) {
22
36
  if (!operatorKey)
23
37
  throw new Error('PaymentsClient: operatorKey is required');
24
38
  this.operatorKey = operatorKey;
25
39
  this.agentId = agentId;
26
40
  this.baseUrl = baseUrl || getBackendUrl();
41
+ this.laneId = laneId;
27
42
  }
28
43
  async balance() {
29
44
  const w = (await this._get('/payments/wallet'));
@@ -180,7 +195,7 @@ export class PaymentsClient {
180
195
  * (retired GET /payments/grants), following the cursor to completion.
181
196
  */
182
197
  async listGrants() {
183
- const grants = new GrantsClient(this.operatorKey, this.agentId, this.baseUrl);
198
+ const grants = new GrantsClient(this.operatorKey, this.agentId, this.baseUrl, this.laneId);
184
199
  return grants.listAllGrants({ scopeKind: 'wallet', health: 'active' });
185
200
  }
186
201
  async createTopUpIntent({ amount, description, currency, } = {}) {
@@ -261,7 +276,7 @@ export class PaymentsClient {
261
276
  async _request(method, path, body) {
262
277
  const init = {
263
278
  method,
264
- headers: buildOperatorHeaders(this.operatorKey, this.agentId, body !== undefined ? { 'content-type': 'application/json' } : {}),
279
+ headers: buildOperatorHeaders(this.operatorKey, this.agentId, body !== undefined ? { 'content-type': 'application/json' } : {}, this.laneId),
265
280
  };
266
281
  if (body !== undefined)
267
282
  init.body = JSON.stringify(body);
@@ -165,7 +165,7 @@ export async function getActiveTasksForAgent(agentId, creds) {
165
165
  * assignee's own work list hands over a task nobody woke it for. Every caller of
166
166
  * this function is asking "what can I act on now", and for a withheld row the
167
167
  * answer is nothing — `updateTaskState` refuses it with `dependencies_unmet`.
168
- * Two of AgentHost's three callers do worse than waste a wake: the needs gate
168
+ * Two of Agent's three callers do worse than waste a wake: the needs gate
169
169
  * and the unmet-access sweep both FAIL the tasks they find, so a withheld node
170
170
  * was one missing grant away from being failed before its turn came.
171
171
  *
@@ -23,7 +23,7 @@ export { PaymentsClient } from './PaymentsClient.js';
23
23
  export type { PaymentsError, WalletBalance, WalletRef, PaymentTransactionView, 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
- export { fetchMyOrgs, resolveOrgSelector, fetchDelegateAccess, fetchHostedAgentAccess, fetchSessionAccess, isMcpOAuthDelegateAgentId, MCP_OAUTH_DELEGATE_AGENT_ID_PREFIX, } from './OrgsClient.js';
26
+ export { fetchMyOrgs, resolveOrgSelector, fetchDelegateAccess, fetchHostedAgentAccess, fetchSessionAccess, } from './OrgsClient.js';
27
27
  export type { MyOrg, OrgResolution } from './OrgsClient.js';
28
28
  export { AgentSearchClient } from './AgentSearchClient.js';
29
29
  export { TelemetryClient } from './TelemetryClient.js';
@@ -15,7 +15,7 @@ export { ContextGrantsClient } from './ContextGrantsClient.js';
15
15
  export { grantCaveat, CONTEXT_GRANT_SCOPE_KINDS, GRANT_SCOPE_KINDS, } from './grants.js';
16
16
  export { PaymentsClient } from './PaymentsClient.js';
17
17
  export { ConnectionsClient, assertNoLeakedConnectionSecret, } from './ConnectionsClient.js';
18
- export { fetchMyOrgs, resolveOrgSelector, fetchDelegateAccess, fetchHostedAgentAccess, fetchSessionAccess, isMcpOAuthDelegateAgentId, MCP_OAUTH_DELEGATE_AGENT_ID_PREFIX, } from './OrgsClient.js';
18
+ export { fetchMyOrgs, resolveOrgSelector, fetchDelegateAccess, fetchHostedAgentAccess, fetchSessionAccess, } from './OrgsClient.js';
19
19
  export { AgentSearchClient } from './AgentSearchClient.js';
20
20
  export { TelemetryClient } from './TelemetryClient.js';
21
21
  export { InboxClient } from './InboxClient.js';
@@ -39,6 +39,11 @@ export interface GrantSelection {
39
39
  * spend names one, then the standing grant. A standing grant is the buyer
40
40
  * bootstrap and the right default; reaching for it while an agreement grant
41
41
  * covers the same spend would bill the wrong budget.
42
+ *
43
+ * This is a preference, never a fence. Every grant here is already one the
44
+ * server said this wake may spend, so picking a different one cannot reach
45
+ * another party's authority — it would only bill the wrong budget of the same
46
+ * party. That is why the mismatch is no longer a rejection above.
42
47
  */
43
48
  export declare function selectPaymentGrant(grants: readonly GrantView[], spend: SpendShape): GrantSelection;
44
49
  /**
@@ -52,18 +52,18 @@ function whyNot(grant, spend) {
52
52
  if (allowed && spend.toWalletId && !allowed.includes(spend.toWalletId)) {
53
53
  return `may only pay ${allowed.join(', ')}, and this one pays ${spend.toWalletId}`;
54
54
  }
55
- // An agreement-conferred grant is FOR that agreement's work. Spending it on
56
- // something else would pass every caveat above and still be the wrong money:
57
- // the ceiling was agreed for one engagement, and nothing on the wire would
58
- // show it had been used for another.
59
- if (grant.agreementId) {
60
- if (!spend.agreementId) {
61
- return `belongs to agreement ${grant.agreementId}, and this spend names no agreement`;
62
- }
63
- if (grant.agreementId !== spend.agreementId) {
64
- return `belongs to agreement ${grant.agreementId}, not ${spend.agreementId}`;
65
- }
66
- }
55
+ // A grant's agreement is NOT checked here, deliberately.
56
+ //
57
+ // It used to be refused when it did not match the spend's agreement, which
58
+ // made a customer's own grant unusable inside their second hire of the same
59
+ // agent, and made a standing grant unusable on any spend that named a deal.
60
+ // Whether a grant may be spent at all is the server's answer, and it turns on
61
+ // who GAVE the grant against who the agent is acting for — not on which of
62
+ // that party's deals is running. A list the server answered for this wake
63
+ // holds only grants it may spend, so there is nothing left to refuse here.
64
+ //
65
+ // The agreement still decides the ORDER below, which is a budget preference
66
+ // rather than a question of authority.
67
67
  return null;
68
68
  }
69
69
  /**
@@ -73,6 +73,11 @@ function whyNot(grant, spend) {
73
73
  * spend names one, then the standing grant. A standing grant is the buyer
74
74
  * bootstrap and the right default; reaching for it while an agreement grant
75
75
  * covers the same spend would bill the wrong budget.
76
+ *
77
+ * This is a preference, never a fence. Every grant here is already one the
78
+ * server said this wake may spend, so picking a different one cannot reach
79
+ * another party's authority — it would only bill the wrong budget of the same
80
+ * party. That is why the mismatch is no longer a rejection above.
76
81
  */
77
82
  export function selectPaymentGrant(grants, spend) {
78
83
  const rejected = [];
@@ -86,10 +91,15 @@ export function selectPaymentGrant(grants, spend) {
86
91
  }
87
92
  if (fitting.length === 0)
88
93
  return { grant: null, rejected };
89
- const forAgreement = spend.agreementId
90
- ? fitting.find((g) => g.agreementId === spend.agreementId)
91
- : undefined;
92
- return { grant: forAgreement ?? fitting[0], rejected };
94
+ // Explicit both ways, because the deal is no longer a fence: until it was
95
+ // removed, a deal-bound grant simply never reached this list on a spend that
96
+ // named nothing, so "prefer the standing grant" happened by accident of the
97
+ // rejection. Now it has to be said.
98
+ const standing = fitting.find((g) => !g.agreementId);
99
+ const preferred = spend.agreementId
100
+ ? (fitting.find((g) => g.agreementId === spend.agreementId) ?? standing)
101
+ : standing;
102
+ return { grant: preferred ?? fitting[0], rejected };
93
103
  }
94
104
  /**
95
105
  * The sentence an agent gets when nothing fits.
package/dist/index.d.ts CHANGED
@@ -12,3 +12,6 @@ export { parseErrorMessage, parseErrorCode, throwApiError, } from './shared/apiE
12
12
  export { ApiError } from './types.js';
13
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';
14
14
  export type { ProposeTerms, ProposeDirectInput, ProposeBroadcastInput, ProposeAgreementData, ClaimOptions, } from './http/AgreementClient.js';
15
+ export { agreementLaneId, agreementIdFromLane, isAgreementLaneId, isAgreementId } from '@ziggs-ai/contracts';
16
+ export * from './shared/operatorKey.js';
17
+ export * from './utils/appUrls.js';
package/dist/index.js CHANGED
@@ -13,3 +13,6 @@ export { RateLimitedError, isRateLimited, parseRetryAfterMs, pollSurfaceError, }
13
13
  // half of them had drifted).
14
14
  export { parseErrorMessage, parseErrorCode, throwApiError, } from './shared/apiError.js';
15
15
  export { ApiError } from './types.js';
16
+ export { agreementLaneId, agreementIdFromLane, isAgreementLaneId, isAgreementId } from '@ziggs-ai/contracts';
17
+ export * from './shared/operatorKey.js';
18
+ export * from './utils/appUrls.js';
@@ -0,0 +1,47 @@
1
+ /** JWT payload fields we read client-side (signature not verified — identity hint only). */
2
+ export interface OperatorKeyClaims {
3
+ type?: string;
4
+ keyId?: string;
5
+ ownerId?: string;
6
+ boundAgentId?: string | null;
7
+ /**
8
+ * How the key was authorized. MCP consent uses `mcp_oauth` or `device_code`;
9
+ * provisioning and other mint paths carry their own provenance.
10
+ */
11
+ issuedVia?: string;
12
+ exp?: number;
13
+ }
14
+ /** Decode operator JWT payload without verifying signature (boundAgentId). */
15
+ export declare function decodeOperatorKeyClaims(token: string): OperatorKeyClaims | null;
16
+ 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";
21
+ /**
22
+ * Did a person board this credential through the connector directory?
23
+ *
24
+ * That is the question the tool surface turns on: a connected assistant
25
+ * (Claude, ChatGPT, Cursor) gets the listed, directory-reviewed surface, and
26
+ * anyone who configured this server themselves with an operator key gets the
27
+ * whole thing.
28
+ *
29
+ * Ask it as a predicate, never as `issuedVia === 'mcp_oauth'`. Two consent rails
30
+ * board an assistant through that door — the authorization-code flow and the
31
+ * device-code flow — and both want the same narrowed surface, but the server
32
+ * records them as the different acts they are. An equality test against one
33
+ * value serves the whole catalogue to every credential from the other one.
34
+ *
35
+ * Read off the unverified payload. On the remote endpoint the backend verified
36
+ * the signature before this ran; on stdio the key is the caller's own. Either
37
+ * way the payload is the one the backend signed. And what hangs on the answer
38
+ * is which tools are registered, not what a call may do: every call is still
39
+ * authorized by the backend. Provenance may decide what is OFFERED, never what
40
+ * is PERMITTED.
41
+ */
42
+ export declare function isDirectoryBoarded(claims: OperatorKeyClaims | null): boolean;
43
+ /** Routing hint only. Every request is still authenticated by the backend. */
44
+ export declare function isMcpOAuthDelegateSession(creds: {
45
+ operatorKey: string;
46
+ agentId: string;
47
+ }): boolean;
@@ -0,0 +1,64 @@
1
+ /** Decode operator JWT payload without verifying signature (boundAgentId). */
2
+ export function decodeOperatorKeyClaims(token) {
3
+ const trimmed = token.trim();
4
+ const parts = trimmed.split('.');
5
+ if (parts.length !== 3)
6
+ return null;
7
+ try {
8
+ const encoded = parts[1].replace(/-/g, '+').replace(/_/g, '/');
9
+ const json = new TextDecoder().decode(Uint8Array.from(atob(encoded), (c) => c.charCodeAt(0)));
10
+ const payload = JSON.parse(json);
11
+ return {
12
+ type: payload.type,
13
+ keyId: payload.keyId,
14
+ ownerId: payload.ownerId,
15
+ boundAgentId: typeof payload.boundAgentId === 'string' ? payload.boundAgentId : null,
16
+ issuedVia: typeof payload.issuedVia === 'string' ? payload.issuedVia : undefined,
17
+ exp: payload.exp,
18
+ };
19
+ }
20
+ catch {
21
+ return null;
22
+ }
23
+ }
24
+ export function isOperatorKeyExpired(claims) {
25
+ if (!claims?.exp)
26
+ return false;
27
+ return claims.exp * 1000 <= Date.now();
28
+ }
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';
33
+ /**
34
+ * Did a person board this credential through the connector directory?
35
+ *
36
+ * That is the question the tool surface turns on: a connected assistant
37
+ * (Claude, ChatGPT, Cursor) gets the listed, directory-reviewed surface, and
38
+ * anyone who configured this server themselves with an operator key gets the
39
+ * whole thing.
40
+ *
41
+ * Ask it as a predicate, never as `issuedVia === 'mcp_oauth'`. Two consent rails
42
+ * board an assistant through that door — the authorization-code flow and the
43
+ * device-code flow — and both want the same narrowed surface, but the server
44
+ * records them as the different acts they are. An equality test against one
45
+ * value serves the whole catalogue to every credential from the other one.
46
+ *
47
+ * Read off the unverified payload. On the remote endpoint the backend verified
48
+ * the signature before this ran; on stdio the key is the caller's own. Either
49
+ * way the payload is the one the backend signed. And what hangs on the answer
50
+ * is which tools are registered, not what a call may do: every call is still
51
+ * authorized by the backend. Provenance may decide what is OFFERED, never what
52
+ * is PERMITTED.
53
+ */
54
+ export function isDirectoryBoarded(claims) {
55
+ return (claims?.issuedVia === ISSUED_VIA_MCP_OAUTH ||
56
+ claims?.issuedVia === ISSUED_VIA_DEVICE_CODE);
57
+ }
58
+ /** Routing hint only. Every request is still authenticated by the backend. */
59
+ export function isMcpOAuthDelegateSession(creds) {
60
+ const claims = decodeOperatorKeyClaims(creds.operatorKey);
61
+ return (!!creds.agentId &&
62
+ claims?.boundAgentId === creds.agentId &&
63
+ isDirectoryBoarded(claims));
64
+ }
package/dist/types.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { InboxRequestChannel } from '@ziggs-ai/contracts';
1
2
  /**
2
3
  * One failure shape for every HTTP client path.
3
4
  *
@@ -259,19 +260,7 @@ export type BroadcastAudience = typeof OPEN_AGREEMENT_TARGET | typeof ORG_AGREEM
259
260
  export declare const BROADCAST_TARGETS: readonly ["everyone", "org"];
260
261
  /** True when `id` is a broadcast sentinel ('everyone' | 'org') rather than a concrete principal id. */
261
262
  export declare function isBroadcastTarget(id: string | null | undefined): boolean;
262
- /**
263
- * Persona face id (`psn_*`). Non-addressable — never use for agent lookup,
264
- * wake, or payment parties.
265
- */
266
- export declare function isPersonaRef(id: string | null | undefined): boolean;
267
- /**
268
- * Room presentation binding id (`rpb_*`). Opaque to account lookup / wake /
269
- * pay. Chat sends may echo it as `receiverId` — the backend resolves it in-room
270
- *.
271
- */
272
- export declare function isRoomPresentationRef(id: string | null | undefined): boolean;
273
- /** Either opaque presentation ref (`psn_*` | `rpb_*`). */
274
- export declare function isOpaquePresentationRef(id: string | null | undefined): boolean;
263
+ export { isPersonaFaceRef as isPersonaRef, isRoomBindingRef as isRoomPresentationRef, isPresentationRef as isOpaquePresentationRef, } from '@ziggs-ai/contracts';
275
264
  /** Display + entitlement face for a principal on the message/roster wire. */
276
265
  export interface PrincipalPresentation {
277
266
  /** Public, non-addressable reference (`psn_*` or `rpb_*`). */
@@ -332,7 +321,7 @@ export interface MessageMetadata {
332
321
  agreementId?: string | null;
333
322
  /**
334
323
  * Message send time from the wire payload. The live-path inbox
335
- * watermark ack (AgentHost, P4) needs it — without a timestamp the
324
+ * watermark ack (Agent, P4) needs it — without a timestamp the
336
325
  * ack never fires and catch-up re-delivers the chat's whole backlog after a
337
326
  * host eviction, re-running already-answered goals.
338
327
  */
@@ -472,14 +461,8 @@ export interface InboxTaskRef {
472
461
  * A marketplace request doorbell. Own channel so the host can
473
462
  * exact-match triage with zero LLM tokens before any wake.
474
463
  */
475
- export interface InboxRequestRef {
476
- agreementId: string;
477
- /** Exact-match string from the publisher — compare to the agent's tags. */
478
- match: string;
479
- title: string;
480
- ts: string;
481
- }
482
- export interface InboxEnvelope {
464
+ export type { InboxOpenRequestRef as InboxRequestRef } from '@ziggs-ai/contracts';
465
+ export interface InboxEnvelope extends InboxRequestChannel {
483
466
  asOf: string;
484
467
  /**
485
468
  * The mailboxes this reader's view merges — its owner's, plus whatever
@@ -487,7 +470,7 @@ export interface InboxEnvelope {
487
470
  */
488
471
  sources?: InboxSourceRef[];
489
472
  /**
490
- * Unacked rows across every source, newest first, copies of one event
473
+ * Unacked rows across every source, oldest first, copies of one event
491
474
  * collapsed. This IS the inbox — read straight out of the party delivery
492
475
  * logs, not derived from grants. Rows with `assigneeId === me` are mine to
493
476
  * act on; the rest are readable context.
@@ -509,9 +492,6 @@ export interface InboxEnvelope {
509
492
  /** Open tasks assigned to this agent — the work channel. */
510
493
  tasksAwaitingMe: InboxTaskRef[];
511
494
  truncatedTasks: number;
512
- /** Unacked request deliveries — triaged before any LLM wake. */
513
- requestsAwaitingMe?: InboxRequestRef[];
514
- truncatedRequests?: number;
515
495
  proposalsAwaitingMe: InboxProposalRef[];
516
496
  truncatedProposals: number;
517
497
  connectionRequestsAwaitingMe: InboxConnectionRequestRef[];
package/dist/types.js CHANGED
@@ -95,22 +95,4 @@ export const BROADCAST_TARGETS = [OPEN_AGREEMENT_TARGET, ORG_AGREEMENT_TARGET];
95
95
  export function isBroadcastTarget(id) {
96
96
  return id === OPEN_AGREEMENT_TARGET || id === ORG_AGREEMENT_TARGET;
97
97
  }
98
- /**
99
- * Persona face id (`psn_*`). Non-addressable — never use for agent lookup,
100
- * wake, or payment parties.
101
- */
102
- export function isPersonaRef(id) {
103
- return typeof id === 'string' && id.startsWith('psn_');
104
- }
105
- /**
106
- * Room presentation binding id (`rpb_*`). Opaque to account lookup / wake /
107
- * pay. Chat sends may echo it as `receiverId` — the backend resolves it in-room
108
- *.
109
- */
110
- export function isRoomPresentationRef(id) {
111
- return typeof id === 'string' && id.startsWith('rpb_');
112
- }
113
- /** Either opaque presentation ref (`psn_*` | `rpb_*`). */
114
- export function isOpaquePresentationRef(id) {
115
- return isPersonaRef(id) || isRoomPresentationRef(id);
116
- }
98
+ export { isPersonaFaceRef as isPersonaRef, isRoomBindingRef as isRoomPresentationRef, isPresentationRef as isOpaquePresentationRef, } from '@ziggs-ai/contracts';
@@ -0,0 +1,7 @@
1
+ /** Resolve the app origin once for SDK, MCP, and capability links. */
2
+ export declare function resolveWebAppOrigin(webUrl?: string | null, backendUrl?: string): string;
3
+ export declare function agreementAppUrl(origin: string, id: string): string;
4
+ export declare function agreementsListAppUrl(origin: string): string;
5
+ export declare function connectInviteAppUrl(origin: string, id: string): string;
6
+ export declare function connectionsSettingsAppUrl(origin: string, requestId?: string | null): string;
7
+ export declare function walletAppUrl(origin: string): string;
@@ -0,0 +1,38 @@
1
+ /** Resolve the app origin once for SDK, MCP, and capability links. */
2
+ export function resolveWebAppOrigin(webUrl, backendUrl) {
3
+ const explicit = webUrl
4
+ ?.split(';')
5
+ .map((value) => value.trim())
6
+ .find(Boolean);
7
+ if (explicit)
8
+ return explicit.replace(/\/+$/, '');
9
+ if (backendUrl) {
10
+ const backend = new URL(backendUrl);
11
+ if (backend.hostname === 'localhost' || backend.hostname === '127.0.0.1')
12
+ return 'http://localhost:1234';
13
+ backend.hostname = backend.hostname.replace(/^api\./, '');
14
+ return backend.origin;
15
+ }
16
+ return 'https://ziggsai.com';
17
+ }
18
+ function appUrl(origin, path) {
19
+ return new URL(path, `${resolveWebAppOrigin(origin)}/`).toString();
20
+ }
21
+ export function agreementAppUrl(origin, id) {
22
+ return appUrl(origin, `/app/agreements/${encodeURIComponent(id)}`);
23
+ }
24
+ export function agreementsListAppUrl(origin) {
25
+ return appUrl(origin, '/app/agreements');
26
+ }
27
+ export function connectInviteAppUrl(origin, id) {
28
+ return appUrl(origin, `/connect/${encodeURIComponent(id)}`);
29
+ }
30
+ export function connectionsSettingsAppUrl(origin, requestId) {
31
+ const url = new URL(appUrl(origin, '/app/access'));
32
+ if (requestId)
33
+ url.searchParams.set('request', requestId);
34
+ return url.toString();
35
+ }
36
+ export function walletAppUrl(origin) {
37
+ return appUrl(origin, '/app/settings/organization/billing');
38
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ziggs-ai/api-client",
3
- "version": "0.15.1",
3
+ "version": "0.16.0",
4
4
  "description": "HTTP and WebSocket client for the Ziggs backend API",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -27,7 +27,8 @@
27
27
  "test:watch": "node --import tsx/esm --test --watch test/*.test.ts"
28
28
  },
29
29
  "dependencies": {
30
- "socket.io-client": "^4.7.0"
30
+ "socket.io-client": "^4.7.0",
31
+ "@ziggs-ai/contracts": "^0.6.0"
31
32
  },
32
33
  "keywords": [
33
34
  "ziggs",