@anteros/core 0.0.42 → 0.0.44
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/database/replication.ts +614 -26
- package/lib/agent.ts +279 -39
- package/lib/agentMemory.ts +103 -0
- package/lib/agents.ts +25 -2
- package/lib/define.ts +1 -1
- package/lib/mcpClient.ts +128 -0
- package/lib/providers.ts +169 -66
- package/lib/skills.ts +481 -0
- package/package.json +1 -1
- package/server/agents.ts +27 -6
- package/server/api.ts +74 -1
- package/tests/agent-mcp.test.ts +116 -0
- package/tests/agent-memory.test.ts +6 -6
- package/tests/agent-provider.test.ts +288 -0
- package/tests/agent-runtime.test.ts +369 -0
- package/tests/agent-skills.test.ts +177 -0
- package/tests/agent-summarize.test.ts +124 -0
- package/tests/agent-working-memory.test.ts +5 -3
- package/tests/agent.test.ts +18 -16
- package/tests/agents-api.test.ts +1 -1
- package/tests/api-private-fields.test.ts +196 -0
- package/tests/file-collection-replication.test.ts +2 -0
- package/tests/fixtures/agent-tenant/agents/support.agent.ts +1 -1
- package/tests/fixtures/agent-tenant/agents/weather.agent.ts +1 -1
- package/tests/fixtures/agents-api/agents/assistant.agent.ts +1 -1
- package/tests/fixtures/agents-api/agents/plain.agent.ts +1 -1
- package/tests/fixtures/agents-api/agents/private.agent.ts +1 -1
- package/tests/fixtures/repl-only/app.ts +3 -0
- package/tests/fixtures/replication-meta/agents/chat.agent.ts +1 -1
- package/tests/fixtures/skills-tenant/agents/docs.agent.ts +23 -0
- package/tests/fixtures/skills-tenant/agents/incomplete.agent.ts +14 -0
- package/tests/fixtures/skills-tenant/broken/bad-name.md +6 -0
- package/tests/fixtures/skills-tenant/broken/dup-a.md +6 -0
- package/tests/fixtures/skills-tenant/broken/dup-b.md +6 -0
- package/tests/fixtures/skills-tenant/broken/empty-body.md +4 -0
- package/tests/fixtures/skills-tenant/broken/long-compat.md +7 -0
- package/tests/fixtures/skills-tenant/broken/long-description.md +6 -0
- package/tests/fixtures/skills-tenant/broken/mismatch/SKILL.md +6 -0
- package/tests/fixtures/skills-tenant/broken/no-description.md +5 -0
- package/tests/fixtures/skills-tenant/broken/no-name.md +5 -0
- package/tests/fixtures/skills-tenant/broken/plain.md +4 -0
- package/tests/fixtures/skills-tenant/broken/unknown-key.md +9 -0
- package/tests/fixtures/skills-tenant/docs/pdf-forms.md +18 -0
- package/tests/fixtures/skills-tenant/guide/ml.md +9 -0
- package/tests/fixtures/skills-tenant/guides/pdf/SKILL.md +9 -0
- package/tests/fixtures/skills-tenant/runbooks/incident.md +9 -0
- package/tests/fixtures/skills-tenant/runbooks/notes.txt +2 -0
- package/tests/replication-change-stream.test.ts +240 -0
- package/tests/replication-control.test.ts +2 -0
- package/tests/replication-initial-sync.test.ts +2 -0
- package/tests/replication-meta.test.ts +3 -0
- package/tests/replication.test.ts +2 -0
- package/tests/vars-replication.test.ts +2 -0
- package/types/agent.d.ts +237 -7
- package/types/replication.d.ts +41 -4
|
@@ -36,7 +36,7 @@ function makeAgent(overrides: Record<string, any> = {}, provider: Record<string,
|
|
|
36
36
|
provider: {
|
|
37
37
|
model: "test-model",
|
|
38
38
|
apiKey: "test-key",
|
|
39
|
-
|
|
39
|
+
protocol: "openai",
|
|
40
40
|
options: { retries: 0 },
|
|
41
41
|
...provider,
|
|
42
42
|
},
|
|
@@ -385,7 +385,7 @@ describe("working memory — Anthropic", () => {
|
|
|
385
385
|
const provider = track(fakeProvider(() => anthropicText("ok")));
|
|
386
386
|
const agent = makeAgent(
|
|
387
387
|
{ memory: new InMemoryAgentMemory(), workingMemory: { template: "# Memo\n- x:" } },
|
|
388
|
-
{
|
|
388
|
+
{ protocol: "anthropic", baseUrl: provider.url },
|
|
389
389
|
);
|
|
390
390
|
|
|
391
391
|
await agent.generate("hi", { thread: "t1", resource: "u1" });
|
|
@@ -397,6 +397,8 @@ describe("working memory — Anthropic", () => {
|
|
|
397
397
|
|
|
398
398
|
describe("working memory — the registry exposes nothing new by accident", () => {
|
|
399
399
|
it("keeps `agents.memory` as the only store factory", () => {
|
|
400
|
-
|
|
400
|
+
// `summarize` is the one deliberate addition: a **processor** factory (it needs
|
|
401
|
+
// your model and your prompt), not a store
|
|
402
|
+
expect(Object.keys(agents.memory).sort()).toEqual(["InMemory", "Mongo", "Redis", "summarize"]);
|
|
401
403
|
});
|
|
402
404
|
});
|
package/tests/agent.test.ts
CHANGED
|
@@ -63,7 +63,7 @@ function makeAgent(
|
|
|
63
63
|
provider: {
|
|
64
64
|
model: "test-model",
|
|
65
65
|
apiKey: "test-key",
|
|
66
|
-
|
|
66
|
+
protocol: "openai",
|
|
67
67
|
// Deterministic by default: a test must not wait on the retry backoff
|
|
68
68
|
options: { retries: 0, ...options },
|
|
69
69
|
...provider,
|
|
@@ -81,7 +81,7 @@ describe("agent … files", () => {
|
|
|
81
81
|
provider: {
|
|
82
82
|
model: "test-model",
|
|
83
83
|
apiKey: "test-key",
|
|
84
|
-
|
|
84
|
+
protocol: "openai",
|
|
85
85
|
options: { retries: 0 },
|
|
86
86
|
...providerOverrides,
|
|
87
87
|
},
|
|
@@ -228,7 +228,7 @@ describe("agent … tools & thinking options", () => {
|
|
|
228
228
|
provider: {
|
|
229
229
|
model: "test-model",
|
|
230
230
|
apiKey: "test-key",
|
|
231
|
-
|
|
231
|
+
protocol: "openai",
|
|
232
232
|
options: { retries: 0 },
|
|
233
233
|
...providerOverrides,
|
|
234
234
|
},
|
|
@@ -307,7 +307,7 @@ describe("agent … tools & thinking options", () => {
|
|
|
307
307
|
expect(request(provider, 1).body.tool_choice).toBe("auto");
|
|
308
308
|
|
|
309
309
|
const anthropic = track(fakeProvider(() => anthropicText("ok")));
|
|
310
|
-
const forced = toolAgent({
|
|
310
|
+
const forced = toolAgent({ protocol: "anthropic", baseUrl: anthropic.url }, { toolChoice: { name: "forecast" } });
|
|
311
311
|
await forced.generate("hi");
|
|
312
312
|
expect(request(anthropic, 0).body.tool_choice).toEqual({ type: "tool", name: "forecast" });
|
|
313
313
|
await forced.generate("hi", { toolChoice: "auto" });
|
|
@@ -347,7 +347,7 @@ describe("agent … tools & thinking options", () => {
|
|
|
347
347
|
it("maps `thinking` to a real budget on Anthropic, and raises max_tokens above it", async () => {
|
|
348
348
|
const provider = track(fakeProvider(() => anthropicText("ok")));
|
|
349
349
|
const agent = toolAgent(
|
|
350
|
-
{
|
|
350
|
+
{ protocol: "anthropic", baseUrl: provider.url, options: { retries: 0, maxTokens: 2000, temperature: 0.5 } },
|
|
351
351
|
);
|
|
352
352
|
|
|
353
353
|
await agent.generate("hi", { thinking: true });
|
|
@@ -377,7 +377,7 @@ describe("agent … tools & thinking options", () => {
|
|
|
377
377
|
stop_reason: "tool_use",
|
|
378
378
|
usage: A_USAGE,
|
|
379
379
|
})));
|
|
380
|
-
const agent = toolAgent({
|
|
380
|
+
const agent = toolAgent({ protocol: "anthropic", baseUrl: provider.url });
|
|
381
381
|
|
|
382
382
|
await agent.generate("Weather?", { thinking: 'medium' });
|
|
383
383
|
|
|
@@ -409,7 +409,7 @@ describe("agent … tools & thinking options", () => {
|
|
|
409
409
|
stop_reason: "end_turn",
|
|
410
410
|
usage: A_USAGE,
|
|
411
411
|
})));
|
|
412
|
-
const agent = toolAgent({
|
|
412
|
+
const agent = toolAgent({ protocol: "anthropic", baseUrl: provider.url });
|
|
413
413
|
|
|
414
414
|
const result = await agent.generate("go", { thinking: true });
|
|
415
415
|
|
|
@@ -430,7 +430,7 @@ describe("agent … tools & thinking options", () => {
|
|
|
430
430
|
JSON.stringify({ type: "content_block_delta", index: 1, delta: { type: "text_delta", text: "Answer" } }),
|
|
431
431
|
JSON.stringify({ type: "message_delta", delta: { stop_reason: "end_turn" }, usage: { output_tokens: 3 } }),
|
|
432
432
|
])));
|
|
433
|
-
const agent = toolAgent({
|
|
433
|
+
const agent = toolAgent({ protocol: "anthropic", baseUrl: provider.url });
|
|
434
434
|
|
|
435
435
|
const stream = agent.stream("hi", { thinking: true });
|
|
436
436
|
const kinds: string[] = [];
|
|
@@ -621,7 +621,7 @@ describe("Agent", () => {
|
|
|
621
621
|
const agent = makeAgent({}, {}, { temperature: 0.2, maxTokens: 1000 });
|
|
622
622
|
const declared = {
|
|
623
623
|
model: "test-model",
|
|
624
|
-
|
|
624
|
+
protocol: "openai" as const,
|
|
625
625
|
options: { retries: 0, temperature: 0.2, maxTokens: 1000 },
|
|
626
626
|
};
|
|
627
627
|
|
|
@@ -831,7 +831,7 @@ describe("agent.generate", () => {
|
|
|
831
831
|
delete Bun.env.ANTHROPIC_API_KEY;
|
|
832
832
|
|
|
833
833
|
try {
|
|
834
|
-
const agent = makeAgent({}, {
|
|
834
|
+
const agent = makeAgent({}, { protocol: "anthropic", apiKey: undefined, baseUrl: provider.url });
|
|
835
835
|
await expect(agent.generate("hi")).rejects.toMatchObject({ code: "AGENT_PROVIDER_NO_API_KEY" });
|
|
836
836
|
|
|
837
837
|
Bun.env.ANTHROPIC_API_KEY = "from-env";
|
|
@@ -849,10 +849,10 @@ describe("agent.generate", () => {
|
|
|
849
849
|
|
|
850
850
|
// ─── Anthropic ───────────────────────────────────────────────────────────
|
|
851
851
|
|
|
852
|
-
describe("agent
|
|
852
|
+
describe("agent speaking the Anthropic protocol", () => {
|
|
853
853
|
it("puts the system prompt out of band and returns the text", async () => {
|
|
854
854
|
const provider = track(fakeProvider(() => anthropicText("Bonjour")));
|
|
855
|
-
const agent = makeAgent({}, {
|
|
855
|
+
const agent = makeAgent({}, { protocol: "anthropic", baseUrl: provider.url });
|
|
856
856
|
|
|
857
857
|
const result = await agent.generate("salut");
|
|
858
858
|
|
|
@@ -869,7 +869,7 @@ describe("agent with an Anthropic-compatible provider", () => {
|
|
|
869
869
|
const provider = track(fakeProvider((_body, _req, hits) =>
|
|
870
870
|
hits === 1 ? anthropicTool("forecast", { city: "Lyon" }) : anthropicText("Lyon: 21°C")));
|
|
871
871
|
|
|
872
|
-
const agent = makeAgent({ tools: { forecast: forecastTool() } }, {
|
|
872
|
+
const agent = makeAgent({ tools: { forecast: forecastTool() } }, { protocol: "anthropic", baseUrl: provider.url });
|
|
873
873
|
const result = await agent.generate("Weather in Lyon?");
|
|
874
874
|
|
|
875
875
|
expect(result.text).toBe("Lyon: 21°C");
|
|
@@ -888,7 +888,7 @@ describe("agent with an Anthropic-compatible provider", () => {
|
|
|
888
888
|
|
|
889
889
|
it("streams text deltas and merges the partial usage", async () => {
|
|
890
890
|
const provider = track(fakeProvider(() => anthropicStream(["Bon", "jour"])));
|
|
891
|
-
const agent = makeAgent({}, {
|
|
891
|
+
const agent = makeAgent({}, { protocol: "anthropic", baseUrl: provider.url });
|
|
892
892
|
|
|
893
893
|
const stream = agent.stream("salut");
|
|
894
894
|
let text = "";
|
|
@@ -1116,9 +1116,11 @@ describe("agent structured output", () => {
|
|
|
1116
1116
|
const object = await agent.generateObject("Weather?", { schema });
|
|
1117
1117
|
|
|
1118
1118
|
expect(object).toEqual({ city: "Paris", celsius: 21 });
|
|
1119
|
-
// The schema
|
|
1119
|
+
// The schema travels twice: in the prompt, and in the field the gateway really
|
|
1120
|
+
// validates against (`response_format.json_schema`, see `agent-provider.test.ts`)
|
|
1120
1121
|
expect(request(provider, 0).body.messages[0].content).toContain("JSON Schema");
|
|
1121
|
-
expect(request(provider, 0).body.response_format).
|
|
1122
|
+
expect(request(provider, 0).body.response_format.type).toBe("json_schema");
|
|
1123
|
+
expect(request(provider, 0).body.response_format.json_schema.schema).toBeDefined();
|
|
1122
1124
|
});
|
|
1123
1125
|
|
|
1124
1126
|
it("repairs an invalid answer with one extra turn", async () => {
|
package/tests/agents-api.test.ts
CHANGED
|
@@ -180,7 +180,7 @@ describe("agents HTTP API — actions", () => {
|
|
|
180
180
|
expect(res.body.agent.id).toBe("assistant");
|
|
181
181
|
expect(res.body.agent.tools).toEqual(["echo"]);
|
|
182
182
|
expect(res.body.agent.provider.model).toBe("test-model");
|
|
183
|
-
expect(res.body.agent.provider.
|
|
183
|
+
expect(res.body.agent.provider.protocol).toBe("openai");
|
|
184
184
|
expect(res.body.agent.maxSteps).toBe(3);
|
|
185
185
|
expect(res.body.agent.memory).toBe(true);
|
|
186
186
|
expect(JSON.stringify(res.body)).not.toContain("test-key");
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll } from "bun:test";
|
|
2
|
+
import { createApp } from "../server/hono";
|
|
3
|
+
import { formatConfig, cfg } from "../server/config";
|
|
4
|
+
import { syncTenants } from "../database/tenant";
|
|
5
|
+
import { useRest } from "../database/rest";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* `api.privateFields` on a **collection reached through a join**.
|
|
9
|
+
*
|
|
10
|
+
* `func.omit` walks the whole response, but only with the matchers of the
|
|
11
|
+
* collection that was queried: an `$include`d document carries the *included*
|
|
12
|
+
* collection's fields, so that collection's `privateFields` must be applied too
|
|
13
|
+
* — at the path the join put it, and nowhere else. Same story for a `$lookup`
|
|
14
|
+
* inside an `aggregate` pipeline.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const TENANT = "private-fields";
|
|
18
|
+
const DB = "mongodb://localhost:27017/_API_PRIVATE_FIELDS_TEST";
|
|
19
|
+
|
|
20
|
+
let url = "";
|
|
21
|
+
let server: any;
|
|
22
|
+
let rest: InstanceType<typeof useRest>;
|
|
23
|
+
let internal: InstanceType<typeof useRest>;
|
|
24
|
+
|
|
25
|
+
async function call(collection: string, action: string, body?: any) {
|
|
26
|
+
const res = await fetch(`${url}/api/${TENANT}/collections/${collection}/${action}`, {
|
|
27
|
+
method: "POST",
|
|
28
|
+
headers: { "Content-Type": "application/json" },
|
|
29
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
30
|
+
});
|
|
31
|
+
return { status: res.status, body: (await res.json()) as any };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
beforeAll(async () => {
|
|
35
|
+
formatConfig({
|
|
36
|
+
server: { port: 4000 },
|
|
37
|
+
tenants: [{ id: TENANT, dir: "src", database: { uri: DB } }],
|
|
38
|
+
});
|
|
39
|
+
await syncTenants();
|
|
40
|
+
|
|
41
|
+
(cfg as any).collections = [
|
|
42
|
+
{
|
|
43
|
+
_tenant_: TENANT,
|
|
44
|
+
slug: "authors",
|
|
45
|
+
fields: [
|
|
46
|
+
{ name: "name", type: "string" },
|
|
47
|
+
{ name: "password", type: "string" },
|
|
48
|
+
{ name: "secret_token", type: "string" },
|
|
49
|
+
],
|
|
50
|
+
api: { access: { "*": true }, privateFields: ["password", /^secret_/] },
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
_tenant_: TENANT,
|
|
54
|
+
slug: "posts",
|
|
55
|
+
fields: [
|
|
56
|
+
{ name: "title", type: "string" },
|
|
57
|
+
// A public field that happens to match the *included* collection's
|
|
58
|
+
// matcher — it must survive: only the joined document is masked.
|
|
59
|
+
{ name: "secret_note", type: "string" },
|
|
60
|
+
{ name: "author", type: "relationship", relation: { to: "authors" } },
|
|
61
|
+
{ name: "reviewers", type: "relationship", relation: { to: "authors", hasMany: true } },
|
|
62
|
+
],
|
|
63
|
+
// The collection being queried declares no private field of its own:
|
|
64
|
+
// only the included one does.
|
|
65
|
+
api: { access: { "*": true } },
|
|
66
|
+
},
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
rest = new useRest({ internal: false, tenant_id: TENANT });
|
|
70
|
+
internal = new useRest({ internal: true, tenant_id: TENANT });
|
|
71
|
+
|
|
72
|
+
const app = createApp();
|
|
73
|
+
server = Bun.serve({ port: 0, fetch: app.fetch });
|
|
74
|
+
url = server.url.href.replace(/\/$/, "");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
afterAll(async () => {
|
|
78
|
+
for (const name of ["authors", "posts", "_locks_", "_audit_"]) {
|
|
79
|
+
try { await rest.db.collection(name).drop(); } catch (_) { /* first run */ }
|
|
80
|
+
}
|
|
81
|
+
server?.stop(true);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
async function seed() {
|
|
85
|
+
await rest.db.collection("authors").deleteMany({});
|
|
86
|
+
await rest.db.collection("posts").deleteMany({});
|
|
87
|
+
const author = await rest.insertOne("authors", {
|
|
88
|
+
name: "Ada",
|
|
89
|
+
password: "s3cret-hash",
|
|
90
|
+
secret_token: "tok_live_123",
|
|
91
|
+
});
|
|
92
|
+
const post = await rest.insertOne("posts", {
|
|
93
|
+
title: "Hello",
|
|
94
|
+
secret_note: "public-note",
|
|
95
|
+
author: author._id,
|
|
96
|
+
reviewers: [author._id],
|
|
97
|
+
});
|
|
98
|
+
return { author, post };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
describe("api.privateFields across a join", () => {
|
|
102
|
+
it("masks the included collection's private fields on `find`", async () => {
|
|
103
|
+
const { post } = await seed();
|
|
104
|
+
|
|
105
|
+
const res = await call("posts", "find", { params: { $match: { _id: post._id }, $include: ["author"] } });
|
|
106
|
+
|
|
107
|
+
expect(res.status).toBe(200);
|
|
108
|
+
const doc = res.body[0];
|
|
109
|
+
expect(doc.author.name).toBe("Ada");
|
|
110
|
+
expect(doc.author.password).toBeUndefined();
|
|
111
|
+
expect(doc.author.secret_token).toBeUndefined();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("masks them on `findOne` too", async () => {
|
|
115
|
+
const { post } = await seed();
|
|
116
|
+
|
|
117
|
+
const res = await call("posts", "findOne", { id: post._id, params: { $include: ["author"] } });
|
|
118
|
+
|
|
119
|
+
expect(res.status).toBe(200);
|
|
120
|
+
expect(res.body.author.name).toBe("Ada");
|
|
121
|
+
expect(res.body.author.password).toBeUndefined();
|
|
122
|
+
expect(res.body.author.secret_token).toBeUndefined();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("masks every element of a `hasMany` include", async () => {
|
|
126
|
+
const { post } = await seed();
|
|
127
|
+
|
|
128
|
+
const res = await call("posts", "find", { params: { $match: { _id: post._id }, $include: ["reviewers"] } });
|
|
129
|
+
|
|
130
|
+
expect(res.status).toBe(200);
|
|
131
|
+
const reviewers = res.body[0].reviewers;
|
|
132
|
+
expect(Array.isArray(reviewers)).toBe(true);
|
|
133
|
+
expect(reviewers[0].name).toBe("Ada");
|
|
134
|
+
expect(reviewers[0].password).toBeUndefined();
|
|
135
|
+
expect(reviewers[0].secret_token).toBeUndefined();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("masks them on a `$lookup` of an `aggregate` pipeline", async () => {
|
|
139
|
+
const { post } = await seed();
|
|
140
|
+
|
|
141
|
+
const res = await call("posts", "aggregate", {
|
|
142
|
+
pipeline: [
|
|
143
|
+
{ $match: { _id: post._id } },
|
|
144
|
+
{ $lookup: { from: "authors", localField: "author", foreignField: "_id", as: "author" } },
|
|
145
|
+
{ $unwind: "$author" },
|
|
146
|
+
],
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
expect(res.status).toBe(200);
|
|
150
|
+
const doc = res.body[0];
|
|
151
|
+
expect(doc.author.name).toBe("Ada");
|
|
152
|
+
expect(doc.author.password).toBeUndefined();
|
|
153
|
+
expect(doc.author.secret_token).toBeUndefined();
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("still masks the queried collection's own private fields", async () => {
|
|
157
|
+
await seed();
|
|
158
|
+
|
|
159
|
+
const res = await call("authors", "find", {});
|
|
160
|
+
|
|
161
|
+
expect(res.status).toBe(200);
|
|
162
|
+
expect(res.body[0].name).toBe("Ada");
|
|
163
|
+
expect(res.body[0].password).toBeUndefined();
|
|
164
|
+
expect(res.body[0].secret_token).toBeUndefined();
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("masks the join only — the queried collection's own matching field survives", async () => {
|
|
168
|
+
const { post } = await seed();
|
|
169
|
+
|
|
170
|
+
const res = await call("posts", "find", { params: { $match: { _id: post._id }, $include: ["author"] } });
|
|
171
|
+
|
|
172
|
+
expect(res.status).toBe(200);
|
|
173
|
+
expect(res.body[0].secret_note).toBe("public-note");
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
describe("api.privateFields and the DB query cache", () => {
|
|
178
|
+
it("does not mutate the cached result (an internal read still sees every field)", async () => {
|
|
179
|
+
const { post } = await seed();
|
|
180
|
+
const params = { $match: { _id: post._id }, $include: ["author"] };
|
|
181
|
+
|
|
182
|
+
const viaHttp = await call("posts", "find", { params, options: { useCache: true } });
|
|
183
|
+
expect(viaHttp.body[0].author.password).toBeUndefined();
|
|
184
|
+
|
|
185
|
+
// Same query, same (warm) cache entry, but through the trusted internal API:
|
|
186
|
+
// the masked fields must still be there — masking is a response concern.
|
|
187
|
+
const viaRest: any = await internal.find("posts", params, { useCache: true });
|
|
188
|
+
expect(viaRest[0].author.password).toBe("s3cret-hash");
|
|
189
|
+
expect(viaRest[0].author.secret_token).toBe("tok_live_123");
|
|
190
|
+
|
|
191
|
+
// And the HTTP layer stays consistent on the second, cached response.
|
|
192
|
+
const again = await call("posts", "find", { params, options: { useCache: true } });
|
|
193
|
+
expect(again.body[0].author.name).toBe("Ada");
|
|
194
|
+
expect(again.body[0].author.password).toBeUndefined();
|
|
195
|
+
});
|
|
196
|
+
});
|
|
@@ -21,6 +21,8 @@ beforeAll(async () => {
|
|
|
21
21
|
dir: "src",
|
|
22
22
|
database: { uri: SRC_DB },
|
|
23
23
|
replication: {
|
|
24
|
+
// These cases exercise the scan engine (file collections)
|
|
25
|
+
mode: "scan",
|
|
24
26
|
runOnBoot: false,
|
|
25
27
|
schedule: { interval: "1h" },
|
|
26
28
|
destinations: [{ id: "backup", uri: DEST_DB }],
|
|
@@ -5,5 +5,5 @@ export default define.Agent({
|
|
|
5
5
|
id: "support",
|
|
6
6
|
description: "Answers support questions.",
|
|
7
7
|
instructions: "You are a support agent.",
|
|
8
|
-
provider: { model: "claude-test",
|
|
8
|
+
provider: { model: "claude-test", protocol: "anthropic", apiKey: "fixture-key" },
|
|
9
9
|
});
|
|
@@ -9,7 +9,7 @@ export default define.Agent({
|
|
|
9
9
|
name: "Weather Agent",
|
|
10
10
|
description: "Answers weather questions with a tool call",
|
|
11
11
|
instructions: "You are a concise weather assistant.",
|
|
12
|
-
provider: { model: "test-model",
|
|
12
|
+
provider: { model: "test-model", protocol: "openai", apiKey: "fixture-key" },
|
|
13
13
|
tools: {
|
|
14
14
|
forecast: define.Tool({
|
|
15
15
|
id: "forecast",
|
|
@@ -13,7 +13,7 @@ export default define.Agent({
|
|
|
13
13
|
instructions: "You are a fixture agent.",
|
|
14
14
|
provider: {
|
|
15
15
|
model: "test-model",
|
|
16
|
-
|
|
16
|
+
protocol: "openai",
|
|
17
17
|
apiKey: "test-key",
|
|
18
18
|
baseUrl: Bun.env.AGENT_API_TEST_URL ?? "http://127.0.0.1:1",
|
|
19
19
|
options: { retries: 0 },
|
|
@@ -10,7 +10,7 @@ export default define.Agent({
|
|
|
10
10
|
instructions: "You are a plain agent.",
|
|
11
11
|
provider: {
|
|
12
12
|
model: "test-model",
|
|
13
|
-
|
|
13
|
+
protocol: "openai",
|
|
14
14
|
apiKey: "test-key",
|
|
15
15
|
baseUrl: Bun.env.AGENT_API_TEST_URL ?? "http://127.0.0.1:1",
|
|
16
16
|
options: { retries: 0 },
|
|
@@ -7,7 +7,7 @@ export default define.Agent({
|
|
|
7
7
|
instructions: "You are a private agent.",
|
|
8
8
|
provider: {
|
|
9
9
|
model: "test-model",
|
|
10
|
-
|
|
10
|
+
protocol: "openai",
|
|
11
11
|
apiKey: "test-key",
|
|
12
12
|
baseUrl: Bun.env.AGENT_API_TEST_URL ?? "http://127.0.0.1:1",
|
|
13
13
|
options: { retries: 0 },
|
|
@@ -18,6 +18,9 @@ await bootApp({
|
|
|
18
18
|
database: { uri: Bun.env.RO_SRC ?? "mongodb://localhost:27017/_RO_SRC" },
|
|
19
19
|
replication: {
|
|
20
20
|
enabled: true,
|
|
21
|
+
// Boot capabilities are under test here, not the replication engine:
|
|
22
|
+
// the scan mode keeps the assertions deterministic
|
|
23
|
+
mode: "scan",
|
|
21
24
|
runOnBoot: true,
|
|
22
25
|
schedule: { interval: "1h" },
|
|
23
26
|
destinations: [{ id: "backup", uri: Bun.env.RO_DST ?? "mongodb://localhost:27017/_RO_DST" }],
|
|
@@ -11,6 +11,6 @@ export default define.Agent({
|
|
|
11
11
|
id: "chat",
|
|
12
12
|
description: "Fixture agent whose memory lives in a tenant-named collection.",
|
|
13
13
|
instructions: "Fixture agent — no model call is ever made in these tests.",
|
|
14
|
-
provider: { model: "test-model",
|
|
14
|
+
provider: { model: "test-model", protocol: "openai", apiKey: "fixture" },
|
|
15
15
|
memory: new agents.memory.Mongo({ collection: AGENT_THREADS }),
|
|
16
16
|
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { define } from "../../../../index";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Fixture — an agent whose system prompt carries skills, declared every way a
|
|
5
|
+
* source can be: a glob of a folder, one file, a regex over the tenant, and the
|
|
6
|
+
* `SKILL.md` directory layout.
|
|
7
|
+
*
|
|
8
|
+
* The last two patterns overlap on purpose (`./docs/*.md` and the `docs` regex):
|
|
9
|
+
* a file matched twice is one skill.
|
|
10
|
+
*/
|
|
11
|
+
export default define.Agent({
|
|
12
|
+
id: "docs",
|
|
13
|
+
description: "Answers from the tenant's own documentation.",
|
|
14
|
+
instructions: "Answer from the documentation you were given.",
|
|
15
|
+
provider: { model: "claude-test", protocol: "anthropic", apiKey: "fixture-key" },
|
|
16
|
+
skills: [
|
|
17
|
+
"./docs/*.md",
|
|
18
|
+
"./guide/ml.md",
|
|
19
|
+
/^docs\/.*\.md$/,
|
|
20
|
+
/^runbooks\/.*$/,
|
|
21
|
+
"./guides/**/SKILL.md",
|
|
22
|
+
],
|
|
23
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { define } from "../../../../index";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Fixture — an agent whose skill is malformed (`broken/no-name.md` has no `name`).
|
|
5
|
+
*
|
|
6
|
+
* A broken skill refuses **this agent** at load, and must not hide the others.
|
|
7
|
+
*/
|
|
8
|
+
export default define.Agent({
|
|
9
|
+
id: "incomplete",
|
|
10
|
+
description: "Carries a skill that does not follow the format.",
|
|
11
|
+
instructions: "Nothing to see here.",
|
|
12
|
+
provider: { model: "claude-test", protocol: "anthropic", apiKey: "fixture-key" },
|
|
13
|
+
skills: ["./broken/no-name.md"],
|
|
14
|
+
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: long-compat
|
|
3
|
+
description: A skill whose compatibility line is too long.
|
|
4
|
+
compatibility: bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
Body.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: long-description
|
|
3
|
+
description: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Body.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: pdf-forms
|
|
3
|
+
description: Fill, flatten and merge PDF forms. Use it for any AcroForm work.
|
|
4
|
+
license: MIT
|
|
5
|
+
compatibility: Needs the `pdftk` binary on the host.
|
|
6
|
+
metadata:
|
|
7
|
+
owner: platform
|
|
8
|
+
reviewed: 2026-09-24
|
|
9
|
+
allowed-tools: read_pdf write_pdf
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# Filling a form
|
|
13
|
+
|
|
14
|
+
1. Dump the fields: `pdftk form.pdf dump_data_fields`.
|
|
15
|
+
2. Build the FDF, then fill it: `pdftk form.pdf fill_form data.fdf output out.pdf`.
|
|
16
|
+
3. Flatten: `pdftk out.pdf output flat.pdf flatten`.
|
|
17
|
+
|
|
18
|
+
Never flatten before the signature step — it makes the form read-only.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ml-conventions
|
|
3
|
+
description: The tenant's ML conventions — feature naming, training splits, review gates.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Conventions
|
|
7
|
+
|
|
8
|
+
- Features are `snake_case`, prefixed by their source (`crm_`, `billing_`).
|
|
9
|
+
- A model only ships with a held-out split and a written evaluation.
|