@aithos/sdk 0.1.0-alpha.56 → 0.1.0-alpha.59

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
  }
@@ -431,7 +467,7 @@ class DataClientImpl {
431
467
  return state;
432
468
  }
433
469
  async _insert(state, record) {
434
- this.#assertOwner("insert");
470
+ this.#assertCanWrite("insert", state.name);
435
471
  const cmk = this.#cmkCache.get(state.name);
436
472
  if (!cmk)
437
473
  throw new Error("CMK not loaded");
@@ -500,7 +536,7 @@ class DataClientImpl {
500
536
  };
501
537
  }
502
538
  async _update(state, recordId, record) {
503
- this.#assertOwner("update");
539
+ this.#assertCanWrite("update", state.name);
504
540
  const cmk = this.#cmkCache.get(state.name);
505
541
  if (!cmk)
506
542
  throw new Error("CMK not loaded");
@@ -519,7 +555,7 @@ class DataClientImpl {
519
555
  });
520
556
  }
521
557
  async _delete(state, recordId) {
522
- this.#assertOwner("delete");
558
+ this.#assertCanWrite("delete", state.name);
523
559
  await this.#call("/mcp/primitives/write", "aithos.data.delete_record", {
524
560
  collection_urn: state.urn,
525
561
  record_id: recordId,
@@ -33,9 +33,27 @@ export interface AithosSdkEndpoints {
33
33
  * `createAssetsClient`. Override for self-hosting or staging.
34
34
  */
35
35
  readonly assets: string;
36
+ /**
37
+ * Protocol API base URL (ethos reads/writes — `get_ethos_manifest`,
38
+ * `get_ethos_section`, `publish_ethos_edition`). Default `https://api.aithos.be`.
39
+ * Reached through `@aithos/protocol-client`; the SDK propagates this to the
40
+ * client via `configureEndpoints` on construction.
41
+ */
42
+ readonly api: string;
43
+ /**
44
+ * CDN base URL for immutable signed editions / DID documents. Default
45
+ * `https://cdn.aithos.be`. Also propagated to protocol-client.
46
+ */
47
+ readonly cdn: string;
36
48
  }
37
49
  /** Production defaults. */
38
50
  export declare const DEFAULT_SDK_ENDPOINTS: AithosSdkEndpoints;
51
+ /**
52
+ * Dev-account preset — every endpoint on the isolated `*.dev.aithos.be` infra.
53
+ * Pass to the SDK as `new AithosSDK({ ..., endpoints: DEV_SDK_ENDPOINTS })` to
54
+ * point an app at the dev account (api/cdn flow to protocol-client too).
55
+ */
56
+ export declare const DEV_SDK_ENDPOINTS: AithosSdkEndpoints;
39
57
  /** Compose the full compute-invoke URL: `${compute}/v1/invoke`. */
40
58
  export declare function computeInvokeUrl(endpoints: AithosSdkEndpoints): string;
41
59
  /** Compose the full web-extract URL: `${web}/v1/invoke`. */
@@ -7,6 +7,22 @@ export const DEFAULT_SDK_ENDPOINTS = {
7
7
  web: "https://extract.aithos.be",
8
8
  pds: "https://pds.aithos.be",
9
9
  assets: "https://assets.aithos.be",
10
+ api: "https://api.aithos.be",
11
+ cdn: "https://cdn.aithos.be",
12
+ };
13
+ /**
14
+ * Dev-account preset — every endpoint on the isolated `*.dev.aithos.be` infra.
15
+ * Pass to the SDK as `new AithosSDK({ ..., endpoints: DEV_SDK_ENDPOINTS })` to
16
+ * point an app at the dev account (api/cdn flow to protocol-client too).
17
+ */
18
+ export const DEV_SDK_ENDPOINTS = {
19
+ compute: "https://compute.dev.aithos.be",
20
+ wallet: "https://wallet.dev.aithos.be",
21
+ web: "https://extract.dev.aithos.be",
22
+ pds: "https://pds.dev.aithos.be",
23
+ assets: "https://assets.dev.aithos.be",
24
+ api: "https://api.dev.aithos.be",
25
+ cdn: "https://cdn.dev.aithos.be",
10
26
  };
11
27
  /** Compose the full compute-invoke URL: `${compute}/v1/invoke`. */
12
28
  export function computeInvokeUrl(endpoints) {
@@ -1,12 +1,17 @@
1
- export declare const VERSION = "0.1.0-alpha.56";
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
- 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";
7
+ export { DEFAULT_SDK_ENDPOINTS, DEV_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, 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,15 +17,18 @@
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.56";
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
24
24
  // `instanceof`-check server-side errors and inspect the JSON-RPC code
25
25
  // without taking a direct dependency on @aithos/protocol-client.
26
26
  export { AithosRpcError } from "@aithos/protocol-client";
27
- export { DEFAULT_SDK_ENDPOINTS } from "./endpoints.js";
27
+ export { DEFAULT_SDK_ENDPOINTS, DEV_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.
package/dist/src/sdk.js CHANGED
@@ -3,6 +3,7 @@
3
3
  import { AppsNamespace } from "./apps.js";
4
4
  import { ComputeNamespace, } from "./compute.js";
5
5
  import { resolveEndpoints } from "./endpoints.js";
6
+ import { configureEndpoints as configureProtocolClientEndpoints } from "@aithos/protocol-client";
6
7
  import { EthosNamespace } from "./ethos.js";
7
8
  import { MandatesNamespace } from "./mandates.js";
8
9
  import { AithosSDKError } from "./types.js";
@@ -41,6 +42,10 @@ export class AithosSDK {
41
42
  this.endpoints = resolveEndpoints(config.endpoints);
42
43
  this.appDid = config.appDid;
43
44
  this.auth = config.auth;
45
+ // Ethos reads/writes flow through @aithos/protocol-client, which keeps its
46
+ // own (module-scoped) api/cdn config. Propagate ours so that pointing the
47
+ // SDK at a dev account (endpoints.api/cdn) also redirects the ethos calls.
48
+ configureProtocolClientEndpoints({ api: this.endpoints.api, cdn: this.endpoints.cdn });
44
49
  const fetchImpl = config.fetch ?? globalThis.fetch.bind(globalThis);
45
50
  this.compute = new ComputeNamespace({
46
51
  auth: config.auth,
@@ -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