@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/agent-dispatch.d.ts +18 -0
- package/dist/src/agent-dispatch.js +178 -0
- package/dist/src/agent-loop.d.ts +94 -0
- package/dist/src/agent-loop.js +95 -0
- package/dist/src/agent-tools.d.ts +24 -0
- package/dist/src/agent-tools.js +147 -0
- package/dist/src/auth.d.ts +59 -0
- package/dist/src/auth.js +121 -0
- package/dist/src/compute.d.ts +112 -0
- package/dist/src/compute.js +175 -0
- package/dist/src/data.d.ts +14 -9
- package/dist/src/data.js +102 -53
- package/dist/src/index.d.ts +8 -3
- package/dist/src/index.js +12 -2
- package/dist/test/agent-dispatch.test.d.ts +2 -0
- package/dist/test/agent-dispatch.test.js +222 -0
- package/dist/test/agent-loop.test.d.ts +2 -0
- package/dist/test/agent-loop.test.js +117 -0
- package/dist/test/agent-tools.test.d.ts +2 -0
- package/dist/test/agent-tools.test.js +50 -0
- package/dist/test/invoke-turn-sdk.test.d.ts +2 -0
- package/dist/test/invoke-turn-sdk.test.js +177 -0
- package/dist/test/owner-data-client.test.d.ts +2 -0
- package/dist/test/owner-data-client.test.js +88 -0
- package/dist/test/schema-autoresolve.test.js +14 -4
- package/package.json +1 -1
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` —
|
|
4
|
+
* `sdk.data` — the Aithos data sub-protocol PDS as a plain database.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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
|
-
*
|
|
12
|
-
* const
|
|
13
|
-
* const id = await contacts.insert({ name: "Jean"
|
|
14
|
-
* const
|
|
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
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
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
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
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
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
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
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
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
|
-
*
|
|
72
|
-
*
|
|
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
|
-
/**
|
|
175
|
-
*
|
|
176
|
-
*
|
|
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
|
|
180
|
-
`
|
|
181
|
-
`
|
|
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 —
|
|
458
|
+
// network / not-found — leave `schema` undefined (reads still work)
|
|
423
459
|
}
|
|
424
460
|
}
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
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.#
|
|
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
|
|
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.#
|
|
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
|
|
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.#
|
|
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
|
|
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 = {};
|
package/dist/src/index.d.ts
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
|
-
export declare const VERSION = "0.1.0-alpha.
|
|
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 {
|
|
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.
|
|
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
|
-
|
|
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,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
|