@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/data.js CHANGED
@@ -1,35 +1,42 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
2
  // Copyright 2026 Mathieu Colla
3
3
  /**
4
- * `sdk.data` — high-level API for the Aithos data sub-protocol PDS.
4
+ * `sdk.data` — the Aithos data sub-protocol PDS as a plain database.
5
5
  *
6
- * The Aithos data sub-protocol stores operational records (contacts,
7
- * messages, ...) under a subject's identity, encrypted client-side,
8
- * accessible to authorized apps via signed mandates. This module is the
9
- * ergonomic façade developers consume:
6
+ * Records (contacts, prospects, messages, …) live under a subject's identity,
7
+ * encrypted client-side, sealed under the subject's dedicated **`#data`**
8
+ * sphere. A developer never chooses a sphere, never handles a key — the SDK
9
+ * derives everything from the session. There are exactly two ways in, both off
10
+ * the auth session:
10
11
  *
11
- * const client = createDataClient({ pdsUrl, did, sphereSeed });
12
- * const contacts = client.collection("contacts");
13
- * const id = await contacts.insert({ name: "Jean", phone: "+33..." });
14
- * const list = await contacts.list({ filter: { status: "lead" } });
12
+ * // 1. As the OWNER (signed in) your own database:
13
+ * const db = auth.data; // === auth.ownerDataClient()
14
+ * const id = await db.collection("contacts").insert({ name: "Jean" });
15
+ * const leads = await db.collection("contacts").list({ filter: { status: "lead" } });
15
16
  *
16
- * Under the hood the module handles: envelope signing per spec §11,
17
- * CMK / DEK lifecycle (generate, wrap, unwrap), per-record AEAD
18
- * encryption, split between indexable metadata (server-visible) and
19
- * encrypted payload (server-blind), JSON-RPC dispatch.
17
+ * // 2. As a DELEGATE (you imported a mandate) the subject's database:
18
+ * const db = auth.delegateDataClient(); // single active mandate; or { subjectDid }
19
+ * await db.collection("prospects").insert({ ... }); // needs data.prospects.write
20
20
  *
21
- * It does not (yet) handle: mandate-delegate authentication on the
22
- * caller side (the SDK only signs as owner in v0.1), full schema
23
- * publication (no `registerSchema` RPC yet that lands with A2b, cf.
24
- * aithos-protocol/PLAN-A2b-schema-self-registration.md), forward-secrecy
25
- * CMK rotation primitives. Those land in later Sub-jalons.
21
+ * `auth.data` returns whichever applies to how you connected, with an identical
22
+ * CRUD surface. Owner-only operations (createCollection, authorizeDelegate,
23
+ * registerSchema) belong to the owner and throw `-32042` on the delegate path:
24
+ * the owner holds the CMK and decides who may access the collection (a one-time
25
+ * onboarding step), then the delegate does record CRUD bounded by its scope.
26
26
  *
27
- * Apps that need to use a vendor schema (`aithos.x.<vendor>.<name>.v<N>`,
28
- * or any non-`aithos.*` namespace) pass their schema definitions to
29
- * `createDataClient({ schemas: [...] })`. The PDS accepts these at face
30
- * value (no server-side metadata validation pending A2b); the SDK uses
31
- * the supplied schema definitions to split records into indexable
32
- * metadata and encrypted payload.
27
+ * The low-level `createDataClient` / `createDelegateDataClient` (which take a
28
+ * raw seed) are NOT exported from the package — they exist only as internals of
29
+ * the session accessors above. This is deliberate: passing a seed by hand is
30
+ * exactly what let an app seal data under the wrong key.
31
+ *
32
+ * Under the hood the module handles: envelope signing per spec §11, CMK / DEK
33
+ * lifecycle (generate, wrap, unwrap), per-record AEAD encryption, the split
34
+ * between indexable metadata (server-visible) and encrypted payload
35
+ * (server-blind), and JSON-RPC dispatch.
36
+ *
37
+ * Vendor schemas (`aithos.x.<vendor>.<name>.v<N>`) are passed via the
38
+ * `{ schemas: [...] }` option on the session accessor; the SDK uses them to
39
+ * split records into indexable metadata and encrypted payload.
33
40
  *
34
41
  * Spec ref: spec/data/01..10 of the aithos-protocol repo.
35
42
  */
@@ -61,15 +68,20 @@ export function createDataClient(args) {
61
68
  return new DataClientImpl(args);
62
69
  }
63
70
  /**
64
- * Build a read-only data client that reads a subject's collections under
65
- * a mandate (delegate path). The returned {@link ReadonlyDataClient}
66
- * signs every request as the delegate (bare-multibase verificationMethod
67
- * + the mandate attached to the envelope), and decrypts records using the
68
- * CMK the owner re-wrapped for this delegate via
69
- * {@link DataClient.authorizeDelegate}.
71
+ * Build a data client that operates on a subject's collections under a
72
+ * mandate (delegate path). It signs every request as the delegate
73
+ * (bare-multibase verificationMethod + the mandate attached to the
74
+ * envelope) and decrypts/encrypts records using the CMK the owner
75
+ * re-wrapped for this delegate via {@link DataClient.authorizeDelegate}.
70
76
  *
71
- * Writes are not available on the returned type and throw `-32042` if
72
- * forced.
77
+ * Record CRUD is bounded by the mandate scope: reads need
78
+ * `data.<col>.read`, writes need `data.<col>.write` (or `.admin` /
79
+ * wildcard) — enforced client-side and by the PDS. Owner-only operations
80
+ * (createCollection, authorizeDelegate, revokeDelegate, registerSchema)
81
+ * always throw `-32042`: the owner holds the CMK and controls access.
82
+ *
83
+ * @internal Prefer the session accessor `auth.data` (owner) / the delegate
84
+ * session over hand-constructing this with a raw seed.
73
85
  */
74
86
  export function createDelegateDataClient(args) {
75
87
  const granteePubMb = args.granteePubkeyMultibase ??
@@ -171,14 +183,38 @@ class DataClientImpl {
171
183
  this.#deposit = args.deposit;
172
184
  this.#localSchemas = new Map((args.schemas ?? []).map((s) => [s.schema, s]));
173
185
  }
174
- /** Throw a read-only error when a mutating verb is called on a delegate
175
- * client. The PDS rejects these server-side too; this is the fast,
176
- * local guard with a precise message. */
186
+ /** Owner-only operations: collection lifecycle (create/ensure), delegate
187
+ * management (authorize/revoke) and schema registration. A delegate even
188
+ * with a write mandate — cannot perform these: the owner holds the CMK and
189
+ * controls who may access the collection. The PDS enforces the same. */
177
190
  #assertOwner(op) {
178
191
  if (this.#delegate) {
179
- const e = new Error(`sdk.data: "${op}" is not permitted for a delegate client (read-only mandate). ` +
180
- `A data.<collection>.read mandate grants get/list only; writes require the owner ` +
181
- `or a data.<collection>.write mandate (not yet supported on the delegate path).`);
192
+ const e = new Error(`sdk.data: "${op}" is owner-only. A delegate (even with a write mandate) cannot ` +
193
+ `create collections, authorize/revoke delegates, or register schemas the owner ` +
194
+ `holds the CMK and controls access. Record CRUD (insert/get/list/update/delete) ` +
195
+ `is available to a delegate whose mandate carries the matching scope.`);
196
+ e.code = -32042;
197
+ throw e;
198
+ }
199
+ }
200
+ /** Record-write guard. Owner: always allowed. Delegate: allowed iff the
201
+ * mandate carries a write/admin scope for this collection (data.<col>.write,
202
+ * data.*.write, data.<col>.admin, data.*.admin). The PDS enforces the same;
203
+ * this is the fast, precise local error. */
204
+ #assertCanWrite(op, collectionName) {
205
+ if (!this.#delegate)
206
+ return; // owner can always write
207
+ const scopes = this.#delegate.mandate.scopes ?? [];
208
+ const needed = [
209
+ `data.${collectionName}.write`,
210
+ `data.*.write`,
211
+ `data.${collectionName}.admin`,
212
+ `data.*.admin`,
213
+ ];
214
+ const ok = scopes.some((s) => needed.includes(s.split(".").slice(0, 3).join(".")));
215
+ if (!ok) {
216
+ const e = new Error(`sdk.data: "${op}" on "${collectionName}" requires a data.${collectionName}.write ` +
217
+ `(or .admin / wildcard) scope. This mandate grants: [${scopes.join(", ")}].`);
182
218
  e.code = -32042;
183
219
  throw e;
184
220
  }
@@ -419,25 +455,23 @@ class DataClientImpl {
419
455
  schema = liteFromPublishedSchema(published);
420
456
  }
421
457
  catch {
422
- // network / not-found — surface the friendly error below
458
+ // network / not-found — leave `schema` undefined (reads still work)
423
459
  }
424
460
  }
425
- if (!schema) {
426
- throw new Error(`sdk.data: schema "${meta.schema}" not known to the SDK and not published ` +
427
- `on the PDS for ${this.#did}. Pass it via createDataClient({ schemas: [...] }) ` +
428
- `if it's an app-defined (vendor) schema, register it via registerSchema, ` +
429
- `or upgrade the SDK if it's a core schema added in a later release.`);
430
- }
431
- const state = { name, urn: meta.urn, schema };
461
+ // Do NOT throw when the schema is unknown: reads decrypt from the CMK +
462
+ // metadata/payload alone (the schema is only needed to SPLIT on write).
463
+ // Leaving `schema` undefined keeps the collection fully readable; an
464
+ // insert/update will surface a precise "schema required to write" error.
465
+ const state = { name, urn: meta.urn, ...(schema ? { schema } : {}), schemaId: meta.schema };
432
466
  this.#colCache.set(name, state);
433
467
  return state;
434
468
  }
435
469
  async _insert(state, record) {
436
- this.#assertOwner("insert");
470
+ this.#assertCanWrite("insert", state.name);
437
471
  const cmk = this.#cmkCache.get(state.name);
438
472
  if (!cmk)
439
473
  throw new Error("CMK not loaded");
440
- const { metadata, payload } = splitRecord(record, state.schema);
474
+ const { metadata, payload } = splitRecord(record, requireSchema(state));
441
475
  const recordId = `record_${makeUlid()}`;
442
476
  const encrypted = this.#encryptPayload({
443
477
  collectionName: state.name,
@@ -502,11 +536,11 @@ class DataClientImpl {
502
536
  };
503
537
  }
504
538
  async _update(state, recordId, record) {
505
- this.#assertOwner("update");
539
+ this.#assertCanWrite("update", state.name);
506
540
  const cmk = this.#cmkCache.get(state.name);
507
541
  if (!cmk)
508
542
  throw new Error("CMK not loaded");
509
- const { metadata, payload } = splitRecord(record, state.schema);
543
+ const { metadata, payload } = splitRecord(record, requireSchema(state));
510
544
  const encrypted = this.#encryptPayload({
511
545
  collectionName: state.name,
512
546
  recordId,
@@ -521,7 +555,7 @@ class DataClientImpl {
521
555
  });
522
556
  }
523
557
  async _delete(state, recordId) {
524
- this.#assertOwner("delete");
558
+ this.#assertCanWrite("delete", state.name);
525
559
  await this.#call("/mcp/primitives/write", "aithos.data.delete_record", {
526
560
  collection_urn: state.urn,
527
561
  record_id: recordId,
@@ -757,7 +791,7 @@ class DataClientImpl {
757
791
  if (!this.#deposit) {
758
792
  throw new Error("sdk.data: _insertDeposit called without a deposit session");
759
793
  }
760
- const { metadata, payload } = splitRecord(record, state.schema);
794
+ const { metadata, payload } = splitRecord(record, requireSchema(state));
761
795
  const recordId = `record_${makeUlid()}`;
762
796
  const encrypted = this.#encryptPayloadForOwner({
763
797
  collectionName: state.name,
@@ -777,7 +811,7 @@ class DataClientImpl {
777
811
  /** Build a local collection state for the deposit path (no server fetch:
778
812
  * append clients are not authorized to read collection metadata). */
779
813
  _depositCollectionState(name, schema) {
780
- return { name, urn: this.#collectionUrn(name), schema };
814
+ return { name, urn: this.#collectionUrn(name), schema, schemaId: schema.schema };
781
815
  }
782
816
  }
783
817
  class DataCollectionImpl {
@@ -848,6 +882,21 @@ export function liteFromPublishedSchema(doc) {
848
882
  defaults: {},
849
883
  };
850
884
  }
885
+ /**
886
+ * Return the collection's write schema or throw a precise error. Writes need
887
+ * the schema to split a record into indexable metadata vs encrypted payload (and
888
+ * so the PDS can validate); reads never call this.
889
+ */
890
+ function requireSchema(state) {
891
+ if (!state.schema) {
892
+ const e = new Error(`sdk.data: writing to "${state.name}" needs its schema "${state.schemaId}", which is ` +
893
+ `neither bundled in this client (createDataClient({ schemas: [...] })) nor published on ` +
894
+ `the PDS (registerSchema). Reads work without it — only inserts/updates require it.`);
895
+ e.code = -32602;
896
+ throw e;
897
+ }
898
+ return state.schema;
899
+ }
851
900
  function splitRecord(record, schema) {
852
901
  const metadata = {};
853
902
  const payload = {};
@@ -1,12 +1,17 @@
1
- export declare const VERSION = "0.1.0-alpha.55";
1
+ export declare const VERSION = "0.1.0-alpha.57";
2
2
  export { AithosSDK } from "./sdk.js";
3
3
  export type { AithosSDKConfig } from "./types.js";
4
4
  export { AithosSDKError } from "./types.js";
5
5
  export { AithosRpcError } from "@aithos/protocol-client";
6
6
  export type { AithosSdkEndpoints } from "./endpoints.js";
7
7
  export { DEFAULT_SDK_ENDPOINTS } from "./endpoints.js";
8
- export type { ComputeMessage, ImageAspectRatio, ImageModelId, InvokeBedrockArgs, InvokeBedrockResult, InvokeBedrockVisionArgs, InvokeBedrockVisionResult, InvokeImageArgs, InvokeImageImage, InvokeImageResult, InvokeSegmentationArgs, InvokeSegmentationResult, SegmentPolygon, StopReason, RunConversationArgs, RunConversationResult, ComputeWorkingSet, WorkingSetSection, ConverseToolCall, ConverseStopReason, TranscribeModelId, TranscribeProgressState, TranscribeSegment, TranscribeWord, InvokeTranscribeArgs, InvokeTranscribeResult, PrepareTranscribeArgs, PrepareTranscribeResult, StartTranscribeArgs, StartTranscribeResult, TranscribeStatusResult, TranscribeJobSummary, } from "./compute.js";
8
+ export type { ComputeMessage, ImageAspectRatio, ImageModelId, InvokeBedrockArgs, InvokeBedrockResult, InvokeBedrockVisionArgs, InvokeBedrockVisionResult, InvokeImageArgs, InvokeImageImage, InvokeImageResult, InvokeSegmentationArgs, InvokeSegmentationResult, SegmentPolygon, StopReason, RunConversationArgs, RunConversationResult, ComputeWorkingSet, WorkingSetSection, ConverseToolCall, ConverseStopReason, InvokeTurnArgs, InvokeTurnResult, RunConversationLocalArgs, RunConversationLocalResult, TranscribeModelId, TranscribeProgressState, TranscribeSegment, TranscribeWord, InvokeTranscribeArgs, InvokeTranscribeResult, PrepareTranscribeArgs, PrepareTranscribeResult, StartTranscribeArgs, StartTranscribeResult, TranscribeStatusResult, TranscribeJobSummary, } from "./compute.js";
9
9
  export { ComputeNamespace } from "./compute.js";
10
+ export type { AgentMessage, AgentToolSpec, AgentTurnResult, ContentBlock, ToolCallTrace, LoopStopReason, DispatchOutcome, } from "./agent-loop.js";
11
+ export { runAgenticLoopLocal } from "./agent-loop.js";
12
+ export { AITHOS_AGENT_TOOLS, AITHOS_AGENT_READ_TOOLS, AITHOS_AGENT_WRITE_TOOLS, selectAgentTools, isWriteTool, } from "./agent-tools.js";
13
+ export type { AgentDispatchContext, DataProvider } from "./agent-dispatch.js";
14
+ export { dispatchAgentToolLocal } from "./agent-dispatch.js";
10
15
  export type { LocalPendingEntry, LocalPendingStatus, TranscribeDraftMeta, TranscribeDraftRecord, } from "./transcribe-resilience.js";
11
16
  export { LocalPendingTranscribeTracker, TranscribeDraftStore, TranscribeDraftUnavailableError, } from "./transcribe-resilience.js";
12
17
  export type { CreditPackId, CreateTopupSessionArgs, CreateTopupSessionResult, GetBalanceArgs, GetBalanceResult, } from "./wallet.js";
@@ -27,7 +32,7 @@ export type { AudienceSet, AppCreditPackId, CreateAppTopupSessionArgs, CreateApp
27
32
  export * as onboarding from "./onboarding.js";
28
33
  export { createBrowserIdentity, browserIdentityFromStored, type BrowserIdentity, } from "@aithos/protocol-client";
29
34
  export type { Section } from "@aithos/protocol-client";
30
- export { createDataClient, createDelegateDataClient, createAppendDataClient, type CreateDataClientArgs, type CreateDelegateDataClientArgs, type CreateAppendDataClientArgs, type DataClient, type DataCollection, type ReadonlyDataClient, type ReadonlyDataCollection, type AppendOnlyDataClient, type AppendOnlyDataCollection, type ListOpts, type AithosSchemaLite, liteFromPublishedSchema, } from "./data.js";
35
+ export { createAppendDataClient, type CreateAppendDataClientArgs, type DataClient, type DataCollection, type ReadonlyDataClient, type ReadonlyDataCollection, type AppendOnlyDataClient, type AppendOnlyDataCollection, type ListOpts, type AithosSchemaLite, liteFromPublishedSchema, } from "./data.js";
31
36
  export { ensureDataSphere, rekeyLegacyCollections, addDataSphereWrap, migrateLegacyEthosToDataSphere, type MigrateOptions, type MigrateProgress, type EnsureDataSphereResult, type RekeyReport, type CollectionRekeyEntry, type CollectionRekeyStatus, type FullMigrationResult, } from "./migrate.js";
32
37
  export { buildSignedRotatedDidDocument, rotateEthos, type RotationSeeds, type DidDocumentLike, type RotateEthosOptions, type RotateEthosResult, } from "./rotate.js";
33
38
  export { createAssetsClient, AssetsClient, type CreateAssetsClientArgs, type AttachedContext, type AssetUploadInput, type AssetUploadResult, type AssetFetchResult, type AssetBrief, type ListAssetsOpts, type ThumbnailUploadInput, type ThumbnailUploadResult, type RecipientResolver, type RecipientSet, } from "./assets.js";
package/dist/src/index.js CHANGED
@@ -17,7 +17,7 @@
17
17
  // Public types specific to the SDK (`AithosSDKConfig`, `AithosSDKError`)
18
18
  // are exported from here. Endpoint config (`AithosSdkEndpoints`,
19
19
  // `DEFAULT_SDK_ENDPOINTS`) likewise.
20
- export const VERSION = "0.1.0-alpha.55";
20
+ export const VERSION = "0.1.0-alpha.57";
21
21
  export { AithosSDK } from "./sdk.js";
22
22
  export { AithosSDKError } from "./types.js";
23
23
  // Re-export protocol-client's JSON-RPC error type so consumers can
@@ -26,6 +26,9 @@ export { AithosSDKError } from "./types.js";
26
26
  export { AithosRpcError } from "@aithos/protocol-client";
27
27
  export { DEFAULT_SDK_ENDPOINTS } from "./endpoints.js";
28
28
  export { ComputeNamespace } from "./compute.js";
29
+ export { runAgenticLoopLocal } from "./agent-loop.js";
30
+ export { AITHOS_AGENT_TOOLS, AITHOS_AGENT_READ_TOOLS, AITHOS_AGENT_WRITE_TOOLS, selectAgentTools, isWriteTool, } from "./agent-tools.js";
31
+ export { dispatchAgentToolLocal } from "./agent-dispatch.js";
29
32
  export { LocalPendingTranscribeTracker, TranscribeDraftStore, TranscribeDraftUnavailableError, } from "./transcribe-resilience.js";
30
33
  export { WalletNamespace } from "./wallet.js";
31
34
  export { WebNamespace, WEB_EXTRACT_SCOPE } from "./web.js";
@@ -68,7 +71,14 @@ export { createBrowserIdentity, browserIdentityFromStored, } from "@aithos/proto
68
71
  // `sdk.data` namespace — Aithos data sub-protocol PDS client. Manages
69
72
  // the lifecycle of subject-owned, encrypted, schema-validated records.
70
73
  // See spec/data/ in the aithos-protocol repo.
71
- export { createDataClient, createDelegateDataClient, createAppendDataClient,
74
+ // The low-level `createDataClient` (owner) and `createDelegateDataClient`
75
+ // (delegate) factories are intentionally NOT exported: they take a raw sphere
76
+ // seed, which is exactly the footgun that let apps sign data ops with the wrong
77
+ // key. Use the session instead — `auth.data` / `auth.ownerDataClient()` for the
78
+ // owner (always `#data`), `auth.delegateDataClient()` for a mandate-bearing
79
+ // delegate. The key/sphere is derived under the hood; the developer only ever
80
+ // sees a database.
81
+ export { createAppendDataClient,
72
82
  // Derive an AithosSchemaLite from a published JSON Schema (vendor schemas the
73
83
  // client didn't bundle). The SDK uses this internally to auto-resolve unknown
74
84
  // collection schemas from the PDS; exported for apps that want it directly.
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=agent-dispatch.test.d.ts.map
@@ -0,0 +1,222 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright 2026 Mathieu Colla
3
+ // Tests for the CLIENT-SIDE local tool dispatch: read navigation, write
4
+ // staging + publish, and the ethos.write.* authorisation gate. The EthosClient
5
+ // is faked (no network) — we assert on what got staged/published and on the
6
+ // is_error outcomes for out-of-scope or malformed calls.
7
+ import { strict as assert } from "node:assert";
8
+ import { describe, it } from "node:test";
9
+ import { dispatchAgentToolLocal, AithosSDKError, } from "../src/index.js";
10
+ /**
11
+ * Build a fake EthosClient. `zones` maps zone→sections (a zone absent from the
12
+ * map is treated as unreadable and throws like an ungranted/undecryptable
13
+ * zone, exercising the dispatch's skip-on-error path).
14
+ */
15
+ function fakeEthos(mode, zones) {
16
+ const state = { staged: [], publishes: 0 };
17
+ const client = {
18
+ mode,
19
+ subjectDid: "did:aithos:subject",
20
+ zone(name) {
21
+ return {
22
+ async sections() {
23
+ const s = zones[name];
24
+ if (!s) {
25
+ throw new AithosSDKError("ethos_zone_unreadable", `cannot read ${name}`);
26
+ }
27
+ return s;
28
+ },
29
+ addSection(input) {
30
+ state.staged.push({ op: "add", zone: name, arg: input });
31
+ },
32
+ updateSection(id, patch) {
33
+ state.staged.push({ op: "update", zone: name, arg: { id, patch } });
34
+ },
35
+ deleteSection(id) {
36
+ state.staged.push({ op: "delete", zone: name, arg: { id } });
37
+ },
38
+ };
39
+ },
40
+ async publish() {
41
+ state.publishes++;
42
+ return {
43
+ editionHeight: 2,
44
+ manifestHash: "",
45
+ subjectDid: "did:aithos:subject",
46
+ zonesPublished: [],
47
+ };
48
+ },
49
+ };
50
+ return { client, state };
51
+ }
52
+ function ownerCtx(zones) {
53
+ const { client, state } = fakeEthos("owner", zones);
54
+ return { ctx: { ethos: client, delegateScopes: [] }, state };
55
+ }
56
+ function delegateCtx(zones, scopes) {
57
+ const { client, state } = fakeEthos("delegate", zones);
58
+ return { ctx: { ethos: client, delegateScopes: scopes }, state };
59
+ }
60
+ const parse = (o) => JSON.parse(o.payload);
61
+ describe("dispatch — reads", () => {
62
+ it("ethos_list_sections aggregates readable zones (skips unreadable)", async () => {
63
+ const { ctx } = ownerCtx({
64
+ public: [{ id: "p1", title: "Bio", body: "..." }],
65
+ // circle absent → unreadable → skipped
66
+ self: [{ id: "s1", title: "Secret", body: "..." }],
67
+ });
68
+ const out = await dispatchAgentToolLocal(ctx, "ethos_list_sections", {});
69
+ assert.equal(out.isError, false);
70
+ const { sections } = parse(out);
71
+ assert.deepEqual(sections.sort((a, b) => a.id.localeCompare(b.id)), [
72
+ { zone: "public", id: "p1", title: "Bio" },
73
+ { zone: "self", id: "s1", title: "Secret" },
74
+ ]);
75
+ });
76
+ it("ethos_read_section returns body for a readable section", async () => {
77
+ const { ctx } = ownerCtx({ public: [{ id: "p1", title: "Bio", body: "hello" }] });
78
+ const out = await dispatchAgentToolLocal(ctx, "ethos_read_section", { section_id: "p1" });
79
+ assert.equal(out.isError, false);
80
+ assert.deepEqual(parse(out), { zone: "public", title: "Bio", body: "hello" });
81
+ });
82
+ it("ethos_read_section is_error for unknown id", async () => {
83
+ const { ctx } = ownerCtx({ public: [] });
84
+ const out = await dispatchAgentToolLocal(ctx, "ethos_read_section", { section_id: "nope" });
85
+ assert.equal(out.isError, true);
86
+ });
87
+ it("data_query is_error without a data provider", async () => {
88
+ const { ctx } = ownerCtx({ public: [] });
89
+ const out = await dispatchAgentToolLocal(ctx, "data_query", { collection: "contacts" });
90
+ assert.equal(out.isError, true);
91
+ });
92
+ it("data_query uses the provider when present (limit clamped)", async () => {
93
+ const { client } = fakeEthos("owner", { public: [] });
94
+ let seenLimit = -1;
95
+ const ctx = {
96
+ ethos: client,
97
+ delegateScopes: [],
98
+ dataProvider: async (_c, limit) => {
99
+ seenLimit = limit;
100
+ return [{ a: 1 }];
101
+ },
102
+ };
103
+ const out = await dispatchAgentToolLocal(ctx, "data_query", { collection: "c", limit: 999 });
104
+ assert.equal(out.isError, false);
105
+ assert.equal(seenLimit, 100); // clamped to max
106
+ assert.deepEqual(parse(out).records, [{ a: 1 }]);
107
+ });
108
+ });
109
+ describe("dispatch — writes (owner)", () => {
110
+ it("ethos_add_section stages + publishes", async () => {
111
+ const { ctx, state } = ownerCtx({ public: [] });
112
+ const out = await dispatchAgentToolLocal(ctx, "ethos_add_section", {
113
+ zone: "public",
114
+ title: "New",
115
+ body: "content",
116
+ });
117
+ assert.equal(out.isError, false);
118
+ assert.equal(parse(out).published, true);
119
+ assert.equal(state.publishes, 1);
120
+ assert.deepEqual(state.staged, [
121
+ { op: "add", zone: "public", arg: { title: "New", body: "content" } },
122
+ ]);
123
+ });
124
+ it("ethos_update_section locates the zone then publishes", async () => {
125
+ const { ctx, state } = ownerCtx({
126
+ circle: [{ id: "c1", title: "Old", body: "x" }],
127
+ });
128
+ const out = await dispatchAgentToolLocal(ctx, "ethos_update_section", {
129
+ section_id: "c1",
130
+ body: "new body",
131
+ });
132
+ assert.equal(out.isError, false);
133
+ assert.equal(parse(out).zone, "circle");
134
+ assert.equal(state.publishes, 1);
135
+ assert.deepEqual(state.staged[0], {
136
+ op: "update",
137
+ zone: "circle",
138
+ arg: { id: "c1", patch: { body: "new body" } },
139
+ });
140
+ });
141
+ it("ethos_delete_section locates the zone then publishes", async () => {
142
+ const { ctx, state } = ownerCtx({ self: [{ id: "s9", title: "T", body: "B" }] });
143
+ const out = await dispatchAgentToolLocal(ctx, "ethos_delete_section", { section_id: "s9" });
144
+ assert.equal(out.isError, false);
145
+ assert.equal(parse(out).zone, "self");
146
+ assert.equal(state.publishes, 1);
147
+ });
148
+ it("invalid zone / missing fields → is_error, nothing published", async () => {
149
+ const { ctx, state } = ownerCtx({ public: [] });
150
+ const bad = await dispatchAgentToolLocal(ctx, "ethos_add_section", {
151
+ zone: "nope",
152
+ title: "x",
153
+ body: "y",
154
+ });
155
+ assert.equal(bad.isError, true);
156
+ const noTitle = await dispatchAgentToolLocal(ctx, "ethos_add_section", {
157
+ zone: "public",
158
+ body: "y",
159
+ });
160
+ assert.equal(noTitle.isError, true);
161
+ assert.equal(state.publishes, 0);
162
+ });
163
+ it("update with nothing to change → is_error", async () => {
164
+ const { ctx } = ownerCtx({ public: [{ id: "p1", title: "T", body: "B" }] });
165
+ const out = await dispatchAgentToolLocal(ctx, "ethos_update_section", { section_id: "p1" });
166
+ assert.equal(out.isError, true);
167
+ });
168
+ });
169
+ describe("dispatch — write authorisation gate (delegate)", () => {
170
+ it("publishes when the mandate grants ethos.write.<zone>", async () => {
171
+ const { ctx, state } = delegateCtx({ public: [] }, ["ethos.write.public"]);
172
+ const out = await dispatchAgentToolLocal(ctx, "ethos_add_section", {
173
+ zone: "public",
174
+ title: "T",
175
+ body: "B",
176
+ });
177
+ assert.equal(out.isError, false);
178
+ assert.equal(state.publishes, 1);
179
+ });
180
+ it("refuses (is_error, no publish) when the write scope is missing", async () => {
181
+ const { ctx, state } = delegateCtx({ self: [] }, ["ethos.write.public"]);
182
+ const out = await dispatchAgentToolLocal(ctx, "ethos_add_section", {
183
+ zone: "self",
184
+ title: "T",
185
+ body: "B",
186
+ });
187
+ assert.equal(out.isError, true);
188
+ assert.match(parse(out).error, /ethos\.write\.self/);
189
+ assert.equal(state.publishes, 0);
190
+ assert.equal(state.staged.length, 0);
191
+ });
192
+ it("update refuses when write scope for the located zone is missing", async () => {
193
+ const { ctx, state } = delegateCtx({ circle: [{ id: "c1", title: "T", body: "B" }] }, ["ethos.write.public"]);
194
+ const out = await dispatchAgentToolLocal(ctx, "ethos_update_section", {
195
+ section_id: "c1",
196
+ body: "new",
197
+ });
198
+ assert.equal(out.isError, true);
199
+ assert.equal(state.publishes, 0);
200
+ });
201
+ });
202
+ describe("dispatch — anonymous cannot write", () => {
203
+ it("ethos_add_section is_error for anonymous", async () => {
204
+ const { client, state } = fakeEthos("anonymous", { public: [] });
205
+ const ctx = { ethos: client, delegateScopes: [] };
206
+ const out = await dispatchAgentToolLocal(ctx, "ethos_add_section", {
207
+ zone: "public",
208
+ title: "T",
209
+ body: "B",
210
+ });
211
+ assert.equal(out.isError, true);
212
+ assert.equal(state.publishes, 0);
213
+ });
214
+ });
215
+ describe("dispatch — unknown tool", () => {
216
+ it("returns is_error for an unknown tool name", async () => {
217
+ const { ctx } = ownerCtx({ public: [] });
218
+ const out = await dispatchAgentToolLocal(ctx, "bogus_tool", {});
219
+ assert.equal(out.isError, true);
220
+ });
221
+ });
222
+ //# sourceMappingURL=agent-dispatch.test.js.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=agent-loop.test.d.ts.map