agentkey-ai 1.0.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/README.md CHANGED
@@ -16,11 +16,24 @@ JavaScript / TypeScript (Node 18+, zero dependencies, ESM):
16
16
  npm install agentkey-ai
17
17
  ```
18
18
 
19
+ ### Next steps after install
20
+
21
+ pip wheels run no code on install, so pip cannot print guidance for you. npm is different, and this section states its behavior precisely rather than assuming it: dependency install scripts still run by default on npm 11 and earlier; npm 11.16.0 and later print a warning whenever an install executes them; and npm 12 no longer executes preinstall, install, or postinstall scripts from dependencies "unless they are explicitly allowed in your project" (GitHub changelog, June 2026), with `npm approve-scripts` as the approval path. A postinstall banner in this package is therefore technically possible on npm 11 and earlier, but we do not ship one: it would be suppressed or gated on current npm, and a security tool should not run code at install time. The immediate next step:
22
+
23
+ ```bash
24
+ agentkey init # paste your API key (masked) and validate it against production
25
+ agentkey doctor # verify the full setup: SDK, credentials, API, key validity
26
+ ```
27
+
28
+ After `npm install agentkey-ai`, invoke the same commands with npx: `npx agentkey init`, `npx agentkey doctor`.
29
+
30
+ The CLI never prints your key. `agentkey init` saves a pasted key to a local config file (`~/.agentkey/config`, or `%APPDATA%\agentkey\config` on Windows; permissions 0600, the only file the key is ever written to). The `AGENTKEY_API_KEY` environment variable overrides it, and `agentkey logout` deletes it. See "Developer CLI" below.
31
+
19
32
  ## Get an API key
20
33
 
21
- 1. Sign up at the AgentKey dashboard: https://agentkey.base44.app
34
+ 1. Sign up at the AgentKey dashboard: https://agentkey.us
22
35
  2. Open the Connect wizard (or Agents, then create an agent).
23
- 3. Generate an API key. It is shown once; store it as an environment variable and do not hard-code it.
36
+ 3. Generate an API key. It is shown once. Run `agentkey init` to save it to the local config file, or export it as `AGENTKEY_API_KEY`. Never hard-code it.
24
37
 
25
38
  ## First authorization (Python)
26
39
 
@@ -131,9 +144,23 @@ If authorize denies, `guard` returns the denial and does not run the function.
131
144
 
132
145
  A parent agent can delegate a scoped subset of its permissions to a child agent. Scopes are `resource:action` strings, must be a subset of the parent's own permissions, and chains are depth-limited. See `delegate()` in the source docstrings.
133
146
 
147
+ ## Developer CLI
148
+
149
+ Both packages ship the same commands (`init`, `doctor`, `logout`). They validate your setup against the production API and never change SDK authorization behavior. Every CLI network call has an explicit 10 second timeout, so a wrong URL, an offline machine, or firewalled egress fails closed with a next step instead of hanging.
150
+
151
+ The API key is resolved, highest first, from: an explicit constructor argument (SDK), the `AGENTKEY_API_KEY` environment variable, then the config file (`~/.agentkey/config`, or `%APPDATA%\agentkey\config` on Windows). The environment variable always wins, so CI and anyone already exporting it are unaffected.
152
+
153
+ `agentkey init` validates the configured key against the production API. With no key configured it prompts with masked input, validates the pasted key, and saves it to the config file (created with permissions 0600 inside a 0700 directory) so later commands and SDK clients resolve it automatically. The key is never printed or logged, and that file is the only place it is ever written.
154
+
155
+ `agentkey doctor` runs a fast end-to-end check: SDK installed, credentials configured (reporting whether the key came from the environment or the config file), API reachable, credentials valid, authorization endpoint reachable. When the API is unreachable the network-dependent checks are skipped and reported as such. Each failed check states the practical next step. No stack traces.
156
+
157
+ `agentkey logout` deletes the saved config file. The environment variable, if set, is untouched (the CLI cannot unset your shell).
158
+
159
+ A successful `init` or `doctor` means the developer is authenticated and configured. It never means the agent's actions are automatically allowed; authorization decisions remain fail-closed exactly as before.
160
+
134
161
  ## Production API
135
162
 
136
- Base URL: `https://agentkey.base44.app` (the SDK default). Override it with `base_url` (Python) or `baseUrl` (JavaScript) if you self-host.
163
+ Base URL: `https://agentkey.us` (the SDK default). Override it with `base_url` (Python) or `baseUrl` (JavaScript) if you self-host.
137
164
 
138
165
  All endpoints are POST with a Bearer API key, under `/api/functions/`:
139
166
 
@@ -147,7 +174,7 @@ All endpoints are POST with a Bearer API key, under `/api/functions/`:
147
174
  Raw HTTP:
148
175
 
149
176
  ```bash
150
- curl -X POST https://agentkey.base44.app/api/functions/authorize \
177
+ curl -X POST https://agentkey.us/api/functions/authorize \
151
178
  -H "Authorization: Bearer agent_live_xxxxx" \
152
179
  -H "Content-Type: application/json" \
153
180
  -d '{"action":"send_email","resource":"gmail","arguments":{"to":"x@company.com"}}'
@@ -155,7 +182,12 @@ curl -X POST https://agentkey.base44.app/api/functions/authorize \
155
182
 
156
183
  ## Dashboard
157
184
 
158
- Sessions, evidence events, approvals, findings and permission settings: https://agentkey.base44.app
185
+ Sessions, evidence events, approvals, findings and permission settings: https://agentkey.us
186
+
187
+ ## Troubleshooting
188
+
189
+ - "Request blocked by edge WAF (Cloudflare error 1010). This is not an authentication failure." The production API sits behind a WAF that blocks some client signatures. The SDK sends an identifying User-Agent on every request (`agentkey-sdk-js/<version>` in the JavaScript SDK, `agentkey-python-sdk` in the Python SDK). If you see this error, something between your code and the API (a proxy, a modified SDK, or a non-SDK integration) is stripping or replacing that header. Restore it, then retry. This error never means a bad key.
190
+ - Both SDKs detect the Cloudflare 1010 HTML page and raise the explicit error above; it never surfaces as a generic parse error, an unreachable API, or an authentication failure.
159
191
 
160
192
  ## Security limitations (current, accurate)
161
193
 
package/agentkey.d.ts CHANGED
@@ -47,12 +47,26 @@ export interface WrapOptions {
47
47
  }
48
48
 
49
49
  export interface ClientOptions {
50
- apiKey: string;
50
+ /** Optional: the key resolves, highest first, from this argument, then the
51
+ * AGENTKEY_API_KEY environment variable, then the config file written by
52
+ * `agentkey init` (~/.agentkey/config). */
53
+ apiKey?: string;
51
54
  baseUrl?: string;
52
55
  }
53
56
 
57
+ /** The config file location: ~/.agentkey/config (or %APPDATA%\agentkey\config
58
+ * on Windows), or null when no home directory can be determined. */
59
+ export function configFilePath(): string | null;
60
+ export function readConfigKey(): Promise<string | null>;
61
+ export function saveConfigKey(apiKey: string): Promise<string>;
62
+ export function deleteConfigKey(): Promise<boolean>;
63
+
54
64
  export class AgentKeyClient {
55
- constructor(options: ClientOptions);
65
+ constructor(options?: ClientOptions);
66
+
67
+ /** "constructor argument", "environment variable", "config file", or null
68
+ * (the config file is resolved lazily on first use). */
69
+ keySource: string | null;
56
70
 
57
71
  checkPermission(args: {
58
72
  action: string;
@@ -82,6 +96,8 @@ export class AgentKeyClient {
82
96
 
83
97
  endSession(args: { sessionId: string; status?: string }): Promise<Record<string, unknown>>;
84
98
 
99
+ verifySession(args: { sessionId: string }): Promise<Record<string, unknown>>;
100
+
85
101
  recordAction(args: {
86
102
  sessionId?: string;
87
103
  authorizationId?: string;
package/agentkey.js CHANGED
@@ -20,11 +20,92 @@ const TIMEOUT_MS = 5000;
20
20
 
21
21
  // Production API. Override with baseUrl only if you self-host.
22
22
  export const DEFAULT_BASE_URL = "https://agentkey.base44.app";
23
- const USER_AGENT = "agentkey-js-sdk/1.0.0";
23
+ export const VERSION = "1.2.0";
24
+ const USER_AGENT = `agentkey-sdk-js/${VERSION}`;
24
25
 
25
26
  // Process-wide flag so the first-run info block prints once per process.
26
27
  let _wrapFirstRunPrinted = false;
27
28
 
29
+ // --- Credential config file (Node only; browsers have no filesystem) --------
30
+ //
31
+ // Resolution order, highest first: an explicit constructor argument, the
32
+ // AGENTKEY_API_KEY environment variable, then the config file written by
33
+ // `agentkey init` (~/.agentkey/config, or %APPDATA%\agentkey\config on
34
+ // Windows). The environment variable always wins, so CI and anyone already
35
+ // exporting it are unaffected. Zero dependencies, like gh/stripe/aws: no
36
+ // keyring, no native modules. The file is created 0600 inside a 0700
37
+ // directory, and it is the only place the key is ever written.
38
+
39
+ export function configFilePath() {
40
+ if (typeof process === "undefined" || !process.env) return null;
41
+ if (process.platform === "win32") {
42
+ return process.env.APPDATA ? `${process.env.APPDATA}\\agentkey\\config` : null;
43
+ }
44
+ const home = process.env.HOME || process.env.USERPROFILE || "";
45
+ return home ? `${home}/.agentkey/config` : null;
46
+ }
47
+
48
+ let _fsModulePromise = null;
49
+ async function _fs() {
50
+ if (typeof process === "undefined" || !process.versions || !process.versions.node) return null;
51
+ if (!_fsModulePromise) {
52
+ _fsModulePromise = import("node:fs").catch(() => null);
53
+ }
54
+ return _fsModulePromise;
55
+ }
56
+
57
+ export async function readConfigKey() {
58
+ const fs = await _fs();
59
+ const p = configFilePath();
60
+ if (!fs || !p) return null;
61
+ try {
62
+ const text = fs.readFileSync(p, "utf8");
63
+ for (const line of text.split("\n")) {
64
+ if (line.startsWith("api_key=")) {
65
+ const value = line.slice("api_key=".length).trim();
66
+ if (value) return value;
67
+ }
68
+ }
69
+ } catch (e) {}
70
+ return null;
71
+ }
72
+
73
+ export async function saveConfigKey(apiKey) {
74
+ const fs = await _fs();
75
+ const p = configFilePath();
76
+ if (!fs || !p) {
77
+ throw new Error("AgentKey: no home directory found; use the AGENTKEY_API_KEY environment variable instead");
78
+ }
79
+ const dir = p.replace(/[\\/][^\\/]+$/, "");
80
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
81
+ fs.writeFileSync(p, `api_key=${apiKey}\n`, { mode: 0o600 });
82
+ fs.chmodSync(dir, 0o700);
83
+ fs.chmodSync(p, 0o600);
84
+ return p;
85
+ }
86
+
87
+ export async function deleteConfigKey() {
88
+ const fs = await _fs();
89
+ const p = configFilePath();
90
+ if (!fs || !p) return false;
91
+ try { fs.unlinkSync(p); return true; } catch (e) { return false; }
92
+ }
93
+
94
+ // Cloudflare blocks some client signatures with error 1010, returning an HTML
95
+ // body and no useful signal. Detect that exact case so it surfaces as what it
96
+ // is: an edge block, never a rejected key or a broken server. The User-Agent
97
+ // above is set on every request, but a future WAF rule change could still
98
+ // block a client; this keeps the failure explainable.
99
+ function _wafBlockedError(body) {
100
+ if (typeof body !== "string") return null;
101
+ if (body.includes("error code: 1010") || (body.includes("Attention Required") && body.includes("Cloudflare"))) {
102
+ const err = new Error("Request blocked by edge WAF (Cloudflare error 1010). This is not an authentication failure.");
103
+ err.agentkeyWaf = true;
104
+ return err;
105
+ }
106
+ return null;
107
+ }
108
+
28
109
  export class AgentKeyDenied extends Error {
29
110
  constructor(reason, result) {
30
111
  super(reason || "denied by AgentKey policy");
@@ -34,12 +115,33 @@ export class AgentKeyDenied extends Error {
34
115
  }
35
116
 
36
117
  export class AgentKeyClient {
37
- constructor({ apiKey, baseUrl = DEFAULT_BASE_URL }) {
38
- if (!apiKey) throw new Error("AgentKey: apiKey is required");
39
- this.apiKey = apiKey;
118
+ // The key resolves, highest first, from an explicit apiKey argument, then
119
+ // the AGENTKEY_API_KEY environment variable, then (lazily, on first use)
120
+ // the config file written by `agentkey init`. In a browser neither the
121
+ // environment nor the file exists, so an explicit apiKey is required.
122
+ constructor({ apiKey, baseUrl = DEFAULT_BASE_URL } = {}) {
123
+ const explicit = String(apiKey || "").trim();
124
+ const fromEnv = (typeof process !== "undefined" && process.env
125
+ ? String(process.env.AGENTKEY_API_KEY || "") : "").trim();
126
+ this.apiKey = explicit || fromEnv || null;
127
+ this.keySource = explicit ? "constructor argument" : (fromEnv ? "environment variable" : null);
40
128
  this.baseUrl = String(baseUrl).replace(/\/$/, "");
41
129
  }
42
130
 
131
+ async _ensureKey() {
132
+ if (!this.apiKey) {
133
+ const fromFile = await readConfigKey();
134
+ if (fromFile) {
135
+ this.apiKey = fromFile;
136
+ this.keySource = "config file";
137
+ }
138
+ }
139
+ if (!this.apiKey) {
140
+ throw new Error("AgentKey: no API key configured. Pass apiKey to the constructor, set AGENTKEY_API_KEY, or run: agentkey init");
141
+ }
142
+ return this.apiKey;
143
+ }
144
+
43
145
  async _post(path, body, extraHeaders = {}) {
44
146
  const controller = new AbortController();
45
147
  const timeout = setTimeout(() => controller.abort(), TIMEOUT_MS);
@@ -51,9 +153,14 @@ export class AgentKeyClient {
51
153
  signal: controller.signal,
52
154
  });
53
155
  const data = await res.json().catch(() => null);
156
+ if (!data) {
157
+ const waf = _wafBlockedError(await res.text().catch(() => ""));
158
+ if (waf) throw waf;
159
+ }
54
160
  if (!data || typeof data.allowed !== "boolean") return { ...FAIL_CLOSED };
55
161
  return data;
56
162
  } catch (e) {
163
+ if (e && e.agentkeyWaf) throw e;
57
164
  return { ...FAIL_CLOSED };
58
165
  } finally {
59
166
  clearTimeout(timeout);
@@ -71,8 +178,13 @@ export class AgentKeyClient {
71
178
  signal: controller.signal,
72
179
  });
73
180
  const data = await res.json().catch(() => null);
181
+ if (!data) {
182
+ const waf = _wafBlockedError(await res.text().catch(() => ""));
183
+ if (waf) throw waf;
184
+ }
74
185
  return data || { error: "agentkey_unreachable" };
75
186
  } catch (e) {
187
+ if (e && e.agentkeyWaf) throw e;
76
188
  return { error: "agentkey_unreachable" };
77
189
  } finally {
78
190
  clearTimeout(timeout);
@@ -85,6 +197,7 @@ export class AgentKeyClient {
85
197
  * hash chain. Fails closed (allowed: false) on any error or timeout. */
86
198
  async checkPermission({ action, resource, tool, metadata = {}, arguments: args, sessionId, parentEventId, parentAgentId, source, scanId, delegationId }) {
87
199
  if (!action || !resource) throw new Error("AgentKey: action and resource are required");
200
+ await this._ensureKey();
88
201
  return this._post(
89
202
  "/api/functions/authorize",
90
203
  { action, resource, tool, metadata, arguments: args, session_id: sessionId, parent_event_id: parentEventId, parent_agent_id: parentAgentId, source, input_scan_id: scanId, delegation_id: delegationId },
@@ -105,6 +218,7 @@ export class AgentKeyClient {
105
218
  if (!childAgentId || !Array.isArray(scopes) || scopes.length === 0) {
106
219
  throw new Error("AgentKey: childAgentId and a non-empty scopes array are required");
107
220
  }
221
+ await this._ensureKey();
108
222
  return this._postJson(
109
223
  "/api/functions/delegate",
110
224
  {
@@ -120,19 +234,25 @@ export class AgentKeyClient {
120
234
 
121
235
  /** Validate the key is active. Returns { valid, message, agent_id, agent_name }. */
122
236
  async validate() {
237
+ await this._ensureKey();
123
238
  const controller = new AbortController();
124
239
  const timeout = setTimeout(() => controller.abort(), TIMEOUT_MS);
125
240
  try {
126
241
  const res = await fetch(`${this.baseUrl}/api/functions/validate_api_key`, {
127
- headers: { Authorization: `Bearer ${this.apiKey}` },
242
+ headers: { Authorization: `Bearer ${this.apiKey}`, "User-Agent": USER_AGENT },
128
243
  signal: controller.signal,
129
244
  });
130
245
  const data = await res.json().catch(() => null);
246
+ if (!data) {
247
+ const waf = _wafBlockedError(await res.text().catch(() => ""));
248
+ if (waf) throw waf;
249
+ }
131
250
  if (!data || typeof data.valid !== "boolean") {
132
251
  return { valid: false, reason: "agentkey_unreachable", fail_closed: true };
133
252
  }
134
253
  return data;
135
254
  } catch (e) {
255
+ if (e && e.agentkeyWaf) throw e;
136
256
  return { valid: false, reason: "agentkey_unreachable", fail_closed: true };
137
257
  } finally {
138
258
  clearTimeout(timeout);
@@ -146,15 +266,31 @@ export class AgentKeyClient {
146
266
  * decision in the session unless overridden per call. Returns { session_id,
147
267
  * agent_id, started_at, preceding_scan_id }. */
148
268
  async startSession({ metadata, scanId } = {}) {
269
+ await this._ensureKey();
149
270
  return this._postJson("/api/functions/start_session", { metadata, scan_id: scanId }, { Authorization: `Bearer ${this.apiKey}` });
150
271
  }
151
272
 
152
273
  /** End an evidence session. Returns { session_id, status, event_count }.
153
274
  * Pass status: "failed" to mark a failed session. */
154
275
  async endSession({ sessionId, status } = {}) {
276
+ await this._ensureKey();
155
277
  return this._postJson("/api/functions/end_session", { session_id: sessionId, status }, { Authorization: `Bearer ${this.apiKey}` });
156
278
  }
157
279
 
280
+ /** Verify a session's evidence chain server-side: the hash chain is
281
+ * recomputed from the stored events, the HMAC-SHA256 attestation is
282
+ * checked, and the Ed25519 signature is checked when one is present
283
+ * (an optional asymmetric layer — sessions closed without it verify as
284
+ * before). The response's `signature` object distinguishes the two:
285
+ * signature_status is the HMAC result, ed25519_status the asymmetric
286
+ * one, each with its own key_id. Authentication is your agent API key.
287
+ * Fails closed: an unreachable API returns
288
+ * { error: "agentkey_unreachable" } and is never treated as verified. */
289
+ async verifySession({ sessionId } = {}) {
290
+ await this._ensureKey();
291
+ return this._postJson("/api/functions/verify_session", { session_id: sessionId }, { Authorization: `Bearer ${this.apiKey}` });
292
+ }
293
+
158
294
  /** Record what an agent ACTUALLY did, as an execution event on the same hash
159
295
  * chain as the decision that permitted it. Pass the `event_id` returned by
160
296
  * checkPermission as authorizationId to link the execution to its decision.
@@ -163,6 +299,7 @@ export class AgentKeyClient {
163
299
  * reported. `metadata` is sanitized to primitives before storage (used by
164
300
  * wrap() to mark would_block in observe mode). */
165
301
  async recordAction({ sessionId, authorizationId, attemptId, tool, action, resource, arguments: args, resultStatus, resultHash, durationMs, errorMessage, metadata, scanId }) {
302
+ await this._ensureKey();
166
303
  return this._postJson("/api/functions/record_action", {
167
304
  session_id: sessionId, authorization_id: authorizationId, attempt_id: attemptId, tool, action, resource, arguments: args,
168
305
  result_status: resultStatus, result_hash: resultHash, duration_ms: durationMs, error_message: errorMessage,
@@ -313,6 +450,7 @@ function _wrapTarget(client, target, { sessionId, mode = "observe", scanId } = {
313
450
  try {
314
451
  auth = await client.checkPermission({ action: "invoke", resource: name, arguments: argumentsObj, sessionId: sid });
315
452
  } catch (e) {
453
+ if (e && e.agentkeyWaf) throw e; // an edge block must surface, not mask as a denial
316
454
  auth = { allowed: false, reason: "agentkey_unreachable", fail_closed: true };
317
455
  }
318
456
  const allowed = !!auth && auth.allowed === true;
package/cli.mjs ADDED
@@ -0,0 +1,323 @@
1
+ #!/usr/bin/env node
2
+ // agentkey-ai CLI: agentkey init / agentkey doctor / agentkey logout
3
+ //
4
+ // Zero dependencies (Node 18+). The API key is resolved, highest first, from
5
+ // the AGENTKEY_API_KEY environment variable, then the config file written by
6
+ // init (~/.agentkey/config, or %APPDATA%\agentkey\config on Windows; created
7
+ // 0600 inside a 0700 directory, the only file the key is ever written to).
8
+ // The environment variable always wins, so CI and anyone already exporting it
9
+ // are unaffected. The CLI never prints the key. Every network call has an
10
+ // explicit 10 second timeout, so a wrong URL, an offline machine, or firewalled
11
+ // egress fails closed with a next step instead of hanging. A successful init
12
+ // means the developer is authenticated and configured — it never means the
13
+ // agent's actions are automatically allowed. Authorization stays fail-closed
14
+ // in the SDK.
15
+
16
+ import {
17
+ DEFAULT_BASE_URL,
18
+ VERSION,
19
+ configFilePath,
20
+ readConfigKey,
21
+ saveConfigKey,
22
+ deleteConfigKey,
23
+ } from "./agentkey.js";
24
+
25
+ const TIMEOUT_MS = 10000;
26
+ const RULE = "─".repeat(41);
27
+ const DASHBOARD_URL = "https://agentkey.us";
28
+ const QUICKSTART_URL = "https://agentkey.us/quickstart";
29
+ const UA = `agentkey-cli-js/${VERSION}`;
30
+
31
+ function step(ok, label, detail, nextStep) {
32
+ console.log(` ${ok ? "✓" : "✗"} ${label}${detail ? ` - ${detail}` : ""}`);
33
+ if (!ok && nextStep) console.log(` → ${nextStep}`);
34
+ return ok;
35
+ }
36
+
37
+ // Any HTTP response (including 401/400) proves the endpoint is reachable.
38
+ // Only a network-level failure (DNS, timeout, refused) means unreachable.
39
+ async function apiReachable(baseUrl, path, method = "POST") {
40
+ const controller = new AbortController();
41
+ const timeout = setTimeout(() => controller.abort(), TIMEOUT_MS);
42
+ try {
43
+ const res = await fetch(baseUrl + path, {
44
+ method,
45
+ headers: { "Content-Type": "application/json", "User-Agent": UA },
46
+ body: method === "POST" ? "{}" : undefined,
47
+ signal: controller.signal,
48
+ });
49
+ return [true, res.status];
50
+ } catch {
51
+ return [false, null];
52
+ } finally {
53
+ clearTimeout(timeout);
54
+ }
55
+ }
56
+
57
+ // GET validate_api_key and read the body, so a rejected key is distinguishable
58
+ // from an unreachable API (the SDK's validate() fails closed on any error and
59
+ // cannot tell the two apart).
60
+ async function validateRaw(baseUrl, key) {
61
+ const controller = new AbortController();
62
+ const timeout = setTimeout(() => controller.abort(), TIMEOUT_MS);
63
+ try {
64
+ const res = await fetch(`${baseUrl}/api/functions/validate_api_key`, {
65
+ headers: { Authorization: `Bearer ${key}`, "User-Agent": UA },
66
+ signal: controller.signal,
67
+ });
68
+ const text = await res.text();
69
+ try {
70
+ const data = JSON.parse(text);
71
+ if (data && typeof data.valid === "boolean") return data;
72
+ } catch {}
73
+ if (text.includes("error code: 1010") || (text.includes("Attention Required") && text.includes("Cloudflare"))) {
74
+ return { valid: false, error: "request blocked by the edge WAF (Cloudflare error 1010) - not an authentication failure" };
75
+ }
76
+ return { valid: false, error: `API returned HTTP ${res.status}` };
77
+ } catch (e) {
78
+ if (process.env.AGENTKEY_CLI_DEBUG) console.error("debug: validate_api_key failed:", String((e && e.cause) || e));
79
+ return { valid: false, error: "could not reach the API (fail-closed)" };
80
+ } finally {
81
+ clearTimeout(timeout);
82
+ }
83
+ }
84
+
85
+ // Masked prompt: raw-mode stdin, one █ echoed per keystroke, never the key
86
+ // itself. Returns null when there is no interactive terminal or nothing was
87
+ // pasted, so init falls back to the configuration instructions.
88
+ function promptKey() {
89
+ return new Promise((resolve) => {
90
+ if (!process.stdin.isTTY) { resolve(null); return; }
91
+ process.stdout.write("API key: ");
92
+ process.stdin.setRawMode(true);
93
+ process.stdin.resume();
94
+ const chunks = [];
95
+ const finish = (value) => {
96
+ process.stdin.setRawMode(false);
97
+ process.stdin.pause();
98
+ process.stdin.removeListener("data", onData);
99
+ process.stdout.write("\n");
100
+ resolve(value);
101
+ };
102
+ const onData = (buf) => {
103
+ for (const byte of buf) {
104
+ if (byte === 13 || byte === 10) { finish(Buffer.concat(chunks).toString("utf8").trim() || null); return; }
105
+ if (byte === 3) { finish(null); process.exit(130); return; }
106
+ if (byte === 127 || byte === 8) { if (chunks.length) { chunks.pop(); process.stdout.write("\h \b"); } continue; }
107
+ chunks.push(Buffer.from([byte]));
108
+ process.stdout.write("█");
109
+ }
110
+ };
111
+ process.stdin.on("data", onData);
112
+ });
113
+ }
114
+
115
+ // Resolve the key: environment first, then the config file. The CLI takes no
116
+ // key argument on purpose (command-line arguments leak into shell history and
117
+ // process listings); the masked prompt is the only interactive input.
118
+ async function resolveKey() {
119
+ const env = (process.env.AGENTKEY_API_KEY || "").trim();
120
+ if (env) return { key: env, source: "environment" };
121
+ const fromFile = await readConfigKey();
122
+ if (fromFile) return { key: fromFile, source: "config" };
123
+ return { key: null, source: null };
124
+ }
125
+
126
+ async function cmdInit() {
127
+ const baseUrl = (process.env.AGENTKEY_BASE_URL || "").trim() || DEFAULT_BASE_URL;
128
+ console.log("AgentKey");
129
+ console.log(RULE);
130
+ console.log("Let's get your agent protected.");
131
+ console.log();
132
+ console.log("Checking existing configuration…");
133
+ console.log();
134
+ const resolved = await resolveKey();
135
+ let key = resolved.key;
136
+ let pasted = false;
137
+ if (key) {
138
+ step(true, "API key found", resolved.source === "environment"
139
+ ? "environment: AGENTKEY_API_KEY"
140
+ : `config file: ${configFilePath()}`);
141
+ } else {
142
+ console.log("AgentKey needs an API key to connect to your account.");
143
+ console.log();
144
+ console.log(`Get your key from: ${DASHBOARD_URL}`);
145
+ console.log(" (Connect wizard, pick an agent, Create API key. It is shown once.)");
146
+ console.log();
147
+ const entered = await promptKey();
148
+ if (!entered) {
149
+ console.log("AgentKey needs an API key and this session has no interactive");
150
+ console.log("terminal to ask for one. Run `agentkey init` in a terminal to");
151
+ console.log("enter and save it, or set the environment variable:");
152
+ console.log();
153
+ console.log(' export AGENTKEY_API_KEY="agent_live_…"');
154
+ console.log();
155
+ console.log("Then run: agentkey init");
156
+ return 1;
157
+ }
158
+ key = entered;
159
+ pasted = true;
160
+ step(true, "API key received");
161
+ }
162
+ const v = await validateRaw(baseUrl, key);
163
+ if (v.valid === true) {
164
+ const who = v.agent_name || v.agent_id || "your agent";
165
+ step(true, "API key validated", `agent: ${who}`);
166
+ step(true, `Connected to ${baseUrl}`);
167
+ let savedAt = null;
168
+ if (pasted) {
169
+ try {
170
+ savedAt = await saveConfigKey(key);
171
+ step(true, "API key saved", `${savedAt} (permissions 0600)`);
172
+ } catch (e) {
173
+ if (process.env.AGENTKEY_CLI_DEBUG) console.error("debug: could not save config:", String(e));
174
+ step(false, "API key saved", "could not write the config file");
175
+ console.log();
176
+ console.log("FAILED: the key validated but could not be saved. Use the");
177
+ console.log("environment variable instead:");
178
+ console.log(' export AGENTKEY_API_KEY="agent_live_…"');
179
+ return 1;
180
+ }
181
+ }
182
+ console.log();
183
+ console.log("AgentKey is ready.");
184
+ console.log();
185
+ if (savedAt) {
186
+ console.log("The key is saved at the config file above. The AGENTKEY_API_KEY");
187
+ console.log("environment variable overrides it; unset yours to use the saved");
188
+ console.log("key. Remove it any time: agentkey logout");
189
+ }
190
+ console.log("Next:");
191
+ console.log(" 1. agentkey doctor");
192
+ console.log(` 2. Make your first authorization. Quickstart: ${QUICKSTART_URL}`);
193
+ return 0;
194
+ }
195
+ const detail = String(v.error || v.message || v.reason || "rejected by the API");
196
+ step(false, "API key validated", detail);
197
+ console.log();
198
+ if (detail.includes("WAF") || detail.includes("1010")) {
199
+ console.log("FAILED: the request was blocked by the edge WAF. This is not an");
200
+ console.log("authentication failure; a proxy or modified client is likely");
201
+ console.log("stripping the SDK's User-Agent header. Restore it, then run:");
202
+ console.log(" agentkey init");
203
+ } else if (detail.includes("could not reach")) {
204
+ console.log("FAILED: the API did not answer within the timeout. Authorization");
205
+ console.log("checks fail closed (allowed=false) until it does. Check the URL");
206
+ console.log("(AGENTKEY_BASE_URL overrides it) and retry: agentkey init");
207
+ } else {
208
+ console.log("FAILED: your AgentKey API key could not be authenticated.");
209
+ console.log("Run: agentkey init");
210
+ console.log(`Create a fresh key at ${DASHBOARD_URL}/connect if you need one.`);
211
+ }
212
+ return 1;
213
+ }
214
+
215
+ async function cmdDoctor() {
216
+ const baseUrl = (process.env.AGENTKEY_BASE_URL || "").trim() || DEFAULT_BASE_URL;
217
+ console.log("AgentKey doctor");
218
+ console.log(RULE);
219
+ console.log();
220
+ let ok = true;
221
+ ok = step(true, "SDK installed", `agentkey-ai ${VERSION}`) && ok;
222
+ const { key, source } = await resolveKey();
223
+ const [reach, status] = await apiReachable(baseUrl, "/api/functions/validate_api_key", "GET");
224
+ ok = step(reach, "API reachable", `${baseUrl}${reach ? ` (HTTP ${status})` : " - unreachable"}`,
225
+ "check the URL (AGENTKEY_BASE_URL overrides it) and your network") && ok;
226
+ if (!key) {
227
+ ok = step(false, "Credentials configured", "no API key configured", "run: agentkey init") && ok;
228
+ if (reach) {
229
+ ok = step(false, "Credentials valid", "skipped, no key", "run: agentkey init") && ok;
230
+ const [areach, astatus] = await apiReachable(baseUrl, "/api/functions/authorize");
231
+ ok = step(areach, "Authorization endpoint reachable", areach ? `HTTP ${astatus}` : "unreachable",
232
+ "authorization checks fail closed until this is reachable") && ok;
233
+ } else {
234
+ // Same host, same fate: no point making more doomed requests.
235
+ ok = step(false, "Credentials valid", "skipped, the API is unreachable",
236
+ "check the URL (AGENTKEY_BASE_URL overrides it) and your network") && ok;
237
+ ok = step(false, "Authorization endpoint reachable", "skipped, same host as the API",
238
+ "authorization checks fail closed until the API is reachable") && ok;
239
+ }
240
+ } else {
241
+ const where = source === "environment"
242
+ ? "environment: AGENTKEY_API_KEY"
243
+ : `config file: ${configFilePath()}`;
244
+ ok = step(true, "Credentials configured", where) && ok;
245
+ if (!reach) {
246
+ ok = step(false, "Credentials valid", "skipped, the API is unreachable",
247
+ "check the URL (AGENTKEY_BASE_URL overrides it) and your network") && ok;
248
+ ok = step(false, "Authorization endpoint reachable", "skipped, same host as the API",
249
+ "authorization checks fail closed until the API is reachable") && ok;
250
+ } else {
251
+ const v = await validateRaw(baseUrl, key);
252
+ const valid = v.valid === true;
253
+ const detail = valid ? "" : String(v.error || v.message || v.reason || "rejected by the API");
254
+ ok = step(valid, "Credentials valid", detail,
255
+ `run: agentkey init, or create a fresh key at ${DASHBOARD_URL}/connect`) && ok;
256
+ const [areach, astatus] = await apiReachable(baseUrl, "/api/functions/authorize");
257
+ ok = step(areach, "Authorization endpoint reachable", areach ? `HTTP ${astatus}` : "unreachable",
258
+ "authorization checks fail closed until this is reachable") && ok;
259
+ }
260
+ }
261
+ console.log();
262
+ if (ok) {
263
+ console.log("Ready.");
264
+ console.log();
265
+ console.log("Next: follow the Quickstart to perform your first authorization.");
266
+ console.log(` ${QUICKSTART_URL}`);
267
+ return 0;
268
+ }
269
+ console.log("FAILED. See the ✗ checks above. Start with: agentkey init");
270
+ return 1;
271
+ }
272
+
273
+ async function cmdLogout() {
274
+ const removed = await deleteConfigKey();
275
+ if (removed) {
276
+ console.log(`Removed the saved API key from ${configFilePath()}`);
277
+ } else {
278
+ console.log("No saved API key found (nothing to remove).");
279
+ }
280
+ if ((process.env.AGENTKEY_API_KEY || "").trim()) {
281
+ console.log("AGENTKEY_API_KEY is also set in your environment; unset it too:");
282
+ console.log(" unset AGENTKEY_API_KEY");
283
+ }
284
+ return 0;
285
+ }
286
+
287
+ function printUsage() {
288
+ console.log("Usage: agentkey <command>");
289
+ console.log();
290
+ console.log("Commands:");
291
+ console.log(" init set up your API key: prompt (masked), validate against the production API, save to the config file");
292
+ console.log(" doctor check the full setup: SDK, credentials, API reachability, key validity");
293
+ console.log(" logout delete the API key saved by init (the environment variable is unaffected)");
294
+ }
295
+
296
+ function onboarding() {
297
+ console.log("AgentKey - authorization and evidence for AI agents");
298
+ console.log(RULE);
299
+ console.log();
300
+ console.log("New here? The fastest path to your first authorization:");
301
+ console.log();
302
+ console.log(" 1. agentkey init - paste your API key, validate against production");
303
+ console.log(" 2. agentkey doctor - verify the full setup");
304
+ console.log(" 3. Quickstart - your first authorization in about 5 minutes:");
305
+ console.log(` ${QUICKSTART_URL}`);
306
+ console.log();
307
+ console.log(`Get an API key at ${DASHBOARD_URL} (Connect wizard).`);
308
+ console.log("agentkey logout deletes the key saved by init.");
309
+ return 0;
310
+ }
311
+
312
+ async function run() {
313
+ const cmd = process.argv[2];
314
+ if (cmd === "init") return cmdInit();
315
+ if (cmd === "doctor") return cmdDoctor();
316
+ if (cmd === "logout") return cmdLogout();
317
+ if (cmd === "--help" || cmd === "-h" || cmd === "help") { printUsage(); return 0; }
318
+ if (cmd === undefined) return onboarding();
319
+ printUsage();
320
+ return 1;
321
+ }
322
+
323
+ run().then((code) => { process.exitCode = code; });
package/package.json CHANGED
@@ -1,8 +1,11 @@
1
1
  {
2
2
  "name": "agentkey-ai",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "description": "Authorization and evidence SDK for AI agents: check permissions before every action, record what agents actually did.",
5
5
  "type": "module",
6
+ "bin": {
7
+ "agentkey": "cli.mjs"
8
+ },
6
9
  "main": "./agentkey.js",
7
10
  "types": "./agentkey.d.ts",
8
11
  "exports": {
@@ -15,6 +18,7 @@
15
18
  "files": [
16
19
  "agentkey.js",
17
20
  "agentkey.d.ts",
21
+ "cli.mjs",
18
22
  "README.md",
19
23
  "LICENSE"
20
24
  ],
@@ -33,4 +37,4 @@
33
37
  ],
34
38
  "license": "MIT",
35
39
  "homepage": "https://agentkey.base44.app"
36
- }
40
+ }