@ziggs-ai/api-client 0.30.2 → 0.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -106,7 +106,7 @@ Cross-org inbox and chat payloads mask counterparties behind presentation faces:
106
106
 
107
107
  - Inbox connection requests expose `requesterRef` (`psn_*`) plus display name/org — not `requesterAgentId`.
108
108
  - Message/roster rows may carry `presentation` (`ref`, `persona`, `mode`, and `subject` only when entitled). Masked senders omit `underAgreementId` / `presentedAs`.
109
- - Opaque `psn_*` / `rpb_*` refs are **not** account ids. Do not use them for agent lookup, wake, or payment parties. Chat sends may echo an `rpb_*` as `receiverId` — the backend resolves it in-room.
109
+ - Opaque `psn_*` / `rpb_*` refs are **not** account ids. Do not use them for agent lookup, wake, or payment parties. Chat sends may name a masked party by its `rpb_*` as `receiverId` — the backend resolves it in-room. Leave the receiver out to write back in the room.
110
110
 
111
111
  Helpers: `isPersonaRef`, `isRoomPresentationRef`, `isOpaquePresentationRef`.
112
112
 
@@ -1,3 +1,4 @@
1
+ import { isContextAccessRequest } from '../shared/contextAccessRequest.js';
1
2
  import { presentResourceActions } from './resourceActions.js';
2
3
  import { ContextReadClient, CONTEXT_READ_TYPES, CONTEXT_READ_VIA, parseVia, viaHint, } from '../http/ContextReadClient.js';
3
4
  import { ContextOpenClient } from '../http/ContextOpenClient.js';
@@ -65,7 +66,7 @@ export const contextReadCapability = {
65
66
  title: 'Read context you hold',
66
67
  descriptions: {
67
68
  sdk: `Read the contents of a scope you already hold a grant for: ${CONTEXT_READ_TYPES.join(' | ')}. Each type reads through its own entries — ${VIA_BY_TYPE}. Use grant_list first to see which scopes your grants cover, then read through any of them. ${ARTIFACT_VIA_NOTE}. Cursored; all access is grant-fenced server-side.`,
68
- mcp: `Read the contents of a scope you already hold: ${CONTEXT_READ_TYPES.join(' | ')} (the type param). Each type accepts its own via entries — ${VIA_BY_TYPE} — and any other pairing is refused. ${ARTIFACT_VIA_NOTE} (e.g. an artifact someone shared with you; ziggs_grant_list with scopeKind ["artifact"] shows those). Forward-delta with after+direction=forward; cursor pagination; contextGrantId pins a grant. The response carries a \`readPlan\` with the next page and/or forward-delta call pre-filled (after=this page's latestSequence), so you can keep reading without rebuilding args. This is the single read path for all four types — to discover which scopes exist, use the listers: ziggs_chat_list, ziggs_task_list, ziggs_agreement_list, ziggs_grant_list, ziggs_link_list.`,
69
+ mcp: `Read the contents of a scope you already hold: ${CONTEXT_READ_TYPES.join(' | ')} (the type param). Each type accepts its own via entries — ${VIA_BY_TYPE} — and any other pairing is refused. ${ARTIFACT_VIA_NOTE} (e.g. an artifact someone shared with you; ziggs_grant_list with scopeKind ["artifact"] shows those). Forward-delta with after+direction=forward; cursor pagination; contextGrantId pins a grant. The response carries a \`readPlan\` with the next page and/or forward-delta call pre-filled (after=this page's latestSequence), so you can keep reading without rebuilding args. This is the single read path for all context types — to discover which scopes exist, use the listers: ziggs_chat_list, ziggs_task_list, ziggs_agreement_list, ziggs_grant_list, ziggs_link_list.`,
69
70
  },
70
71
  annotation: 'read-only',
71
72
  params: {
@@ -301,7 +302,7 @@ function presentOpenResult(result, env) {
301
302
  return [];
302
303
  const row = item;
303
304
  const ask = row.grantRequest;
304
- if (row.status !== 'active' || ask?.primitive !== 'context' || ask.kind !== 'access-request' || ask.holderId !== fullCreds(env).agentId || !ask.scope?.id || !['artifact', 'chat', 'agreement', 'task'].includes(ask.scope.kind ?? ''))
305
+ if (row.status !== 'active' || !isContextAccessRequest(ask) || ask.holderId !== fullCreds(env).agentId || !ask.scope?.id || !['artifact', 'chat', 'agreement', 'task'].includes(ask.scope.kind ?? ''))
305
306
  return [];
306
307
  return [{ kind: 'resume', tool: env.surface === 'mcp' ? 'ziggs_open' : 'open', args: { [`${ask.scope.kind}Id`]: ask.scope.id }, effect: 'read_only', note: 'Approval recorded. Open the original scope; the server rechecks expiry and revocation.' }];
307
308
  });
@@ -406,30 +407,35 @@ export const accessRequestCapability = {
406
407
  key: 'access_request',
407
408
  names: { sdk: 'access_request', mcp: 'ziggs_access_request' },
408
409
  title: 'Request access for yourself',
410
+ annotation: 'write', needsAgentId: true,
409
411
  descriptions: {
410
- sdk: 'Create a pending approval agreement for the calling agent to access one artifact, chat, agreement context, or task branch for 1–168 hours. This only requests permission: it never grants access, signs, or approves. After the authorized person approves, open the original resource. Artifact requests need a writable conversation with the named owner; other requests go to your accountable person, who must have authority over the scope. Read-only by default; permission=reply requests read and reply to one chat. No spending or work commitment. Reuse idempotencyKey for a retry.',
411
- mcp: 'Ask for missing access through a pending approval agreement. Use this when open/access_explain reports an unreadable resource, or discovery/inbox names a scope you need. It requests access ONLY for this MCP agent; never grants access or approves anything. Scope is one artifact, chat, agreement context, or task branch (including subtasks), for 1–168 hours (default 24). Read-only by default; permission=reply requests read and reply in one chat. For earlier chat messages explicitly set temporal=from-start. Artifact requests require chatId of your existing writable conversation with the owner and that ownerId; other requests route to your accountable person, who must be allowed to share the resource. Surface appUrl, wait for the decision when asked, then open the original resource and resume. Pending grants no access. Never use issue_grant, change identity, or form a generic work agreement to recover missing access.',
412
+ sdk: 'Request optional bounded access to one artifact, chat, agreement context or task. Creates only a pending approval; never grants access or approves. Artifact requests need chatId and ownerId of the person authorized to share it. Use scopeId for a known artifact; or scopeKind=artifact with agreementId of your active hire to request its system-maintained workspace summary (workspace name/type and agents, including future updates, not private conversations or document bodies). Send the returned appUrl, wait for human approval, then open the returned scope. Hiring alone grants no source access. Revocation and expiry are checked by the system.',
413
+ mcp: 'Request access for this agent, never approve or grant it. Use scopeId for an artifact, chat, agreement or task; scopeKind=artifact with agreementId of your active hire resolves its system-maintained workspace summary artifact. Artifact requests require chatId and ownerId of the authorized person in that conversation. Explain the scope and lifetime; summary access includes future updates only to that artifact. Surface appUrl and wait for human approval; then open the returned scope. Pending grants no access.',
412
414
  },
413
- annotation: 'write', needsAgentId: true,
414
415
  params: {
415
416
  scopeKind: { type: 'string', required: true, enum: ['artifact', 'chat', 'agreement', 'task'] },
416
- scopeId: { type: 'string', required: true, description: 'Original returned resource id; this request does not open a new resource' },
417
+ scopeId: { type: 'string', description: 'Original resource id. Omit when using a hire agreementId to request its workspace summary.' },
418
+ agreementId: { type: 'string', description: 'Artifact scope only: current hire id, used instead of scopeId to request the system-maintained workspace summary.' },
417
419
  reason: { type: 'string', required: true, description: 'Why access is needed and who will receive the reply or summary' },
418
420
  permission: { type: 'string', enum: ['read', 'reply'], description: 'read by default; reply is read and reply and is only valid for a chat' },
419
421
  temporal: { type: 'string', enum: ['from-now', 'from-start'], description: 'Chat history requested; default from-now. Earlier messages need from-start. Other resources use from-start.' },
420
422
  durationHours: { type: 'number', description: 'Integer 1–168 hours after approval; default 24' },
421
- chatId: { type: 'string', description: 'Artifact requests only: existing conversation with the owner' },
422
- ownerId: { type: 'string', description: 'Artifact requests only: human account or participant reference in that conversation' },
423
+ chatId: { type: 'string', description: 'Artifact or workspace requests only: existing conversation with the owner' },
424
+ ownerId: { type: 'string', description: 'Artifact or workspace requests only: human account or participant reference in that conversation' },
423
425
  idempotencyKey: { type: 'string', description: 'Reuse for the same logical request' },
424
426
  },
425
427
  handler: async (args, env) => {
426
- for (const key of ['scopeKind', 'scopeId', 'reason']) {
428
+ for (const key of ['scopeKind', 'reason']) {
427
429
  if (typeof args[key] !== 'string' || !String(args[key]).trim())
428
430
  throw new Error(`${key} is required`);
429
431
  }
430
432
  const scopeKind = args.scopeKind;
431
433
  if (!['artifact', 'chat', 'agreement', 'task'].includes(scopeKind))
432
434
  throw new Error('Unsupported scopeKind');
435
+ const agreementId = typeof args.agreementId === 'string' ? args.agreementId.trim() : undefined;
436
+ const scopeId = typeof args.scopeId === 'string' ? args.scopeId.trim() : undefined;
437
+ if (Boolean(agreementId) === Boolean(scopeId) || (agreementId && scopeKind !== 'artifact'))
438
+ throw new Error('Provide scopeId, or agreementId for its workspace summary artifact');
433
439
  const permission = args.permission ?? 'read';
434
440
  if (!['read', 'reply'].includes(String(permission)) || (permission === 'reply' && scopeKind !== 'chat'))
435
441
  throw new Error('Only chat access may request reply permission');
@@ -440,15 +446,15 @@ export const accessRequestCapability = {
440
446
  throw new Error('durationHours must be an integer between 1 and 168');
441
447
  const creds = fullCreds(env);
442
448
  const result = await new ContextGrantsClient(creds.operatorKey, creds.agentId, env.baseUrl).requestAccess({
443
- scopeKind, scopeId: String(args.scopeId).trim(), reason: String(args.reason).trim(),
449
+ scopeKind, scopeId, agreementId, reason: String(args.reason).trim(),
444
450
  permission: permission, temporal: args.temporal,
445
451
  durationHours, chatId: args.chatId, ownerId: args.ownerId,
446
452
  }, { idempotencyKey: args.idempotencyKey, laneId: creds.laneId });
447
453
  return {
448
454
  ...result, outcome: 'pending', state: 'requested', actingAgentId: creds.agentId,
449
- scope: { kind: scopeKind, id: args.scopeId },
455
+ scope: result.scope ?? { kind: scopeKind, id: scopeId },
450
456
  appUrl: `${(env.webUrl ?? 'https://ziggsai.com').replace(/\/$/, '')}/app/agreements/${encodeURIComponent(result.agreementId)}`,
451
- resumeAfterApproval: { tool: env.surface === 'mcp' ? 'ziggs_open' : 'open', args: { [`${scopeKind}Id`]: args.scopeId } },
457
+ resumeAfterApproval: { tool: env.surface === 'mcp' ? 'ziggs_open' : 'open', args: result.scope ? { [`${result.scope.kind}Id`]: result.scope.id } : { [`${scopeKind}Id`]: scopeId } },
452
458
  note: 'No access granted. The authorized person must approve; then open the original resource using the same agent identity.',
453
459
  };
454
460
  },
@@ -99,7 +99,8 @@ export declare class ContextGrantsClient {
99
99
  /** Request-only endpoint: a successful call must still have granted no access. */
100
100
  requestAccess(input: {
101
101
  scopeKind: 'artifact' | 'chat' | 'agreement' | 'task';
102
- scopeId: string;
102
+ scopeId?: string;
103
+ agreementId?: string;
103
104
  reason: string;
104
105
  permission?: 'read' | 'reply';
105
106
  temporal?: 'from-now' | 'from-start';
@@ -113,6 +114,10 @@ export declare class ContextGrantsClient {
113
114
  status: 'pending_approval';
114
115
  agreementId: string;
115
116
  accessGranted: false;
117
+ scope?: {
118
+ kind: string;
119
+ id: string;
120
+ };
116
121
  }>;
117
122
  requestArtifactAccess(artifactId: string, input: {
118
123
  chatId: string;
@@ -80,7 +80,7 @@ export class ContextGrantsClient {
80
80
  if (result.status !== 'pending_approval' || !result.agreementId || result.accessGranted !== false) {
81
81
  throw new Error('Expected a pending access request with accessGranted=false');
82
82
  }
83
- return { status: 'pending_approval', agreementId: result.agreementId, accessGranted: false };
83
+ return { status: 'pending_approval', agreementId: result.agreementId, accessGranted: false, ...(result.scope ? { scope: result.scope } : {}) };
84
84
  }
85
85
  async requestArtifactAccess(artifactId, input, opts) {
86
86
  const res = await fetch(`${this.baseUrl}/context/artifacts/${encodeURIComponent(artifactId)}/access-requests`, {
@@ -117,7 +117,6 @@ export declare class ContextReadClient {
117
117
  contextGrantId?: string;
118
118
  agreementId?: string;
119
119
  taskId?: string;
120
- messageId?: string;
121
120
  artifactId?: string;
122
121
  via?: string;
123
122
  }): Promise<ContextSnapshotResult>;
@@ -124,8 +124,6 @@ export class ContextReadClient {
124
124
  url.searchParams.set('via', opts.taskId ? `task:${opts.taskId}` : opts.via ?? (chatId.startsWith('agrn-') ? `agreement:${opts.agreementId ?? chatId.slice(5)}` : `chat:${chatId}`));
125
125
  if (opts.agreementId)
126
126
  url.searchParams.set('agreementId', opts.agreementId);
127
- if (opts.messageId)
128
- url.searchParams.set('messageId', opts.messageId);
129
127
  if (opts.artifactId)
130
128
  url.searchParams.set('artifactId', opts.artifactId);
131
129
  if (opts.maxMessages != null) {
@@ -0,0 +1,25 @@
1
+ import type { Creds } from '../types.js';
2
+ /** One catalog row as the picker reads it. */
3
+ export interface ToolPickOption {
4
+ name: string;
5
+ line: string;
6
+ }
7
+ /**
8
+ * - `pick` one tool does the need: `tool` names it.
9
+ * - `nothing_fits` no tool does it.
10
+ * - `unsure` / `off` no answer worth giving: show the list.
11
+ */
12
+ export interface ToolPickAnswer {
13
+ how: 'pick' | 'nothing_fits' | 'unsure' | 'off';
14
+ tool?: string;
15
+ confidence?: number;
16
+ }
17
+ /** Null means "no answer": the surface shows the list it always has. */
18
+ export type PickTool = (need: string, options: ToolPickOption[]) => Promise<ToolPickAnswer | null>;
19
+ /**
20
+ * The backend picker: `POST /internal/tool-pick` with this connection's key.
21
+ * The model call and its key live on the backend, since this server also runs
22
+ * on a caller's own machine. Any failure is null, including the 404 from a
23
+ * backend that predates the route.
24
+ */
25
+ export declare function backendToolPick(creds: Creds): PickTool;
@@ -0,0 +1,35 @@
1
+ import { buildCredsHeaders } from './operatorHeaders.js';
2
+ import { getBackendUrl } from '../utils/urlUtils.js';
3
+ /** A listing is always the fallback, so waiting long for a pick buys nothing. */
4
+ const TIMEOUT_MS = 6000;
5
+ /**
6
+ * The backend picker: `POST /internal/tool-pick` with this connection's key.
7
+ * The model call and its key live on the backend, since this server also runs
8
+ * on a caller's own machine. Any failure is null, including the 404 from a
9
+ * backend that predates the route.
10
+ */
11
+ export function backendToolPick(creds) {
12
+ return async (need, options) => {
13
+ if (!creds.operatorKey)
14
+ return null;
15
+ try {
16
+ const res = await fetch(`${getBackendUrl()}/internal/tool-pick`, {
17
+ method: 'POST',
18
+ headers: buildCredsHeaders(creds),
19
+ body: JSON.stringify({ need, options }),
20
+ signal: AbortSignal.timeout(TIMEOUT_MS),
21
+ });
22
+ if (!res.ok) {
23
+ await res.body?.cancel().catch(() => undefined);
24
+ return null;
25
+ }
26
+ const data = (await res.json().catch(() => null));
27
+ if (!data || typeof data.how !== 'string')
28
+ return null;
29
+ return data;
30
+ }
31
+ catch {
32
+ return null;
33
+ }
34
+ };
35
+ }
@@ -33,3 +33,4 @@ export { InboxClient } from './InboxClient.js';
33
33
  export { planInboxAck, planPartialInboxAck, inboxAckAllowed, inboxAckHandledIds, } from '../inboxAck.js';
34
34
  export type { InboxAckDecision, InboxAckStep, InboxAckEnvelope, PlanInboxAckOptions, } from '../inboxAck.js';
35
35
  export { WAKE_LANE_HEADER, laneHeader, buildOperatorHeaders, buildCredsHeaders, } from './operatorHeaders.js';
36
+ export { backendToolPick, type ToolPickOption, type ToolPickAnswer, type PickTool } from './ToolPickClient.js';
@@ -25,3 +25,4 @@ export { planInboxAck, planPartialInboxAck, inboxAckAllowed, inboxAckHandledIds,
25
25
  // because the MCP transports in agent-sdk and ziggs-mcp send it too, and a
26
26
  // copy of a header nobody notices missing goes stale unseen.
27
27
  export { WAKE_LANE_HEADER, laneHeader, buildOperatorHeaders, buildCredsHeaders, } from './operatorHeaders.js';
28
+ export { backendToolPick } from './ToolPickClient.js';
package/dist/index.d.ts CHANGED
@@ -24,3 +24,4 @@ export { sessionOrientation, } from './sessionOrientation.js';
24
24
  export type { ContinuationKind, InboxAckDriver, InboxHandling, RepresentedPerson, SessionContinuation, SessionOrientation, SessionOrientationInput, } from './sessionOrientation.js';
25
25
  export { inboxEngagement, isEchoOrDuplicate, isHireWorkOrder, contactPath, formationGuide, marketplaceFormation, liveHireChangedTerms, liveHireConflictFromStatus, withdrawHeldGraph, reminderHonesty, graphReleaseHonesty, hintsFromTasks, } from './engagementGuide.js';
26
26
  export type { EngagementGuide, EngagementGuideKind, EngagementOutcome, EngagementTaskHint, ContactPathInput, FormationInput, GraphHonesty, HireTermsHonesty, WaitingOn, } from './engagementGuide.js';
27
+ export { isContextAccessRequest } from './shared/contextAccessRequest.js';
package/dist/index.js CHANGED
@@ -23,3 +23,4 @@ export { INBOX_HOST_CLAIMANT_HEADER, INSTANCE_IDENTITY_MAX_LENGTH, cliHostIdenti
23
23
  export * from './utils/appUrls.js';
24
24
  export { sessionOrientation, } from './sessionOrientation.js';
25
25
  export { inboxEngagement, isEchoOrDuplicate, isHireWorkOrder, contactPath, formationGuide, marketplaceFormation, liveHireChangedTerms, liveHireConflictFromStatus, withdrawHeldGraph, reminderHonesty, graphReleaseHonesty, hintsFromTasks, } from './engagementGuide.js';
26
+ export { isContextAccessRequest } from './shared/contextAccessRequest.js';
@@ -0,0 +1,12 @@
1
+ /** The common marker carried by access-request approvals and notifications. */
2
+ export interface ContextAccessRequest {
3
+ primitive: 'context';
4
+ kind: 'access-request';
5
+ holderId?: string;
6
+ scope?: {
7
+ kind?: string;
8
+ id?: string;
9
+ };
10
+ resumeAgreementId?: string;
11
+ }
12
+ export declare function isContextAccessRequest(value: unknown): value is ContextAccessRequest;
@@ -0,0 +1,6 @@
1
+ export function isContextAccessRequest(value) {
2
+ if (!value || typeof value !== 'object')
3
+ return false;
4
+ const row = value;
5
+ return row.primitive === 'context' && row.kind === 'access-request';
6
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ziggs-ai/api-client",
3
- "version": "0.30.2",
3
+ "version": "0.31.0",
4
4
  "description": "HTTP and WebSocket client for the Ziggs backend API",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",