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.
- package/README.md +51 -16
- package/agentkey.js +2 -2
- package/cli.mjs +99 -28
- 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 (
|
|
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(
|
|
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
|
-
|
|
46
|
-
if result
|
|
47
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
if (result.
|
|
65
|
-
|
|
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
|
-
|
|
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.
|
|
23
|
-
export const VERSION = "1.
|
|
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
|
-
//
|
|
38
|
-
//
|
|
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
|
-
|
|
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
|
|
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 (
|
|
74
|
-
|
|
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 (
|
|
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
|
|
224
|
-
|
|
225
|
-
|
|
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 (
|
|
229
|
-
ok = step(false, "Credentials valid",
|
|
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
|
|
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 (!
|
|
246
|
-
ok = step(false, "Credentials valid",
|
|
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
|
|
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
|
-
|
|
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.
|
|
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.
|
|
40
|
-
}
|
|
39
|
+
"homepage": "https://agentkey.us"
|
|
40
|
+
}
|