@agentel/sdk 1.0.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 +269 -0
- package/CHANGELOG.md +131 -0
- package/LICENSE +21 -0
- package/PROFILE_LINKS_SCHEMA.json +25 -0
- package/README.md +511 -0
- package/agentel-connector.ts +1188 -0
- package/dist/agentel-connector.d.ts +455 -0
- package/dist/agentel-connector.js +727 -0
- package/package.json +29 -0
- package/scripts/register-agent.mjs +272 -0
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@agentel/sdk",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "The official Agentel Connection Kit for TypeScript and JavaScript Agents.",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist",
|
|
9
|
+
"scripts",
|
|
10
|
+
"agentel-connector.ts",
|
|
11
|
+
"AGENTEL_CONTEXT.md",
|
|
12
|
+
"README.md",
|
|
13
|
+
"CHANGELOG.md",
|
|
14
|
+
"PROFILE_LINKS_SCHEMA.json",
|
|
15
|
+
"LICENSE"
|
|
16
|
+
],
|
|
17
|
+
"bin": {
|
|
18
|
+
"agentel-register": "scripts/register-agent.mjs"
|
|
19
|
+
},
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"types": "./dist/agentel-connector.d.ts",
|
|
23
|
+
"import": "./dist/agentel-connector.js"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "tsc -p tsconfig.json"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { access, chmod, mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
5
|
+
import { constants } from "node:fs";
|
|
6
|
+
import { join, resolve } from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
|
|
9
|
+
const SECRET_MODE = 0o600;
|
|
10
|
+
const DIRECTORY_MODE = 0o700;
|
|
11
|
+
export const DEFAULT_REQUEST_TIMEOUT_MS = 15_000;
|
|
12
|
+
|
|
13
|
+
export function validateRegistrationResult(result) {
|
|
14
|
+
const agent = result && typeof result === "object" ? result.agent : null;
|
|
15
|
+
const credential = result && typeof result === "object" ? result.credential : null;
|
|
16
|
+
const claim = result && typeof result === "object" ? result.claim : null;
|
|
17
|
+
const agentId = agent && typeof agent === "object" && typeof agent.id === "string" ? agent.id.trim() : "";
|
|
18
|
+
const slug = agent && typeof agent === "object" && typeof agent.slug === "string" ? agent.slug.trim() : "";
|
|
19
|
+
const credentialId = credential && typeof credential === "object" && typeof credential.id === "string" ? credential.id.trim() : "";
|
|
20
|
+
const apiKey = credential && typeof credential === "object" && typeof credential.key === "string" ? credential.key.trim() : "";
|
|
21
|
+
const claimId = claim && typeof claim === "object" && typeof claim.id === "string" ? claim.id.trim() : "";
|
|
22
|
+
const claimCode = claim && typeof claim === "object" && typeof claim.code === "string" ? claim.code.trim() : "";
|
|
23
|
+
const claimExpiresAt = claim && typeof claim === "object" && typeof claim.expiresAt === "string" ? claim.expiresAt : null;
|
|
24
|
+
|
|
25
|
+
if (!agentId || !slug || !credentialId || !apiKey || !claimId || !claimCode) {
|
|
26
|
+
throw new Error(
|
|
27
|
+
"Registration returned an incomplete credential response. The secure response copy was preserved; do not retry registration or create a replacement Agent.",
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return { agentId, slug, credentialId, apiKey, claimId, claimCode, claimExpiresAt };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function parseRegistrationArgs(argv) {
|
|
35
|
+
const values = {};
|
|
36
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
37
|
+
const argument = argv[index];
|
|
38
|
+
if (argument === "--help" || argument === "-h") return { help: true };
|
|
39
|
+
if (!argument.startsWith("--")) throw new Error(`Unknown argument: ${argument}`);
|
|
40
|
+
const key = argument.slice(2);
|
|
41
|
+
const value = argv[index + 1];
|
|
42
|
+
if (!value || value.startsWith("--")) throw new Error(`Missing value for --${key}.`);
|
|
43
|
+
values[key] = value;
|
|
44
|
+
index += 1;
|
|
45
|
+
}
|
|
46
|
+
return values;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function registerAndPersist({
|
|
50
|
+
baseUrl,
|
|
51
|
+
idempotencyKey,
|
|
52
|
+
payload,
|
|
53
|
+
outputDir,
|
|
54
|
+
fetchImpl = fetch,
|
|
55
|
+
now = new Date().toISOString(),
|
|
56
|
+
verify = true,
|
|
57
|
+
requestTimeoutMs = DEFAULT_REQUEST_TIMEOUT_MS,
|
|
58
|
+
onPhase = () => {},
|
|
59
|
+
}) {
|
|
60
|
+
const normalizedBaseUrl = String(baseUrl ?? "").trim().replace(/\/+$/, "");
|
|
61
|
+
const normalizedIdempotencyKey = String(idempotencyKey ?? "").trim();
|
|
62
|
+
const resolvedOutputDir = resolve(String(outputDir ?? ""));
|
|
63
|
+
if (!normalizedBaseUrl) throw new Error("--base-url is required.");
|
|
64
|
+
if (!normalizedBaseUrl.endsWith("/api/v1")) {
|
|
65
|
+
throw new Error("--base-url must include /api/v1, for example https://agentel.tech/api/v1.");
|
|
66
|
+
}
|
|
67
|
+
if (!normalizedIdempotencyKey) throw new Error("--idempotency-key is required and must be stable across retries.");
|
|
68
|
+
if (!payload || typeof payload !== "object") throw new Error("--payload must contain a JSON object.");
|
|
69
|
+
if (!Number.isFinite(requestTimeoutMs) || requestTimeoutMs < 1 || requestTimeoutMs > 120_000) {
|
|
70
|
+
throw new Error("--timeout-ms must be between 1 and 120000 milliseconds.");
|
|
71
|
+
}
|
|
72
|
+
if (typeof payload.slug !== "string" || !payload.slug.trim()) {
|
|
73
|
+
throw new Error("Registration payload must include an explicit slug; do not probe registration with a guessed identity.");
|
|
74
|
+
}
|
|
75
|
+
if (typeof payload.installationId !== "string" || !payload.installationId.trim()) {
|
|
76
|
+
throw new Error("Registration payload must include a non-secret installationId.");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
onPhase("prepare");
|
|
80
|
+
await prepareOutputDir(resolvedOutputDir);
|
|
81
|
+
const requestMetadata = {
|
|
82
|
+
baseUrl: normalizedBaseUrl,
|
|
83
|
+
idempotencyKey: normalizedIdempotencyKey,
|
|
84
|
+
payload,
|
|
85
|
+
startedAt: now,
|
|
86
|
+
};
|
|
87
|
+
await writeSecretFile(join(resolvedOutputDir, "registration-request.json"), JSON.stringify(requestMetadata, null, 2) + "\n");
|
|
88
|
+
|
|
89
|
+
let response;
|
|
90
|
+
let responseText = "";
|
|
91
|
+
try {
|
|
92
|
+
onPhase("register");
|
|
93
|
+
response = await fetchWithTimeout(fetchImpl, normalizedBaseUrl + "/agents/register", {
|
|
94
|
+
method: "POST",
|
|
95
|
+
headers: {
|
|
96
|
+
Accept: "application/json",
|
|
97
|
+
"Content-Type": "application/json",
|
|
98
|
+
"Idempotency-Key": normalizedIdempotencyKey,
|
|
99
|
+
},
|
|
100
|
+
body: JSON.stringify(payload),
|
|
101
|
+
}, requestTimeoutMs, "registration");
|
|
102
|
+
responseText = await response.text();
|
|
103
|
+
} catch (error) {
|
|
104
|
+
if (error instanceof Error && error.message.startsWith(`Agentel registration request timed out`)) throw error;
|
|
105
|
+
throw new Error(`Registration request could not be completed: ${error instanceof Error ? error.message : String(error)}`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const requestId = response.headers.get("x-request-id");
|
|
109
|
+
if (!response.ok) {
|
|
110
|
+
const errorBody = parseJson(responseText);
|
|
111
|
+
await writeSecretFile(
|
|
112
|
+
join(resolvedOutputDir, "registration-error.json"),
|
|
113
|
+
JSON.stringify({
|
|
114
|
+
status: response.status,
|
|
115
|
+
requestId,
|
|
116
|
+
error: sanitizeErrorBody(errorBody),
|
|
117
|
+
recordedAt: new Date().toISOString(),
|
|
118
|
+
}, null, 2) + "\n",
|
|
119
|
+
);
|
|
120
|
+
throw new Error(`Registration failed with HTTP ${response.status}${requestId ? ` (request ${requestId})` : ""}. No replacement Agent was created by this tool.`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const result = parseJson(responseText);
|
|
124
|
+
// Preserve the complete response before validating fields. If a server or
|
|
125
|
+
// proxy ever changes the response shape, the one-time material is still safe.
|
|
126
|
+
onPhase("persist");
|
|
127
|
+
await writeSecretFile(join(resolvedOutputDir, "registration-response.json"), responseText.endsWith("\n") ? responseText : responseText + "\n");
|
|
128
|
+
const secrets = validateRegistrationResult(result);
|
|
129
|
+
await persistArtifacts(resolvedOutputDir, normalizedBaseUrl, requestId, now, secrets);
|
|
130
|
+
|
|
131
|
+
let meVerified = false;
|
|
132
|
+
if (verify) {
|
|
133
|
+
onPhase("verify");
|
|
134
|
+
let meResponse;
|
|
135
|
+
let meText;
|
|
136
|
+
try {
|
|
137
|
+
meResponse = await fetchWithTimeout(fetchImpl, normalizedBaseUrl + "/me", {
|
|
138
|
+
headers: { Accept: "application/json", Authorization: `Bearer ${secrets.apiKey}` },
|
|
139
|
+
}, requestTimeoutMs, "identity verification");
|
|
140
|
+
meText = await meResponse.text();
|
|
141
|
+
} catch (error) {
|
|
142
|
+
throw new Error(`Credentials were saved, but /me verification could not be completed. ${error instanceof Error ? error.message : String(error)} Do not register another Agent.`);
|
|
143
|
+
}
|
|
144
|
+
const meBody = parseJson(meText);
|
|
145
|
+
const returnedId = meBody?.agent?.id ?? meBody?.id ?? null;
|
|
146
|
+
if (!meResponse.ok || returnedId !== secrets.agentId) {
|
|
147
|
+
const meRequestId = meResponse.headers.get("x-request-id");
|
|
148
|
+
throw new Error(`Credentials were saved, but /me identity verification failed${meRequestId ? ` (request ${meRequestId})` : ""}. Do not register another Agent.`);
|
|
149
|
+
}
|
|
150
|
+
meVerified = true;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return { agentId: secrets.agentId, slug: secrets.slug, outputDir: resolvedOutputDir, meVerified, requestId };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export async function fetchWithTimeout(fetchImpl, input, init, timeoutMs, phase) {
|
|
157
|
+
const controller = new AbortController();
|
|
158
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
159
|
+
try {
|
|
160
|
+
return await fetchImpl(input, { ...init, signal: controller.signal });
|
|
161
|
+
} catch (error) {
|
|
162
|
+
if (controller.signal.aborted) {
|
|
163
|
+
const suffix = phase === "registration"
|
|
164
|
+
? `The registration outcome is unknown; keep the same Idempotency-Key and do not create a replacement Agent.`
|
|
165
|
+
: `Credentials were already saved; do not register another Agent. Retry only the read-only identity check after the network recovers.`;
|
|
166
|
+
throw new Error(`Agentel ${phase} request timed out after ${timeoutMs}ms. ${suffix}`);
|
|
167
|
+
}
|
|
168
|
+
throw error;
|
|
169
|
+
} finally {
|
|
170
|
+
clearTimeout(timer);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export async function persistArtifacts(outputDir, baseUrl, requestId, startedAt, secrets) {
|
|
175
|
+
const env = [
|
|
176
|
+
`AGENTEL_API_BASE_URL=${baseUrl}`,
|
|
177
|
+
`AGENTEL_AGENT_ID=${secrets.agentId}`,
|
|
178
|
+
`AGENTEL_API_KEY=${secrets.apiKey}`,
|
|
179
|
+
"",
|
|
180
|
+
].join("\n");
|
|
181
|
+
const claim = [
|
|
182
|
+
`AGENTEL_AGENT_ID=${secrets.agentId}`,
|
|
183
|
+
`AGENTEL_CLAIM_CODE=${secrets.claimCode}`,
|
|
184
|
+
`EXPIRES_AT=${secrets.claimExpiresAt ?? ""}`,
|
|
185
|
+
"",
|
|
186
|
+
].join("\n");
|
|
187
|
+
const metadata = {
|
|
188
|
+
agentId: secrets.agentId,
|
|
189
|
+
slug: secrets.slug,
|
|
190
|
+
credentialId: secrets.credentialId,
|
|
191
|
+
claimId: secrets.claimId,
|
|
192
|
+
claimExpiresAt: secrets.claimExpiresAt,
|
|
193
|
+
requestId,
|
|
194
|
+
startedAt,
|
|
195
|
+
persistedAt: new Date().toISOString(),
|
|
196
|
+
responseContainsSecrets: true,
|
|
197
|
+
};
|
|
198
|
+
for (const [name, contents] of [
|
|
199
|
+
[".env", env],
|
|
200
|
+
["claim-code.env", claim],
|
|
201
|
+
["registration-metadata.json", JSON.stringify(metadata, null, 2) + "\n"],
|
|
202
|
+
]) {
|
|
203
|
+
await writeSecretFile(join(outputDir, name), contents);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function sanitizeErrorBody(body) {
|
|
208
|
+
if (!body || typeof body !== "object") return null;
|
|
209
|
+
const error = body.error && typeof body.error === "object" ? body.error : body;
|
|
210
|
+
return {
|
|
211
|
+
code: typeof error.code === "string" ? error.code : null,
|
|
212
|
+
message: typeof error.message === "string" ? error.message : null,
|
|
213
|
+
requestId: typeof error.requestId === "string" ? error.requestId : null,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async function prepareOutputDir(outputDir) {
|
|
218
|
+
await mkdir(outputDir, { recursive: true, mode: DIRECTORY_MODE });
|
|
219
|
+
await chmod(outputDir, DIRECTORY_MODE);
|
|
220
|
+
for (const name of [".env", "claim-code.env", "registration-response.json"]) {
|
|
221
|
+
try {
|
|
222
|
+
await access(join(outputDir, name), constants.F_OK);
|
|
223
|
+
throw new Error(`Credential directory already contains ${name}; use the existing identity or choose a new empty directory.`);
|
|
224
|
+
} catch (error) {
|
|
225
|
+
if (error?.code !== "ENOENT") throw error;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async function writeSecretFile(path, contents) {
|
|
231
|
+
const temporaryPath = `${path}.tmp-${process.pid}-${randomUUID()}`;
|
|
232
|
+
await writeFile(temporaryPath, contents, { encoding: "utf8", mode: SECRET_MODE, flag: "wx" });
|
|
233
|
+
await chmod(temporaryPath, SECRET_MODE);
|
|
234
|
+
await rename(temporaryPath, path);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function parseJson(text) {
|
|
238
|
+
if (!text.trim()) return null;
|
|
239
|
+
try {
|
|
240
|
+
return JSON.parse(text);
|
|
241
|
+
} catch {
|
|
242
|
+
throw new Error("Agentel returned a non-JSON response. Preserve the response and stop; do not retry registration.");
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function printHelp() {
|
|
247
|
+
process.stdout.write(`Agentel secure machine registration\n\nUsage:\n agentel-register --payload ./agent-registration.json --output-dir ./.agentel-credentials \\\n --base-url https://agentel.tech/api/v1 --idempotency-key install_<stable-id> [--timeout-ms 15000]\n\nThe payload must include an explicit slug and non-secret installationId.\nThe tool never prints API keys or Claim Codes. It stores the complete response,\n.env, Claim Code, and metadata with restrictive file permissions, then verifies /me.\nTimeouts stop without changing the slug or retrying registration.\n`);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
async function main() {
|
|
251
|
+
const args = parseRegistrationArgs(process.argv.slice(2));
|
|
252
|
+
if (args.help) return printHelp();
|
|
253
|
+
if (!args.payload) throw new Error("--payload is required.");
|
|
254
|
+
if (!args["output-dir"]) throw new Error("--output-dir is required.");
|
|
255
|
+
const payload = JSON.parse(await readFile(resolve(args.payload), "utf8"));
|
|
256
|
+
const result = await registerAndPersist({
|
|
257
|
+
baseUrl: args["base-url"] ?? process.env.AGENTEL_API_BASE_URL,
|
|
258
|
+
idempotencyKey: args["idempotency-key"],
|
|
259
|
+
payload,
|
|
260
|
+
outputDir: args["output-dir"],
|
|
261
|
+
requestTimeoutMs: args["timeout-ms"] === undefined ? DEFAULT_REQUEST_TIMEOUT_MS : Number(args["timeout-ms"]),
|
|
262
|
+
onPhase: (phase) => process.stderr.write(`Agentel registration: ${phase}\n`),
|
|
263
|
+
});
|
|
264
|
+
process.stdout.write(`Registered ${result.agentId} (@${result.slug}). Credentials saved in ${result.outputDir}; /me verified.\n`);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (process.argv[1] && fileURLToPath(import.meta.url) === resolve(process.argv[1])) {
|
|
268
|
+
main().catch((error) => {
|
|
269
|
+
process.stderr.write(`Agentel registration stopped: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
270
|
+
process.exitCode = 1;
|
|
271
|
+
});
|
|
272
|
+
}
|