@aithos/sdk 0.1.0-alpha.6 → 0.1.0-alpha.60

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.
Files changed (105) hide show
  1. package/README.md +202 -7
  2. package/dist/src/agent-dispatch.d.ts +18 -0
  3. package/dist/src/agent-dispatch.js +178 -0
  4. package/dist/src/agent-loop.d.ts +94 -0
  5. package/dist/src/agent-loop.js +95 -0
  6. package/dist/src/agent-tools.d.ts +24 -0
  7. package/dist/src/agent-tools.js +147 -0
  8. package/dist/src/apps.d.ts +224 -0
  9. package/dist/src/apps.js +432 -0
  10. package/dist/src/assets.d.ts +225 -0
  11. package/dist/src/assets.js +534 -0
  12. package/dist/src/auth-api.d.ts +219 -0
  13. package/dist/src/auth-api.js +248 -0
  14. package/dist/src/auth.d.ts +591 -0
  15. package/dist/src/auth.js +947 -31
  16. package/dist/src/compute.d.ts +674 -6
  17. package/dist/src/compute.js +968 -20
  18. package/dist/src/data-schema-contacts-v1.d.ts +14 -0
  19. package/dist/src/data-schema-contacts-v1.js +28 -0
  20. package/dist/src/data.d.ts +368 -0
  21. package/dist/src/data.js +1124 -0
  22. package/dist/src/endpoints.d.ts +43 -0
  23. package/dist/src/endpoints.js +23 -0
  24. package/dist/src/ethos.d.ts +85 -0
  25. package/dist/src/ethos.js +463 -7
  26. package/dist/src/index.d.ts +22 -4
  27. package/dist/src/index.js +47 -2
  28. package/dist/src/internal/cmk-wrap.d.ts +41 -0
  29. package/dist/src/internal/cmk-wrap.js +132 -0
  30. package/dist/src/internal/delegate-bundle.js +7 -2
  31. package/dist/src/internal/envelope.d.ts +93 -0
  32. package/dist/src/internal/envelope.js +59 -0
  33. package/dist/src/internal/owner-signers.d.ts +5 -2
  34. package/dist/src/internal/owner-signers.js +22 -1
  35. package/dist/src/internal/recovery-file.d.ts +2 -0
  36. package/dist/src/internal/recovery-file.js +7 -0
  37. package/dist/src/key-store.d.ts +10 -0
  38. package/dist/src/key-store.js +6 -0
  39. package/dist/src/mandates.d.ts +58 -1
  40. package/dist/src/mandates.js +46 -3
  41. package/dist/src/migrate.d.ts +105 -0
  42. package/dist/src/migrate.js +367 -0
  43. package/dist/src/react/AithosAsset.d.ts +66 -0
  44. package/dist/src/react/AithosAsset.js +67 -0
  45. package/dist/src/react/context.d.ts +29 -0
  46. package/dist/src/react/context.js +31 -0
  47. package/dist/src/react/index.d.ts +29 -0
  48. package/dist/src/react/index.js +31 -0
  49. package/dist/src/react/use-aithos-asset.d.ts +39 -0
  50. package/dist/src/react/use-aithos-asset.js +118 -0
  51. package/dist/src/react/use-transcribe-pending.d.ts +21 -0
  52. package/dist/src/react/use-transcribe-pending.js +47 -0
  53. package/dist/src/rotate.d.ts +94 -0
  54. package/dist/src/rotate.js +298 -0
  55. package/dist/src/sdk.d.ts +36 -2
  56. package/dist/src/sdk.js +72 -1
  57. package/dist/src/transcribe-resilience.d.ts +57 -0
  58. package/dist/src/transcribe-resilience.js +203 -0
  59. package/dist/src/web.d.ts +279 -0
  60. package/dist/src/web.js +186 -0
  61. package/dist/test/agent-dispatch.test.d.ts +2 -0
  62. package/dist/test/agent-dispatch.test.js +222 -0
  63. package/dist/test/agent-loop.test.d.ts +2 -0
  64. package/dist/test/agent-loop.test.js +117 -0
  65. package/dist/test/agent-tools.test.d.ts +2 -0
  66. package/dist/test/agent-tools.test.js +50 -0
  67. package/dist/test/auth-j3.test.js +32 -1
  68. package/dist/test/canonical-conformance.test.d.ts +2 -0
  69. package/dist/test/canonical-conformance.test.js +86 -0
  70. package/dist/test/compute-delegate-path.test.d.ts +2 -0
  71. package/dist/test/compute-delegate-path.test.js +183 -0
  72. package/dist/test/compute.test.js +4 -0
  73. package/dist/test/converse.test.d.ts +2 -0
  74. package/dist/test/converse.test.js +162 -0
  75. package/dist/test/data-sphere.test.d.ts +2 -0
  76. package/dist/test/data-sphere.test.js +57 -0
  77. package/dist/test/endpoints.test.js +40 -1
  78. package/dist/test/envelope-core-conformance.test.d.ts +2 -0
  79. package/dist/test/envelope-core-conformance.test.js +75 -0
  80. package/dist/test/envelope.test.d.ts +2 -0
  81. package/dist/test/envelope.test.js +318 -0
  82. package/dist/test/ethos-first-edition.test.d.ts +2 -0
  83. package/dist/test/ethos-first-edition.test.js +371 -0
  84. package/dist/test/invoke-turn-sdk.test.d.ts +2 -0
  85. package/dist/test/invoke-turn-sdk.test.js +177 -0
  86. package/dist/test/migrate.test.d.ts +2 -0
  87. package/dist/test/migrate.test.js +340 -0
  88. package/dist/test/owner-data-client.test.d.ts +2 -0
  89. package/dist/test/owner-data-client.test.js +88 -0
  90. package/dist/test/rotate-ethos.test.d.ts +2 -0
  91. package/dist/test/rotate-ethos.test.js +151 -0
  92. package/dist/test/rotate.test.d.ts +2 -0
  93. package/dist/test/rotate.test.js +63 -0
  94. package/dist/test/schema-autoresolve.test.d.ts +2 -0
  95. package/dist/test/schema-autoresolve.test.js +146 -0
  96. package/dist/test/sdk.test.js +11 -2
  97. package/dist/test/signup-bootstrap.test.d.ts +2 -0
  98. package/dist/test/signup-bootstrap.test.js +311 -0
  99. package/dist/test/transcribe-invoke.test.d.ts +2 -0
  100. package/dist/test/transcribe-invoke.test.js +204 -0
  101. package/dist/test/transcribe.test.d.ts +2 -0
  102. package/dist/test/transcribe.test.js +186 -0
  103. package/dist/test/web.test.d.ts +2 -0
  104. package/dist/test/web.test.js +270 -0
  105. package/package.json +20 -3
@@ -0,0 +1,371 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright 2026 Mathieu Colla
3
+ // Tests for the alpha.7 first-edition path in EthosClient — the case
4
+ // where an Ethos identity exists on api.aithos.be (provisioned by
5
+ // auth.signUp() since alpha.6) but no edition has been published yet.
6
+ //
7
+ // Two flows must work:
8
+ // 1. Reading any zone returns an empty list (instead of throwing
9
+ // "not found: edition").
10
+ // 2. Publishing for the first time builds height=1 from staged
11
+ // mutations and POSTs publish_ethos_edition directly, instead
12
+ // of going through publishZoneEdit (which requires a previous
13
+ // manifest).
14
+ //
15
+ // We mock global fetch end-to-end so the tests run offline.
16
+ import { strict as assert } from "node:assert";
17
+ import { afterEach, beforeEach, describe, it } from "node:test";
18
+ import { createBrowserIdentity } from "@aithos/protocol-client";
19
+ import { AithosAuth, AithosSDKError, EthosNamespace, memoryKeyStore, noopStore, } from "../src/index.js";
20
+ import { serializeRecoveryFile } from "../src/internal/recovery-file.js";
21
+ import { DEFAULT_SDK_ENDPOINTS } from "../src/endpoints.js";
22
+ let fetchCalls = [];
23
+ let savedFetch;
24
+ function installFetchMock(handlers) {
25
+ savedFetch = globalThis.fetch;
26
+ fetchCalls = [];
27
+ globalThis.fetch = (async (input, init) => {
28
+ const url = String(input);
29
+ const method = init?.method ?? "GET";
30
+ const bodyText = typeof init?.body === "string"
31
+ ? init.body
32
+ : init?.body == null
33
+ ? null
34
+ : String(init.body);
35
+ const body = bodyText ? JSON.parse(bodyText) : null;
36
+ const call = { url, method, body };
37
+ fetchCalls.push(call);
38
+ for (const h of handlers) {
39
+ if (!url.includes(h.url))
40
+ continue;
41
+ if (h.rpcMethod && body?.method !== h.rpcMethod)
42
+ continue;
43
+ const out = h.respond(call);
44
+ const status = out.status ?? 200;
45
+ return new Response(JSON.stringify(out.json), {
46
+ status,
47
+ headers: { "content-type": "application/json" },
48
+ });
49
+ }
50
+ throw new Error(`unhandled fetch: ${method} ${url} (rpc: ${body?.method ?? "n/a"})`);
51
+ });
52
+ }
53
+ function uninstallFetchMock() {
54
+ if (savedFetch) {
55
+ globalThis.fetch = savedFetch;
56
+ savedFetch = undefined;
57
+ }
58
+ fetchCalls = [];
59
+ }
60
+ function makeAuth() {
61
+ return new AithosAuth({
62
+ authBaseUrl: "https://auth.test",
63
+ apiBaseUrl: "https://api.test",
64
+ fetch: (() => {
65
+ throw new Error("AithosAuth.fetch must not be called in these tests");
66
+ }),
67
+ sessionStore: noopStore(),
68
+ keyStore: memoryKeyStore(),
69
+ });
70
+ }
71
+ function makeNamespace(auth) {
72
+ return new EthosNamespace({
73
+ auth,
74
+ endpoints: DEFAULT_SDK_ENDPOINTS,
75
+ // EthosNamespace itself doesn't read fetch from this slot today;
76
+ // protocol-client uses the global fetch we mock above.
77
+ fetch: globalThis.fetch.bind(globalThis),
78
+ });
79
+ }
80
+ async function signInAsAlice(auth) {
81
+ const id = createBrowserIdentity("alice", "Alice");
82
+ const { text } = serializeRecoveryFile(id);
83
+ const info = await auth.signInWithRecovery({ file: text });
84
+ return { did: info.did };
85
+ }
86
+ function noEditionYetResponse() {
87
+ // Mirrors the server's primitives-read `notFound("edition for <did>")`:
88
+ // JSON-RPC error code -32020, message starts with "not found: edition for ".
89
+ return {
90
+ json: {
91
+ jsonrpc: "2.0",
92
+ id: "aithos.get_ethos_manifest",
93
+ error: {
94
+ code: -32020,
95
+ message: "not found: edition for did:aithos:zSomething",
96
+ },
97
+ },
98
+ };
99
+ }
100
+ function publishOkResponse() {
101
+ return {
102
+ json: {
103
+ jsonrpc: "2.0",
104
+ id: "publish_ethos_edition",
105
+ result: { ok: true, height: 1, manifest_uri: "s3://aithos/.../manifest.json" },
106
+ },
107
+ };
108
+ }
109
+ /* -------------------------------------------------------------------------- */
110
+ /* Tests */
111
+ /* -------------------------------------------------------------------------- */
112
+ describe("EthosClient — fresh Ethos (no edition published yet)", () => {
113
+ beforeEach(() => {
114
+ fetchCalls = [];
115
+ });
116
+ afterEach(() => {
117
+ uninstallFetchMock();
118
+ });
119
+ it("zone(public).sections() returns [] when server says 'not found: edition'", async () => {
120
+ installFetchMock([
121
+ {
122
+ url: "/mcp/primitives/read",
123
+ rpcMethod: "aithos.get_ethos_manifest",
124
+ respond: noEditionYetResponse,
125
+ },
126
+ ]);
127
+ const auth = makeAuth();
128
+ await signInAsAlice(auth);
129
+ const me = makeNamespace(auth).me();
130
+ const sections = await me.zone("public").sections();
131
+ assert.deepEqual(sections, []);
132
+ });
133
+ it("zone(public).sections() reflects locally staged adds even when no edition exists", async () => {
134
+ installFetchMock([
135
+ {
136
+ url: "/mcp/primitives/read",
137
+ rpcMethod: "aithos.get_ethos_manifest",
138
+ respond: noEditionYetResponse,
139
+ },
140
+ ]);
141
+ const auth = makeAuth();
142
+ await signInAsAlice(auth);
143
+ const me = makeNamespace(auth).me();
144
+ me.zone("public").addSection({ title: "Hello", body: "World" });
145
+ const sections = await me.zone("public").sections();
146
+ assert.equal(sections.length, 1);
147
+ assert.equal(sections[0].title, "Hello");
148
+ });
149
+ it("publish() routes to publish_ethos_edition with height=1 on first publish", async () => {
150
+ let publishBody = null;
151
+ installFetchMock([
152
+ {
153
+ url: "/mcp/primitives/read",
154
+ rpcMethod: "aithos.get_ethos_manifest",
155
+ respond: noEditionYetResponse,
156
+ },
157
+ {
158
+ url: "/mcp/primitives/write",
159
+ rpcMethod: "aithos.publish_ethos_edition",
160
+ respond: (call) => {
161
+ publishBody = call.body;
162
+ return publishOkResponse();
163
+ },
164
+ },
165
+ ]);
166
+ const auth = makeAuth();
167
+ const alice = await signInAsAlice(auth);
168
+ const me = makeNamespace(auth).me();
169
+ me.zone("public").addSection({ title: "First", body: "Hello." });
170
+ me.zone("public").addSection({ title: "Second", body: "World." });
171
+ const r = await me.publish();
172
+ assert.equal(r.editionHeight, 1);
173
+ assert.equal(r.subjectDid, alice.did);
174
+ assert.deepEqual(r.zonesPublished, ["public"]);
175
+ // Verify the wire shape: JSON-RPC publish_ethos_edition with a height=1
176
+ // manifest containing both staged sections.
177
+ assert.equal(publishBody.method, "aithos.publish_ethos_edition");
178
+ const manifest = publishBody.params.manifest;
179
+ assert.equal(manifest.edition.height, 1);
180
+ assert.equal(manifest.edition.prev_hash, null);
181
+ assert.equal(manifest.edition.supersedes, null);
182
+ assert.deepEqual(manifest.zones.public.section_titles, ["First", "Second"]);
183
+ // Envelope is signed under #public.
184
+ const env = publishBody.params._envelope;
185
+ assert.equal(env.method, "aithos.publish_ethos_edition");
186
+ assert.match(env.proof.verificationMethod, /#public$/);
187
+ });
188
+ it("publish() accepts circle mutations on first edition with auto-injected public sentinel", async () => {
189
+ // Regression target: this used to throw `ethos_first_edition_public_only`.
190
+ // Since aithos-sdk@0.1.0-alpha.43 + protocol-client@0.1.0-alpha.14, the
191
+ // SDK auto-injects an `aithos-init` public section and seals the circle
192
+ // sections in the same height=1 manifest.
193
+ let publishBody = null;
194
+ installFetchMock([
195
+ {
196
+ url: "/mcp/primitives/read",
197
+ rpcMethod: "aithos.get_ethos_manifest",
198
+ respond: noEditionYetResponse,
199
+ },
200
+ {
201
+ url: "/mcp/primitives/write",
202
+ rpcMethod: "aithos.publish_ethos_edition",
203
+ respond: (call) => {
204
+ publishBody = call.body;
205
+ return publishOkResponse();
206
+ },
207
+ },
208
+ ]);
209
+ const auth = makeAuth();
210
+ await signInAsAlice(auth);
211
+ const me = makeNamespace(auth).me();
212
+ me.zone("circle").addSection({ title: "Private", body: "..." });
213
+ const r = await me.publish();
214
+ assert.equal(r.editionHeight, 1);
215
+ assert.deepEqual(r.zonesPublished, ["public", "circle"]);
216
+ const manifest = publishBody.params.manifest;
217
+ assert.equal(manifest.edition.height, 1);
218
+ assert.equal(manifest.edition.prev_hash, null);
219
+ // Auto-injected sentinel.
220
+ assert.deepEqual(manifest.zones.public.section_titles, ["aithos-init"]);
221
+ assert.equal(manifest.zones.public.encrypted, false);
222
+ // Sealed circle zone.
223
+ assert.deepEqual(manifest.zones.circle.section_titles, ["Private"]);
224
+ assert.equal(manifest.zones.circle.encrypted, true);
225
+ assert.ok(manifest.zones.circle.cipher, "circle cipher must be present");
226
+ // Both zones uploaded.
227
+ assert.ok(publishBody.params.zones.public?.bytes_base64);
228
+ assert.ok(publishBody.params.zones.circle?.bytes_base64);
229
+ assert.equal(publishBody.params.zones.self, undefined);
230
+ });
231
+ it("publish() preserves the caller's explicit public section when mixed with circle", async () => {
232
+ // When the caller stages BOTH a public and a circle add, the SDK
233
+ // must NOT inject a sentinel — the user's public section is enough.
234
+ let publishBody = null;
235
+ installFetchMock([
236
+ {
237
+ url: "/mcp/primitives/read",
238
+ rpcMethod: "aithos.get_ethos_manifest",
239
+ respond: noEditionYetResponse,
240
+ },
241
+ {
242
+ url: "/mcp/primitives/write",
243
+ rpcMethod: "aithos.publish_ethos_edition",
244
+ respond: (call) => {
245
+ publishBody = call.body;
246
+ return publishOkResponse();
247
+ },
248
+ },
249
+ ]);
250
+ const auth = makeAuth();
251
+ await signInAsAlice(auth);
252
+ const me = makeNamespace(auth).me();
253
+ me.zone("public").addSection({ title: "About", body: "Public bio." });
254
+ me.zone("circle").addSection({ title: "Notes", body: "Private notes." });
255
+ const r = await me.publish();
256
+ assert.deepEqual(r.zonesPublished, ["public", "circle"]);
257
+ const manifest = publishBody.params.manifest;
258
+ // No sentinel — the user's section is the public titles entry.
259
+ assert.deepEqual(manifest.zones.public.section_titles, ["About"]);
260
+ assert.deepEqual(manifest.zones.circle.section_titles, ["Notes"]);
261
+ });
262
+ it("publish() auto-injects public sentinel when only self mutations are staged", async () => {
263
+ let publishBody = null;
264
+ installFetchMock([
265
+ {
266
+ url: "/mcp/primitives/read",
267
+ rpcMethod: "aithos.get_ethos_manifest",
268
+ respond: noEditionYetResponse,
269
+ },
270
+ {
271
+ url: "/mcp/primitives/write",
272
+ rpcMethod: "aithos.publish_ethos_edition",
273
+ respond: (call) => {
274
+ publishBody = call.body;
275
+ return publishOkResponse();
276
+ },
277
+ },
278
+ ]);
279
+ const auth = makeAuth();
280
+ await signInAsAlice(auth);
281
+ const me = makeNamespace(auth).me();
282
+ me.zone("self").addSection({ title: "Journal", body: "Private." });
283
+ const r = await me.publish();
284
+ assert.deepEqual(r.zonesPublished, ["public", "self"]);
285
+ const manifest = publishBody.params.manifest;
286
+ assert.deepEqual(manifest.zones.public.section_titles, ["aithos-init"]);
287
+ assert.deepEqual(manifest.zones.self.section_titles, ["Journal"]);
288
+ assert.equal(manifest.zones.self.encrypted, true);
289
+ assert.equal(manifest.zones.circle, undefined);
290
+ });
291
+ it("publish() lands public + circle + self in a single height=1 edition", async () => {
292
+ let publishBody = null;
293
+ installFetchMock([
294
+ {
295
+ url: "/mcp/primitives/read",
296
+ rpcMethod: "aithos.get_ethos_manifest",
297
+ respond: noEditionYetResponse,
298
+ },
299
+ {
300
+ url: "/mcp/primitives/write",
301
+ rpcMethod: "aithos.publish_ethos_edition",
302
+ respond: (call) => {
303
+ publishBody = call.body;
304
+ return publishOkResponse();
305
+ },
306
+ },
307
+ ]);
308
+ const auth = makeAuth();
309
+ await signInAsAlice(auth);
310
+ const me = makeNamespace(auth).me();
311
+ me.zone("public").addSection({ title: "Bio", body: "Bio body." });
312
+ me.zone("circle").addSection({ title: "Circle", body: "Circle body." });
313
+ me.zone("self").addSection({ title: "Self", body: "Self body." });
314
+ const r = await me.publish();
315
+ assert.equal(r.editionHeight, 1);
316
+ assert.deepEqual(r.zonesPublished, ["public", "circle", "self"]);
317
+ const manifest = publishBody.params.manifest;
318
+ assert.deepEqual(manifest.zones.public.section_titles, ["Bio"]);
319
+ assert.deepEqual(manifest.zones.circle.section_titles, ["Circle"]);
320
+ assert.deepEqual(manifest.zones.self.section_titles, ["Self"]);
321
+ assert.ok(publishBody.params.zones.public?.bytes_base64);
322
+ assert.ok(publishBody.params.zones.circle?.bytes_base64);
323
+ assert.ok(publishBody.params.zones.self?.bytes_base64);
324
+ });
325
+ it("publish() rejects update/delete operations on a fresh Ethos", async () => {
326
+ installFetchMock([
327
+ {
328
+ url: "/mcp/primitives/read",
329
+ rpcMethod: "aithos.get_ethos_manifest",
330
+ respond: noEditionYetResponse,
331
+ },
332
+ ]);
333
+ const auth = makeAuth();
334
+ await signInAsAlice(auth);
335
+ const me = makeNamespace(auth).me();
336
+ // Stage a delete for a section that doesn't exist (no edition exists at all).
337
+ me.zone("public")["_parent"]; // type-safety placeholder; we use the public API
338
+ // EthosZone exposes deleteSection — go via that.
339
+ me.zone("public").deleteSection("sec_doesnotexist000");
340
+ await assert.rejects(() => me.publish(), (e) => e instanceof AithosSDKError && e.code === "ethos_first_edition_invalid_op");
341
+ });
342
+ it("publish() surfaces server JSON-RPC errors as ethos_first_edition_rejected", async () => {
343
+ installFetchMock([
344
+ {
345
+ url: "/mcp/primitives/read",
346
+ rpcMethod: "aithos.get_ethos_manifest",
347
+ respond: noEditionYetResponse,
348
+ },
349
+ {
350
+ url: "/mcp/primitives/write",
351
+ rpcMethod: "aithos.publish_ethos_edition",
352
+ respond: () => ({
353
+ json: {
354
+ jsonrpc: "2.0",
355
+ id: "publish_ethos_edition",
356
+ error: {
357
+ code: -32020,
358
+ message: "subject identity not published (call publish_identity first)",
359
+ },
360
+ },
361
+ }),
362
+ },
363
+ ]);
364
+ const auth = makeAuth();
365
+ await signInAsAlice(auth);
366
+ const me = makeNamespace(auth).me();
367
+ me.zone("public").addSection({ title: "Hi", body: "There." });
368
+ await assert.rejects(() => me.publish(), (e) => e instanceof AithosSDKError && e.code === "ethos_first_edition_rejected");
369
+ });
370
+ });
371
+ //# sourceMappingURL=ethos-first-edition.test.js.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=invoke-turn-sdk.test.d.ts.map
@@ -0,0 +1,177 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright 2026 Mathieu Colla
3
+ // Wire tests for sdk.compute.invokeTurn + sdk.compute.runConversationLocal,
4
+ // mirroring converse.test.ts: a real BrowserIdentity drives envelope signing
5
+ // and we assert on the JSON-RPC body posted to the proxy.
6
+ import { strict as assert } from "node:assert";
7
+ import { describe, it } from "node:test";
8
+ import { createBrowserIdentity } from "@aithos/protocol-client";
9
+ import { AithosAuth, AithosSDK, AithosSDKError, memoryKeyStore, noopStore, AITHOS_AGENT_TOOLS, } from "../src/index.js";
10
+ import { serializeRecoveryFile } from "../src/internal/recovery-file.js";
11
+ const APP_DID = "did:aithos:app:test";
12
+ async function makeSdk(fetchImpl) {
13
+ const id = createBrowserIdentity("turn-handle", "Turn User");
14
+ const auth = new AithosAuth({
15
+ authBaseUrl: "https://auth.test",
16
+ fetch: (() => {
17
+ throw new Error("auth not used");
18
+ }),
19
+ sessionStore: noopStore(),
20
+ keyStore: memoryKeyStore(),
21
+ });
22
+ const { text } = serializeRecoveryFile(id);
23
+ await auth.signInWithRecovery({ file: text });
24
+ const sdk = new AithosSDK({
25
+ auth,
26
+ appDid: APP_DID,
27
+ endpoints: { compute: "https://compute.example.test" },
28
+ fetch: fetchImpl,
29
+ });
30
+ return { sdk, did: id.did };
31
+ }
32
+ const TURN_RESULT = {
33
+ content: [{ type: "text", text: "Réponse." }],
34
+ stopReason: "end_turn",
35
+ usage: { inputTokens: 100, outputTokens: 20 },
36
+ creditsCharged: 12,
37
+ walletBalance: 9_988,
38
+ auditId: "audit-turn-1",
39
+ fundedBy: "purchase",
40
+ };
41
+ describe("compute.invokeTurn — wire", () => {
42
+ it("posts aithos.compute_invoke_turn with tools + mapped params", async () => {
43
+ let capturedUrl;
44
+ let capturedInit;
45
+ const fakeFetch = async (input, init) => {
46
+ capturedUrl = typeof input === "string" ? input : input.toString();
47
+ capturedInit = init;
48
+ return new Response(JSON.stringify({ result: TURN_RESULT }), {
49
+ status: 200,
50
+ headers: { "content-type": "application/json" },
51
+ });
52
+ };
53
+ const { sdk } = await makeSdk(fakeFetch);
54
+ const out = await sdk.compute.invokeTurn({
55
+ mandateId: "mandate:abc",
56
+ model: "claude-sonnet-4-6",
57
+ system: "sys",
58
+ messages: [{ role: "user", content: "salut" }],
59
+ tools: [
60
+ {
61
+ name: "ethos_list_sections",
62
+ description: "list",
63
+ input_schema: { type: "object", properties: {} },
64
+ },
65
+ ],
66
+ maxTokens: 512,
67
+ temperature: 0.2,
68
+ });
69
+ assert.deepEqual(out, TURN_RESULT);
70
+ assert.equal(capturedUrl, "https://compute.example.test/v1/invoke");
71
+ const body = JSON.parse(capturedInit?.body);
72
+ assert.equal(body.method, "aithos.compute_invoke_turn");
73
+ assert.equal(body.params.app_did, APP_DID);
74
+ assert.equal(body.params.mandate_id, "mandate:abc");
75
+ assert.equal(body.params.model, "claude-sonnet-4-6");
76
+ assert.equal(body.params.system, "sys");
77
+ assert.equal(body.params.max_tokens, 512);
78
+ assert.equal(body.params.temperature, 0.2);
79
+ assert.ok(Array.isArray(body.params.tools), "tools must be on the wire");
80
+ assert.equal(body.params.tools.length, 1);
81
+ assert.match(body.params.idempotency_key, /^[0-9a-f]{32}$/);
82
+ assert.ok(body.params._envelope, "must carry a signed envelope");
83
+ // SDK-only camelCase keys must not leak.
84
+ assert.equal(body.params.maxTokens, undefined);
85
+ });
86
+ it("maps a JSON-RPC error to AithosSDKError", async () => {
87
+ const fakeFetch = async () => new Response(JSON.stringify({ error: { code: -32071, message: "insufficient credits" } }), {
88
+ status: 200,
89
+ headers: { "content-type": "application/json" },
90
+ });
91
+ const { sdk } = await makeSdk(fakeFetch);
92
+ await assert.rejects(() => sdk.compute.invokeTurn({
93
+ model: "claude-sonnet-4-6",
94
+ messages: [{ role: "user", content: "hi" }],
95
+ tools: [],
96
+ }), (err) => {
97
+ assert.ok(err instanceof AithosSDKError);
98
+ assert.equal(err.code, "-32071");
99
+ return true;
100
+ });
101
+ });
102
+ });
103
+ describe("compute.runConversationLocal — wiring", () => {
104
+ it("drives the loop: single end_turn → 1 iteration, full catalogue forwarded", async () => {
105
+ const bodies = [];
106
+ const fakeFetch = async (_input, init) => {
107
+ bodies.push(JSON.parse(init?.body));
108
+ return new Response(JSON.stringify({ result: TURN_RESULT }), {
109
+ status: 200,
110
+ headers: { "content-type": "application/json" },
111
+ });
112
+ };
113
+ const { sdk } = await makeSdk(fakeFetch);
114
+ const out = await sdk.compute.runConversationLocal({
115
+ model: "claude-sonnet-4-6",
116
+ messages: [{ role: "user", content: "Mets à jour ma bio si besoin." }],
117
+ system: "Écris seulement si nécessaire.",
118
+ });
119
+ assert.equal(out.iterations, 1);
120
+ assert.equal(out.stopReason, "end_turn");
121
+ assert.equal(out.content, "Réponse.");
122
+ assert.equal(out.creditsCharged, 12);
123
+ assert.equal(out.walletBalance, 9_988);
124
+ assert.equal(out.auditId, "audit-turn-1");
125
+ assert.equal(out.fundedBy, "purchase");
126
+ assert.deepEqual(out.toolCalls, []);
127
+ // One proxy turn, the right method, the full tool catalogue forwarded.
128
+ assert.equal(bodies.length, 1);
129
+ assert.equal(bodies[0].method, "aithos.compute_invoke_turn");
130
+ assert.equal(bodies[0].params.tools.length, AITHOS_AGENT_TOOLS.length);
131
+ });
132
+ it("readOnly forwards only the read family", async () => {
133
+ const bodies = [];
134
+ const fakeFetch = async (_input, init) => {
135
+ bodies.push(JSON.parse(init?.body));
136
+ return new Response(JSON.stringify({ result: TURN_RESULT }), {
137
+ status: 200,
138
+ headers: { "content-type": "application/json" },
139
+ });
140
+ };
141
+ const { sdk } = await makeSdk(fakeFetch);
142
+ await sdk.compute.runConversationLocal({
143
+ model: "claude-sonnet-4-6",
144
+ messages: [{ role: "user", content: "résume" }],
145
+ readOnly: true,
146
+ });
147
+ const toolNames = bodies[0].params.tools.map((t) => t.name).sort();
148
+ assert.deepEqual(toolNames, ["data_query", "ethos_list_sections", "ethos_read_section"]);
149
+ });
150
+ it("throws sdk_no_signer when no owner and no mandate/subject", async () => {
151
+ const auth = new AithosAuth({
152
+ authBaseUrl: "https://auth.test",
153
+ fetch: (() => {
154
+ throw new Error("unused");
155
+ }),
156
+ sessionStore: noopStore(),
157
+ keyStore: memoryKeyStore(),
158
+ });
159
+ const sdk = new AithosSDK({
160
+ auth,
161
+ appDid: APP_DID,
162
+ endpoints: { compute: "https://compute.example.test" },
163
+ fetch: (() => {
164
+ throw new Error("fetch must not be reached");
165
+ }),
166
+ });
167
+ await assert.rejects(() => sdk.compute.runConversationLocal({
168
+ model: "claude-sonnet-4-6",
169
+ messages: [{ role: "user", content: "hi" }],
170
+ }), (err) => {
171
+ assert.ok(err instanceof AithosSDKError);
172
+ assert.equal(err.code, "sdk_no_signer");
173
+ return true;
174
+ });
175
+ });
176
+ });
177
+ //# sourceMappingURL=invoke-turn-sdk.test.js.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=migrate.test.d.ts.map