@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.
- package/README.md +202 -7
- 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/apps.d.ts +224 -0
- package/dist/src/apps.js +432 -0
- package/dist/src/assets.d.ts +225 -0
- package/dist/src/assets.js +534 -0
- package/dist/src/auth-api.d.ts +219 -0
- package/dist/src/auth-api.js +248 -0
- package/dist/src/auth.d.ts +591 -0
- package/dist/src/auth.js +947 -31
- package/dist/src/compute.d.ts +674 -6
- package/dist/src/compute.js +968 -20
- package/dist/src/data-schema-contacts-v1.d.ts +14 -0
- package/dist/src/data-schema-contacts-v1.js +28 -0
- package/dist/src/data.d.ts +368 -0
- package/dist/src/data.js +1124 -0
- package/dist/src/endpoints.d.ts +43 -0
- package/dist/src/endpoints.js +23 -0
- package/dist/src/ethos.d.ts +85 -0
- package/dist/src/ethos.js +463 -7
- package/dist/src/index.d.ts +22 -4
- package/dist/src/index.js +47 -2
- package/dist/src/internal/cmk-wrap.d.ts +41 -0
- package/dist/src/internal/cmk-wrap.js +132 -0
- package/dist/src/internal/delegate-bundle.js +7 -2
- package/dist/src/internal/envelope.d.ts +93 -0
- package/dist/src/internal/envelope.js +59 -0
- package/dist/src/internal/owner-signers.d.ts +5 -2
- package/dist/src/internal/owner-signers.js +22 -1
- package/dist/src/internal/recovery-file.d.ts +2 -0
- package/dist/src/internal/recovery-file.js +7 -0
- package/dist/src/key-store.d.ts +10 -0
- package/dist/src/key-store.js +6 -0
- package/dist/src/mandates.d.ts +58 -1
- package/dist/src/mandates.js +46 -3
- package/dist/src/migrate.d.ts +105 -0
- package/dist/src/migrate.js +367 -0
- package/dist/src/react/AithosAsset.d.ts +66 -0
- package/dist/src/react/AithosAsset.js +67 -0
- package/dist/src/react/context.d.ts +29 -0
- package/dist/src/react/context.js +31 -0
- package/dist/src/react/index.d.ts +29 -0
- package/dist/src/react/index.js +31 -0
- package/dist/src/react/use-aithos-asset.d.ts +39 -0
- package/dist/src/react/use-aithos-asset.js +118 -0
- package/dist/src/react/use-transcribe-pending.d.ts +21 -0
- package/dist/src/react/use-transcribe-pending.js +47 -0
- package/dist/src/rotate.d.ts +94 -0
- package/dist/src/rotate.js +298 -0
- package/dist/src/sdk.d.ts +36 -2
- package/dist/src/sdk.js +72 -1
- package/dist/src/transcribe-resilience.d.ts +57 -0
- package/dist/src/transcribe-resilience.js +203 -0
- package/dist/src/web.d.ts +279 -0
- package/dist/src/web.js +186 -0
- 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/auth-j3.test.js +32 -1
- package/dist/test/canonical-conformance.test.d.ts +2 -0
- package/dist/test/canonical-conformance.test.js +86 -0
- package/dist/test/compute-delegate-path.test.d.ts +2 -0
- package/dist/test/compute-delegate-path.test.js +183 -0
- package/dist/test/compute.test.js +4 -0
- package/dist/test/converse.test.d.ts +2 -0
- package/dist/test/converse.test.js +162 -0
- package/dist/test/data-sphere.test.d.ts +2 -0
- package/dist/test/data-sphere.test.js +57 -0
- package/dist/test/endpoints.test.js +40 -1
- package/dist/test/envelope-core-conformance.test.d.ts +2 -0
- package/dist/test/envelope-core-conformance.test.js +75 -0
- package/dist/test/envelope.test.d.ts +2 -0
- package/dist/test/envelope.test.js +318 -0
- package/dist/test/ethos-first-edition.test.d.ts +2 -0
- package/dist/test/ethos-first-edition.test.js +371 -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/migrate.test.d.ts +2 -0
- package/dist/test/migrate.test.js +340 -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/rotate-ethos.test.d.ts +2 -0
- package/dist/test/rotate-ethos.test.js +151 -0
- package/dist/test/rotate.test.d.ts +2 -0
- package/dist/test/rotate.test.js +63 -0
- package/dist/test/schema-autoresolve.test.d.ts +2 -0
- package/dist/test/schema-autoresolve.test.js +146 -0
- package/dist/test/sdk.test.js +11 -2
- package/dist/test/signup-bootstrap.test.d.ts +2 -0
- package/dist/test/signup-bootstrap.test.js +311 -0
- package/dist/test/transcribe-invoke.test.d.ts +2 -0
- package/dist/test/transcribe-invoke.test.js +204 -0
- package/dist/test/transcribe.test.d.ts +2 -0
- package/dist/test/transcribe.test.js +186 -0
- package/dist/test/web.test.d.ts +2 -0
- package/dist/test/web.test.js +270 -0
- package/package.json +20 -3
|
@@ -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,117 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright 2026 Mathieu Colla
|
|
3
|
+
// Tests for the pure CLIENT-SIDE agentic loop (ported from the proxy's
|
|
4
|
+
// converse-loop tests, adapted to the async local dispatch + per-turn billing
|
|
5
|
+
// accumulation).
|
|
6
|
+
import { strict as assert } from "node:assert";
|
|
7
|
+
import { describe, it } from "node:test";
|
|
8
|
+
import { runAgenticLoopLocal, } from "../src/index.js";
|
|
9
|
+
function turn(content, stopReason, i = 100, o = 50, credits = 10, balance = 1000) {
|
|
10
|
+
return {
|
|
11
|
+
content,
|
|
12
|
+
stopReason,
|
|
13
|
+
usage: { inputTokens: i, outputTokens: o },
|
|
14
|
+
creditsCharged: credits,
|
|
15
|
+
walletBalance: balance,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
const okDispatch = async () => ({
|
|
19
|
+
payload: '{"ok":true}',
|
|
20
|
+
isError: false,
|
|
21
|
+
});
|
|
22
|
+
describe("runAgenticLoopLocal", () => {
|
|
23
|
+
it("returns immediately on a single end_turn (no tools)", async () => {
|
|
24
|
+
let calls = 0;
|
|
25
|
+
const r = await runAgenticLoopLocal({
|
|
26
|
+
messages: [{ role: "user", content: "salut" }],
|
|
27
|
+
maxIterations: 6,
|
|
28
|
+
invokeTurn: async () => {
|
|
29
|
+
calls++;
|
|
30
|
+
return turn([{ type: "text", text: "Réponse directe." }], "end_turn");
|
|
31
|
+
},
|
|
32
|
+
dispatch: okDispatch,
|
|
33
|
+
});
|
|
34
|
+
assert.equal(r.iterations, 1);
|
|
35
|
+
assert.equal(r.stopReason, "end_turn");
|
|
36
|
+
assert.equal(r.finalContent, "Réponse directe.");
|
|
37
|
+
assert.deepEqual(r.toolCalls, []);
|
|
38
|
+
assert.equal(calls, 1);
|
|
39
|
+
assert.equal(r.creditsCharged, 10);
|
|
40
|
+
assert.equal(r.walletBalance, 1000);
|
|
41
|
+
});
|
|
42
|
+
it("runs a tool turn then concludes, summing usage + per-turn credits", async () => {
|
|
43
|
+
const seq = [
|
|
44
|
+
turn([
|
|
45
|
+
{ type: "text", text: "je lis" },
|
|
46
|
+
{ type: "tool_use", id: "tu1", name: "ethos_read_section", input: { section_id: "s1" } },
|
|
47
|
+
], "tool_use", 100, 20, 7, 993),
|
|
48
|
+
turn([{ type: "text", text: "Fini." }], "end_turn", 50, 30, 5, 988),
|
|
49
|
+
];
|
|
50
|
+
let n = 0;
|
|
51
|
+
const dispatched = [];
|
|
52
|
+
const r = await runAgenticLoopLocal({
|
|
53
|
+
messages: [{ role: "user", content: "lis s1" }],
|
|
54
|
+
maxIterations: 6,
|
|
55
|
+
invokeTurn: async (messages) => {
|
|
56
|
+
// After the first turn, the running list must carry the assistant
|
|
57
|
+
// tool_use turn + the user tool_result turn.
|
|
58
|
+
if (n === 1) {
|
|
59
|
+
assert.equal(messages.length, 3);
|
|
60
|
+
assert.equal(messages[1].role, "assistant");
|
|
61
|
+
assert.equal(messages[2].role, "user");
|
|
62
|
+
}
|
|
63
|
+
return seq[n++];
|
|
64
|
+
},
|
|
65
|
+
dispatch: async (name) => {
|
|
66
|
+
dispatched.push(name);
|
|
67
|
+
return { payload: '{"body":"x"}', isError: false };
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
assert.equal(r.iterations, 2);
|
|
71
|
+
assert.equal(r.stopReason, "end_turn");
|
|
72
|
+
assert.equal(r.finalContent, "Fini.");
|
|
73
|
+
assert.deepEqual(dispatched, ["ethos_read_section"]);
|
|
74
|
+
assert.deepEqual(r.toolCalls, [{ name: "ethos_read_section", ok: true, turn: 1 }]);
|
|
75
|
+
assert.equal(r.usage.inputTokens, 150);
|
|
76
|
+
assert.equal(r.usage.outputTokens, 50);
|
|
77
|
+
assert.equal(r.creditsCharged, 12); // 7 + 5
|
|
78
|
+
assert.equal(r.walletBalance, 988); // last turn
|
|
79
|
+
});
|
|
80
|
+
it("a tool error becomes is_error and does NOT stop the loop", async () => {
|
|
81
|
+
const seq = [
|
|
82
|
+
turn([{ type: "tool_use", id: "tu1", name: "ethos_add_section", input: {} }], "tool_use"),
|
|
83
|
+
turn([{ type: "text", text: "ok malgré erreur" }], "end_turn"),
|
|
84
|
+
];
|
|
85
|
+
let n = 0;
|
|
86
|
+
const r = await runAgenticLoopLocal({
|
|
87
|
+
messages: [{ role: "user", content: "ajoute" }],
|
|
88
|
+
maxIterations: 6,
|
|
89
|
+
invokeTurn: async () => seq[n++],
|
|
90
|
+
dispatch: async () => ({ payload: '{"error":"nope"}', isError: true }),
|
|
91
|
+
});
|
|
92
|
+
assert.equal(r.stopReason, "end_turn");
|
|
93
|
+
assert.deepEqual(r.toolCalls, [{ name: "ethos_add_section", ok: false, turn: 1 }]);
|
|
94
|
+
assert.equal(r.finalContent, "ok malgré erreur");
|
|
95
|
+
});
|
|
96
|
+
it("stops at the iteration cap when the model keeps calling tools", async () => {
|
|
97
|
+
let calls = 0;
|
|
98
|
+
const r = await runAgenticLoopLocal({
|
|
99
|
+
messages: [{ role: "user", content: "boucle" }],
|
|
100
|
+
maxIterations: 3,
|
|
101
|
+
invokeTurn: async () => {
|
|
102
|
+
calls++;
|
|
103
|
+
return turn([
|
|
104
|
+
{ type: "text", text: `t${calls}` },
|
|
105
|
+
{ type: "tool_use", id: `tu${calls}`, name: "ethos_list_sections", input: {} },
|
|
106
|
+
], "tool_use");
|
|
107
|
+
},
|
|
108
|
+
dispatch: okDispatch,
|
|
109
|
+
});
|
|
110
|
+
assert.equal(r.stopReason, "max_iterations");
|
|
111
|
+
assert.equal(r.iterations, 3);
|
|
112
|
+
assert.equal(calls, 3);
|
|
113
|
+
assert.equal(r.toolCalls.length, 3);
|
|
114
|
+
assert.equal(r.finalContent, "t3");
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
//# sourceMappingURL=agent-loop.test.js.map
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright 2026 Mathieu Colla
|
|
3
|
+
import { strict as assert } from "node:assert";
|
|
4
|
+
import { describe, it } from "node:test";
|
|
5
|
+
import { AITHOS_AGENT_TOOLS, AITHOS_AGENT_READ_TOOLS, AITHOS_AGENT_WRITE_TOOLS, selectAgentTools, isWriteTool, } from "../src/index.js";
|
|
6
|
+
const names = (ts) => ts.map((t) => t.name).sort();
|
|
7
|
+
describe("agent tool catalogue", () => {
|
|
8
|
+
it("the full catalogue = read + write families", () => {
|
|
9
|
+
assert.equal(AITHOS_AGENT_TOOLS.length, AITHOS_AGENT_READ_TOOLS.length + AITHOS_AGENT_WRITE_TOOLS.length);
|
|
10
|
+
assert.deepEqual(names(AITHOS_AGENT_READ_TOOLS), [
|
|
11
|
+
"data_query",
|
|
12
|
+
"ethos_list_sections",
|
|
13
|
+
"ethos_read_section",
|
|
14
|
+
]);
|
|
15
|
+
assert.deepEqual(names(AITHOS_AGENT_WRITE_TOOLS), [
|
|
16
|
+
"ethos_add_section",
|
|
17
|
+
"ethos_delete_section",
|
|
18
|
+
"ethos_update_section",
|
|
19
|
+
]);
|
|
20
|
+
});
|
|
21
|
+
it("every tool has a name, description, and object input_schema", () => {
|
|
22
|
+
for (const t of AITHOS_AGENT_TOOLS) {
|
|
23
|
+
assert.ok(t.name.length > 0);
|
|
24
|
+
assert.ok(t.description.length > 0);
|
|
25
|
+
assert.equal(typeof t.input_schema, "object");
|
|
26
|
+
assert.equal(t.input_schema.type, "object");
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
it("isWriteTool flags only the write family", () => {
|
|
30
|
+
assert.equal(isWriteTool("ethos_add_section"), true);
|
|
31
|
+
assert.equal(isWriteTool("ethos_update_section"), true);
|
|
32
|
+
assert.equal(isWriteTool("ethos_delete_section"), true);
|
|
33
|
+
assert.equal(isWriteTool("ethos_read_section"), false);
|
|
34
|
+
assert.equal(isWriteTool("data_query"), false);
|
|
35
|
+
assert.equal(isWriteTool("bogus"), false);
|
|
36
|
+
});
|
|
37
|
+
it("selectAgentTools: default = full catalogue", () => {
|
|
38
|
+
assert.equal(selectAgentTools().length, AITHOS_AGENT_TOOLS.length);
|
|
39
|
+
assert.equal(selectAgentTools({}).length, AITHOS_AGENT_TOOLS.length);
|
|
40
|
+
assert.equal(selectAgentTools({ tools: [] }).length, AITHOS_AGENT_TOOLS.length);
|
|
41
|
+
});
|
|
42
|
+
it("selectAgentTools: readOnly = read family only", () => {
|
|
43
|
+
assert.deepEqual(names(selectAgentTools({ readOnly: true })), names(AITHOS_AGENT_READ_TOOLS));
|
|
44
|
+
});
|
|
45
|
+
it("selectAgentTools: subset by name, unknown names ignored", () => {
|
|
46
|
+
const sel = selectAgentTools({ tools: ["ethos_add_section", "nope", "data_query"] });
|
|
47
|
+
assert.deepEqual(names(sel), ["data_query", "ethos_add_section"]);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
//# sourceMappingURL=agent-tools.test.js.map
|
|
@@ -90,7 +90,7 @@ describe("recovery file: parse + serialize", () => {
|
|
|
90
90
|
/* parseDelegateBundle */
|
|
91
91
|
/* -------------------------------------------------------------------------- */
|
|
92
92
|
describe("delegate bundle: parse", () => {
|
|
93
|
-
it("parses a well-formed bundle", () => {
|
|
93
|
+
it("parses a well-formed bundle (legacy subject_did field)", () => {
|
|
94
94
|
const text = delegateBundleText({
|
|
95
95
|
mandateId: "mandate:01H8XYZ",
|
|
96
96
|
subjectDid: "did:aithos:zCarol",
|
|
@@ -103,6 +103,37 @@ describe("delegate bundle: parse", () => {
|
|
|
103
103
|
assert.equal(parsed.granteeId, "urn:aithos:agent:bob1");
|
|
104
104
|
assert.equal(parsed.delegateSeedHex.length, 64);
|
|
105
105
|
});
|
|
106
|
+
it("parses a bundle minted by mintDelegateBundle (issuer field)", () => {
|
|
107
|
+
// Real wire shape emitted by `mintDelegateBundle` in protocol-client:
|
|
108
|
+
// SignedMandate carries the subject's DID under `issuer`, NOT
|
|
109
|
+
// `subject_did`. Regression test for the import flow that broke
|
|
110
|
+
// every freshly-minted mandate before this fix.
|
|
111
|
+
const text = JSON.stringify({
|
|
112
|
+
aithos_delegate_version: "0.1.0",
|
|
113
|
+
mandate: {
|
|
114
|
+
"aithos-mandate": "0.1",
|
|
115
|
+
id: "mandate:01H8ISSUER",
|
|
116
|
+
issuer: "did:aithos:zCarol",
|
|
117
|
+
issued_by_key: "did:aithos:zCarol#root",
|
|
118
|
+
grantee: {
|
|
119
|
+
id: "urn:aithos:agent:bob1",
|
|
120
|
+
pubkey: "z6MkqGenericPubKey",
|
|
121
|
+
},
|
|
122
|
+
actor_sphere: "self",
|
|
123
|
+
scopes: ["ethos.read.public", "ethos.write.public"],
|
|
124
|
+
not_before: "2026-05-10T00:00:00Z",
|
|
125
|
+
not_after: "2026-05-11T00:00:00Z",
|
|
126
|
+
issued_at: "2026-05-10T00:00:00Z",
|
|
127
|
+
nonce: "abc",
|
|
128
|
+
signature: { alg: "ed25519", key: "...", value: "..." },
|
|
129
|
+
},
|
|
130
|
+
delegate_seed_hex: "11".repeat(32),
|
|
131
|
+
});
|
|
132
|
+
const parsed = parseDelegateBundle(text);
|
|
133
|
+
assert.equal(parsed.subjectDid, "did:aithos:zCarol");
|
|
134
|
+
assert.equal(parsed.mandateId, "mandate:01H8ISSUER");
|
|
135
|
+
assert.equal(parsed.granteeId, "urn:aithos:agent:bob1");
|
|
136
|
+
});
|
|
106
137
|
it("readDelegateBundleText accepts string passthrough", async () => {
|
|
107
138
|
const text = delegateBundleText({
|
|
108
139
|
mandateId: "m",
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright 2026 Mathieu Colla
|
|
3
|
+
/**
|
|
4
|
+
* Safety gate for the canonicalization refactor.
|
|
5
|
+
*
|
|
6
|
+
* The SDK historically carried three hand-rolled copies of `jcsCanonicalize`
|
|
7
|
+
* (internal/envelope.ts, data.ts, apps.ts). data.ts uses it to canonicalize a
|
|
8
|
+
* record payload BEFORE encryption (it feeds the AAD/plaintext), so a single
|
|
9
|
+
* differing byte vs. the canonicalization used at decrypt/verify time would
|
|
10
|
+
* silently corrupt data. Before swapping those copies onto
|
|
11
|
+
* `@aithos/protocol-core`'s `canonicalize`, this test proves the two produce
|
|
12
|
+
* identical output across a representative corpus.
|
|
13
|
+
*
|
|
14
|
+
* Known, accepted divergence: lone UTF-16 surrogates. `JSON.stringify` escapes
|
|
15
|
+
* them (\uXXXX) whereas core emits the raw code unit. Lone surrogates never
|
|
16
|
+
* appear in Aithos payloads (record fields are well-formed JSON strings), so
|
|
17
|
+
* the corpus excludes them by design.
|
|
18
|
+
*/
|
|
19
|
+
import { describe, test } from "node:test";
|
|
20
|
+
import { strict as assert } from "node:assert";
|
|
21
|
+
import { canonicalize } from "@aithos/protocol-core/canonical";
|
|
22
|
+
/** Exact copy of the SDK's legacy jcsCanonicalize (pre-refactor reference). */
|
|
23
|
+
function jcsCanonicalize(value) {
|
|
24
|
+
if (value === null)
|
|
25
|
+
return "null";
|
|
26
|
+
if (value === undefined)
|
|
27
|
+
throw new Error("Cannot canonicalize undefined");
|
|
28
|
+
if (typeof value === "boolean")
|
|
29
|
+
return value ? "true" : "false";
|
|
30
|
+
if (typeof value === "number") {
|
|
31
|
+
if (!Number.isFinite(value))
|
|
32
|
+
throw new Error("non-finite number");
|
|
33
|
+
return value.toString();
|
|
34
|
+
}
|
|
35
|
+
if (typeof value === "string")
|
|
36
|
+
return JSON.stringify(value);
|
|
37
|
+
if (Array.isArray(value)) {
|
|
38
|
+
return "[" + value.map(jcsCanonicalize).join(",") + "]";
|
|
39
|
+
}
|
|
40
|
+
if (typeof value === "object") {
|
|
41
|
+
const obj = value;
|
|
42
|
+
const keys = Object.keys(obj).sort();
|
|
43
|
+
return ("{" +
|
|
44
|
+
keys
|
|
45
|
+
.map((k) => JSON.stringify(k) + ":" + jcsCanonicalize(obj[k]))
|
|
46
|
+
.join(",") +
|
|
47
|
+
"}");
|
|
48
|
+
}
|
|
49
|
+
throw new Error(`Cannot canonicalize ${typeof value}`);
|
|
50
|
+
}
|
|
51
|
+
const corpus = [
|
|
52
|
+
null,
|
|
53
|
+
true,
|
|
54
|
+
false,
|
|
55
|
+
0,
|
|
56
|
+
-1,
|
|
57
|
+
42,
|
|
58
|
+
Number.MAX_SAFE_INTEGER,
|
|
59
|
+
"",
|
|
60
|
+
"hello",
|
|
61
|
+
"with \"quotes\" and \\ backslash",
|
|
62
|
+
"tab\tnewline\nreturn\rbackspace\bform\f",
|
|
63
|
+
"control end",
|
|
64
|
+
"accented éàùçö and emoji 😀🚀 and 漢字",
|
|
65
|
+
"slash / and at @ and unicode nbsp",
|
|
66
|
+
[],
|
|
67
|
+
[1, 2, 3],
|
|
68
|
+
["z", "a", "m"],
|
|
69
|
+
[{ b: 1, a: 2 }, [3, [4, 5]]],
|
|
70
|
+
{},
|
|
71
|
+
{ b: 2, a: 1, c: 3 },
|
|
72
|
+
{ z: { y: { x: [1, "two", false, null] } } },
|
|
73
|
+
{ "key with spaces": 1, "weird:char": 2, "": "empty key" },
|
|
74
|
+
{ émoji: "😀", "漢字": 1, A: 0, a: 0 }, // mixed-case + non-ascii keys (UTF-16 order)
|
|
75
|
+
{
|
|
76
|
+
record: { name: "Aïko", tags: ["x", "y"], n: 7, active: true, meta: null },
|
|
77
|
+
},
|
|
78
|
+
];
|
|
79
|
+
describe("canonicalize conformance — core vs legacy SDK jcsCanonicalize", () => {
|
|
80
|
+
for (const [i, value] of corpus.entries()) {
|
|
81
|
+
test(`corpus[${i}] is byte-identical`, () => {
|
|
82
|
+
assert.equal(canonicalize(value), jcsCanonicalize(value));
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
//# sourceMappingURL=canonical-conformance.test.js.map
|