agentkey-ai 1.2.0 → 1.3.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.
Files changed (4) hide show
  1. package/README.md +51 -16
  2. package/agentkey.js +2 -2
  3. package/cli.mjs +99 -28
  4. package/package.json +3 -3
package/README.md CHANGED
@@ -35,39 +35,74 @@ The CLI never prints your key. `agentkey init` saves a pasted key to a local con
35
35
  2. Open the Connect wizard (or Agents, then create an agent).
36
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.
37
37
 
38
- ## First authorization (Python)
38
+ ## First authorization (guided)
39
+
40
+ Run this right after `agentkey init` (or with `AGENTKEY_API_KEY` exported). It performs ONE real, least-privilege authorization: `sandbox.send_test`, an isolated test capability every new agent ships with. It can only ever return allow or deny; it never performs a real external side effect, never touches a real tool, and grants nothing beyond that single permission. The decision is made by the real policy engine and recorded as hash-chained evidence.
41
+
42
+ Python:
39
43
 
40
44
  ```python
41
45
  from agentkey import AgentKeyClient
42
46
 
43
- ak = AgentKeyClient(api_key="agent_live_xxxxx") # production API by default
47
+ ak = AgentKeyClient() # reads AGENTKEY_API_KEY, or the config file from `agentkey init`
48
+
49
+ result = ak.check_permission(action="send_test", resource="sandbox")
44
50
 
45
- result = ak.check_permission(action="send_email", resource="gmail", arguments={"to": "x@company.com"})
46
- if result["allowed"]:
47
- send_email(...) # your code
51
+ print("session_id: ", result.get("session_id"))
52
+ decision = "ALLOW" if result.get("allowed") else ("APPROVAL REQUIRED" if result.get("approval_required") else "DENY")
53
+ print("decision: ", decision, "-", result.get("reason"))
54
+ if result.get("event_id"):
55
+ print("evidence_event_id:", result["event_id"])
56
+ print()
57
+ print("YOU'RE DONE - your first real authorization was decided by the policy")
58
+ print("engine and recorded as verifiable evidence. See it in the dashboard:")
59
+ print("https://agentkey.us/sessions/" + str(result.get("session_id")))
48
60
  else:
49
- print("Blocked:", result["reason"], "approval_required:", result.get("approval_required", False))
61
+ reason = str(result.get("reason") or "")
62
+ if "Invalid API key" in reason:
63
+ print("Your key is invalid - create one at https://agentkey.us/connect")
64
+ elif "No permission configured" in reason:
65
+ print("This agent has no sandbox.send_test permission yet. Add one in the")
66
+ print("dashboard (Agents -> Permissions): resource 'sandbox', action")
67
+ print("'send_test', decision allow - then run this again.")
68
+ else:
69
+ print("Denied by policy:", reason)
50
70
  ```
51
71
 
52
- ## First authorization (JavaScript / TypeScript)
72
+ JavaScript / TypeScript (save as `first-authorization.mjs`, run with `node first-authorization.mjs`):
53
73
 
54
74
  ```js
55
75
  import { AgentKeyClient } from "agentkey-ai";
56
76
 
57
- const ak = new AgentKeyClient({ apiKey: "agent_live_xxxxx" }); // production API by default
77
+ const ak = new AgentKeyClient(); // reads AGENTKEY_API_KEY, or the config file from `npx agentkey init`
58
78
 
59
- const result = await ak.checkPermission({
60
- action: "send_email",
61
- resource: "gmail",
62
- arguments: { to: "x@company.com" },
63
- });
64
- if (result.allowed) {
65
- await sendEmail(); // your code
79
+ const result = await ak.checkPermission({ action: "send_test", resource: "sandbox" });
80
+
81
+ console.log("session_id: ", result.session_id);
82
+ const decision = result.allowed ? "ALLOW" : (result.approval_required ? "APPROVAL REQUIRED" : "DENY");
83
+ console.log("decision: ", decision, "-", result.reason);
84
+ if (result.event_id) {
85
+ console.log("evidence_event_id:", result.event_id);
86
+ console.log();
87
+ console.log("YOU'RE DONE - your first real authorization was decided by the policy");
88
+ console.log("engine and recorded as verifiable evidence. See it in the dashboard:");
89
+ console.log("https://agentkey.us/sessions/" + result.session_id);
66
90
  } else {
67
- console.log("Blocked:", result.reason, "approval_required:", result.approval_required ?? false);
91
+ const reason = String(result.reason || "");
92
+ if (reason.includes("Invalid API key")) {
93
+ console.log("Your key is invalid - create one at https://agentkey.us/connect");
94
+ } else if (reason.includes("No permission configured")) {
95
+ console.log("This agent has no sandbox.send_test permission yet. Add one in the");
96
+ console.log("dashboard (Agents -> Permissions): resource 'sandbox', action");
97
+ console.log("'send_test', decision allow - then run this again.");
98
+ } else {
99
+ console.log("Denied by policy:", reason);
100
+ }
68
101
  }
69
102
  ```
70
103
 
104
+ A wrong, expired, or revoked key fails closed exactly like any other call (`allowed: false`); the scripts above tell you which case you are in.
105
+
71
106
  ## Decisions: allow, deny, ask
72
107
 
73
108
  `check_permission` / `checkPermission` evaluates the permissions you configured for the agent:
package/agentkey.js CHANGED
@@ -19,8 +19,8 @@ const FAIL_CLOSED = { allowed: false, reason: "agentkey_unreachable", fail_close
19
19
  const TIMEOUT_MS = 5000;
20
20
 
21
21
  // Production API. Override with baseUrl only if you self-host.
22
- export const DEFAULT_BASE_URL = "https://agentkey.base44.app";
23
- export const VERSION = "1.2.0";
22
+ export const DEFAULT_BASE_URL = "https://agentkey.us";
23
+ export const VERSION = "1.3.0";
24
24
  const USER_AGENT = `agentkey-sdk-js/${VERSION}`;
25
25
 
26
26
  // Process-wide flag so the first-run info block prints once per process.
package/cli.mjs CHANGED
@@ -34,8 +34,67 @@ function step(ok, label, detail, nextStep) {
34
34
  return ok;
35
35
  }
36
36
 
37
- // Any HTTP response (including 401/400) proves the endpoint is reachable.
38
- // Only a network-level failure (DNS, timeout, refused) means unreachable.
37
+ // A warning is not a pass mark: doctor prints " ! " and exits non-zero when
38
+ // one appears. Used for the edge-firewall block, a broken setup that must
39
+ // not read as healthy.
40
+ function stepWarn(label, detail, nextStep) {
41
+ console.log(` ! ${label}${detail ? ` - ${detail}` : ""}`);
42
+ if (nextStep) console.log(` → ${nextStep}`);
43
+ return false;
44
+ }
45
+
46
+ // The backend host is internal plumbing: output shows a friendly alias, and
47
+ // the raw host appears only when AGENTKEY_DEBUG is set.
48
+ const API_ALIAS = "AgentKey API";
49
+ const displayTarget = (baseUrl) => (process.env.AGENTKEY_DEBUG ? baseUrl : API_ALIAS);
50
+
51
+ // An edge firewall (e.g. Cloudflare) can block a network or IP outright,
52
+ // answering 403 with an HTML block page while the real API is fine. Detect
53
+ // the block from the body and headers, never the status code alone: a plain
54
+ // 401/400 from the API still proves it is reachable.
55
+ function looksBlocked(headers, bodyText) {
56
+ const h = (n) => String((headers && headers.get && headers.get(n)) || "");
57
+ const body = String(bodyText || "");
58
+ if (h("cf-mitigated").toLowerCase() === "block") return true;
59
+ if (body.includes("error code: 1010")) return true;
60
+ if (body.includes("Attention Required") && body.includes("Cloudflare")) return true;
61
+ if (body.includes("Access denied") && body.includes("Cloudflare")) return true;
62
+ return false;
63
+ }
64
+
65
+ // The ray ID to hand support when an edge block is reported.
66
+ function rayIdFrom(headers, bodyText) {
67
+ const h = String((headers && headers.get && headers.get("cf-ray")) || "");
68
+ if (h) return h;
69
+ const m = String(bodyText || "").match(/Ray ID:\s*([0-9a-f-]{8,})/i);
70
+ return m ? m[1] : "unknown";
71
+ }
72
+
73
+ function skippedReason(r) {
74
+ return r.blocked ? "skipped, your network or IP appears blocked" : "skipped, the API is unreachable";
75
+ }
76
+
77
+ function nextStepFor(r) {
78
+ if (r.blocked) return `try another network, or contact support with ray ID ${r.rayId || "unknown"}`;
79
+ return "check the URL (AGENTKEY_BASE_URL overrides it) and your network";
80
+ }
81
+
82
+ async function authEndpointCheck(baseUrl) {
83
+ const a = await apiReachable(baseUrl, "/api/functions/authorize");
84
+ if (!a.reachable) {
85
+ return step(false, "Authorization endpoint reachable", "unreachable",
86
+ "authorization checks fail closed until this is reachable");
87
+ }
88
+ if (a.blocked) {
89
+ return stepWarn("Authorization endpoint reachable",
90
+ `blocked by the edge firewall (HTTP ${a.status})`, nextStepFor(a));
91
+ }
92
+ return step(true, "Authorization endpoint reachable");
93
+ }
94
+
95
+ // Any plain HTTP response (including 401/400) proves the endpoint is
96
+ // reachable; an edge-firewall block does not (see looksBlocked). Only a
97
+ // network-level failure (DNS, timeout, refused) means unreachable.
39
98
  async function apiReachable(baseUrl, path, method = "POST") {
40
99
  const controller = new AbortController();
41
100
  const timeout = setTimeout(() => controller.abort(), TIMEOUT_MS);
@@ -46,9 +105,13 @@ async function apiReachable(baseUrl, path, method = "POST") {
46
105
  body: method === "POST" ? "{}" : undefined,
47
106
  signal: controller.signal,
48
107
  });
49
- return [true, res.status];
108
+ const text = await res.text().catch(() => "");
109
+ if (looksBlocked(res.headers, text)) {
110
+ return { reachable: true, blocked: true, status: res.status, rayId: rayIdFrom(res.headers, text) };
111
+ }
112
+ return { reachable: true, blocked: false, status: res.status, rayId: "" };
50
113
  } catch {
51
- return [false, null];
114
+ return { reachable: false, blocked: false, status: null, rayId: "" };
52
115
  } finally {
53
116
  clearTimeout(timeout);
54
117
  }
@@ -70,8 +133,11 @@ async function validateRaw(baseUrl, key) {
70
133
  const data = JSON.parse(text);
71
134
  if (data && typeof data.valid === "boolean") return data;
72
135
  } 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" };
136
+ if (looksBlocked(res.headers, text)) {
137
+ const error = text.includes("error code: 1010")
138
+ ? "request blocked by the edge WAF (Cloudflare error 1010) - not an authentication failure"
139
+ : "request blocked by the edge firewall - not an authentication failure";
140
+ return { valid: false, error, blocked: true, rayId: rayIdFrom(res.headers, text) };
75
141
  }
76
142
  return { valid: false, error: `API returned HTTP ${res.status}` };
77
143
  } catch (e) {
@@ -186,6 +252,8 @@ async function cmdInit() {
186
252
  console.log("The key is saved at the config file above. The AGENTKEY_API_KEY");
187
253
  console.log("environment variable overrides it; unset yours to use the saved");
188
254
  console.log("key. Remove it any time: agentkey logout");
255
+ } else {
256
+ console.log("To remove a key saved by init later: agentkey logout");
189
257
  }
190
258
  console.log("Next:");
191
259
  console.log(" 1. agentkey doctor");
@@ -195,7 +263,11 @@ async function cmdInit() {
195
263
  const detail = String(v.error || v.message || v.reason || "rejected by the API");
196
264
  step(false, "API key validated", detail);
197
265
  console.log();
198
- if (detail.includes("WAF") || detail.includes("1010")) {
266
+ if (v.blocked && !detail.includes("1010")) {
267
+ console.log("FAILED: the request was blocked by the edge firewall. This is not an");
268
+ console.log("authentication failure. Your network or IP may be blocked; try");
269
+ console.log(`another network, or contact support with this ray ID: ${v.rayId || "unknown"}`);
270
+ } else if (detail.includes("WAF") || detail.includes("1010")) {
199
271
  console.log("FAILED: the request was blocked by the edge WAF. This is not an");
200
272
  console.log("authentication failure; a proxy or modified client is likely");
201
273
  console.log("stripping the SDK's User-Agent header. Restore it, then run:");
@@ -214,48 +286,48 @@ async function cmdInit() {
214
286
 
215
287
  async function cmdDoctor() {
216
288
  const baseUrl = (process.env.AGENTKEY_BASE_URL || "").trim() || DEFAULT_BASE_URL;
289
+ const target = displayTarget(baseUrl);
217
290
  console.log("AgentKey doctor");
218
291
  console.log(RULE);
219
292
  console.log();
220
293
  let ok = true;
221
294
  ok = step(true, "SDK installed", `agentkey-ai ${VERSION}`) && ok;
222
295
  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;
296
+ const reach = await apiReachable(baseUrl, "/api/functions/validate_api_key", "GET");
297
+ if (!reach.reachable) {
298
+ ok = step(false, "API reachable", `${target} - unreachable`, nextStepFor(reach)) && ok;
299
+ } else if (reach.blocked) {
300
+ ok = stepWarn("API reachable", `${target} - blocked by the edge firewall (HTTP ${reach.status})`, nextStepFor(reach)) && ok;
301
+ } else {
302
+ ok = step(true, "API reachable", target) && ok;
303
+ }
304
+ const usable = reach.reachable && !reach.blocked;
226
305
  if (!key) {
227
306
  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;
307
+ if (!usable) {
308
+ ok = step(false, "Credentials valid", skippedReason(reach), nextStepFor(reach)) && ok;
237
309
  ok = step(false, "Authorization endpoint reachable", "skipped, same host as the API",
238
- "authorization checks fail closed until the API is reachable") && ok;
310
+ "authorization checks fail closed until the API is usable") && ok;
311
+ } else {
312
+ ok = step(false, "Credentials valid", "skipped, no key", "run: agentkey init") && ok;
313
+ ok = (await authEndpointCheck(baseUrl)) && ok;
239
314
  }
240
315
  } else {
241
316
  const where = source === "environment"
242
317
  ? "environment: AGENTKEY_API_KEY"
243
318
  : `config file: ${configFilePath()}`;
244
319
  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;
320
+ if (!usable) {
321
+ ok = step(false, "Credentials valid", skippedReason(reach), nextStepFor(reach)) && ok;
248
322
  ok = step(false, "Authorization endpoint reachable", "skipped, same host as the API",
249
- "authorization checks fail closed until the API is reachable") && ok;
323
+ "authorization checks fail closed until the API is usable") && ok;
250
324
  } else {
251
325
  const v = await validateRaw(baseUrl, key);
252
326
  const valid = v.valid === true;
253
327
  const detail = valid ? "" : String(v.error || v.message || v.reason || "rejected by the API");
254
328
  ok = step(valid, "Credentials valid", detail,
255
329
  `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;
330
+ ok = (await authEndpointCheck(baseUrl)) && ok;
259
331
  }
260
332
  }
261
333
  console.log();
@@ -305,7 +377,6 @@ function onboarding() {
305
377
  console.log(` ${QUICKSTART_URL}`);
306
378
  console.log();
307
379
  console.log(`Get an API key at ${DASHBOARD_URL} (Connect wizard).`);
308
- console.log("agentkey logout deletes the key saved by init.");
309
380
  return 0;
310
381
  }
311
382
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentkey-ai",
3
- "version": "1.2.0",
3
+ "version": "1.3.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
6
  "bin": {
@@ -36,5 +36,5 @@
36
36
  "llm"
37
37
  ],
38
38
  "license": "MIT",
39
- "homepage": "https://agentkey.base44.app"
40
- }
39
+ "homepage": "https://agentkey.us"
40
+ }