@aithos/sdk 0.1.0-alpha.55 → 0.1.0-alpha.58

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/dist/src/auth.js CHANGED
@@ -28,6 +28,9 @@ import { parseDelegateBundle, readDelegateBundleText, } from "./internal/delegat
28
28
  import { DelegateActor, DelegateRegistry, } from "./internal/delegate-state.js";
29
29
  import { signOwnerEnvelope, } from "./internal/envelope.js";
30
30
  import { OwnerSigners } from "./internal/owner-signers.js";
31
+ import { createDataClient, createDelegateDataClient, } from "./data.js";
32
+ import { delegateKeyPair } from "./internal/protocol-client-bridge.js";
33
+ import { DEFAULT_SDK_ENDPOINTS } from "./endpoints.js";
31
34
  import { parseRecoveryFile, readRecoveryFileText, serializeRecoveryFile, } from "./internal/recovery-file.js";
32
35
  import { AithosSDKError } from "./types.js";
33
36
  /** Default URL of the Aithos auth backend. */
@@ -221,6 +224,118 @@ export class AithosAuth {
221
224
  _getOwnerSigners() {
222
225
  return this.#ownerSigners;
223
226
  }
227
+ /**
228
+ * Ready-made owner data client bound to the signed-in account, signing +
229
+ * sealing under the dedicated **`#data`** sphere (the protocol-intended owner
230
+ * data key). This is the one-liner apps should use instead of hand-rolling
231
+ * `createDataClient` with a raw seed — hand-rolling with `#root` is exactly
232
+ * what left legacy collections sealed to the wrong key.
233
+ *
234
+ * const data = auth.ownerDataClient({ schemas: [myVendorLite] });
235
+ * await data.collection("notes").insert({ ... }); // owned under #data
236
+ *
237
+ * Throws when no owner is signed in, or when the account has no `#data`
238
+ * sphere (legacy accounts created before #data, or imported from a 4-seed
239
+ * recovery). Add one first with `rotateEthos` / the migration scripts, then
240
+ * re-import the resulting recovery — the error message says so.
241
+ *
242
+ * @param args.pdsUrl PDS base URL. Defaults to the SDK default (pds.aithos.be).
243
+ * @param args.schemas Vendor `AithosSchemaLite` definitions to register for
244
+ * WRITES (reads auto-resolve published schemas from the PDS).
245
+ */
246
+ ownerDataClient(args = {}) {
247
+ if (!this.#ownerSigners || this.#ownerSigners.destroyed) {
248
+ throw new AithosSDKError("auth_not_signed_in", "ownerDataClient: no owner is signed in. Call signIn / signUp / signInCustodial first.");
249
+ }
250
+ const stored = this.#ownerSigners._unsafeStoredIdentity();
251
+ if (!stored.seeds.data) {
252
+ throw new AithosSDKError("auth_no_data_sphere", "ownerDataClient: this account has no #data sphere, so it cannot own collections under " +
253
+ "the #data convention. New accounts get one at sign-up; a legacy account (or a 4-seed " +
254
+ "recovery) must add it first via rotateEthos / the migration scripts, then re-import the " +
255
+ "resulting recovery (which carries the #data seed).");
256
+ }
257
+ return createDataClient({
258
+ pdsUrl: args.pdsUrl ?? DEFAULT_SDK_ENDPOINTS.pds,
259
+ did: stored.did,
260
+ sphereSeed: hexToBytesLocal(stored.seeds.data),
261
+ verificationMethod: `${stored.did}#data`,
262
+ ...(args.schemas ? { schemas: args.schemas } : {}),
263
+ fetch: this.#fetchImpl,
264
+ });
265
+ }
266
+ /**
267
+ * Ready-made DELEGATE data client, bound to a mandate held in this session
268
+ * (imported via `importMandate` / an accepted invite). Same record-CRUD
269
+ * surface as the owner client, bounded by the mandate's scope — you never
270
+ * pass a key, a sphere, or the mandate itself to the data calls.
271
+ *
272
+ * const db = auth.delegateDataClient(); // single active mandate
273
+ * await db.collection("prospects").insert({ ... }); // needs data.prospects.write
274
+ *
275
+ * With several active mandates, pass `{ subjectDid }` or `{ mandateId }`.
276
+ * Owner-only ops (createCollection, authorizeDelegate, …) throw -32042 — the
277
+ * owner does those once, at onboarding.
278
+ */
279
+ delegateDataClient(args = {}) {
280
+ let actor;
281
+ if (args.mandateId) {
282
+ actor = this.#delegates.get(args.mandateId);
283
+ }
284
+ else if (args.subjectDid) {
285
+ actor = this.#delegates.findForSubject(args.subjectDid);
286
+ }
287
+ else {
288
+ const all = this.#delegates.list();
289
+ if (all.length === 1) {
290
+ actor = all[0];
291
+ }
292
+ else if (all.length === 0) {
293
+ throw new AithosSDKError("auth_no_delegate", "delegateDataClient: no mandate imported in this session. Call importMandate / accept an invite first.");
294
+ }
295
+ else {
296
+ throw new AithosSDKError("auth_ambiguous_delegate", `delegateDataClient: ${all.length} mandates are active — pass { subjectDid } or { mandateId } to choose one.`);
297
+ }
298
+ }
299
+ if (!actor || actor.destroyed) {
300
+ throw new AithosSDKError("auth_no_delegate", "delegateDataClient: the requested mandate is not active in this session.");
301
+ }
302
+ return createDelegateDataClient({
303
+ pdsUrl: args.pdsUrl ?? DEFAULT_SDK_ENDPOINTS.pds,
304
+ subjectDid: actor.subjectDid,
305
+ mandate: actor.mandate,
306
+ delegateSeed: delegateKeyPair(actor).seed,
307
+ granteePubkeyMultibase: actor.granteePubkeyMultibase,
308
+ ...(args.schemas ? { schemas: args.schemas } : {}),
309
+ fetch: this.#fetchImpl,
310
+ });
311
+ }
312
+ /**
313
+ * Unified data accessor — the database for "however you connected":
314
+ * - signed in as owner → your own collections under `#data`;
315
+ * - acting under an imported mandate → the subject's collections (per scope).
316
+ *
317
+ * Identical CRUD surface either way; the developer never sees a sphere, a
318
+ * key, or the mandate. The mode follows how you authenticated, not a flag on
319
+ * the data calls.
320
+ *
321
+ * const db = auth.data;
322
+ * await db.collection("prospects").insert({ ... });
323
+ *
324
+ * Only ambiguous when you are BOTH signed in as owner AND holding mandates;
325
+ * then call `ownerDataClient()` / `delegateDataClient({ … })` explicitly.
326
+ */
327
+ get data() {
328
+ const ownerActive = !!this.#ownerSigners && !this.#ownerSigners.destroyed;
329
+ const delegateCount = this.#delegates.list().length;
330
+ if (ownerActive && delegateCount === 0)
331
+ return this.ownerDataClient();
332
+ if (!ownerActive && delegateCount >= 1)
333
+ return this.delegateDataClient();
334
+ if (ownerActive && delegateCount >= 1) {
335
+ throw new AithosSDKError("auth_data_ambiguous", "auth.data: both an owner and delegate mandate(s) are active. Use ownerDataClient() or delegateDataClient({ … }) explicitly.");
336
+ }
337
+ throw new AithosSDKError("auth_not_signed_in", "auth.data: no owner signed in and no mandate imported. Call signIn / signUp or importMandate first.");
338
+ }
224
339
  /**
225
340
  * Internal accessor — looks up an active delegate by mandate id.
226
341
  * @internal
@@ -1445,6 +1560,12 @@ function bytesToHex(b) {
1445
1560
  out += b[i].toString(16).padStart(2, "0");
1446
1561
  return out;
1447
1562
  }
1563
+ function hexToBytesLocal(hex) {
1564
+ const out = new Uint8Array(hex.length / 2);
1565
+ for (let i = 0; i < out.length; i++)
1566
+ out[i] = parseInt(hex.substr(i * 2, 2), 16);
1567
+ return out;
1568
+ }
1448
1569
  /**
1449
1570
  * Split a custodial seed bundle into the keystore's hex seeds, zeroizing the
1450
1571
  * raw bytes (the bundle and every slice) before returning.
@@ -1,6 +1,8 @@
1
1
  import type { AithosAuth } from "./auth.js";
2
2
  import { type AithosSdkEndpoints } from "./endpoints.js";
3
3
  import { type LocalPendingEntry, type TranscribeDraftMeta, type TranscribeDraftRecord } from "./transcribe-resilience.js";
4
+ import { type AgentMessage, type AgentToolSpec, type AgentTurnStopReason, type ContentBlock, type LoopStopReason, type ToolCallTrace } from "./agent-loop.js";
5
+ import { type DataProvider } from "./agent-dispatch.js";
4
6
  export interface ComputeMessage {
5
7
  readonly role: "user" | "assistant";
6
8
  readonly content: string;
@@ -173,6 +175,81 @@ export interface RunConversationResult {
173
175
  readonly receiptId?: string;
174
176
  readonly sponsoredBy?: string;
175
177
  }
178
+ export interface InvokeTurnArgs {
179
+ /** Mandate id — optional for owner sessions, required for delegate sessions. */
180
+ readonly mandateId?: string;
181
+ readonly model: string;
182
+ /** Running conversation. `content` may be a string OR Anthropic blocks. */
183
+ readonly messages: readonly AgentMessage[];
184
+ /** Anthropic tool specs to expose this turn. */
185
+ readonly tools: readonly AgentToolSpec[];
186
+ readonly system?: string;
187
+ readonly maxTokens?: number;
188
+ readonly temperature?: number;
189
+ readonly idempotencyKey?: string;
190
+ readonly signal?: AbortSignal;
191
+ }
192
+ export interface InvokeTurnResult {
193
+ /** Raw content blocks (text + tool_use) — the caller detects tool calls. */
194
+ readonly content: readonly ContentBlock[];
195
+ readonly stopReason: AgentTurnStopReason;
196
+ readonly usage: {
197
+ readonly inputTokens: number;
198
+ readonly outputTokens: number;
199
+ };
200
+ /** Microcredits charged for THIS single turn. */
201
+ readonly creditsCharged: number;
202
+ readonly walletBalance: number;
203
+ readonly auditId: string;
204
+ readonly fundedBy?: "sponsored" | "grant" | "purchase";
205
+ readonly receiptId?: string;
206
+ readonly sponsoredBy?: string;
207
+ }
208
+ export interface RunConversationLocalArgs {
209
+ /** Mandate id — optional for owner sessions, required for delegate sessions. */
210
+ readonly mandateId?: string;
211
+ /**
212
+ * Subject DID whose ethos the agent reads/writes. Defaults to the signed-in
213
+ * owner, or (delegate session) the mandate's subject.
214
+ */
215
+ readonly subjectDid?: string;
216
+ readonly model: string;
217
+ /** Initial conversation (plain string turns). */
218
+ readonly messages: readonly ComputeMessage[];
219
+ readonly system?: string;
220
+ /**
221
+ * Subset of Aithos tool names to expose. Omit for the full catalogue
222
+ * (read + write). Ignored when `readOnly` is set.
223
+ */
224
+ readonly tools?: readonly string[];
225
+ /** Expose only the read family (no mutations). */
226
+ readonly readOnly?: boolean;
227
+ /** Cap on proxy turns (each is a billed call). Default 6, hard max 12. */
228
+ readonly maxIterations?: number;
229
+ readonly maxTokens?: number;
230
+ readonly temperature?: number;
231
+ /** Optional gamma reader powering `data_query`. Absent → that tool errors. */
232
+ readonly dataProvider?: DataProvider;
233
+ readonly idempotencyKey?: string;
234
+ readonly signal?: AbortSignal;
235
+ }
236
+ export interface RunConversationLocalResult {
237
+ readonly content: string;
238
+ readonly stopReason: LoopStopReason;
239
+ readonly iterations: number;
240
+ readonly usage: {
241
+ readonly inputTokens: number;
242
+ readonly outputTokens: number;
243
+ };
244
+ readonly toolCalls: readonly ToolCallTrace[];
245
+ /** Sum of per-turn charges (HANDOFF §1: per-turn cumulative billing). */
246
+ readonly creditsCharged: number;
247
+ readonly walletBalance: number;
248
+ /** Audit id of the LAST turn. */
249
+ readonly auditId: string;
250
+ readonly fundedBy?: "sponsored" | "grant" | "purchase";
251
+ readonly receiptId?: string;
252
+ }
176
253
  /**
177
254
  * Stable cross-provider image model ids supported by the Aithos compute
178
255
  * proxy. New models can be added on the server side without an SDK
@@ -497,6 +574,41 @@ export declare class ComputeNamespace {
497
574
  * `mandate_revoked`, `insufficient_credits`, …).
498
575
  */
499
576
  runConversation(args: RunConversationArgs): Promise<RunConversationResult>;
577
+ /**
578
+ * Run ONE Bedrock turn with tool-calling through the proxy
579
+ * (`aithos.compute_invoke_turn`). Returns the raw content blocks — including
580
+ * any `tool_use` — so the caller can dispatch tools and loop. This is the
581
+ * per-turn primitive the CLIENT-SIDE agentic loop is built on; most callers
582
+ * want {@link runConversationLocal} instead, which drives the loop and
583
+ * dispatches Aithos tools locally.
584
+ *
585
+ * Same signer paths and billing as {@link invokeBedrock}; billed once per
586
+ * turn.
587
+ */
588
+ invokeTurn(args: InvokeTurnArgs): Promise<InvokeTurnResult>;
589
+ /**
590
+ * Run a CLIENT-SIDE agentic conversation with tool-calling: a multi-turn
591
+ * loop where each turn is one signed proxy call ({@link invokeTurn}) and the
592
+ * tools the model requests are dispatched LOCALLY against the user's own
593
+ * ethos (reads decrypt locally; writes stage + sign + publish locally). The
594
+ * proxy does pure per-turn inference and never holds a decryption key — keys,
595
+ * data, and dispatch all stay on the client (HANDOFF-AGENT-WRITE-MODE.md
596
+ * §1/§3).
597
+ *
598
+ * Authorisation: an owner has full authority over their own ethos; a
599
+ * delegate is bounded by the mandate's `ethos.read.*` / `ethos.write.*`
600
+ * scopes (a tool out of scope returns an error to the model — nothing is
601
+ * published). The spend capability (`compute.invoke`) is checked server-side
602
+ * on every turn.
603
+ *
604
+ * Billing is PER-TURN cumulative: each turn is billed once and the result's
605
+ * `creditsCharged` is the sum across turns.
606
+ *
607
+ * @throws {AithosSDKError} `sdk_no_signer`, `sdk_no_delegate_for_mandate`,
608
+ * `network`, `http`, `empty`, or any proxy code. Tool-level failures do
609
+ * NOT throw — they are fed back to the model as errors.
610
+ */
611
+ runConversationLocal(args: RunConversationLocalArgs): Promise<RunConversationLocalResult>;
500
612
  /**
501
613
  * Multimodal Bedrock invoke — image + text → text response.
502
614
  * Default model: `claude-sonnet-4-6` (vision-capable, reliable JSON).
@@ -21,6 +21,13 @@ import { computeInvokeUrl, } from "./endpoints.js";
21
21
  import { delegateKeyPair, ownerKeyPair, } from "./internal/protocol-client-bridge.js";
22
22
  import { AithosSDKError } from "./types.js";
23
23
  import { LocalPendingTranscribeTracker, TranscribeDraftStore, } from "./transcribe-resilience.js";
24
+ import { runAgenticLoopLocal, } from "./agent-loop.js";
25
+ import { selectAgentTools } from "./agent-tools.js";
26
+ import { dispatchAgentToolLocal, } from "./agent-dispatch.js";
27
+ import { EthosNamespace } from "./ethos.js";
28
+ /** Default / hard cap on client-side loop iterations (mirrors the proxy). */
29
+ const DEFAULT_LOCAL_MAX_ITERATIONS = 6;
30
+ const HARD_LOCAL_MAX_ITERATIONS = 12;
24
31
  /**
25
32
  * `sdk.compute` namespace. Constructed once by the {@link AithosSDK}
26
33
  * constructor; reads the active owner from the supplied
@@ -148,6 +155,134 @@ export class ComputeNamespace {
148
155
  signal: args.signal,
149
156
  });
150
157
  }
158
+ /**
159
+ * Run ONE Bedrock turn with tool-calling through the proxy
160
+ * (`aithos.compute_invoke_turn`). Returns the raw content blocks — including
161
+ * any `tool_use` — so the caller can dispatch tools and loop. This is the
162
+ * per-turn primitive the CLIENT-SIDE agentic loop is built on; most callers
163
+ * want {@link runConversationLocal} instead, which drives the loop and
164
+ * dispatches Aithos tools locally.
165
+ *
166
+ * Same signer paths and billing as {@link invokeBedrock}; billed once per
167
+ * turn.
168
+ */
169
+ async invokeTurn(args) {
170
+ const { endpoints, fetch: fetchImpl } = this.#deps;
171
+ const choice = this.#resolveSigner(args.mandateId);
172
+ const url = computeInvokeUrl(endpoints);
173
+ const idempotencyKey = args.idempotencyKey ?? generateIdempotencyKey();
174
+ const params = {
175
+ app_did: this.#deps.appDid,
176
+ mandate_id: this.#resolveMandateIdForWire(args.mandateId, choice),
177
+ model: args.model,
178
+ messages: args.messages,
179
+ tools: args.tools,
180
+ idempotency_key: idempotencyKey,
181
+ };
182
+ if (args.system !== undefined)
183
+ params.system = args.system;
184
+ if (args.maxTokens !== undefined)
185
+ params.max_tokens = args.maxTokens;
186
+ if (args.temperature !== undefined)
187
+ params.temperature = args.temperature;
188
+ return await this.#signAndPost({
189
+ url,
190
+ method: "aithos.compute_invoke_turn",
191
+ params,
192
+ choice,
193
+ fetchImpl,
194
+ signal: args.signal,
195
+ });
196
+ }
197
+ /**
198
+ * Run a CLIENT-SIDE agentic conversation with tool-calling: a multi-turn
199
+ * loop where each turn is one signed proxy call ({@link invokeTurn}) and the
200
+ * tools the model requests are dispatched LOCALLY against the user's own
201
+ * ethos (reads decrypt locally; writes stage + sign + publish locally). The
202
+ * proxy does pure per-turn inference and never holds a decryption key — keys,
203
+ * data, and dispatch all stay on the client (HANDOFF-AGENT-WRITE-MODE.md
204
+ * §1/§3).
205
+ *
206
+ * Authorisation: an owner has full authority over their own ethos; a
207
+ * delegate is bounded by the mandate's `ethos.read.*` / `ethos.write.*`
208
+ * scopes (a tool out of scope returns an error to the model — nothing is
209
+ * published). The spend capability (`compute.invoke`) is checked server-side
210
+ * on every turn.
211
+ *
212
+ * Billing is PER-TURN cumulative: each turn is billed once and the result's
213
+ * `creditsCharged` is the sum across turns.
214
+ *
215
+ * @throws {AithosSDKError} `sdk_no_signer`, `sdk_no_delegate_for_mandate`,
216
+ * `network`, `http`, `empty`, or any proxy code. Tool-level failures do
217
+ * NOT throw — they are fed back to the model as errors.
218
+ */
219
+ async runConversationLocal(args) {
220
+ // Resolve the subject whose ethos the agent operates on.
221
+ const subjectDid = this.#resolveSubjectDid(args.subjectDid, args.mandateId);
222
+ // Resolve the ethos client + the delegate scopes that bound writes.
223
+ const ethosNs = this.#ethosNamespace();
224
+ const ethosClient = await ethosNs.of(subjectDid);
225
+ const delegateScopes = ethosClient.mode === "delegate"
226
+ ? this.#delegateScopesForSubject(subjectDid)
227
+ : [];
228
+ const dispatchCtx = {
229
+ ethos: ethosClient,
230
+ delegateScopes,
231
+ ...(args.dataProvider ? { dataProvider: args.dataProvider } : {}),
232
+ };
233
+ const tools = selectAgentTools({
234
+ ...(args.tools ? { tools: args.tools } : {}),
235
+ ...(args.readOnly ? { readOnly: true } : {}),
236
+ });
237
+ const maxIterations = Math.max(1, Math.min(HARD_LOCAL_MAX_ITERATIONS, args.maxIterations ?? DEFAULT_LOCAL_MAX_ITERATIONS));
238
+ // Carry the last turn's billing metadata out of the loop.
239
+ let lastAuditId = "";
240
+ let lastFundedBy;
241
+ let lastReceiptId;
242
+ const initialMessages = args.messages.map((m) => ({
243
+ role: m.role,
244
+ content: m.content,
245
+ }));
246
+ const loop = await runAgenticLoopLocal({
247
+ messages: initialMessages,
248
+ maxIterations,
249
+ invokeTurn: async (messages) => {
250
+ const r = await this.invokeTurn({
251
+ ...(args.mandateId !== undefined ? { mandateId: args.mandateId } : {}),
252
+ model: args.model,
253
+ messages,
254
+ tools,
255
+ ...(args.system !== undefined ? { system: args.system } : {}),
256
+ ...(args.maxTokens !== undefined ? { maxTokens: args.maxTokens } : {}),
257
+ ...(args.temperature !== undefined ? { temperature: args.temperature } : {}),
258
+ ...(args.signal ? { signal: args.signal } : {}),
259
+ });
260
+ lastAuditId = r.auditId;
261
+ lastFundedBy = r.fundedBy;
262
+ lastReceiptId = r.receiptId;
263
+ return {
264
+ content: r.content,
265
+ stopReason: r.stopReason,
266
+ usage: r.usage,
267
+ creditsCharged: r.creditsCharged,
268
+ walletBalance: r.walletBalance,
269
+ };
270
+ },
271
+ dispatch: (name, input) => dispatchAgentToolLocal(dispatchCtx, name, input),
272
+ });
273
+ return {
274
+ content: loop.finalContent,
275
+ stopReason: loop.stopReason,
276
+ iterations: loop.iterations,
277
+ usage: loop.usage,
278
+ toolCalls: loop.toolCalls,
279
+ creditsCharged: loop.creditsCharged,
280
+ walletBalance: loop.walletBalance,
281
+ auditId: lastAuditId,
282
+ ...(lastFundedBy ? { fundedBy: lastFundedBy } : {}),
283
+ ...(lastReceiptId ? { receiptId: lastReceiptId } : {}),
284
+ };
285
+ }
151
286
  /**
152
287
  * Multimodal Bedrock invoke — image + text → text response.
153
288
  * Default model: `claude-sonnet-4-6` (vision-capable, reliable JSON).
@@ -597,6 +732,46 @@ export class ComputeNamespace {
597
732
  },
598
733
  };
599
734
  }
735
+ /**
736
+ * Lazily-built EthosNamespace for the local agent dispatch — reuses the
737
+ * compute deps (auth / endpoints / fetch), so no extra wiring in sdk.ts.
738
+ */
739
+ #ethosNs = null;
740
+ #ethosNamespace() {
741
+ if (!this.#ethosNs) {
742
+ this.#ethosNs = new EthosNamespace({
743
+ auth: this.#deps.auth,
744
+ endpoints: this.#deps.endpoints,
745
+ fetch: this.#deps.fetch,
746
+ });
747
+ }
748
+ return this.#ethosNs;
749
+ }
750
+ /**
751
+ * Resolve the subject DID the local agent operates on:
752
+ * 1. explicit `subjectDid` always wins;
753
+ * 2. else the signed-in owner;
754
+ * 3. else (delegate session) the subject of the imported mandate.
755
+ */
756
+ #resolveSubjectDid(explicit, mandateId) {
757
+ if (explicit && explicit.length > 0)
758
+ return explicit;
759
+ const owner = this.#deps.auth._getOwnerSigners();
760
+ if (owner && !owner.destroyed)
761
+ return owner.did;
762
+ if (mandateId) {
763
+ const actor = this.#deps.auth._getDelegateActor(mandateId);
764
+ if (actor && !actor.destroyed)
765
+ return actor.subjectDid;
766
+ }
767
+ throw new AithosSDKError("sdk_no_signer", "cannot determine the subject DID: sign in as an owner, pass a mandateId for a delegate session, or pass subjectDid explicitly.");
768
+ }
769
+ /** Scopes carried by the delegate mandate for `did` (empty if none). */
770
+ #delegateScopesForSubject(did) {
771
+ const actor = this.#deps.auth._findDelegateForSubject(did);
772
+ const raw = actor?.mandate?.scopes;
773
+ return Array.isArray(raw) ? raw.filter((s) => typeof s === "string") : [];
774
+ }
600
775
  /**
601
776
  * Resolve the active signer (owner takes precedence over delegate).
602
777
  *
@@ -278,17 +278,22 @@ export interface CreateDelegateDataClientArgs {
278
278
  readonly fetch?: typeof fetch;
279
279
  }
280
280
  /**
281
- * Build a read-only data client that reads a subject's collections under
282
- * a mandate (delegate path). The returned {@link ReadonlyDataClient}
283
- * signs every request as the delegate (bare-multibase verificationMethod
284
- * + the mandate attached to the envelope), and decrypts records using the
285
- * CMK the owner re-wrapped for this delegate via
286
- * {@link DataClient.authorizeDelegate}.
281
+ * Build a data client that operates on a subject's collections under a
282
+ * mandate (delegate path). It signs every request as the delegate
283
+ * (bare-multibase verificationMethod + the mandate attached to the
284
+ * envelope) and decrypts/encrypts records using the CMK the owner
285
+ * re-wrapped for this delegate via {@link DataClient.authorizeDelegate}.
287
286
  *
288
- * Writes are not available on the returned type and throw `-32042` if
289
- * forced.
287
+ * Record CRUD is bounded by the mandate scope: reads need
288
+ * `data.<col>.read`, writes need `data.<col>.write` (or `.admin` /
289
+ * wildcard) — enforced client-side and by the PDS. Owner-only operations
290
+ * (createCollection, authorizeDelegate, revokeDelegate, registerSchema)
291
+ * always throw `-32042`: the owner holds the CMK and controls access.
292
+ *
293
+ * @internal Prefer the session accessor `auth.data` (owner) / the delegate
294
+ * session over hand-constructing this with a raw seed.
290
295
  */
291
- export declare function createDelegateDataClient(args: CreateDelegateDataClientArgs): ReadonlyDataClient;
296
+ export declare function createDelegateDataClient(args: CreateDelegateDataClientArgs): DataClient;
292
297
  /** An append-only handle on one collection: `insert` and nothing else. */
293
298
  export interface AppendOnlyDataCollection {
294
299
  readonly name: string;