@agentvalet/register 0.1.0 → 0.1.2
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/dist/index.js +197 -12
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -56,9 +56,9 @@ async function runWizard(prefilled) {
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
// src/register.ts
|
|
59
|
-
import { fetch } from "undici";
|
|
59
|
+
import { fetch as fetch2 } from "undici";
|
|
60
60
|
async function registerAgent(apiUrl, body) {
|
|
61
|
-
const res = await
|
|
61
|
+
const res = await fetch2(`${apiUrl}/v1/register`, {
|
|
62
62
|
method: "POST",
|
|
63
63
|
headers: { "Content-Type": "application/json" },
|
|
64
64
|
body: JSON.stringify(body)
|
|
@@ -76,18 +76,30 @@ async function registerAgent(apiUrl, body) {
|
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
// src/wait.ts
|
|
79
|
-
import { fetch as
|
|
79
|
+
import { fetch as fetch3 } from "undici";
|
|
80
80
|
async function waitForApproval(apiUrl, token, timeoutMs, onTick) {
|
|
81
81
|
const deadline = Date.now() + timeoutMs;
|
|
82
82
|
const sseResult = await trySSE(apiUrl, token, Math.min(3e4, timeoutMs), onTick);
|
|
83
|
-
if (sseResult) return sseResult;
|
|
84
|
-
|
|
83
|
+
if (sseResult) return enrichWithKey(apiUrl, token, sseResult);
|
|
84
|
+
const pollResult = await poll(apiUrl, token, deadline, onTick);
|
|
85
|
+
return enrichWithKey(apiUrl, token, pollResult);
|
|
86
|
+
}
|
|
87
|
+
async function enrichWithKey(apiUrl, token, result) {
|
|
88
|
+
if (result.status !== "approved") return result;
|
|
89
|
+
try {
|
|
90
|
+
const res = await fetch3(`${apiUrl}/v1/register/claim-key/${token}`);
|
|
91
|
+
if (!res.ok) return result;
|
|
92
|
+
const json = await res.json();
|
|
93
|
+
return { ...result, privateKey: json.private_key ?? "" };
|
|
94
|
+
} catch {
|
|
95
|
+
return result;
|
|
96
|
+
}
|
|
85
97
|
}
|
|
86
98
|
async function trySSE(apiUrl, token, windowMs, onTick) {
|
|
87
99
|
const controller = new AbortController();
|
|
88
100
|
const timer = setTimeout(() => controller.abort(), windowMs);
|
|
89
101
|
try {
|
|
90
|
-
const res = await
|
|
102
|
+
const res = await fetch3(`${apiUrl}/v1/register/stream/${token}`, {
|
|
91
103
|
signal: controller.signal,
|
|
92
104
|
headers: { Accept: "text/event-stream" }
|
|
93
105
|
});
|
|
@@ -130,7 +142,7 @@ async function poll(apiUrl, token, deadline, onTick) {
|
|
|
130
142
|
await sleep(5e3);
|
|
131
143
|
onTick?.();
|
|
132
144
|
try {
|
|
133
|
-
const res = await
|
|
145
|
+
const res = await fetch3(`${apiUrl}/v1/register/status/${token}`);
|
|
134
146
|
if (!res.ok) continue;
|
|
135
147
|
const json = await res.json();
|
|
136
148
|
const data = json.data ?? json;
|
|
@@ -152,7 +164,7 @@ function parseEvent(data) {
|
|
|
152
164
|
return null;
|
|
153
165
|
}
|
|
154
166
|
function sleep(ms) {
|
|
155
|
-
return new Promise((
|
|
167
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
156
168
|
}
|
|
157
169
|
|
|
158
170
|
// src/save.ts
|
|
@@ -163,10 +175,11 @@ async function saveOutput(opts) {
|
|
|
163
175
|
const envPath = resolve(opts.envFilePath);
|
|
164
176
|
await mkdir(dirname(keyPath), { recursive: true });
|
|
165
177
|
await mkdir(dirname(envPath), { recursive: true });
|
|
166
|
-
await writeFile(keyPath,
|
|
178
|
+
await writeFile(keyPath, opts.privateKey, { mode: 384 });
|
|
167
179
|
const envContent = [
|
|
168
180
|
`# AgentValet \u2014 generated by agentvalet-register`,
|
|
169
181
|
`AGENTVALET_AGENT_ID=${opts.agentId}`,
|
|
182
|
+
`AGENTVALET_OWNER_ID=${opts.ownerId}`,
|
|
170
183
|
`AGENTVALET_API_URL=${opts.apiUrl}`,
|
|
171
184
|
`AGENTVALET_PRIVATE_KEY_PATH=${keyPath}`,
|
|
172
185
|
""
|
|
@@ -175,6 +188,140 @@ async function saveOutput(opts) {
|
|
|
175
188
|
return { keyPath, envPath };
|
|
176
189
|
}
|
|
177
190
|
|
|
191
|
+
// src/commands/refresh.ts
|
|
192
|
+
import { readFileSync, writeFileSync, mkdirSync, statSync } from "fs";
|
|
193
|
+
import { createSign, createPrivateKey } from "crypto";
|
|
194
|
+
import { dirname as dirname2, resolve as resolve2 } from "path";
|
|
195
|
+
|
|
196
|
+
// src/lib/skill-writer.ts
|
|
197
|
+
function generatePermissionsFile(agentId, ownerId, permissions) {
|
|
198
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
199
|
+
const platformSections = permissions.length === 0 ? "_No platforms approved yet. Visit https://agentvalet.ai to connect platforms._" : permissions.map((p) => {
|
|
200
|
+
const scopeList = p.scopes.map((s) => ` - \`${s}\``).join("\n");
|
|
201
|
+
const approvalNote = p.requireApproval ? "\n > Requires human approval before executing.\n" : "";
|
|
202
|
+
return `### ${p.platformName} (\`${p.platformId}\`)
|
|
203
|
+
|
|
204
|
+
Approved scopes:
|
|
205
|
+
${scopeList}
|
|
206
|
+
${approvalNote}`;
|
|
207
|
+
}).join("\n\n");
|
|
208
|
+
return `# AgentValet Permissions
|
|
209
|
+
|
|
210
|
+
Agent ID: \`${agentId}\`
|
|
211
|
+
Owner ID: \`${ownerId}\`
|
|
212
|
+
Last refreshed: ${now}
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
## Approved Platforms
|
|
217
|
+
|
|
218
|
+
${platformSections}
|
|
219
|
+
|
|
220
|
+
---
|
|
221
|
+
|
|
222
|
+
## How to call a platform
|
|
223
|
+
|
|
224
|
+
All platform calls **must** go through AgentValet \u2014 never call Slack, Gmail, Stripe etc. directly.
|
|
225
|
+
|
|
226
|
+
Copy the pattern from \`test-agent.mjs\` (already in this repo):
|
|
227
|
+
|
|
228
|
+
1. Read the private key from \`agentvalet.key\`
|
|
229
|
+
2. Sign a short-lived RS256 JWT: \`{ agent_id, owner_id, iat, exp: iat + 300 }\`
|
|
230
|
+
3. POST to \`https://api.agentvalet.ai/v1/actions\`:
|
|
231
|
+
|
|
232
|
+
\`\`\`json
|
|
233
|
+
{
|
|
234
|
+
"platform": "<platformId>",
|
|
235
|
+
"endpoint": "<endpoint>",
|
|
236
|
+
"method": "POST",
|
|
237
|
+
"scope": "<scope>",
|
|
238
|
+
"data": { ... }
|
|
239
|
+
}
|
|
240
|
+
\`\`\`
|
|
241
|
+
|
|
242
|
+
Run \`node test-agent.mjs\` for a live smoke-test.
|
|
243
|
+
To refresh this file: \`agentvalet-register refresh\`
|
|
244
|
+
`;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// src/commands/refresh.ts
|
|
248
|
+
function base64url(input) {
|
|
249
|
+
return Buffer.from(input).toString("base64url");
|
|
250
|
+
}
|
|
251
|
+
function signJwt(agentId, ownerId, privateKeyPem) {
|
|
252
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
253
|
+
const header = base64url(JSON.stringify({ alg: "RS256", typ: "JWT" }));
|
|
254
|
+
const payload = base64url(JSON.stringify({ agent_id: agentId, owner_id: ownerId, iat: now, exp: now + 300 }));
|
|
255
|
+
const signingInput = `${header}.${payload}`;
|
|
256
|
+
const sign = createSign("RSA-SHA256");
|
|
257
|
+
sign.update(signingInput);
|
|
258
|
+
const signature = sign.sign(createPrivateKey(privateKeyPem), "base64url");
|
|
259
|
+
return `${signingInput}.${signature}`;
|
|
260
|
+
}
|
|
261
|
+
function parseEnvFile(content) {
|
|
262
|
+
const result = {};
|
|
263
|
+
for (const line of content.split("\n")) {
|
|
264
|
+
const trimmed = line.trim();
|
|
265
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
266
|
+
const eq = trimmed.indexOf("=");
|
|
267
|
+
if (eq === -1) continue;
|
|
268
|
+
result[trimmed.slice(0, eq)] = trimmed.slice(eq + 1);
|
|
269
|
+
}
|
|
270
|
+
return result;
|
|
271
|
+
}
|
|
272
|
+
async function runRefresh(options) {
|
|
273
|
+
const outputPath = resolve2(options.outputPath);
|
|
274
|
+
if (options.ifStale) {
|
|
275
|
+
try {
|
|
276
|
+
const stat = statSync(outputPath);
|
|
277
|
+
if (Date.now() - stat.mtimeMs < 36e5) {
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
} catch {
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
const envPath = resolve2(options.envFile);
|
|
284
|
+
let envVars;
|
|
285
|
+
try {
|
|
286
|
+
envVars = parseEnvFile(readFileSync(envPath, "utf-8"));
|
|
287
|
+
} catch {
|
|
288
|
+
throw new Error(`Cannot read env file: ${envPath}
|
|
289
|
+
Run 'agentvalet-register' first to create it.`);
|
|
290
|
+
}
|
|
291
|
+
const agentId = envVars["AGENTVALET_AGENT_ID"];
|
|
292
|
+
const ownerId = envVars["AGENTVALET_OWNER_ID"];
|
|
293
|
+
const apiUrl = envVars["AGENTVALET_API_URL"] ?? "https://api.agentvalet.ai";
|
|
294
|
+
const keyPath = envVars["AGENTVALET_PRIVATE_KEY_PATH"];
|
|
295
|
+
if (!agentId || !ownerId || !keyPath) {
|
|
296
|
+
throw new Error(`Env file is missing required keys (AGENTVALET_AGENT_ID, AGENTVALET_OWNER_ID, AGENTVALET_PRIVATE_KEY_PATH)`);
|
|
297
|
+
}
|
|
298
|
+
let privateKeyPem;
|
|
299
|
+
try {
|
|
300
|
+
privateKeyPem = readFileSync(keyPath, "utf-8");
|
|
301
|
+
} catch {
|
|
302
|
+
throw new Error(`Cannot read private key: ${keyPath}`);
|
|
303
|
+
}
|
|
304
|
+
if (!privateKeyPem.trim()) {
|
|
305
|
+
throw new Error(`Private key file is empty: ${keyPath}`);
|
|
306
|
+
}
|
|
307
|
+
const jwt = signJwt(agentId, ownerId, privateKeyPem);
|
|
308
|
+
const res = await fetch(`${apiUrl}/v1/agent/permissions`, {
|
|
309
|
+
headers: { Authorization: `Bearer ${jwt}` }
|
|
310
|
+
});
|
|
311
|
+
if (!res.ok) {
|
|
312
|
+
const body = await res.text().catch(() => "");
|
|
313
|
+
throw new Error(`Permissions API returned ${res.status}: ${body}`);
|
|
314
|
+
}
|
|
315
|
+
const json = await res.json();
|
|
316
|
+
const platforms = json.data?.platforms ?? [];
|
|
317
|
+
const content = generatePermissionsFile(agentId, ownerId, platforms);
|
|
318
|
+
mkdirSync(dirname2(outputPath), { recursive: true });
|
|
319
|
+
writeFileSync(outputPath, content, "utf-8");
|
|
320
|
+
if (!options.quiet) {
|
|
321
|
+
console.log(`Permissions refreshed \u2192 ${outputPath} (${platforms.length} platform${platforms.length !== 1 ? "s" : ""})`);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
178
325
|
// src/index.ts
|
|
179
326
|
function collectScope(value, previous) {
|
|
180
327
|
return [...previous, value];
|
|
@@ -275,12 +422,14 @@ Approve at: https://agentvalet.ai/registrations
|
|
|
275
422
|
}
|
|
276
423
|
process.exit(2);
|
|
277
424
|
}
|
|
278
|
-
const { agentId } = result;
|
|
425
|
+
const { agentId, privateKey } = result;
|
|
279
426
|
let keyPath;
|
|
280
427
|
let envPath;
|
|
281
428
|
try {
|
|
282
429
|
({ keyPath, envPath } = await saveOutput({
|
|
283
430
|
agentId,
|
|
431
|
+
ownerId,
|
|
432
|
+
privateKey: privateKey ?? "",
|
|
284
433
|
apiUrl: opts.apiUrl,
|
|
285
434
|
keyFilePath: opts.keyPath,
|
|
286
435
|
envFilePath: opts.output
|
|
@@ -290,19 +439,55 @@ Approve at: https://agentvalet.ai/registrations
|
|
|
290
439
|
console.error(isJson ? JSON.stringify({ error: msg }) : chalk2.red(`Failed to save output: ${msg}`));
|
|
291
440
|
process.exit(3);
|
|
292
441
|
}
|
|
442
|
+
const keyWritten = !!privateKey;
|
|
293
443
|
if (isJson) {
|
|
294
|
-
console.log(JSON.stringify({ status: "approved", agentId, keyPath, envPath }));
|
|
444
|
+
console.log(JSON.stringify({ status: "approved", agentId, keyPath: keyWritten ? keyPath : null, envPath }));
|
|
295
445
|
} else {
|
|
296
446
|
console.log(chalk2.green(`
|
|
297
447
|
Approved! Agent ID: ${agentId}`));
|
|
298
|
-
|
|
448
|
+
if (keyWritten) {
|
|
449
|
+
console.log(chalk2.gray(`Key: ${keyPath}`));
|
|
450
|
+
} else {
|
|
451
|
+
console.log(chalk2.yellow(`Key: not written \u2014 copy it from the dashboard and paste into ${keyPath}`));
|
|
452
|
+
}
|
|
299
453
|
console.log(chalk2.gray(`Env file: ${envPath}`));
|
|
300
454
|
console.log(`
|
|
301
455
|
Source the env file before calling any tool:`);
|
|
302
456
|
console.log(chalk2.cyan(` source ${envPath}`));
|
|
303
457
|
}
|
|
458
|
+
if (keyWritten) {
|
|
459
|
+
try {
|
|
460
|
+
await runRefresh({
|
|
461
|
+
envFile: opts.output,
|
|
462
|
+
outputPath: ".claude/agentvalet-permissions.md",
|
|
463
|
+
ifStale: false,
|
|
464
|
+
quiet: isJson
|
|
465
|
+
});
|
|
466
|
+
if (!isJson) {
|
|
467
|
+
console.log(chalk2.gray(`Permissions: .claude/agentvalet-permissions.md`));
|
|
468
|
+
}
|
|
469
|
+
} catch {
|
|
470
|
+
if (!isJson) {
|
|
471
|
+
console.log(chalk2.yellow(`Permissions: run 'agentvalet-register refresh' to generate`));
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
304
475
|
process.exit(0);
|
|
305
476
|
});
|
|
477
|
+
program.command("refresh").description("Refresh approved permissions and update .claude/agentvalet-permissions.md").option("--env-file <path>", "Path to .env.agentvalet file", "./.env.agentvalet").option("--output <path>", "Path to write permissions file", "./.claude/agentvalet-permissions.md").option("--if-stale", "Skip if file is less than 1 hour old").option("--quiet", "Suppress output").action(async (opts) => {
|
|
478
|
+
try {
|
|
479
|
+
await runRefresh({
|
|
480
|
+
envFile: opts.envFile,
|
|
481
|
+
outputPath: opts.output,
|
|
482
|
+
ifStale: !!opts.ifStale,
|
|
483
|
+
quiet: !!opts.quiet
|
|
484
|
+
});
|
|
485
|
+
} catch (err) {
|
|
486
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
487
|
+
console.error(chalk2.red(`Refresh failed: ${msg}`));
|
|
488
|
+
process.exit(3);
|
|
489
|
+
}
|
|
490
|
+
});
|
|
306
491
|
program.parseAsync().catch((err) => {
|
|
307
492
|
console.error(err);
|
|
308
493
|
process.exit(3);
|