@agentvalet/register 0.1.1 → 0.1.3
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 +198 -7
- 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,7 +76,7 @@ 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);
|
|
@@ -87,7 +87,7 @@ async function waitForApproval(apiUrl, token, timeoutMs, onTick) {
|
|
|
87
87
|
async function enrichWithKey(apiUrl, token, result) {
|
|
88
88
|
if (result.status !== "approved") return result;
|
|
89
89
|
try {
|
|
90
|
-
const res = await
|
|
90
|
+
const res = await fetch3(`${apiUrl}/v1/register/claim-key/${token}`);
|
|
91
91
|
if (!res.ok) return result;
|
|
92
92
|
const json = await res.json();
|
|
93
93
|
return { ...result, privateKey: json.private_key ?? "" };
|
|
@@ -99,7 +99,7 @@ async function trySSE(apiUrl, token, windowMs, onTick) {
|
|
|
99
99
|
const controller = new AbortController();
|
|
100
100
|
const timer = setTimeout(() => controller.abort(), windowMs);
|
|
101
101
|
try {
|
|
102
|
-
const res = await
|
|
102
|
+
const res = await fetch3(`${apiUrl}/v1/register/stream/${token}`, {
|
|
103
103
|
signal: controller.signal,
|
|
104
104
|
headers: { Accept: "text/event-stream" }
|
|
105
105
|
});
|
|
@@ -142,7 +142,7 @@ async function poll(apiUrl, token, deadline, onTick) {
|
|
|
142
142
|
await sleep(5e3);
|
|
143
143
|
onTick?.();
|
|
144
144
|
try {
|
|
145
|
-
const res = await
|
|
145
|
+
const res = await fetch3(`${apiUrl}/v1/register/status/${token}`);
|
|
146
146
|
if (!res.ok) continue;
|
|
147
147
|
const json = await res.json();
|
|
148
148
|
const data = json.data ?? json;
|
|
@@ -164,7 +164,7 @@ function parseEvent(data) {
|
|
|
164
164
|
return null;
|
|
165
165
|
}
|
|
166
166
|
function sleep(ms) {
|
|
167
|
-
return new Promise((
|
|
167
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
168
168
|
}
|
|
169
169
|
|
|
170
170
|
// src/save.ts
|
|
@@ -179,6 +179,7 @@ async function saveOutput(opts) {
|
|
|
179
179
|
const envContent = [
|
|
180
180
|
`# AgentValet \u2014 generated by agentvalet-register`,
|
|
181
181
|
`AGENTVALET_AGENT_ID=${opts.agentId}`,
|
|
182
|
+
`AGENTVALET_OWNER_ID=${opts.ownerId}`,
|
|
182
183
|
`AGENTVALET_API_URL=${opts.apiUrl}`,
|
|
183
184
|
`AGENTVALET_PRIVATE_KEY_PATH=${keyPath}`,
|
|
184
185
|
""
|
|
@@ -187,6 +188,164 @@ async function saveOutput(opts) {
|
|
|
187
188
|
return { keyPath, envPath };
|
|
188
189
|
}
|
|
189
190
|
|
|
191
|
+
// src/commands/refresh.ts
|
|
192
|
+
import { readFileSync, writeFileSync, existsSync, 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
|
+
ensureClaudeMd(options.outputPath);
|
|
321
|
+
if (!options.quiet) {
|
|
322
|
+
console.log(`Permissions refreshed \u2192 ${outputPath} (${platforms.length} platform${platforms.length !== 1 ? "s" : ""})`);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
function ensureClaudeMd(permissionsRelativePath) {
|
|
326
|
+
const importLine = `@${permissionsRelativePath.replace(/\\/g, "/").replace(/^\.\//, "")}`;
|
|
327
|
+
const claudeMdPath = resolve2("CLAUDE.md");
|
|
328
|
+
if (!existsSync(claudeMdPath)) {
|
|
329
|
+
writeFileSync(
|
|
330
|
+
claudeMdPath,
|
|
331
|
+
`# Agent Instructions
|
|
332
|
+
|
|
333
|
+
@.claude/agentvalet-permissions.md
|
|
334
|
+
`,
|
|
335
|
+
"utf-8"
|
|
336
|
+
);
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
const existing = readFileSync(claudeMdPath, "utf-8");
|
|
340
|
+
if (existing.includes(importLine) || existing.includes("@.claude/agentvalet-permissions.md")) {
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
writeFileSync(claudeMdPath, `${existing.trimEnd()}
|
|
344
|
+
|
|
345
|
+
${importLine}
|
|
346
|
+
`, "utf-8");
|
|
347
|
+
}
|
|
348
|
+
|
|
190
349
|
// src/index.ts
|
|
191
350
|
function collectScope(value, previous) {
|
|
192
351
|
return [...previous, value];
|
|
@@ -293,6 +452,7 @@ Approve at: https://agentvalet.ai/registrations
|
|
|
293
452
|
try {
|
|
294
453
|
({ keyPath, envPath } = await saveOutput({
|
|
295
454
|
agentId,
|
|
455
|
+
ownerId,
|
|
296
456
|
privateKey: privateKey ?? "",
|
|
297
457
|
apiUrl: opts.apiUrl,
|
|
298
458
|
keyFilePath: opts.keyPath,
|
|
@@ -319,8 +479,39 @@ Approved! Agent ID: ${agentId}`));
|
|
|
319
479
|
Source the env file before calling any tool:`);
|
|
320
480
|
console.log(chalk2.cyan(` source ${envPath}`));
|
|
321
481
|
}
|
|
482
|
+
if (keyWritten) {
|
|
483
|
+
try {
|
|
484
|
+
await runRefresh({
|
|
485
|
+
envFile: opts.output,
|
|
486
|
+
outputPath: ".claude/agentvalet-permissions.md",
|
|
487
|
+
ifStale: false,
|
|
488
|
+
quiet: isJson
|
|
489
|
+
});
|
|
490
|
+
if (!isJson) {
|
|
491
|
+
console.log(chalk2.gray(`Permissions: .claude/agentvalet-permissions.md`));
|
|
492
|
+
}
|
|
493
|
+
} catch {
|
|
494
|
+
if (!isJson) {
|
|
495
|
+
console.log(chalk2.yellow(`Permissions: run 'agentvalet-register refresh' to generate`));
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
322
499
|
process.exit(0);
|
|
323
500
|
});
|
|
501
|
+
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) => {
|
|
502
|
+
try {
|
|
503
|
+
await runRefresh({
|
|
504
|
+
envFile: opts.envFile,
|
|
505
|
+
outputPath: opts.output,
|
|
506
|
+
ifStale: !!opts.ifStale,
|
|
507
|
+
quiet: !!opts.quiet
|
|
508
|
+
});
|
|
509
|
+
} catch (err) {
|
|
510
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
511
|
+
console.error(chalk2.red(`Refresh failed: ${msg}`));
|
|
512
|
+
process.exit(3);
|
|
513
|
+
}
|
|
514
|
+
});
|
|
324
515
|
program.parseAsync().catch((err) => {
|
|
325
516
|
console.error(err);
|
|
326
517
|
process.exit(3);
|