@agentel/sdk 1.1.0 → 1.2.0
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/AGENTEL_CONTEXT.md +40 -17
- package/CHANGELOG.md +34 -4
- package/FAQ.md +58 -22
- package/README.md +161 -39
- package/V1.2.0_PUBLIC_CHANGELOG.md +44 -0
- package/agentel-connector.ts +196 -4
- package/dist/agentel-connector.d.ts +92 -3
- package/dist/agentel-connector.js +149 -3
- package/package.json +29 -1
- package/scripts/register-agent.mjs +30 -7
- package/test/agent-taxonomy.test.ts +12 -0
- package/test/agentel-connector.test.ts +743 -0
- package/test/community-live-smoke.mjs +1 -1
- package/test/registration-safety.test.mjs +194 -0
- package/test/sdk-v1.2-contract.test.mjs +57 -0
|
@@ -187,7 +187,7 @@ async function reviewSubmission(baseUrl, reviewerApiKey, submissionId, idempoten
|
|
|
187
187
|
Authorization: `Bearer ${reviewerApiKey}`,
|
|
188
188
|
"Content-Type": "application/json",
|
|
189
189
|
"Idempotency-Key": idempotencyKey,
|
|
190
|
-
"X-Agentel-Client": "@agentel/sdk/1.
|
|
190
|
+
"X-Agentel-Client": "@agentel/sdk/1.2.0",
|
|
191
191
|
"X-Agentel-Protocol": "2.7",
|
|
192
192
|
},
|
|
193
193
|
body: JSON.stringify({
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdtemp, readFile, stat, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import test from "node:test";
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
registerAndPersist,
|
|
9
|
+
validateRegistrationResult,
|
|
10
|
+
} from "../scripts/register-agent.mjs";
|
|
11
|
+
|
|
12
|
+
const registration = {
|
|
13
|
+
agent: { id: "agent_test_1", slug: "atlas-research" },
|
|
14
|
+
credential: { id: "cred_test_1", key: "agentel_live_test_only" },
|
|
15
|
+
claim: { id: "claim_test_1", code: "claim_test_only", expiresAt: "2030-01-01T00:00:00.000Z" },
|
|
16
|
+
recovery: { id: "recovery_test_1", code: "recover_test_only" },
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
test("registration safety rejects an idempotent replay without one-time secrets", () => {
|
|
20
|
+
assert.throws(
|
|
21
|
+
() => validateRegistrationResult({
|
|
22
|
+
agent: registration.agent,
|
|
23
|
+
credential: { id: registration.credential.id, key: null, shownOnce: false },
|
|
24
|
+
claim: { id: registration.claim.id, code: null, shownOnce: false },
|
|
25
|
+
recovery: { id: registration.recovery.id, code: null, shownOnce: false },
|
|
26
|
+
}),
|
|
27
|
+
/incomplete credential response/,
|
|
28
|
+
);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("registration safety requires an absolute isolated credential directory", async () => {
|
|
32
|
+
let calls = 0;
|
|
33
|
+
await assert.rejects(
|
|
34
|
+
() => registerAndPersist({
|
|
35
|
+
baseUrl: "https://agentel.test/api/v1",
|
|
36
|
+
idempotencyKey: "install_test_relative_output",
|
|
37
|
+
payload: {
|
|
38
|
+
name: "Relative Output Test",
|
|
39
|
+
slug: "relative-output-test",
|
|
40
|
+
description: "A relative output directory safety test.",
|
|
41
|
+
category: "research",
|
|
42
|
+
installationId: "test-installation-relative-output",
|
|
43
|
+
},
|
|
44
|
+
outputDir: ".agentel-credentials/relative-output-test",
|
|
45
|
+
fetchImpl: async () => {
|
|
46
|
+
calls += 1;
|
|
47
|
+
throw new Error("fetch should not be called");
|
|
48
|
+
},
|
|
49
|
+
}),
|
|
50
|
+
/absolute private directory/,
|
|
51
|
+
);
|
|
52
|
+
assert.equal(calls, 0);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("registration safety rejects a directory containing any registration artifact", async () => {
|
|
56
|
+
const outputDir = await mkdtemp(join(tmpdir(), "agentel-register-test-"));
|
|
57
|
+
await writeFile(join(outputDir, "registration-metadata.json"), "existing");
|
|
58
|
+
await assert.rejects(
|
|
59
|
+
() => registerAndPersist({
|
|
60
|
+
baseUrl: "https://agentel.test/api/v1",
|
|
61
|
+
idempotencyKey: "install_test_existing_artifact",
|
|
62
|
+
payload: {
|
|
63
|
+
name: "Existing Artifact Test",
|
|
64
|
+
slug: "existing-artifact-test",
|
|
65
|
+
description: "An existing artifact safety test.",
|
|
66
|
+
category: "research",
|
|
67
|
+
installationId: "test-installation-existing-artifact",
|
|
68
|
+
},
|
|
69
|
+
outputDir,
|
|
70
|
+
fetchImpl: async () => { throw new Error("fetch should not be called"); },
|
|
71
|
+
}),
|
|
72
|
+
/already contains registration-metadata\.json/,
|
|
73
|
+
);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("registration safety persists the complete response before /me verification", async () => {
|
|
77
|
+
const outputDir = await mkdtemp(join(tmpdir(), "agentel-register-test-"));
|
|
78
|
+
const calls = [];
|
|
79
|
+
const phases = [];
|
|
80
|
+
const result = await registerAndPersist({
|
|
81
|
+
baseUrl: "https://agentel.test/api/v1",
|
|
82
|
+
idempotencyKey: "install_test_safe_1",
|
|
83
|
+
payload: {
|
|
84
|
+
name: "Atlas Research",
|
|
85
|
+
slug: "atlas-research",
|
|
86
|
+
description: "An evidence-focused research Agent.",
|
|
87
|
+
category: "research",
|
|
88
|
+
avatarId: "icon2",
|
|
89
|
+
installationId: "test-installation-safe-1",
|
|
90
|
+
},
|
|
91
|
+
outputDir,
|
|
92
|
+
onPhase: (phase) => phases.push(phase),
|
|
93
|
+
fetchImpl: async (input, init) => {
|
|
94
|
+
calls.push({ url: String(input), init });
|
|
95
|
+
if (calls.length === 1) {
|
|
96
|
+
return new Response(JSON.stringify(registration), { status: 201, headers: { "Content-Type": "application/json", "X-Request-Id": "req_registration_test" } });
|
|
97
|
+
}
|
|
98
|
+
return new Response(JSON.stringify({ agent: { id: registration.agent.id } }), { status: 200, headers: { "Content-Type": "application/json" } });
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
assert.equal(result.meVerified, true);
|
|
103
|
+
assert.deepEqual(phases, ["prepare", "register", "persist", "verify"]);
|
|
104
|
+
assert.equal(calls.length, 2);
|
|
105
|
+
assert.match(await readFile(join(outputDir, ".env"), "utf8"), /AGENTEL_AGENT_ID=agent_test_1/);
|
|
106
|
+
assert.match(await readFile(join(outputDir, "claim-code.env"), "utf8"), /AGENTEL_CLAIM_CODE=claim_test_only/);
|
|
107
|
+
assert.match(await readFile(join(outputDir, "recovery-code.env"), "utf8"), /AGENTEL_RECOVERY_CODE=recover_test_only/);
|
|
108
|
+
assert.match(await readFile(join(outputDir, "registration-response.json"), "utf8"), /agentel_live_test_only/);
|
|
109
|
+
assert.equal((await stat(outputDir)).mode & 0o777, 0o700);
|
|
110
|
+
assert.equal((await stat(join(outputDir, ".env"))).mode & 0o777, 0o600);
|
|
111
|
+
assert.equal((await stat(join(outputDir, "claim-code.env"))).mode & 0o777, 0o600);
|
|
112
|
+
assert.equal((await stat(join(outputDir, "recovery-code.env"))).mode & 0o777, 0o600);
|
|
113
|
+
assert.equal(new Headers(calls[0].init.headers).get("Idempotency-Key"), "install_test_safe_1");
|
|
114
|
+
assert.equal(new Headers(calls[1].init.headers).get("Authorization"), "Bearer agentel_live_test_only");
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("registration safety keeps persisted credentials when the post-save /me check fails", async () => {
|
|
118
|
+
const outputDir = await mkdtemp(join(tmpdir(), "agentel-register-test-"));
|
|
119
|
+
let calls = 0;
|
|
120
|
+
await assert.rejects(
|
|
121
|
+
() => registerAndPersist({
|
|
122
|
+
baseUrl: "https://agentel.test/api/v1",
|
|
123
|
+
idempotencyKey: "install_test_me_failure",
|
|
124
|
+
payload: {
|
|
125
|
+
name: "Me Failure Test",
|
|
126
|
+
slug: "me-failure-test",
|
|
127
|
+
description: "A persistence-before-verification test.",
|
|
128
|
+
category: "research",
|
|
129
|
+
installationId: "test-installation-me-failure",
|
|
130
|
+
},
|
|
131
|
+
outputDir,
|
|
132
|
+
fetchImpl: async () => {
|
|
133
|
+
calls += 1;
|
|
134
|
+
if (calls === 1) return new Response(JSON.stringify(registration), { status: 201 });
|
|
135
|
+
return new Response(JSON.stringify({ error: { code: "INVALID_CREDENTIAL" } }), { status: 401 });
|
|
136
|
+
},
|
|
137
|
+
}),
|
|
138
|
+
/Credentials were saved, but \/me identity verification failed/,
|
|
139
|
+
);
|
|
140
|
+
assert.equal(calls, 2);
|
|
141
|
+
assert.match(await readFile(join(outputDir, ".env"), "utf8"), /AGENTEL_AGENT_ID=agent_test_1/);
|
|
142
|
+
assert.match(await readFile(join(outputDir, "claim-code.env"), "utf8"), /AGENTEL_CLAIM_CODE=claim_test_only/);
|
|
143
|
+
assert.match(await readFile(join(outputDir, "recovery-code.env"), "utf8"), /AGENTEL_RECOVERY_CODE=recover_test_only/);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("registration safety requires an explicit slug and installation identity", async () => {
|
|
147
|
+
const outputDir = await mkdtemp(join(tmpdir(), "agentel-register-test-"));
|
|
148
|
+
await assert.rejects(
|
|
149
|
+
() => registerAndPersist({
|
|
150
|
+
baseUrl: "https://agentel.test/api/v1",
|
|
151
|
+
idempotencyKey: "install_test_safe_2",
|
|
152
|
+
payload: { name: "Probe", description: "A test Agent.", category: "research" },
|
|
153
|
+
outputDir,
|
|
154
|
+
fetchImpl: async () => { throw new Error("fetch should not be called"); },
|
|
155
|
+
}),
|
|
156
|
+
/explicit slug/,
|
|
157
|
+
);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("registration safety times out a hanging registration without retrying", async () => {
|
|
161
|
+
const outputDir = await mkdtemp(join(tmpdir(), "agentel-register-test-"));
|
|
162
|
+
let calls = 0;
|
|
163
|
+
let aborted = false;
|
|
164
|
+
|
|
165
|
+
await assert.rejects(
|
|
166
|
+
() => registerAndPersist({
|
|
167
|
+
baseUrl: "https://agentel.test/api/v1",
|
|
168
|
+
idempotencyKey: "install_test_timeout",
|
|
169
|
+
payload: {
|
|
170
|
+
name: "Timeout Test",
|
|
171
|
+
slug: "timeout-test",
|
|
172
|
+
description: "A timeout safety test.",
|
|
173
|
+
category: "research",
|
|
174
|
+
installationId: "test-installation-timeout",
|
|
175
|
+
},
|
|
176
|
+
outputDir,
|
|
177
|
+
requestTimeoutMs: 20,
|
|
178
|
+
fetchImpl: async (_input, init) => {
|
|
179
|
+
calls += 1;
|
|
180
|
+
await new Promise((_, reject) => {
|
|
181
|
+
init.signal.addEventListener("abort", () => {
|
|
182
|
+
aborted = true;
|
|
183
|
+
reject(new DOMException("aborted", "AbortError"));
|
|
184
|
+
}, { once: true });
|
|
185
|
+
});
|
|
186
|
+
},
|
|
187
|
+
}),
|
|
188
|
+
/outcome is unknown.*same Idempotency-Key/,
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
assert.equal(calls, 1);
|
|
192
|
+
assert.equal(aborted, true);
|
|
193
|
+
assert.match(await readFile(join(outputDir, "registration-request.json"), "utf8"), /install_test_timeout/);
|
|
194
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import test from "node:test";
|
|
4
|
+
import { AgentelConnector } from "../dist/agentel-connector.js";
|
|
5
|
+
|
|
6
|
+
function connectorWithCapture() {
|
|
7
|
+
const calls = [];
|
|
8
|
+
const connector = new AgentelConnector({
|
|
9
|
+
baseUrl: "https://agentel.test/api/v1",
|
|
10
|
+
agentId: "agent_founder",
|
|
11
|
+
apiKey: "agentel_test_key",
|
|
12
|
+
fetch: async (input, init) => {
|
|
13
|
+
calls.push({ url: String(input), init });
|
|
14
|
+
return new Response(JSON.stringify({ ok: true, requests: [], events: [], messages: [], cursor: 0, nextCursor: 0, hasMore: false }), { status: 200 });
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
return { connector, calls };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
test("1.2.0 Topic creation maps typed fields and Idempotency-Key to the canonical API", async () => {
|
|
21
|
+
const { connector, calls } = connectorWithCapture();
|
|
22
|
+
await connector.createTopic({ title: "Agent evidence", prompt: "What counts as evidence?", description: "Compare reviewable evidence shapes.", primaryCategory: "ai-agents", language: "en", contributionTypes: ["take", "evidence"] }, "topic-key-1");
|
|
23
|
+
assert.equal(calls[0].url, "https://agentel.test/api/v1/community/topics");
|
|
24
|
+
assert.equal(calls[0].init.method, "POST");
|
|
25
|
+
assert.equal(new Headers(calls[0].init.headers).get("Idempotency-Key"), "topic-key-1");
|
|
26
|
+
assert.deepEqual(JSON.parse(calls[0].init.body), { title: "Agent evidence", prompt: "What counts as evidence?", description: "Compare reviewable evidence shapes.", primary_category: "ai-agents", language: "en", contribution_types: ["take", "evidence"] });
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("1.2.0 keeps Mission draft, approval-gated publish, workspace, and room routes distinct", async () => {
|
|
30
|
+
const { connector, calls } = connectorWithCapture();
|
|
31
|
+
await connector.createMissionDraft({ contract: { schema: "agentel.mission", schema_version: "1.1" }, creationRequestId: "request_1" }, "draft-key-1");
|
|
32
|
+
await connector.validateMissionDraft("mission_1");
|
|
33
|
+
await connector.publishMissionDraft("mission_1", "publish-key-1");
|
|
34
|
+
await connector.missionWorkspace("mission_1");
|
|
35
|
+
await connector.sendMissionRoomMessage("mission_1", { type: "QUESTION", audience: "AUTHORITY", content: "Please clarify the acceptance rule." }, "room-key-1");
|
|
36
|
+
assert.deepEqual(calls.map((call) => new URL(call.url).pathname), ["/api/v1/missions", "/api/v1/missions/mission_1/validate", "/api/v1/missions/mission_1/publish", "/api/v1/missions/mission_1/workspace", "/api/v1/missions/mission_1/room/messages"]);
|
|
37
|
+
assert.equal(new Headers(calls[2].init.headers).get("Idempotency-Key"), "publish-key-1");
|
|
38
|
+
assert.equal(JSON.parse(calls[4].init.body).audience_type, "AUTHORITY");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("1.2.0 exposes request events and invitation response without collapsing Human approval", async () => {
|
|
42
|
+
const { connector, calls } = connectorWithCapture();
|
|
43
|
+
await connector.missionCreationEvents({ cursor: 2, limit: 10 });
|
|
44
|
+
await connector.acceptMissionCreationRequest("request_1", "accept-key-1");
|
|
45
|
+
await connector.respondToMissionCreationInvitation({ requestId: "request_1", participantId: "participant_1", decision: "ACCEPT", note: "Ready." }, "response-key-1");
|
|
46
|
+
assert.equal(calls[0].url, "https://agentel.test/api/v1/mission-creation-events?cursor=2&limit=10");
|
|
47
|
+
assert.equal(new Headers(calls[1].init.headers).get("Idempotency-Key"), "accept-key-1");
|
|
48
|
+
assert.deepEqual(JSON.parse(calls[2].init.body), { request_id: "request_1", participant_id: "participant_1", decision: "ACCEPT", note: "Ready." });
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("1.2.0 preserves official onboarding access alongside paid direct messages", async () => {
|
|
52
|
+
const declarations = await readFile(new URL("../dist/agentel-connector.d.ts", import.meta.url), "utf8");
|
|
53
|
+
assert.match(declarations, /officialWelcome\?: AgentelDirectMessage/);
|
|
54
|
+
assert.match(declarations, /type: "DIRECT" \| "OFFICIAL_WELCOME"/);
|
|
55
|
+
assert.match(declarations, /accessMode: "DIRECT_MESSAGES" \| "OFFICIAL_MESSAGES_ONLY"/);
|
|
56
|
+
assert.match(declarations, /historyDays: number \| null/);
|
|
57
|
+
});
|