agentkey-ai 1.0.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/LICENSE +21 -0
- package/README.md +167 -0
- package/agentkey.d.ts +118 -0
- package/agentkey.js +372 -0
- package/package.json +36 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 AgentKey
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# AgentKey SDK
|
|
2
|
+
|
|
3
|
+
AgentKey is an authorization and evidence layer for AI agents. Before an agent takes an action, the SDK asks the AgentKey API whether it is allowed, denied, or requires human approval. Both the decision and what the agent actually did are recorded as hash-chained evidence events you can inspect in a dashboard.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Python (3.8+, zero dependencies):
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install agentkey
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
JavaScript / TypeScript (Node 18+, zero dependencies, ESM):
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install agentkey-ai
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Get an API key
|
|
20
|
+
|
|
21
|
+
1. Sign up at the AgentKey dashboard: https://agentkey.base44.app
|
|
22
|
+
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.
|
|
24
|
+
|
|
25
|
+
## First authorization (Python)
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
from agentkey import AgentKeyClient
|
|
29
|
+
|
|
30
|
+
ak = AgentKeyClient(api_key="agent_live_xxxxx") # production API by default
|
|
31
|
+
|
|
32
|
+
result = ak.check_permission(action="send_email", resource="gmail", arguments={"to": "x@company.com"})
|
|
33
|
+
if result["allowed"]:
|
|
34
|
+
send_email(...) # your code
|
|
35
|
+
else:
|
|
36
|
+
print("Blocked:", result["reason"], "approval_required:", result.get("approval_required", False))
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## First authorization (JavaScript / TypeScript)
|
|
40
|
+
|
|
41
|
+
```js
|
|
42
|
+
import { AgentKeyClient } from "agentkey-ai";
|
|
43
|
+
|
|
44
|
+
const ak = new AgentKeyClient({ apiKey: "agent_live_xxxxx" }); // production API by default
|
|
45
|
+
|
|
46
|
+
const result = await ak.checkPermission({
|
|
47
|
+
action: "send_email",
|
|
48
|
+
resource: "gmail",
|
|
49
|
+
arguments: { to: "x@company.com" },
|
|
50
|
+
});
|
|
51
|
+
if (result.allowed) {
|
|
52
|
+
await sendEmail(); // your code
|
|
53
|
+
} else {
|
|
54
|
+
console.log("Blocked:", result.reason, "approval_required:", result.approval_required ?? false);
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Decisions: allow, deny, ask
|
|
59
|
+
|
|
60
|
+
`check_permission` / `checkPermission` evaluates the permissions you configured for the agent:
|
|
61
|
+
|
|
62
|
+
- **allow**: `allowed: true`. Run the action, then record the execution (below).
|
|
63
|
+
- **deny**: `allowed: false` with the server's reason. Do not run the action.
|
|
64
|
+
- **ask** (human approval): `allowed: false`, `approval_required: true`. The request appears on the Approvals page in the dashboard, where a human approves or denies it. Do not run the action until it is approved.
|
|
65
|
+
|
|
66
|
+
Fail-closed: if the service cannot return a valid decision within 5 seconds, the SDK returns:
|
|
67
|
+
|
|
68
|
+
```json
|
|
69
|
+
{ "allowed": false, "reason": "agentkey_unreachable", "fail_closed": true }
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Treat `fail_closed: true` as an infrastructure failure and `allowed: false` without it as an authorization decision. Either way the agent must not proceed.
|
|
73
|
+
|
|
74
|
+
## wrap(): authorize every tool call in one line
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
agent = ak.wrap(my_agent) # observe (default): records, blocks nothing
|
|
78
|
+
agent = ak.wrap(my_agent, mode="enforce") # raises AgentKeyDenied on a denial
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
```js
|
|
82
|
+
const agent = ak.wrap(myAgent); // observe (default)
|
|
83
|
+
const agent = ak.wrap(myAgent, { mode: "enforce" }); // raises AgentKeyDenied on a denial
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
`wrap()` detects an MCP client (`callTool` / `call_tool`), a LangChain agent or tool list, a plain object or dict of functions, or a single function. Observe mode records every call and blocks nothing, so you can see what AgentKey would have caught before trusting it with enforcement. Enforce mode raises `AgentKeyDenied` and does not run the tool. If no session id is passed, a session is started automatically and ended best-effort at process exit.
|
|
87
|
+
|
|
88
|
+
## Sessions
|
|
89
|
+
|
|
90
|
+
Every decision and execution is recorded as an evidence event on the session's hash chain. Group a task into one session:
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
s = ak.start_session()
|
|
94
|
+
# ... checks and actions ...
|
|
95
|
+
ak.end_session(s["session_id"])
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
```js
|
|
99
|
+
const s = await ak.startSession();
|
|
100
|
+
// ... checks and actions ...
|
|
101
|
+
await ak.endSession({ sessionId: s.session_id });
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Recording what the agent actually did
|
|
105
|
+
|
|
106
|
+
After an allowed action runs, record the execution, linked back to its decision by `authorization_id`:
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
auth = ak.check_permission(action="send_email", resource="gmail", session_id=sid)
|
|
110
|
+
if auth["allowed"]:
|
|
111
|
+
send_email(...)
|
|
112
|
+
ak.record_action(session_id=sid, authorization_id=auth["event_id"], tool="gmail",
|
|
113
|
+
action="send_email", resource="gmail", result_status="success")
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
`record_action` never refuses to record, so evidence is not lost for billing reasons. Actions that run without any authorization decision are surfaced on the dashboard as findings (`executions_without_authorization`), because the SDK is self-reported: an agent that bypasses the wrapped functions produces no evidence.
|
|
117
|
+
|
|
118
|
+
## guard(): authorize, run, record in one call
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
out = ak.guard(sid, "gmail", "send_email", lambda: send_email(...), arguments={"to": "x@company.com"})
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
```js
|
|
125
|
+
const out = await ak.guard({ sessionId: sid, resource: "gmail", action: "send_email" }, async () => sendEmail());
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
If authorize denies, `guard` returns the denial and does not run the function.
|
|
129
|
+
|
|
130
|
+
## Delegated authorization
|
|
131
|
+
|
|
132
|
+
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
|
+
|
|
134
|
+
## Production API
|
|
135
|
+
|
|
136
|
+
Base URL: `https://agentkey.base44.app` (the SDK default). Override it with `base_url` (Python) or `baseUrl` (JavaScript) if you self-host.
|
|
137
|
+
|
|
138
|
+
All endpoints are POST with a Bearer API key, under `/api/functions/`:
|
|
139
|
+
|
|
140
|
+
- `authorize`
|
|
141
|
+
- `record_action`
|
|
142
|
+
- `start_session`
|
|
143
|
+
- `end_session`
|
|
144
|
+
- `delegate`
|
|
145
|
+
- `validate_api_key` (GET)
|
|
146
|
+
|
|
147
|
+
Raw HTTP:
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
curl -X POST https://agentkey.base44.app/api/functions/authorize \
|
|
151
|
+
-H "Authorization: Bearer agent_live_xxxxx" \
|
|
152
|
+
-H "Content-Type: application/json" \
|
|
153
|
+
-d '{"action":"send_email","resource":"gmail","arguments":{"to":"x@company.com"}}'
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Dashboard
|
|
157
|
+
|
|
158
|
+
Sessions, evidence events, approvals, findings and permission settings: https://agentkey.base44.app
|
|
159
|
+
|
|
160
|
+
## Security limitations (current, accurate)
|
|
161
|
+
|
|
162
|
+
- Evidence events are hash-chained per session, and session Merkle roots are attested with HMAC-SHA256 under a server-side key. This detects altered or missing events in stored evidence. It is not an asymmetric digital signature scheme, it does not make records forgery-proof against a compromised server, and it is not a non-repudiation guarantee.
|
|
163
|
+
- SDK instrumentation is self-reported. `wrap()` and `record_action` record what the agent reports; an agent that calls tools outside the SDK produces no evidence. This is reported as an `executions_without_authorization` finding, but not prevented.
|
|
164
|
+
- Checks fail closed on network errors and timeouts. In observe mode the SDK still lets the call run; only enforce mode blocks it.
|
|
165
|
+
- No SOC 2 or other third-party compliance audit has been completed.
|
|
166
|
+
|
|
167
|
+
License: MIT.
|
package/agentkey.d.ts
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// Type declarations for the AgentKey JavaScript / TypeScript SDK.
|
|
2
|
+
|
|
3
|
+
export const DEFAULT_BASE_URL: string;
|
|
4
|
+
|
|
5
|
+
export class AgentKeyDenied extends Error {
|
|
6
|
+
constructor(reason?: string, result?: unknown);
|
|
7
|
+
reason: string;
|
|
8
|
+
result: unknown;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface CheckResult {
|
|
12
|
+
allowed: boolean;
|
|
13
|
+
reason: string;
|
|
14
|
+
request_id: string;
|
|
15
|
+
session_id?: string;
|
|
16
|
+
event_id?: string;
|
|
17
|
+
attempt_id?: string;
|
|
18
|
+
approval_required?: boolean;
|
|
19
|
+
fail_closed?: boolean;
|
|
20
|
+
usage?: { used: number; limit: number };
|
|
21
|
+
plan?: string;
|
|
22
|
+
[key: string]: unknown;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface DelegateResult {
|
|
26
|
+
allowed?: boolean;
|
|
27
|
+
decision?: string;
|
|
28
|
+
delegation_id?: string;
|
|
29
|
+
scopes_granted?: string[];
|
|
30
|
+
depth?: number;
|
|
31
|
+
expires_at?: string;
|
|
32
|
+
[key: string]: unknown;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface SessionResult {
|
|
36
|
+
session_id?: string;
|
|
37
|
+
agent_id?: string;
|
|
38
|
+
started_at?: string;
|
|
39
|
+
preceding_scan_id?: string;
|
|
40
|
+
[key: string]: unknown;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface WrapOptions {
|
|
44
|
+
sessionId?: string;
|
|
45
|
+
mode?: "observe" | "enforce";
|
|
46
|
+
scanId?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface ClientOptions {
|
|
50
|
+
apiKey: string;
|
|
51
|
+
baseUrl?: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export class AgentKeyClient {
|
|
55
|
+
constructor(options: ClientOptions);
|
|
56
|
+
|
|
57
|
+
checkPermission(args: {
|
|
58
|
+
action: string;
|
|
59
|
+
resource: string;
|
|
60
|
+
tool?: string;
|
|
61
|
+
metadata?: Record<string, unknown>;
|
|
62
|
+
arguments?: Record<string, unknown>;
|
|
63
|
+
sessionId?: string;
|
|
64
|
+
parentEventId?: string;
|
|
65
|
+
parentAgentId?: string;
|
|
66
|
+
source?: string;
|
|
67
|
+
scanId?: string;
|
|
68
|
+
delegationId?: string;
|
|
69
|
+
}): Promise<CheckResult>;
|
|
70
|
+
|
|
71
|
+
delegate(args: {
|
|
72
|
+
childAgentId: string;
|
|
73
|
+
scopes: string[];
|
|
74
|
+
ttlHours?: number;
|
|
75
|
+
sessionId?: string;
|
|
76
|
+
parentDelegationId?: string;
|
|
77
|
+
}): Promise<DelegateResult>;
|
|
78
|
+
|
|
79
|
+
validate(): Promise<{ valid: boolean; reason?: string; fail_closed?: boolean; [key: string]: unknown }>;
|
|
80
|
+
|
|
81
|
+
startSession(args?: { metadata?: Record<string, unknown>; scanId?: string }): Promise<SessionResult>;
|
|
82
|
+
|
|
83
|
+
endSession(args: { sessionId: string; status?: string }): Promise<Record<string, unknown>>;
|
|
84
|
+
|
|
85
|
+
recordAction(args: {
|
|
86
|
+
sessionId?: string;
|
|
87
|
+
authorizationId?: string;
|
|
88
|
+
attemptId?: string;
|
|
89
|
+
tool?: string;
|
|
90
|
+
action: string;
|
|
91
|
+
resource?: string;
|
|
92
|
+
arguments?: Record<string, unknown>;
|
|
93
|
+
resultStatus?: string;
|
|
94
|
+
resultHash?: string;
|
|
95
|
+
durationMs?: number;
|
|
96
|
+
errorMessage?: string;
|
|
97
|
+
metadata?: Record<string, unknown>;
|
|
98
|
+
scanId?: string;
|
|
99
|
+
}): Promise<Record<string, unknown>>;
|
|
100
|
+
|
|
101
|
+
guard(
|
|
102
|
+
args: {
|
|
103
|
+
sessionId?: string;
|
|
104
|
+
resource: string;
|
|
105
|
+
action: string;
|
|
106
|
+
tool?: string;
|
|
107
|
+
arguments?: Record<string, unknown>;
|
|
108
|
+
scanId?: string;
|
|
109
|
+
},
|
|
110
|
+
fn: () => Promise<unknown>
|
|
111
|
+
): Promise<{ allowed: boolean; result?: unknown; authorization?: CheckResult; reason?: string }>;
|
|
112
|
+
|
|
113
|
+
wrap(target: unknown, options?: WrapOptions): unknown;
|
|
114
|
+
|
|
115
|
+
_hashResult(result: unknown): Promise<string>;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export default AgentKeyClient;
|
package/agentkey.js
ADDED
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
// AgentKey — minimal JavaScript / TypeScript SDK.
|
|
2
|
+
// Zero dependencies. Works in Node 18+ and modern browsers (global fetch).
|
|
3
|
+
//
|
|
4
|
+
// One-line install: wrap an existing agent and AgentKey records every tool call
|
|
5
|
+
// as paired authorization + execution evidence — no call-site changes.
|
|
6
|
+
//
|
|
7
|
+
// import { AgentKeyClient } from "./agentkey.js";
|
|
8
|
+
// const ak = new AgentKeyClient({ apiKey: "agent_live_xxxxx" }); // baseUrl defaults to the production API
|
|
9
|
+
// const agent = ak.wrap(myAgent); // observe mode (default): records, blocks nothing
|
|
10
|
+
// // ak.wrap(myAgent, { mode: "enforce" }); // enforce mode: raises AgentKeyDenied on a denial
|
|
11
|
+
//
|
|
12
|
+
// Fail-closed: if the AgentKey service cannot return a valid decision within
|
|
13
|
+
// 5 seconds (network error, timeout, non-JSON body, or a response missing the
|
|
14
|
+
// `allowed` field), every check returns { allowed: false, reason:
|
|
15
|
+
// "agentkey_unreachable", fail_closed: true } so an agent never proceeds on a
|
|
16
|
+
// missing or ambiguous decision.
|
|
17
|
+
|
|
18
|
+
const FAIL_CLOSED = { allowed: false, reason: "agentkey_unreachable", fail_closed: true };
|
|
19
|
+
const TIMEOUT_MS = 5000;
|
|
20
|
+
|
|
21
|
+
// Production API. Override with baseUrl only if you self-host.
|
|
22
|
+
export const DEFAULT_BASE_URL = "https://agentkey.base44.app";
|
|
23
|
+
const USER_AGENT = "agentkey-js-sdk/1.0.0";
|
|
24
|
+
|
|
25
|
+
// Process-wide flag so the first-run info block prints once per process.
|
|
26
|
+
let _wrapFirstRunPrinted = false;
|
|
27
|
+
|
|
28
|
+
export class AgentKeyDenied extends Error {
|
|
29
|
+
constructor(reason, result) {
|
|
30
|
+
super(reason || "denied by AgentKey policy");
|
|
31
|
+
this.name = "AgentKeyDenied";
|
|
32
|
+
this.result = result || null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export class AgentKeyClient {
|
|
37
|
+
constructor({ apiKey, baseUrl = DEFAULT_BASE_URL }) {
|
|
38
|
+
if (!apiKey) throw new Error("AgentKey: apiKey is required");
|
|
39
|
+
this.apiKey = apiKey;
|
|
40
|
+
this.baseUrl = String(baseUrl).replace(/\/$/, "");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async _post(path, body, extraHeaders = {}) {
|
|
44
|
+
const controller = new AbortController();
|
|
45
|
+
const timeout = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
46
|
+
try {
|
|
47
|
+
const res = await fetch(`${this.baseUrl}${path}`, {
|
|
48
|
+
method: "POST",
|
|
49
|
+
headers: { "Content-Type": "application/json", "User-Agent": USER_AGENT, ...extraHeaders },
|
|
50
|
+
body: JSON.stringify(body),
|
|
51
|
+
signal: controller.signal,
|
|
52
|
+
});
|
|
53
|
+
const data = await res.json().catch(() => null);
|
|
54
|
+
if (!data || typeof data.allowed !== "boolean") return { ...FAIL_CLOSED };
|
|
55
|
+
return data;
|
|
56
|
+
} catch (e) {
|
|
57
|
+
return { ...FAIL_CLOSED };
|
|
58
|
+
} finally {
|
|
59
|
+
clearTimeout(timeout);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async _postJson(path, body, extraHeaders = {}) {
|
|
64
|
+
const controller = new AbortController();
|
|
65
|
+
const timeout = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
66
|
+
try {
|
|
67
|
+
const res = await fetch(`${this.baseUrl}${path}`, {
|
|
68
|
+
method: "POST",
|
|
69
|
+
headers: { "Content-Type": "application/json", "User-Agent": USER_AGENT, ...extraHeaders },
|
|
70
|
+
body: JSON.stringify(body),
|
|
71
|
+
signal: controller.signal,
|
|
72
|
+
});
|
|
73
|
+
const data = await res.json().catch(() => null);
|
|
74
|
+
return data || { error: "agentkey_unreachable" };
|
|
75
|
+
} catch (e) {
|
|
76
|
+
return { error: "agentkey_unreachable" };
|
|
77
|
+
} finally {
|
|
78
|
+
clearTimeout(timeout);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Evaluate a permission. Returns { allowed, reason, request_id, session_id,
|
|
83
|
+
* event_id, usage, plan }. Pass sessionId/parentEventId/parentAgentId to
|
|
84
|
+
* thread the decision into an evidence session and record it on the agent's
|
|
85
|
+
* hash chain. Fails closed (allowed: false) on any error or timeout. */
|
|
86
|
+
async checkPermission({ action, resource, tool, metadata = {}, arguments: args, sessionId, parentEventId, parentAgentId, source, scanId, delegationId }) {
|
|
87
|
+
if (!action || !resource) throw new Error("AgentKey: action and resource are required");
|
|
88
|
+
return this._post(
|
|
89
|
+
"/api/functions/authorize",
|
|
90
|
+
{ 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 },
|
|
91
|
+
{ Authorization: `Bearer ${this.apiKey}` }
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Delegated authorization: delegate a scoped subset of THIS agent's
|
|
96
|
+
* authority (the caller of this method is the PARENT agent) to a child
|
|
97
|
+
* agent. Scopes are "resource:action" strings and must be a subset of the
|
|
98
|
+
* parent's own permissions — anything broader is blocked server-side
|
|
99
|
+
* (scope escalation prevention). Returns
|
|
100
|
+
* { allowed, decision, delegation_id, scopes_granted, depth, expires_at }.
|
|
101
|
+
* The delegation_id is runtime-minted: the caller can neither choose nor
|
|
102
|
+
* reset it. The child then presents delegationId to checkPermission() to
|
|
103
|
+
* act under the delegation. Fail-closed. */
|
|
104
|
+
async delegate({ childAgentId, scopes, ttlHours, sessionId, parentDelegationId }) {
|
|
105
|
+
if (!childAgentId || !Array.isArray(scopes) || scopes.length === 0) {
|
|
106
|
+
throw new Error("AgentKey: childAgentId and a non-empty scopes array are required");
|
|
107
|
+
}
|
|
108
|
+
return this._postJson(
|
|
109
|
+
"/api/functions/delegate",
|
|
110
|
+
{
|
|
111
|
+
child_agent_id: childAgentId,
|
|
112
|
+
scopes,
|
|
113
|
+
ttl_hours: ttlHours,
|
|
114
|
+
session_id: sessionId,
|
|
115
|
+
parent_delegation_id: parentDelegationId,
|
|
116
|
+
},
|
|
117
|
+
{ Authorization: `Bearer ${this.apiKey}` }
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Validate the key is active. Returns { valid, message, agent_id, agent_name }. */
|
|
122
|
+
async validate() {
|
|
123
|
+
const controller = new AbortController();
|
|
124
|
+
const timeout = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
125
|
+
try {
|
|
126
|
+
const res = await fetch(`${this.baseUrl}/api/functions/validate_api_key`, {
|
|
127
|
+
headers: { Authorization: `Bearer ${this.apiKey}` },
|
|
128
|
+
signal: controller.signal,
|
|
129
|
+
});
|
|
130
|
+
const data = await res.json().catch(() => null);
|
|
131
|
+
if (!data || typeof data.valid !== "boolean") {
|
|
132
|
+
return { valid: false, reason: "agentkey_unreachable", fail_closed: true };
|
|
133
|
+
}
|
|
134
|
+
return data;
|
|
135
|
+
} catch (e) {
|
|
136
|
+
return { valid: false, reason: "agentkey_unreachable", fail_closed: true };
|
|
137
|
+
} finally {
|
|
138
|
+
clearTimeout(timeout);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Start an evidence session rooted at this agent's key.
|
|
143
|
+
* Returns { session_id, agent_id, started_at }. */
|
|
144
|
+
/** Start an evidence session rooted at this agent's key. Pass scanId to
|
|
145
|
+
* attach an input scan once at the start of a task; it is inherited by every
|
|
146
|
+
* decision in the session unless overridden per call. Returns { session_id,
|
|
147
|
+
* agent_id, started_at, preceding_scan_id }. */
|
|
148
|
+
async startSession({ metadata, scanId } = {}) {
|
|
149
|
+
return this._postJson("/api/functions/start_session", { metadata, scan_id: scanId }, { Authorization: `Bearer ${this.apiKey}` });
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** End an evidence session. Returns { session_id, status, event_count }.
|
|
153
|
+
* Pass status: "failed" to mark a failed session. */
|
|
154
|
+
async endSession({ sessionId, status } = {}) {
|
|
155
|
+
return this._postJson("/api/functions/end_session", { session_id: sessionId, status }, { Authorization: `Bearer ${this.apiKey}` });
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Record what an agent ACTUALLY did, as an execution event on the same hash
|
|
159
|
+
* chain as the decision that permitted it. Pass the `event_id` returned by
|
|
160
|
+
* checkPermission as authorizationId to link the execution to its decision.
|
|
161
|
+
* Self-reported: an agent that does not call it produces no execution
|
|
162
|
+
* evidence — which is exactly why executions_without_authorization is
|
|
163
|
+
* reported. `metadata` is sanitized to primitives before storage (used by
|
|
164
|
+
* wrap() to mark would_block in observe mode). */
|
|
165
|
+
async recordAction({ sessionId, authorizationId, attemptId, tool, action, resource, arguments: args, resultStatus, resultHash, durationMs, errorMessage, metadata, scanId }) {
|
|
166
|
+
return this._postJson("/api/functions/record_action", {
|
|
167
|
+
session_id: sessionId, authorization_id: authorizationId, attempt_id: attemptId, tool, action, resource, arguments: args,
|
|
168
|
+
result_status: resultStatus, result_hash: resultHash, duration_ms: durationMs, error_message: errorMessage,
|
|
169
|
+
metadata, input_scan_id: scanId,
|
|
170
|
+
}, { Authorization: `Bearer ${this.apiKey}` });
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async _sha256(text) {
|
|
174
|
+
try {
|
|
175
|
+
const data = new TextEncoder().encode(String(text));
|
|
176
|
+
const buf = await crypto.subtle.digest("SHA-256", data);
|
|
177
|
+
return Array.from(new Uint8Array(buf)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
178
|
+
} catch (e) { return ""; }
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async _hashResult(result) {
|
|
182
|
+
try { return await this._sha256(JSON.stringify(result)); } catch (e) { return ""; }
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Convenience wrapper: authorize, run `fn` if allowed, then record the
|
|
186
|
+
* execution linked to that decision and hashed against the result. */
|
|
187
|
+
async guard({ sessionId, resource, action, tool, arguments: args, scanId }, fn) {
|
|
188
|
+
const auth = await this.checkPermission({ action, resource, tool, arguments: args, sessionId, scanId });
|
|
189
|
+
if (!auth || !auth.allowed) return auth;
|
|
190
|
+
const t0 = Date.now();
|
|
191
|
+
try {
|
|
192
|
+
const result = await fn();
|
|
193
|
+
const durationMs = Date.now() - t0;
|
|
194
|
+
try {
|
|
195
|
+
const resultHash = await this._hashResult(result);
|
|
196
|
+
await this.recordAction({ sessionId, authorizationId: auth.event_id, attemptId: auth.attempt_id, tool, action, resource, arguments: args, resultStatus: "success", resultHash, durationMs, scanId });
|
|
197
|
+
} catch (e) {}
|
|
198
|
+
return { allowed: true, result, authorization: auth };
|
|
199
|
+
} catch (e) {
|
|
200
|
+
const durationMs = Date.now() - t0;
|
|
201
|
+
try {
|
|
202
|
+
await this.recordAction({ sessionId, authorizationId: auth.event_id, attemptId: auth.attempt_id, tool, action, resource, arguments: args, resultStatus: "error", errorMessage: String(e && e.message ? e.message : e).slice(0, 200), durationMs, scanId });
|
|
203
|
+
} catch (recErr) {}
|
|
204
|
+
throw e;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** One-line auto-instrumentation. Wraps an existing agent — an MCP client
|
|
209
|
+
* (callTool), a LangChain agent or tool list (tools[].invoke/run/_call), a
|
|
210
|
+
* plain object of functions, or a single function — so every tool call is
|
|
211
|
+
* authorized and recorded without changing any call site.
|
|
212
|
+
*
|
|
213
|
+
* mode: "observe" (default) records everything and blocks nothing; a denial
|
|
214
|
+
* is logged ("AgentKey would have blocked this: <reason>") and the call still
|
|
215
|
+
* runs, recorded with would_block: true. mode: "enforce" raises
|
|
216
|
+
* AgentKeyDenied on a denial and does NOT run the tool.
|
|
217
|
+
*
|
|
218
|
+
* If no sessionId is given, a session is started automatically and ended
|
|
219
|
+
* best-effort on process exit (never throws on shutdown). Wrapped functions
|
|
220
|
+
* preserve the original name and return value — a wrapped agent behaves
|
|
221
|
+
* identically when everything is allowed.
|
|
222
|
+
*
|
|
223
|
+
* wrap() is still self-reported: an agent that bypasses the wrapped
|
|
224
|
+
* functions produces no evidence, which is exactly why
|
|
225
|
+
* executions_without_authorization is reported. */
|
|
226
|
+
/** One-line auto-instrumentation (see above). Pass scanId to attach an input
|
|
227
|
+
* scan to the auto-started session so every wrapped tool call inherits it. */
|
|
228
|
+
wrap(target, { sessionId, mode = "observe", scanId } = {}) {
|
|
229
|
+
return _wrapTarget(this, target, { sessionId, mode, scanId });
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// --- wrap() internals ---------------------------------------------------------
|
|
234
|
+
|
|
235
|
+
function _deriveName(t, fallback) {
|
|
236
|
+
if (t && typeof t.name === "string" && t.name) return t.name;
|
|
237
|
+
return fallback;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function _getInvoke(tool) {
|
|
241
|
+
if (typeof tool.invoke === "function") return { fn: tool.invoke.bind(tool), key: "invoke" };
|
|
242
|
+
if (typeof tool.run === "function") return { fn: tool.run.bind(tool), key: "run" };
|
|
243
|
+
if (typeof tool._call === "function") return { fn: tool._call.bind(tool), key: "_call" };
|
|
244
|
+
if (typeof tool === "function") return { fn: tool, key: null };
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function _discover(target) {
|
|
249
|
+
if (target && typeof target.callTool === "function") {
|
|
250
|
+
return [{ name: "call_tool", host: target, inv: { fn: target.callTool.bind(target), key: "callTool" }, kind: "mcp" }];
|
|
251
|
+
}
|
|
252
|
+
if (Array.isArray(target)) {
|
|
253
|
+
return target.map((t, i) => ({ name: _deriveName(t, `tool_${i}`), host: t, inv: _getInvoke(t), kind: "list" }));
|
|
254
|
+
}
|
|
255
|
+
if (target && Array.isArray(target.tools)) {
|
|
256
|
+
return target.tools.map((t, i) => ({ name: _deriveName(t, `tool_${i}`), host: t, inv: _getInvoke(t), kind: "langchain" }));
|
|
257
|
+
}
|
|
258
|
+
if (target && typeof target === "object") {
|
|
259
|
+
return Object.entries(target)
|
|
260
|
+
.filter(([, v]) => typeof v === "function")
|
|
261
|
+
.map(([name, fn]) => ({ name, host: target, inv: { fn: fn.bind(target), key: null }, kind: "dict" }));
|
|
262
|
+
}
|
|
263
|
+
if (typeof target === "function") {
|
|
264
|
+
return [{ name: _deriveName(target, "anonymous"), host: target, inv: { fn: target, key: null }, kind: "fn" }];
|
|
265
|
+
}
|
|
266
|
+
return [];
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function _argsToObject(args) {
|
|
270
|
+
const a = args[0];
|
|
271
|
+
return a && typeof a === "object" && !Array.isArray(a) && a !== null ? a : {};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function _wrapTarget(client, target, { sessionId, mode = "observe", scanId } = {}) {
|
|
275
|
+
const enforce = mode === "enforce";
|
|
276
|
+
const tools = _discover(target);
|
|
277
|
+
const auto = !sessionId;
|
|
278
|
+
const state = { sessionId: sessionId || null, booted: false, ended: false };
|
|
279
|
+
|
|
280
|
+
function _printFirstRun() {
|
|
281
|
+
if (_wrapFirstRunPrinted) return;
|
|
282
|
+
_wrapFirstRunPrinted = true;
|
|
283
|
+
const url = `${client.baseUrl}/sessions/${state.sessionId || ""}`;
|
|
284
|
+
try {
|
|
285
|
+
console.error(`AgentKey: session=${state.sessionId || "?"} tools=${tools.length} mode=${enforce ? "enforce" : "observe"} dashboard=${url}`);
|
|
286
|
+
} catch (e) {}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
async function boot() {
|
|
290
|
+
if (state.booted) return;
|
|
291
|
+
state.booted = true;
|
|
292
|
+
if (!state.sessionId) {
|
|
293
|
+
try { const s = await client.startSession({ scanId }); state.sessionId = (s && s.session_id) || null; } catch (e) { state.sessionId = null; }
|
|
294
|
+
}
|
|
295
|
+
_printFirstRun();
|
|
296
|
+
if (auto && typeof process !== "undefined" && process && typeof process.on === "function") {
|
|
297
|
+
try {
|
|
298
|
+
process.on("beforeExit", async () => {
|
|
299
|
+
if (state.ended || !state.sessionId) return;
|
|
300
|
+
state.ended = true;
|
|
301
|
+
try { await client.endSession({ sessionId: state.sessionId }); } catch (e) {}
|
|
302
|
+
});
|
|
303
|
+
} catch (e) {}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function makeWrapped(name, originalFn) {
|
|
308
|
+
const wrapped = async function (...args) {
|
|
309
|
+
await boot();
|
|
310
|
+
const sid = state.sessionId;
|
|
311
|
+
const argumentsObj = _argsToObject(args);
|
|
312
|
+
let auth;
|
|
313
|
+
try {
|
|
314
|
+
auth = await client.checkPermission({ action: "invoke", resource: name, arguments: argumentsObj, sessionId: sid });
|
|
315
|
+
} catch (e) {
|
|
316
|
+
auth = { allowed: false, reason: "agentkey_unreachable", fail_closed: true };
|
|
317
|
+
}
|
|
318
|
+
const allowed = !!auth && auth.allowed === true;
|
|
319
|
+
if (!allowed && enforce) {
|
|
320
|
+
throw new AgentKeyDenied(auth && auth.reason ? auth.reason : "denied", auth);
|
|
321
|
+
}
|
|
322
|
+
if (!allowed && !enforce) {
|
|
323
|
+
try { console.error(`AgentKey would have blocked this: ${auth && auth.reason ? auth.reason : "denied"}`); } catch (e) {}
|
|
324
|
+
}
|
|
325
|
+
const t0 = Date.now();
|
|
326
|
+
const meta = !allowed ? { would_block: true } : undefined;
|
|
327
|
+
try {
|
|
328
|
+
const result = await originalFn.apply(this, args);
|
|
329
|
+
try {
|
|
330
|
+
const resultHash = await client._hashResult(result);
|
|
331
|
+
await client.recordAction({
|
|
332
|
+
sessionId: sid, authorizationId: auth && auth.event_id, attemptId: auth && auth.attempt_id, tool: name, action: "invoke", resource: name,
|
|
333
|
+
arguments: argumentsObj, resultStatus: "success", resultHash, durationMs: Date.now() - t0, metadata: meta,
|
|
334
|
+
});
|
|
335
|
+
} catch (e) {}
|
|
336
|
+
return result;
|
|
337
|
+
} catch (e) {
|
|
338
|
+
try {
|
|
339
|
+
await client.recordAction({
|
|
340
|
+
sessionId: sid, authorizationId: auth && auth.event_id, attemptId: auth && auth.attempt_id, tool: name, action: "invoke", resource: name,
|
|
341
|
+
arguments: argumentsObj, resultStatus: "error", errorMessage: String(e && e.message ? e.message : e).slice(0, 200),
|
|
342
|
+
durationMs: Date.now() - t0, metadata: meta,
|
|
343
|
+
});
|
|
344
|
+
} catch (recErr) {}
|
|
345
|
+
throw e;
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
try { Object.defineProperty(wrapped, "name", { value: (originalFn && originalFn.name) || name, configurable: true }); } catch (e) {}
|
|
349
|
+
return wrapped;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Build the wrapped target, preserving the original shape.
|
|
353
|
+
if (tools.length === 0) return target;
|
|
354
|
+
|
|
355
|
+
const kinds = new Set(tools.map((t) => t.kind));
|
|
356
|
+
if (kinds.has("dict") || kinds.has("fn")) {
|
|
357
|
+
if (tools[0].kind === "fn") return makeWrapped(tools[0].name, tools[0].inv.fn);
|
|
358
|
+
const out = {};
|
|
359
|
+
for (const t of tools) out[t.name] = makeWrapped(t.name, t.inv.fn);
|
|
360
|
+
return out;
|
|
361
|
+
}
|
|
362
|
+
// list / langchain / mcp: mutate each tool's method in place so an agent that
|
|
363
|
+
// calls its own tools internally still routes through the wrapper.
|
|
364
|
+
for (const t of tools) {
|
|
365
|
+
if (!t.inv) continue;
|
|
366
|
+
const w = makeWrapped(t.name, t.inv.fn);
|
|
367
|
+
try { t.host[t.inv.key] = w; } catch (e) {}
|
|
368
|
+
}
|
|
369
|
+
return target;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
export default AgentKeyClient;
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "agentkey-ai",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Authorization and evidence SDK for AI agents: check permissions before every action, record what agents actually did.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./agentkey.js",
|
|
7
|
+
"types": "./agentkey.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./agentkey.d.ts",
|
|
11
|
+
"default": "./agentkey.js"
|
|
12
|
+
},
|
|
13
|
+
"./package.json": "./package.json"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"agentkey.js",
|
|
17
|
+
"agentkey.d.ts",
|
|
18
|
+
"README.md",
|
|
19
|
+
"LICENSE"
|
|
20
|
+
],
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=18"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"ai",
|
|
26
|
+
"agents",
|
|
27
|
+
"authorization",
|
|
28
|
+
"permissions",
|
|
29
|
+
"audit",
|
|
30
|
+
"mcp",
|
|
31
|
+
"security",
|
|
32
|
+
"llm"
|
|
33
|
+
],
|
|
34
|
+
"license": "MIT",
|
|
35
|
+
"homepage": "https://agentkey.base44.app"
|
|
36
|
+
}
|