@haikeilabs/agentware 0.3.0 → 0.5.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 +44 -0
- package/dist/kei/authorizeClient.d.ts +55 -0
- package/dist/kei/authorizeClient.d.ts.map +1 -0
- package/dist/kei/authorizeClient.js +349 -0
- package/dist/kei/evaluator.d.ts +74 -0
- package/dist/kei/evaluator.d.ts.map +1 -0
- package/dist/kei/evaluator.js +222 -0
- package/dist/kei/index.d.ts +2 -0
- package/dist/kei/index.d.ts.map +1 -1
- package/dist/kei/index.js +2 -0
- package/dist/kei/runtimeLink.d.ts +15 -0
- package/dist/kei/runtimeLink.d.ts.map +1 -1
- package/dist/kei/runtimeLink.js +65 -13
- package/dist/kei/runtimeLinkRuntime.js +1 -0
- package/dist/middleware/types.d.ts +12 -0
- package/dist/middleware/types.d.ts.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -44,6 +44,50 @@ Keep channel ingress, identity verification, delegated credentials, and
|
|
|
44
44
|
domain-specific authorization in the host application. Agentware is not a
|
|
45
45
|
policy engine; a policy gate can wrap this same boundary later.
|
|
46
46
|
|
|
47
|
+
## kei-proxy policy evaluator
|
|
48
|
+
|
|
49
|
+
`KeiProxyEvaluator` runs `kei-proxy authorize` before each tool call and fails
|
|
50
|
+
closed on anything other than an explicit `allow`/`permit` with exit 0.
|
|
51
|
+
`evaluate` is asynchronous: the event loop keeps serving other requests while
|
|
52
|
+
kei-proxy answers. Await the decision before running the tool.
|
|
53
|
+
|
|
54
|
+
`KeiProxyAuthorizeClient` spawns the binary by absolute path (a bare name is
|
|
55
|
+
resolved once against the parent `PATH`) with an allowlisted environment only:
|
|
56
|
+
`KEI_RUNTIME_TOKEN`, `KEI_RUNTIME_CONTROL_PLANE_URL`, `KEI_RUNTIME_VERSION` and
|
|
57
|
+
the other `AUTHORIZE_CHILD_ENV_ALLOWLIST` names. No `PATH`, no `HOME`, and the
|
|
58
|
+
token never goes in argv. The child runs in its own process group, and a
|
|
59
|
+
timeout SIGKILLs the whole group. Set `expectedSha256` to pin the binary: before
|
|
60
|
+
every spawn it is opened with `O_NOFOLLOW` and re-hashed, and any drift denies
|
|
61
|
+
with `kei-proxy pin_mismatch`.
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
import { Action, KeiProxyAuthorizeClient, KeiProxyEvaluator } from "@haikeilabs/agentware";
|
|
65
|
+
|
|
66
|
+
const evaluator = new KeiProxyEvaluator(
|
|
67
|
+
new KeiProxyAuthorizeClient({
|
|
68
|
+
executable: "/usr/local/bin/kei-proxy",
|
|
69
|
+
expectedSha256: process.env.KEI_PROXY_SHA256, // optional pin
|
|
70
|
+
timeoutMs: 10_000,
|
|
71
|
+
}),
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
const decision = await evaluator.evaluate(
|
|
75
|
+
"github.get_issue",
|
|
76
|
+
{ owner: "acme", repo: "pipe" },
|
|
77
|
+
caller,
|
|
78
|
+
);
|
|
79
|
+
if (decision.enrollment) {
|
|
80
|
+
// deny + enrollment claim link: reply to the user privately; never log it.
|
|
81
|
+
}
|
|
82
|
+
if (decision.action !== Action.ALLOW) return;
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Reasons read `kei-proxy <class>[: <detail>]` with a class from
|
|
86
|
+
`KEI_PROXY_REASON_CLASSES`. Because `evaluate` returns a `Promise`, the
|
|
87
|
+
evaluator is not a synchronous `PolicyEvaluator` and cannot be passed to
|
|
88
|
+
`MiddlewareImpl.withPolicy`. The behaviour matches the Python and Go ports
|
|
89
|
+
case for case; see `docs/kei-proxy-evaluator-parity.md`.
|
|
90
|
+
|
|
47
91
|
## Maintainer releases
|
|
48
92
|
|
|
49
93
|
Releases are deliberately manual and use npm staged publishing. Do not run
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { type KeiProxyAuthorizationClient, type KeiProxyAuthorizeRequest } from "./evaluator.js";
|
|
2
|
+
/**
|
|
3
|
+
* Parent variables the kei-proxy child may inherit. PATH and HOME are
|
|
4
|
+
* deliberately absent, as are KEI_PROXY_* identity variables: identity
|
|
5
|
+
* travels as flags.
|
|
6
|
+
*/
|
|
7
|
+
export declare const AUTHORIZE_CHILD_ENV_ALLOWLIST: readonly ["TZ", "KEI_RUNTIME_TOKEN", "KEI_RUNTIME_CONTROL_PLANE_URL", "KEI_RUNTIME_VERSION", "KEI_API_URL", "KEI_PROXY_REGISTRY", "KEI_PROXY_AUDIT", "KEI_PROXY_AUDIT_MAX_BYTES", "KEI_PROXY_PROVIDER_USER", "KEI_CREDENTIAL_STORE_INSTALLATION_ID", "KEI_AWS_SECRETS_MANAGER_ENDPOINT"];
|
|
8
|
+
export declare const DEFAULT_AUTHORIZE_TIMEOUT_MS = 10000;
|
|
9
|
+
export interface KeiProxyAuthorizeClientOptions {
|
|
10
|
+
/**
|
|
11
|
+
* Path or name of the kei-proxy binary. Default `kei-proxy`. A bare name is
|
|
12
|
+
* resolved once against the parent PATH (`env.PATH`, else `process.env.PATH`);
|
|
13
|
+
* the child is always spawned by absolute path.
|
|
14
|
+
*/
|
|
15
|
+
executable?: string;
|
|
16
|
+
/**
|
|
17
|
+
* Expected SHA-256 of the binary (64 hex digits). When set, `executable` must
|
|
18
|
+
* be an absolute, symlink-free path, and the binary is re-hashed before
|
|
19
|
+
* every spawn; a mismatch denies with `pin_mismatch`.
|
|
20
|
+
*/
|
|
21
|
+
expectedSha256?: string;
|
|
22
|
+
/** Milliseconds to wait for one authorize call. Default 10s. */
|
|
23
|
+
timeoutMs?: number;
|
|
24
|
+
/** Parent environment to draw allowlisted variables from. Default `process.env`. */
|
|
25
|
+
env?: Readonly<Record<string, string | undefined>>;
|
|
26
|
+
/** Variables passed to the child verbatim, for settings outside the allowlist. */
|
|
27
|
+
extraEnv?: Readonly<Record<string, string>>;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Runs `kei-proxy authorize` once per call without blocking the event loop.
|
|
31
|
+
* The runtime token reaches the child only through its environment, never argv.
|
|
32
|
+
*/
|
|
33
|
+
export declare class KeiProxyAuthorizeClient implements KeiProxyAuthorizationClient {
|
|
34
|
+
/** Absolute path to spawn, or undefined when a bare name was not found. */
|
|
35
|
+
private readonly executable;
|
|
36
|
+
private readonly expectedDigest;
|
|
37
|
+
private readonly timeoutMs;
|
|
38
|
+
private readonly env?;
|
|
39
|
+
private readonly extraEnv;
|
|
40
|
+
constructor(options?: KeiProxyAuthorizeClientOptions);
|
|
41
|
+
/** The exact environment the kei-proxy child receives. */
|
|
42
|
+
childEnv(): Record<string, string>;
|
|
43
|
+
authorize(request: KeiProxyAuthorizeRequest): Promise<Record<string, unknown>>;
|
|
44
|
+
/** Spawn once in a new process group; resolve with stdout and exit code. */
|
|
45
|
+
private run;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* The argv (after the executable) for one authorize call. `agentId` and
|
|
49
|
+
* `workspaceId` are not sent: kei-proxy (>= 0.1.11) resolves the agent from
|
|
50
|
+
* the runtime installation and workspace scope from the runtime token.
|
|
51
|
+
*/
|
|
52
|
+
export declare function keiProxyAuthorizeArgv(request: KeiProxyAuthorizeRequest): string[];
|
|
53
|
+
/** Validate one authorize result against the exit-code contract. */
|
|
54
|
+
export declare function parseKeiProxyAuthorizeOutput(stdout: string, exitCode: number): Record<string, unknown>;
|
|
55
|
+
//# sourceMappingURL=authorizeClient.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"authorizeClient.d.ts","sourceRoot":"","sources":["../../src/kei/authorizeClient.ts"],"names":[],"mappings":"AAgCA,OAAO,EAGL,KAAK,2BAA2B,EAChC,KAAK,wBAAwB,EAC9B,MAAM,gBAAgB,CAAC;AAExB;;;;GAIG;AACH,eAAO,MAAM,6BAA6B,0RAYhC,CAAC;AAEX,eAAO,MAAM,4BAA4B,QAAS,CAAC;AAWnD,MAAM,WAAW,8BAA8B;IAC7C;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,gEAAgE;IAChE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,oFAAoF;IACpF,GAAG,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC,CAAC;IACnD,kFAAkF;IAClF,QAAQ,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;CAC7C;AAED;;;GAGG;AACH,qBAAa,uBAAwB,YAAW,2BAA2B;IACzE,2EAA2E;IAC3E,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAqB;IAChD,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAqB;IACpD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,CAA+C;IACpE,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAmC;gBAEhD,OAAO,GAAE,8BAAmC;IAoBxD,0DAA0D;IAC1D,QAAQ,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;IAU5B,SAAS,CACb,OAAO,EAAE,wBAAwB,GAChC,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAyBnC,4EAA4E;IAC5E,OAAO,CAAC,GAAG;CAgGZ;AAyFD;;;;GAIG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,wBAAwB,GAChC,MAAM,EAAE,CA6BV;AAED,oEAAoE;AACpE,wBAAgB,4BAA4B,CAC1C,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,GACf,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CA+CzB"}
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `KeiProxyAuthorizeClient`: runs `kei-proxy authorize` for `KeiProxyEvaluator`.
|
|
3
|
+
*
|
|
4
|
+
* TypeScript port of `pedro_agentware.kei.authorize_client`. The kei-proxy
|
|
5
|
+
* contract (kei-connector-runtime `authorize.go`): stdout is one JSON object;
|
|
6
|
+
* exit 0 for allow / enrollment_required, exit 1 for deny, and exit 1 with
|
|
7
|
+
* nothing on stdout for every error.
|
|
8
|
+
*
|
|
9
|
+
* Security contract (`fixtures/kei/authorize-cases.v1.json`):
|
|
10
|
+
*
|
|
11
|
+
* - The child is spawned asynchronously, so the harness's event loop keeps
|
|
12
|
+
* running while kei-proxy answers.
|
|
13
|
+
* - The child environment is exactly the allowlisted parent variables plus
|
|
14
|
+
* explicit `extraEnv`: no PATH, no HOME. The executable is resolved to an
|
|
15
|
+
* absolute path in the parent, once, at construction.
|
|
16
|
+
* - With `expectedSha256` the binary is re-proved before every spawn: the
|
|
17
|
+
* path must equal its realpath, it is opened with O_NOFOLLOW, must be a
|
|
18
|
+
* regular file, and its SHA-256 is streamed from that descriptor and compared
|
|
19
|
+
* in constant time. Any drift denies (`pin_mismatch`) without spawning. What
|
|
20
|
+
* remains is the hash-then-exec race; close it by making the binary's
|
|
21
|
+
* directory and its ancestors writable only by the deploy owner.
|
|
22
|
+
* - The child runs in its own process group; a timeout or oversized stdout
|
|
23
|
+
* SIGKILLs the whole group.
|
|
24
|
+
* - Proxy stdout can carry a credential or an enrollment claim link and stderr
|
|
25
|
+
* is free-form, so stderr is never read and neither is ever logged or copied
|
|
26
|
+
* into an error message.
|
|
27
|
+
*/
|
|
28
|
+
import { spawn } from "node:child_process";
|
|
29
|
+
import { createHash, timingSafeEqual } from "node:crypto";
|
|
30
|
+
import { accessSync, constants as fsConstants, statSync } from "node:fs";
|
|
31
|
+
import { open, realpath } from "node:fs/promises";
|
|
32
|
+
import { delimiter, isAbsolute, join, resolve } from "node:path";
|
|
33
|
+
import { KEI_PROXY_AFFIRMATIVE_DECISIONS, KeiProxyAuthorizeError, } from "./evaluator.js";
|
|
34
|
+
/**
|
|
35
|
+
* Parent variables the kei-proxy child may inherit. PATH and HOME are
|
|
36
|
+
* deliberately absent, as are KEI_PROXY_* identity variables: identity
|
|
37
|
+
* travels as flags.
|
|
38
|
+
*/
|
|
39
|
+
export const AUTHORIZE_CHILD_ENV_ALLOWLIST = [
|
|
40
|
+
"TZ",
|
|
41
|
+
"KEI_RUNTIME_TOKEN",
|
|
42
|
+
"KEI_RUNTIME_CONTROL_PLANE_URL",
|
|
43
|
+
"KEI_RUNTIME_VERSION",
|
|
44
|
+
"KEI_API_URL",
|
|
45
|
+
"KEI_PROXY_REGISTRY",
|
|
46
|
+
"KEI_PROXY_AUDIT",
|
|
47
|
+
"KEI_PROXY_AUDIT_MAX_BYTES",
|
|
48
|
+
"KEI_PROXY_PROVIDER_USER",
|
|
49
|
+
"KEI_CREDENTIAL_STORE_INSTALLATION_ID",
|
|
50
|
+
"KEI_AWS_SECRETS_MANAGER_ENDPOINT",
|
|
51
|
+
];
|
|
52
|
+
export const DEFAULT_AUTHORIZE_TIMEOUT_MS = 10_000;
|
|
53
|
+
/** Upper bound on captured stdout. */
|
|
54
|
+
const MAX_STDOUT_BYTES = 1024 * 1024;
|
|
55
|
+
/** Upper bound on the executable's size when digesting it. */
|
|
56
|
+
const MAX_EXECUTABLE_BYTES = 256 * 1024 * 1024;
|
|
57
|
+
const HASH_CHUNK_BYTES = 64 * 1024;
|
|
58
|
+
const SHA256_HEX = /^[0-9a-fA-F]{64}$/;
|
|
59
|
+
const RUNTIME_TOKEN_ENV = "KEI_RUNTIME_TOKEN";
|
|
60
|
+
/**
|
|
61
|
+
* Runs `kei-proxy authorize` once per call without blocking the event loop.
|
|
62
|
+
* The runtime token reaches the child only through its environment, never argv.
|
|
63
|
+
*/
|
|
64
|
+
export class KeiProxyAuthorizeClient {
|
|
65
|
+
/** Absolute path to spawn, or undefined when a bare name was not found. */
|
|
66
|
+
executable;
|
|
67
|
+
expectedDigest;
|
|
68
|
+
timeoutMs;
|
|
69
|
+
env;
|
|
70
|
+
extraEnv;
|
|
71
|
+
constructor(options = {}) {
|
|
72
|
+
const executable = options.executable ?? "kei-proxy";
|
|
73
|
+
if (options.expectedSha256 !== undefined) {
|
|
74
|
+
if (!SHA256_HEX.test(options.expectedSha256)) {
|
|
75
|
+
throw new TypeError("expectedSha256 must be 64 hex digits");
|
|
76
|
+
}
|
|
77
|
+
if (!isAbsolute(executable)) {
|
|
78
|
+
throw new TypeError("a pinned executable must be an absolute path");
|
|
79
|
+
}
|
|
80
|
+
this.expectedDigest = Buffer.from(options.expectedSha256, "hex");
|
|
81
|
+
}
|
|
82
|
+
this.env = options.env;
|
|
83
|
+
this.executable = resolveExecutable(executable, (this.env ?? process.env).PATH);
|
|
84
|
+
this.timeoutMs = options.timeoutMs ?? DEFAULT_AUTHORIZE_TIMEOUT_MS;
|
|
85
|
+
this.extraEnv = options.extraEnv ?? {};
|
|
86
|
+
}
|
|
87
|
+
/** The exact environment the kei-proxy child receives. */
|
|
88
|
+
childEnv() {
|
|
89
|
+
const source = this.env ?? process.env;
|
|
90
|
+
const out = {};
|
|
91
|
+
for (const key of AUTHORIZE_CHILD_ENV_ALLOWLIST) {
|
|
92
|
+
const value = source[key];
|
|
93
|
+
if (value !== undefined)
|
|
94
|
+
out[key] = value;
|
|
95
|
+
}
|
|
96
|
+
return { ...out, ...this.extraEnv };
|
|
97
|
+
}
|
|
98
|
+
async authorize(request) {
|
|
99
|
+
const env = this.childEnv();
|
|
100
|
+
if (!env[RUNTIME_TOKEN_ENV]) {
|
|
101
|
+
throw new KeiProxyAuthorizeError("missing_token", `${RUNTIME_TOKEN_ENV} is not set`);
|
|
102
|
+
}
|
|
103
|
+
if (this.executable === undefined) {
|
|
104
|
+
throw new KeiProxyAuthorizeError("proxy_unavailable", "kei-proxy was not found on PATH");
|
|
105
|
+
}
|
|
106
|
+
if (this.expectedDigest !== undefined) {
|
|
107
|
+
await proveExecutable(this.executable, this.expectedDigest);
|
|
108
|
+
}
|
|
109
|
+
const { stdout, exitCode } = await this.run(this.executable, keiProxyAuthorizeArgv(request), env);
|
|
110
|
+
return parseKeiProxyAuthorizeOutput(stdout, exitCode);
|
|
111
|
+
}
|
|
112
|
+
/** Spawn once in a new process group; resolve with stdout and exit code. */
|
|
113
|
+
run(executable, argv, env) {
|
|
114
|
+
return new Promise((resolvePromise, rejectPromise) => {
|
|
115
|
+
let child;
|
|
116
|
+
try {
|
|
117
|
+
child = spawn(executable, argv, {
|
|
118
|
+
env,
|
|
119
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
120
|
+
detached: process.platform !== "win32",
|
|
121
|
+
shell: false,
|
|
122
|
+
windowsHide: true,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
catch (err) {
|
|
126
|
+
rejectPromise(unavailable(err));
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
let settled = false;
|
|
130
|
+
const chunks = [];
|
|
131
|
+
let total = 0;
|
|
132
|
+
const killGroup = () => {
|
|
133
|
+
const pid = child.pid;
|
|
134
|
+
if (pid === undefined)
|
|
135
|
+
return;
|
|
136
|
+
try {
|
|
137
|
+
process.kill(process.platform === "win32" ? pid : -pid, "SIGKILL");
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
try {
|
|
141
|
+
child.kill("SIGKILL");
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
// Already gone.
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
const fail = (error, kill) => {
|
|
149
|
+
if (settled)
|
|
150
|
+
return;
|
|
151
|
+
settled = true;
|
|
152
|
+
clearTimeout(timer);
|
|
153
|
+
if (kill)
|
|
154
|
+
killGroup();
|
|
155
|
+
rejectPromise(error);
|
|
156
|
+
};
|
|
157
|
+
const timer = setTimeout(() => {
|
|
158
|
+
fail(new KeiProxyAuthorizeError("proxy_timeout", `no answer within ${this.timeoutMs}ms`), true);
|
|
159
|
+
}, this.timeoutMs);
|
|
160
|
+
child.on("error", (err) => {
|
|
161
|
+
if (child.pid === undefined) {
|
|
162
|
+
fail(unavailable(err), false);
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
fail(new KeiProxyAuthorizeError("proxy_error", "kei-proxy failed"), true);
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
child.stdout?.on("data", (chunk) => {
|
|
169
|
+
if (settled)
|
|
170
|
+
return;
|
|
171
|
+
total += chunk.length;
|
|
172
|
+
if (total > MAX_STDOUT_BYTES) {
|
|
173
|
+
fail(new KeiProxyAuthorizeError("malformed_response", "stdout exceeded the size limit"), true);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
chunks.push(chunk);
|
|
177
|
+
});
|
|
178
|
+
child.on("close", (code, signal) => {
|
|
179
|
+
if (settled)
|
|
180
|
+
return;
|
|
181
|
+
settled = true;
|
|
182
|
+
clearTimeout(timer);
|
|
183
|
+
if (code === null) {
|
|
184
|
+
rejectPromise(new KeiProxyAuthorizeError("proxy_error", `kei-proxy terminated by ${signal ?? "signal"}`));
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
resolvePromise({
|
|
188
|
+
stdout: Buffer.concat(chunks).toString("utf8"),
|
|
189
|
+
exitCode: code,
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
function unavailable(err) {
|
|
196
|
+
const code = err?.code;
|
|
197
|
+
return new KeiProxyAuthorizeError("proxy_unavailable", `could not start kei-proxy (${code ?? "error"})`);
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* An absolute path for `executable`. Names containing a separator resolve
|
|
201
|
+
* against the working directory; bare names are looked up in the absolute
|
|
202
|
+
* entries of `pathVar` (relative entries are skipped).
|
|
203
|
+
*/
|
|
204
|
+
function resolveExecutable(executable, pathVar) {
|
|
205
|
+
if (isAbsolute(executable))
|
|
206
|
+
return executable;
|
|
207
|
+
if (executable.includes("/") || executable.includes("\\")) {
|
|
208
|
+
return resolve(executable);
|
|
209
|
+
}
|
|
210
|
+
for (const dir of (pathVar ?? "").split(delimiter)) {
|
|
211
|
+
if (!dir || !isAbsolute(dir))
|
|
212
|
+
continue;
|
|
213
|
+
const candidate = join(dir, executable);
|
|
214
|
+
try {
|
|
215
|
+
if (!statSync(candidate).isFile())
|
|
216
|
+
continue;
|
|
217
|
+
accessSync(candidate, fsConstants.X_OK);
|
|
218
|
+
return candidate;
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
// Not here; keep looking.
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return undefined;
|
|
225
|
+
}
|
|
226
|
+
function pinMismatch(detail) {
|
|
227
|
+
return new KeiProxyAuthorizeError("pin_mismatch", detail);
|
|
228
|
+
}
|
|
229
|
+
/** Re-prove the pinned executable immediately before a spawn. */
|
|
230
|
+
async function proveExecutable(path, expected) {
|
|
231
|
+
let real;
|
|
232
|
+
try {
|
|
233
|
+
real = await realpath(path);
|
|
234
|
+
}
|
|
235
|
+
catch {
|
|
236
|
+
throw pinMismatch("executable is missing");
|
|
237
|
+
}
|
|
238
|
+
if (real !== path)
|
|
239
|
+
throw pinMismatch("executable path is not canonical");
|
|
240
|
+
const noFollow = fsConstants.O_NOFOLLOW;
|
|
241
|
+
if (noFollow === undefined)
|
|
242
|
+
throw pinMismatch("O_NOFOLLOW is unavailable");
|
|
243
|
+
let handle;
|
|
244
|
+
try {
|
|
245
|
+
handle = await open(path, fsConstants.O_RDONLY | noFollow);
|
|
246
|
+
}
|
|
247
|
+
catch {
|
|
248
|
+
throw pinMismatch("executable could not be opened without following links");
|
|
249
|
+
}
|
|
250
|
+
let actual;
|
|
251
|
+
try {
|
|
252
|
+
if (!(await handle.stat()).isFile()) {
|
|
253
|
+
throw pinMismatch("executable is not a regular file");
|
|
254
|
+
}
|
|
255
|
+
actual = await digestOfHandle(handle);
|
|
256
|
+
}
|
|
257
|
+
finally {
|
|
258
|
+
await handle.close().catch(() => undefined);
|
|
259
|
+
}
|
|
260
|
+
if (actual.length !== expected.length || !timingSafeEqual(actual, expected)) {
|
|
261
|
+
throw pinMismatch("executable digest does not match the pin");
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
/** SHA-256 streamed off an already-open descriptor by explicit position. */
|
|
265
|
+
async function digestOfHandle(handle) {
|
|
266
|
+
const hash = createHash("sha256");
|
|
267
|
+
const buffer = Buffer.allocUnsafe(HASH_CHUNK_BYTES);
|
|
268
|
+
let position = 0;
|
|
269
|
+
for (;;) {
|
|
270
|
+
const { bytesRead } = await handle.read(buffer, 0, buffer.length, position);
|
|
271
|
+
if (bytesRead === 0)
|
|
272
|
+
break;
|
|
273
|
+
position += bytesRead;
|
|
274
|
+
if (position > MAX_EXECUTABLE_BYTES) {
|
|
275
|
+
throw pinMismatch("executable exceeds the size limit");
|
|
276
|
+
}
|
|
277
|
+
hash.update(buffer.subarray(0, bytesRead));
|
|
278
|
+
}
|
|
279
|
+
return hash.digest();
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* The argv (after the executable) for one authorize call. `agentId` and
|
|
283
|
+
* `workspaceId` are not sent: kei-proxy (>= 0.1.11) resolves the agent from
|
|
284
|
+
* the runtime installation and workspace scope from the runtime token.
|
|
285
|
+
*/
|
|
286
|
+
export function keiProxyAuthorizeArgv(request) {
|
|
287
|
+
const argv = [
|
|
288
|
+
"authorize",
|
|
289
|
+
"--user",
|
|
290
|
+
request.userId,
|
|
291
|
+
"--tool",
|
|
292
|
+
request.tool,
|
|
293
|
+
"--action",
|
|
294
|
+
request.action,
|
|
295
|
+
"--resource",
|
|
296
|
+
request.resource,
|
|
297
|
+
];
|
|
298
|
+
const optional = [
|
|
299
|
+
["--span-id", request.spanId],
|
|
300
|
+
["--invoking-subject", request.invokingSubject],
|
|
301
|
+
["--parent-span", request.parentSpan],
|
|
302
|
+
[
|
|
303
|
+
"--delegation-depth",
|
|
304
|
+
request.delegationDepth > 0 ? String(request.delegationDepth) : "",
|
|
305
|
+
],
|
|
306
|
+
["--agent-version", request.agentVersion],
|
|
307
|
+
["--framework", request.framework],
|
|
308
|
+
["--tool-args-digest", request.toolArgsDigest],
|
|
309
|
+
["--resources", request.resources.join(",")],
|
|
310
|
+
];
|
|
311
|
+
for (const [flag, value] of optional) {
|
|
312
|
+
if (value)
|
|
313
|
+
argv.push(flag, value);
|
|
314
|
+
}
|
|
315
|
+
return argv;
|
|
316
|
+
}
|
|
317
|
+
/** Validate one authorize result against the exit-code contract. */
|
|
318
|
+
export function parseKeiProxyAuthorizeOutput(stdout, exitCode) {
|
|
319
|
+
if (exitCode !== 0 && exitCode !== 1) {
|
|
320
|
+
throw new KeiProxyAuthorizeError("proxy_error", `kei-proxy exited ${exitCode}`);
|
|
321
|
+
}
|
|
322
|
+
const text = stdout.trim();
|
|
323
|
+
if (!text) {
|
|
324
|
+
if (exitCode !== 0) {
|
|
325
|
+
throw new KeiProxyAuthorizeError("proxy_error", `kei-proxy exited ${exitCode}`);
|
|
326
|
+
}
|
|
327
|
+
throw new KeiProxyAuthorizeError("empty_response", "kei-proxy printed nothing");
|
|
328
|
+
}
|
|
329
|
+
let parsed;
|
|
330
|
+
try {
|
|
331
|
+
parsed = JSON.parse(text);
|
|
332
|
+
}
|
|
333
|
+
catch {
|
|
334
|
+
throw new KeiProxyAuthorizeError("malformed_response", "stdout is not JSON");
|
|
335
|
+
}
|
|
336
|
+
if (!isPlainObject(parsed)) {
|
|
337
|
+
throw new KeiProxyAuthorizeError("malformed_response", "stdout is not a JSON object");
|
|
338
|
+
}
|
|
339
|
+
const decision = parsed.decision;
|
|
340
|
+
if (exitCode !== 0 &&
|
|
341
|
+
typeof decision === "string" &&
|
|
342
|
+
KEI_PROXY_AFFIRMATIVE_DECISIONS.has(decision.trim().toLowerCase())) {
|
|
343
|
+
throw new KeiProxyAuthorizeError("exit_mismatch", `${decision} with exit ${exitCode}`);
|
|
344
|
+
}
|
|
345
|
+
return parsed;
|
|
346
|
+
}
|
|
347
|
+
function isPlainObject(value) {
|
|
348
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
349
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { type CallerContext, type Decision } from "../middleware/types.js";
|
|
2
|
+
/** kei-proxy's affirmative decisions. Everything else denies. */
|
|
3
|
+
export declare const KEI_PROXY_AFFIRMATIVE_DECISIONS: ReadonlySet<string>;
|
|
4
|
+
/** Every reason reads `kei-proxy <class>` or `kei-proxy <class>: <detail>`. */
|
|
5
|
+
export declare const KEI_PROXY_REASON_CLASSES: readonly ["allow", "deny", "enrollment_required", "unknown_decision", "no_decision", "malformed_response", "empty_response", "proxy_error", "exit_mismatch", "proxy_unavailable", "proxy_timeout", "missing_token", "pin_mismatch"];
|
|
6
|
+
export type KeiProxyReasonClass = (typeof KEI_PROXY_REASON_CLASSES)[number];
|
|
7
|
+
/**
|
|
8
|
+
* An authorize call that produced no usable answer. The message is built by
|
|
9
|
+
* this library and never contains proxy stdout or stderr.
|
|
10
|
+
*/
|
|
11
|
+
export declare class KeiProxyAuthorizeError extends Error {
|
|
12
|
+
readonly reasonClass: KeiProxyReasonClass;
|
|
13
|
+
readonly detail: string;
|
|
14
|
+
constructor(reasonClass: KeiProxyReasonClass, detail: string);
|
|
15
|
+
}
|
|
16
|
+
/** The canonical audit context sent with one authorize call. */
|
|
17
|
+
export interface KeiProxyAuthorizeRequest {
|
|
18
|
+
userId: string;
|
|
19
|
+
tool: string;
|
|
20
|
+
action: string;
|
|
21
|
+
resource: string;
|
|
22
|
+
spanId: string;
|
|
23
|
+
invokingSubject: string;
|
|
24
|
+
parentSpan: string;
|
|
25
|
+
delegationDepth: number;
|
|
26
|
+
agentId: string;
|
|
27
|
+
agentVersion: string;
|
|
28
|
+
framework: string;
|
|
29
|
+
toolArgsDigest: string;
|
|
30
|
+
resources: string[];
|
|
31
|
+
/** Accepted for parity; kei-proxy derives workspace scope from the runtime token. */
|
|
32
|
+
workspaceId: string;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The single call the evaluator needs. Return (or resolve to) the parsed
|
|
36
|
+
* authorize object; throw or reject with `KeiProxyAuthorizeError` (or
|
|
37
|
+
* anything) to deny.
|
|
38
|
+
*/
|
|
39
|
+
export interface KeiProxyAuthorizationClient {
|
|
40
|
+
authorize(request: KeiProxyAuthorizeRequest): unknown;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Derive `type:kind:id` resource identifiers from a tool call's arguments.
|
|
44
|
+
* Only resources named in the arguments are knowable before the call.
|
|
45
|
+
*/
|
|
46
|
+
export declare function keiProxyResourcesTouched(toolName: string, args: Record<string, unknown>): string[];
|
|
47
|
+
/**
|
|
48
|
+
* SHA-256 of the args as compact, sorted-key, ASCII-escaped JSON: byte-for-byte
|
|
49
|
+
* Python's `json.dumps(args, sort_keys=True, separators=(",", ":"))` for
|
|
50
|
+
* strings, integers, booleans, null, arrays and objects.
|
|
51
|
+
*/
|
|
52
|
+
export declare function keiProxyToolArgsDigest(args: Record<string, unknown>): string;
|
|
53
|
+
export type KeiProxyLogger = (level: "debug" | "warn", message: string) => void;
|
|
54
|
+
export interface KeiProxyEvaluatorOptions {
|
|
55
|
+
/** Action verb sent when a tool call names none in `args.action`. Default `execute`. */
|
|
56
|
+
defaultAction?: string;
|
|
57
|
+
/** Receives class-level diagnostics only. Default: `console.warn` for warnings. */
|
|
58
|
+
logger?: KeiProxyLogger;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* A policy evaluator over kei-proxy. Translates the proxy's decision into an
|
|
62
|
+
* agentware Decision and fails closed on every path that is not an explicit
|
|
63
|
+
* affirmative. See `docs/kei-proxy-evaluator-parity.md` for the table.
|
|
64
|
+
*/
|
|
65
|
+
export declare class KeiProxyEvaluator {
|
|
66
|
+
static readonly RULE = "kei-proxy";
|
|
67
|
+
private readonly client;
|
|
68
|
+
private readonly defaultAction;
|
|
69
|
+
private readonly logger;
|
|
70
|
+
constructor(client: KeiProxyAuthorizationClient, options?: KeiProxyEvaluatorOptions);
|
|
71
|
+
evaluate(toolName: string, args: Record<string, unknown>, caller: CallerContext): Promise<Decision>;
|
|
72
|
+
private decision;
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=evaluator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"evaluator.d.ts","sourceRoot":"","sources":["../../src/kei/evaluator.ts"],"names":[],"mappings":"AAiBA,OAAO,EAEL,KAAK,aAAa,EAClB,KAAK,QAAQ,EACd,MAAM,wBAAwB,CAAC;AAEhC,iEAAiE;AACjE,eAAO,MAAM,+BAA+B,EAAE,WAAW,CAAC,MAAM,CAG9D,CAAC;AAEH,+EAA+E;AAC/E,eAAO,MAAM,wBAAwB,qOAc3B,CAAC;AAEX,MAAM,MAAM,mBAAmB,GAAG,CAAC,OAAO,wBAAwB,CAAC,CAAC,MAAM,CAAC,CAAC;AAE5E;;;GAGG;AACH,qBAAa,sBAAuB,SAAQ,KAAK;IAC/C,QAAQ,CAAC,WAAW,EAAE,mBAAmB,CAAC;IAC1C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;gBAEZ,WAAW,EAAE,mBAAmB,EAAE,MAAM,EAAE,MAAM;CAM7D;AAED,gEAAgE;AAChE,MAAM,WAAW,wBAAwB;IACvC,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,eAAe,EAAE,MAAM,CAAC;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,qFAAqF;IACrF,WAAW,EAAE,MAAM,CAAC;CACrB;AAED;;;;GAIG;AACH,MAAM,WAAW,2BAA2B;IAC1C,SAAS,CAAC,OAAO,EAAE,wBAAwB,GAAG,OAAO,CAAC;CACvD;AAED;;;GAGG;AACH,wBAAgB,wBAAwB,CACtC,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC5B,MAAM,EAAE,CAoBV;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAE5E;AAwBD,MAAM,MAAM,cAAc,GAAG,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;AAEhF,MAAM,WAAW,wBAAwB;IACvC,wFAAwF;IACxF,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,mFAAmF;IACnF,MAAM,CAAC,EAAE,cAAc,CAAC;CACzB;AAMD;;;;GAIG;AACH,qBAAa,iBAAiB;IAC5B,MAAM,CAAC,QAAQ,CAAC,IAAI,eAAe;IAEnC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA8B;IACrD,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;IACvC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAiB;gBAGtC,MAAM,EAAE,2BAA2B,EACnC,OAAO,GAAE,wBAA6B;IAOlC,QAAQ,CACZ,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,MAAM,EAAE,aAAa,GACpB,OAAO,CAAC,QAAQ,CAAC;IA2FpB,OAAO,CAAC,QAAQ;CAejB"}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Policy evaluation backed by `kei-proxy authorize`.
|
|
3
|
+
*
|
|
4
|
+
* TypeScript port of `pedro_agentware.kei.evaluator`. All three SDK languages
|
|
5
|
+
* are held to the parity table in `fixtures/kei/authorize-cases.v1.json`
|
|
6
|
+
* (see `docs/kei-proxy-evaluator-parity.md`): the same proxy output yields the
|
|
7
|
+
* same Decision, reason class and enrollment everywhere.
|
|
8
|
+
*
|
|
9
|
+
* Fail closed: only an explicit `allow`/`permit` with exit 0 allows. This
|
|
10
|
+
* module never spawns the proxy: `KeiProxyAuthorizeClient`
|
|
11
|
+
* (`authorizeClient.ts`) does, behind the `KeiProxyAuthorizationClient` seam.
|
|
12
|
+
*
|
|
13
|
+
* `evaluate` is asynchronous so a harness's event loop keeps running while
|
|
14
|
+
* kei-proxy answers. It therefore does not implement the synchronous
|
|
15
|
+
* `PolicyEvaluator`; await it before running the tool.
|
|
16
|
+
*/
|
|
17
|
+
import { createHash } from "node:crypto";
|
|
18
|
+
import { Action, } from "../middleware/types.js";
|
|
19
|
+
/** kei-proxy's affirmative decisions. Everything else denies. */
|
|
20
|
+
export const KEI_PROXY_AFFIRMATIVE_DECISIONS = new Set([
|
|
21
|
+
"permit",
|
|
22
|
+
"allow",
|
|
23
|
+
]);
|
|
24
|
+
/** Every reason reads `kei-proxy <class>` or `kei-proxy <class>: <detail>`. */
|
|
25
|
+
export const KEI_PROXY_REASON_CLASSES = [
|
|
26
|
+
"allow",
|
|
27
|
+
"deny",
|
|
28
|
+
"enrollment_required",
|
|
29
|
+
"unknown_decision",
|
|
30
|
+
"no_decision",
|
|
31
|
+
"malformed_response",
|
|
32
|
+
"empty_response",
|
|
33
|
+
"proxy_error",
|
|
34
|
+
"exit_mismatch",
|
|
35
|
+
"proxy_unavailable",
|
|
36
|
+
"proxy_timeout",
|
|
37
|
+
"missing_token",
|
|
38
|
+
"pin_mismatch",
|
|
39
|
+
];
|
|
40
|
+
/**
|
|
41
|
+
* An authorize call that produced no usable answer. The message is built by
|
|
42
|
+
* this library and never contains proxy stdout or stderr.
|
|
43
|
+
*/
|
|
44
|
+
export class KeiProxyAuthorizeError extends Error {
|
|
45
|
+
reasonClass;
|
|
46
|
+
detail;
|
|
47
|
+
constructor(reasonClass, detail) {
|
|
48
|
+
super(detail);
|
|
49
|
+
this.name = "KeiProxyAuthorizeError";
|
|
50
|
+
this.reasonClass = reasonClass;
|
|
51
|
+
this.detail = detail;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Derive `type:kind:id` resource identifiers from a tool call's arguments.
|
|
56
|
+
* Only resources named in the arguments are knowable before the call.
|
|
57
|
+
*/
|
|
58
|
+
export function keiProxyResourcesTouched(toolName, args) {
|
|
59
|
+
const resourceType = toolName.split(".", 1)[0].split("_", 1)[0].toLowerCase();
|
|
60
|
+
if (!resourceType)
|
|
61
|
+
return [];
|
|
62
|
+
const touched = [];
|
|
63
|
+
const add = (kind, identifier) => {
|
|
64
|
+
if (identifier === undefined || identifier === null)
|
|
65
|
+
return;
|
|
66
|
+
const value = String(identifier).trim();
|
|
67
|
+
if (!value)
|
|
68
|
+
return;
|
|
69
|
+
const entry = `${resourceType}:${kind}:${value}`;
|
|
70
|
+
if (!touched.includes(entry))
|
|
71
|
+
touched.push(entry);
|
|
72
|
+
};
|
|
73
|
+
const owner = args.owner;
|
|
74
|
+
const repo = args.repo || args.repository;
|
|
75
|
+
if (owner && repo)
|
|
76
|
+
add("repo", `${String(owner)}/${String(repo)}`);
|
|
77
|
+
else if (repo)
|
|
78
|
+
add("repo", repo);
|
|
79
|
+
add("issue", args.issue_number);
|
|
80
|
+
add("pull_request", args.pull_number || args.pr_number);
|
|
81
|
+
add("branch", args.branch || args.ref);
|
|
82
|
+
add("file", args.path || args.file_path);
|
|
83
|
+
return touched;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* SHA-256 of the args as compact, sorted-key, ASCII-escaped JSON: byte-for-byte
|
|
87
|
+
* Python's `json.dumps(args, sort_keys=True, separators=(",", ":"))` for
|
|
88
|
+
* strings, integers, booleans, null, arrays and objects.
|
|
89
|
+
*/
|
|
90
|
+
export function keiProxyToolArgsDigest(args) {
|
|
91
|
+
return createHash("sha256").update(canonicalJson(args), "utf8").digest("hex");
|
|
92
|
+
}
|
|
93
|
+
function canonicalJson(value) {
|
|
94
|
+
if (Array.isArray(value))
|
|
95
|
+
return `[${value.map(canonicalJson).join(",")}]`;
|
|
96
|
+
if (isPlainObject(value)) {
|
|
97
|
+
const keys = Object.keys(value).sort();
|
|
98
|
+
return `{${keys.map((k) => `${asciiJson(k)}:${canonicalJson(value[k])}`).join(",")}}`;
|
|
99
|
+
}
|
|
100
|
+
if (typeof value === "string")
|
|
101
|
+
return asciiJson(value);
|
|
102
|
+
if (value === undefined)
|
|
103
|
+
return "null";
|
|
104
|
+
return JSON.stringify(value) ?? "null";
|
|
105
|
+
}
|
|
106
|
+
function asciiJson(text) {
|
|
107
|
+
return JSON.stringify(text).replace(/[\u0080-\uffff]/g, (ch) => `\\u${ch.charCodeAt(0).toString(16).padStart(4, "0")}`);
|
|
108
|
+
}
|
|
109
|
+
function isPlainObject(value) {
|
|
110
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
111
|
+
}
|
|
112
|
+
const defaultLogger = (level, message) => {
|
|
113
|
+
if (level === "warn")
|
|
114
|
+
console.warn(message);
|
|
115
|
+
};
|
|
116
|
+
/**
|
|
117
|
+
* A policy evaluator over kei-proxy. Translates the proxy's decision into an
|
|
118
|
+
* agentware Decision and fails closed on every path that is not an explicit
|
|
119
|
+
* affirmative. See `docs/kei-proxy-evaluator-parity.md` for the table.
|
|
120
|
+
*/
|
|
121
|
+
export class KeiProxyEvaluator {
|
|
122
|
+
static RULE = "kei-proxy";
|
|
123
|
+
client;
|
|
124
|
+
defaultAction;
|
|
125
|
+
logger;
|
|
126
|
+
constructor(client, options = {}) {
|
|
127
|
+
this.client = client;
|
|
128
|
+
this.defaultAction = options.defaultAction ?? "execute";
|
|
129
|
+
this.logger = options.logger ?? defaultLogger;
|
|
130
|
+
}
|
|
131
|
+
async evaluate(toolName, args, caller) {
|
|
132
|
+
const resources = keiProxyResourcesTouched(toolName, args);
|
|
133
|
+
const userId = caller.invoking_subject || caller.user_id || "";
|
|
134
|
+
const meta = caller.metadata ?? {};
|
|
135
|
+
const request = {
|
|
136
|
+
userId,
|
|
137
|
+
tool: toolName,
|
|
138
|
+
action: String(args.action || this.defaultAction),
|
|
139
|
+
resource: resources[0] ?? toolName,
|
|
140
|
+
spanId: meta.span_id ?? "",
|
|
141
|
+
invokingSubject: userId,
|
|
142
|
+
parentSpan: caller.parent_span ?? "",
|
|
143
|
+
delegationDepth: caller.delegation_depth ?? 0,
|
|
144
|
+
agentId: meta.agent_id ?? "",
|
|
145
|
+
agentVersion: meta.agent_version ?? "",
|
|
146
|
+
framework: meta.framework || caller.source || "",
|
|
147
|
+
toolArgsDigest: keiProxyToolArgsDigest(args),
|
|
148
|
+
resources,
|
|
149
|
+
workspaceId: meta.workspace_id ?? "",
|
|
150
|
+
};
|
|
151
|
+
let raw;
|
|
152
|
+
try {
|
|
153
|
+
raw = await this.client.authorize(request);
|
|
154
|
+
}
|
|
155
|
+
catch (err) {
|
|
156
|
+
if (err instanceof KeiProxyAuthorizeError) {
|
|
157
|
+
this.logger("warn", `kei-proxy authorize for ${toolName} denied: ${err.reasonClass}`);
|
|
158
|
+
return this.decision(Action.DENY, reason(err.reasonClass, err.detail));
|
|
159
|
+
}
|
|
160
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
161
|
+
this.logger("warn", `kei-proxy authorize failed for ${toolName}, denying: ${message}`);
|
|
162
|
+
return this.decision(Action.DENY, reason("proxy_error", `authorize failed: ${message}`));
|
|
163
|
+
}
|
|
164
|
+
const response = isPlainObject(raw) ? raw : {};
|
|
165
|
+
const rawDecision = response.decision;
|
|
166
|
+
const decision = rawDecision === undefined || rawDecision === null
|
|
167
|
+
? ""
|
|
168
|
+
: String(rawDecision).trim().toLowerCase();
|
|
169
|
+
const proxyReason = optionalString(response.reason);
|
|
170
|
+
const policyId = optionalString(response.policy_id) ?? optionalString(response.policy);
|
|
171
|
+
if (KEI_PROXY_AFFIRMATIVE_DECISIONS.has(decision)) {
|
|
172
|
+
return this.decision(Action.ALLOW, reason("allow", proxyReason), policyId);
|
|
173
|
+
}
|
|
174
|
+
let reasonClass;
|
|
175
|
+
let detail;
|
|
176
|
+
let enrollment;
|
|
177
|
+
const carried = isPlainObject(response.enrollment)
|
|
178
|
+
? response.enrollment
|
|
179
|
+
: undefined;
|
|
180
|
+
if (!decision) {
|
|
181
|
+
reasonClass = "no_decision";
|
|
182
|
+
}
|
|
183
|
+
else if (decision === "deny") {
|
|
184
|
+
reasonClass = "deny";
|
|
185
|
+
detail = proxyReason;
|
|
186
|
+
enrollment = carried;
|
|
187
|
+
}
|
|
188
|
+
else if (decision === "enrollment_required") {
|
|
189
|
+
reasonClass = "enrollment_required";
|
|
190
|
+
detail = proxyReason ?? "enrollment is required before this call";
|
|
191
|
+
enrollment = carried;
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
reasonClass = "unknown_decision";
|
|
195
|
+
detail = `'${decision}'` + (proxyReason ? ` (${proxyReason})` : "");
|
|
196
|
+
}
|
|
197
|
+
this.logger("debug", `kei-proxy ${reasonClass} for ${toolName}`);
|
|
198
|
+
return this.decision(Action.DENY, reason(reasonClass, detail), policyId, enrollment);
|
|
199
|
+
}
|
|
200
|
+
decision(action, reasonText, policyId, enrollment) {
|
|
201
|
+
const out = {
|
|
202
|
+
action,
|
|
203
|
+
rule: policyId ?? KeiProxyEvaluator.RULE,
|
|
204
|
+
reason: reasonText,
|
|
205
|
+
timestamp: new Date(),
|
|
206
|
+
};
|
|
207
|
+
if (enrollment !== undefined)
|
|
208
|
+
out.enrollment = enrollment;
|
|
209
|
+
return out;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
function reason(reasonClass, detail) {
|
|
213
|
+
return detail
|
|
214
|
+
? `kei-proxy ${reasonClass}: ${detail}`
|
|
215
|
+
: `kei-proxy ${reasonClass}`;
|
|
216
|
+
}
|
|
217
|
+
function optionalString(value) {
|
|
218
|
+
if (value === undefined || value === null)
|
|
219
|
+
return undefined;
|
|
220
|
+
const text = String(value);
|
|
221
|
+
return text ? text : undefined;
|
|
222
|
+
}
|
package/dist/kei/index.d.ts
CHANGED
package/dist/kei/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/kei/index.ts"],"names":[],"mappings":"AAAA,cAAc,kBAAkB,CAAC;AACjC,cAAc,yBAAyB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/kei/index.ts"],"names":[],"mappings":"AAAA,cAAc,kBAAkB,CAAC;AACjC,cAAc,yBAAyB,CAAC;AACxC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,sBAAsB,CAAC"}
|
package/dist/kei/index.js
CHANGED
|
@@ -170,6 +170,19 @@ export interface RuntimeIdentity {
|
|
|
170
170
|
status: string;
|
|
171
171
|
bindingStatus: string;
|
|
172
172
|
runtimeVersion: string;
|
|
173
|
+
/**
|
|
174
|
+
* The installation's default agent from whoami; undefined when the runtime
|
|
175
|
+
* reports none (older runtimes omit it). A harness reads its agent from
|
|
176
|
+
* here and must not require an agent-ID env var.
|
|
177
|
+
*/
|
|
178
|
+
defaultAgentId?: string;
|
|
179
|
+
/** Agents assigned to the installation; empty when the runtime reports none. */
|
|
180
|
+
agents: AssignedAgent[];
|
|
181
|
+
}
|
|
182
|
+
/** One agent assigned to a runtime installation. */
|
|
183
|
+
export interface AssignedAgent {
|
|
184
|
+
agentId: string;
|
|
185
|
+
isDefault: boolean;
|
|
173
186
|
}
|
|
174
187
|
export type ChildLineErrorKind = "line_too_long" | "malformed" | "contract_mismatch";
|
|
175
188
|
/** A child line the SDK must drop and count. */
|
|
@@ -194,6 +207,8 @@ export interface TerminalEvent {
|
|
|
194
207
|
export type ChildEvent = {
|
|
195
208
|
kind: "identity";
|
|
196
209
|
identity: RuntimeIdentity;
|
|
210
|
+
/** Malformed agent entries dropped from the event; counted like dropped lines. */
|
|
211
|
+
droppedAgents: number;
|
|
197
212
|
} | {
|
|
198
213
|
kind: "beat";
|
|
199
214
|
beat: BeatEvent;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runtimeLink.d.ts","sourceRoot":"","sources":["../../src/kei/runtimeLink.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"runtimeLink.d.ts","sourceRoot":"","sources":["../../src/kei/runtimeLink.ts"],"names":[],"mappings":"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"}
|
package/dist/kei/runtimeLink.js
CHANGED
|
@@ -409,6 +409,57 @@ class FieldReader {
|
|
|
409
409
|
return v;
|
|
410
410
|
}
|
|
411
411
|
}
|
|
412
|
+
/**
|
|
413
|
+
* Fill identity.defaultAgentId and identity.agents from the optional agent_id
|
|
414
|
+
* and agents fields. Older runtimes omit both, so absence is not an error; a
|
|
415
|
+
* malformed agent_id, agents list, or entry is dropped and counted rather than
|
|
416
|
+
* failing the whole identity. When agent_id is absent, the first entry marked
|
|
417
|
+
* is_default supplies it. Returns the number of dropped values.
|
|
418
|
+
*/
|
|
419
|
+
function parseAgents(raw, identity) {
|
|
420
|
+
let dropped = 0;
|
|
421
|
+
if (Object.hasOwn(raw, "agent_id")) {
|
|
422
|
+
const v = raw.agent_id;
|
|
423
|
+
if (typeof v === "string" && ID_RE.test(v))
|
|
424
|
+
identity.defaultAgentId = v;
|
|
425
|
+
else
|
|
426
|
+
dropped++;
|
|
427
|
+
}
|
|
428
|
+
if (!Object.hasOwn(raw, "agents"))
|
|
429
|
+
return dropped;
|
|
430
|
+
const entries = raw.agents;
|
|
431
|
+
if (!Array.isArray(entries))
|
|
432
|
+
return dropped + 1;
|
|
433
|
+
for (const entry of entries) {
|
|
434
|
+
const agent = parseAgentEntry(entry);
|
|
435
|
+
if (!agent) {
|
|
436
|
+
dropped++;
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
identity.agents.push(agent);
|
|
440
|
+
if (identity.defaultAgentId === undefined && agent.isDefault) {
|
|
441
|
+
identity.defaultAgentId = agent.agentId;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
return dropped;
|
|
445
|
+
}
|
|
446
|
+
/** Read one {agent_id, is_default} entry; is_default is false when absent. */
|
|
447
|
+
function parseAgentEntry(entry) {
|
|
448
|
+
if (entry === null || typeof entry !== "object" || Array.isArray(entry)) {
|
|
449
|
+
return undefined;
|
|
450
|
+
}
|
|
451
|
+
const e = entry;
|
|
452
|
+
const agentId = e.agent_id;
|
|
453
|
+
if (typeof agentId !== "string" || !ID_RE.test(agentId))
|
|
454
|
+
return undefined;
|
|
455
|
+
let isDefault = false;
|
|
456
|
+
if (Object.hasOwn(e, "is_default")) {
|
|
457
|
+
if (typeof e.is_default !== "boolean")
|
|
458
|
+
return undefined;
|
|
459
|
+
isDefault = e.is_default;
|
|
460
|
+
}
|
|
461
|
+
return { agentId, isDefault };
|
|
462
|
+
}
|
|
412
463
|
const encoder = new TextEncoder();
|
|
413
464
|
const decoder = new TextDecoder();
|
|
414
465
|
/**
|
|
@@ -444,20 +495,21 @@ export function parseChildLine(line) {
|
|
|
444
495
|
throw new ChildLineError("malformed", "event");
|
|
445
496
|
const f = new FieldReader(obj);
|
|
446
497
|
switch (obj.event) {
|
|
447
|
-
case "identity":
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
runtimeVersion: f.id("runtime_version", false),
|
|
459
|
-
},
|
|
498
|
+
case "identity": {
|
|
499
|
+
const identity = {
|
|
500
|
+
runId: f.id("run_id", true),
|
|
501
|
+
installationId: f.id("installation_id", true),
|
|
502
|
+
orgId: f.id("org_id", true),
|
|
503
|
+
workspaceId: f.id("workspace_id", false),
|
|
504
|
+
platform: f.id("platform", false),
|
|
505
|
+
status: f.id("status", false),
|
|
506
|
+
bindingStatus: f.id("binding_status", false),
|
|
507
|
+
runtimeVersion: f.id("runtime_version", false),
|
|
508
|
+
agents: [],
|
|
460
509
|
};
|
|
510
|
+
const droppedAgents = parseAgents(obj, identity);
|
|
511
|
+
return { kind: "identity", identity, droppedAgents };
|
|
512
|
+
}
|
|
461
513
|
case "beat": {
|
|
462
514
|
const runId = f.id("run_id", true);
|
|
463
515
|
const seq = f.integer("seq", false, 0, MAX_SEQ);
|
|
@@ -240,6 +240,7 @@ class RuntimeLinkImpl {
|
|
|
240
240
|
}
|
|
241
241
|
if (ev.kind === "identity") {
|
|
242
242
|
this.identityValue = ev.identity;
|
|
243
|
+
this.consecutiveFails += ev.droppedAgents;
|
|
243
244
|
this.runId = ev.identity.runId;
|
|
244
245
|
identitySeen = true;
|
|
245
246
|
heartbeatDeadline = Date.now() + this.config.beatTimeoutMs;
|
|
@@ -29,12 +29,24 @@ export interface CallerContext {
|
|
|
29
29
|
source?: string;
|
|
30
30
|
trusted: boolean;
|
|
31
31
|
metadata?: Record<string, string>;
|
|
32
|
+
/** The human who originated the request; carried unchanged across delegation hops. */
|
|
33
|
+
invoking_subject?: string;
|
|
34
|
+
/** Span of the delegating caller, when this call was made by a subagent. */
|
|
35
|
+
parent_span?: string;
|
|
36
|
+
/** 0 for a direct call; incremented at each delegation hop. */
|
|
37
|
+
delegation_depth?: number;
|
|
32
38
|
}
|
|
33
39
|
export interface Decision {
|
|
34
40
|
action: Action;
|
|
35
41
|
rule: string;
|
|
36
42
|
reason: string;
|
|
37
43
|
redacted_args?: Record<string, unknown>;
|
|
44
|
+
/**
|
|
45
|
+
* Opaque enrollment object from kei-proxy on a DENY (provider,
|
|
46
|
+
* provider_user_id, org_id, workspace_id, and optionally url/expires_at).
|
|
47
|
+
* The url is a one-time claim link: surface it to the user, never log it.
|
|
48
|
+
*/
|
|
49
|
+
enrollment?: Record<string, unknown>;
|
|
38
50
|
timestamp: Date;
|
|
39
51
|
}
|
|
40
52
|
export declare function createAllowDecision(reason?: string): Decision;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/middleware/types.ts"],"names":[],"mappings":"AAAA,oBAAY,WAAW;IACrB,aAAa,kBAAkB;IAC/B,UAAU,eAAe;IACzB,SAAS,cAAc;IACvB,WAAW,gBAAgB;IAC3B,SAAS,cAAc;IACvB,aAAa,kBAAkB;IAC/B,UAAU,eAAe;IACzB,kBAAkB,uBAAuB;IACzC,WAAW,gBAAgB;IAC3B,eAAe,oBAAoB;IACnC,OAAO,YAAY;CACpB;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,WAAW,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,WAAW,CAAC;IAC5B,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,oBAAY,MAAM;IAChB,KAAK,UAAU;IACf,IAAI,SAAS;IACb,MAAM,WAAW;CAClB;AAED,MAAM,WAAW,aAAa;IAC5B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/middleware/types.ts"],"names":[],"mappings":"AAAA,oBAAY,WAAW;IACrB,aAAa,kBAAkB;IAC/B,UAAU,eAAe;IACzB,SAAS,cAAc;IACvB,WAAW,gBAAgB;IAC3B,SAAS,cAAc;IACvB,aAAa,kBAAkB;IAC/B,UAAU,eAAe;IACzB,kBAAkB,uBAAuB;IACzC,WAAW,gBAAgB;IAC3B,eAAe,oBAAoB;IACnC,OAAO,YAAY;CACpB;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,WAAW,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,WAAW,CAAC;IAC5B,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,oBAAY,MAAM;IAChB,KAAK,UAAU;IACf,IAAI,SAAS;IACb,MAAM,WAAW;CAClB;AAED,MAAM,WAAW,aAAa;IAC5B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAClC,sFAAsF;IACtF,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,4EAA4E;IAC5E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,+DAA+D;IAC/D,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,QAAQ;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACxC;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACrC,SAAS,EAAE,IAAI,CAAC;CACjB;AAED,wBAAgB,mBAAmB,CAAC,MAAM,SAAY,GAAG,QAAQ,CAOhE;AAED,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,QAAQ,CAO3D"}
|