@haikeilabs/agentware 0.4.0 → 0.6.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 +1 -2
- package/dist/kei/runtimeLink.d.ts.map +1 -1
- package/dist/kei/runtimeLink.js +0 -8
- package/dist/kei/runtimeLinkRuntime.d.ts +1 -0
- package/dist/kei/runtimeLinkRuntime.d.ts.map +1 -1
- package/dist/kei/runtimeLinkRuntime.js +24 -4
- 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
|
@@ -41,7 +41,6 @@ export declare const CHILD_ENV_ALLOWLIST: readonly ["KEI_RUNTIME_TOKEN", "KEI_RU
|
|
|
41
41
|
export declare const RUNTIME_LINK_ENV: {
|
|
42
42
|
readonly enabled: "KEI_RUNTIME_ENABLED";
|
|
43
43
|
readonly token: "KEI_RUNTIME_TOKEN";
|
|
44
|
-
readonly legacyToken: "KEI_HARNESS_TOKEN";
|
|
45
44
|
readonly controlPlaneUrl: "KEI_RUNTIME_CONTROL_PLANE_URL";
|
|
46
45
|
readonly proxyPath: "KEI_PROXY_PATH";
|
|
47
46
|
readonly proxySha: "KEI_PROXY_SHA";
|
|
@@ -73,7 +72,7 @@ export declare const RUNTIME_LINK_BOUNDS: {
|
|
|
73
72
|
readonly maxStopTimeoutMs: 60000;
|
|
74
73
|
readonly maxLogCount: 100;
|
|
75
74
|
};
|
|
76
|
-
export declare const RUNTIME_LINK_CONFIG_ERROR_CODES: readonly ["invalid_value", "out_of_range", "beat_timeout", "harness_kind", "envelope", "
|
|
75
|
+
export declare const RUNTIME_LINK_CONFIG_ERROR_CODES: readonly ["invalid_value", "out_of_range", "beat_timeout", "harness_kind", "envelope", "token_missing", "control_plane_url", "binary"];
|
|
77
76
|
export type RuntimeLinkConfigErrorCode = (typeof RUNTIME_LINK_CONFIG_ERROR_CODES)[number];
|
|
78
77
|
/**
|
|
79
78
|
* Invalid RuntimeLink configuration. Carries the offending field and a stable
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runtimeLink.d.ts","sourceRoot":"","sources":["../../src/kei/runtimeLink.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,6DAA6D;AAC7D,eAAO,MAAM,6BAA6B,IAAI,CAAC;AAC/C,6DAA6D;AAC7D,eAAO,MAAM,oBAAoB,OAAO,CAAC;AACzC,8CAA8C;AAC9C,eAAO,MAAM,8BAA8B,KAAK,CAAC;AACjD,eAAO,MAAM,qBAAqB,eAAe,CAAC;AAElD,eAAO,MAAM,aAAa,kFAOhB,CAAC;AACX,MAAM,MAAM,WAAW,GAAG,CAAC,OAAO,aAAa,CAAC,CAAC,MAAM,CAAC,CAAC;AAEzD,eAAO,MAAM,WAAW,mGAQd,CAAC;AACX,MAAM,MAAM,SAAS,GAAG,CAAC,OAAO,WAAW,CAAC,CAAC,MAAM,CAAC,CAAC;AAErD;;;GAGG;AACH,eAAO,MAAM,eAAe,kUAelB,CAAC;AACX,MAAM,MAAM,YAAY,GAAG,CAAC,OAAO,eAAe,CAAC,CAAC,MAAM,CAAC,CAAC;AAE5D,eAAO,MAAM,aAAa,wIAQhB,CAAC;AACX,MAAM,MAAM,WAAW,GAAG,CAAC,OAAO,aAAa,CAAC,CAAC,MAAM,CAAC,CAAC;AAEzD,eAAO,MAAM,qBAAqB,oKAOxB,CAAC;AACX,MAAM,MAAM,kBAAkB,GAAG,CAAC,OAAO,qBAAqB,CAAC,CAAC,MAAM,CAAC,CAAC;AAExE,eAAO,MAAM,SAAS,yCAA0C,CAAC;AACjE,MAAM,MAAM,OAAO,GAAG,CAAC,OAAO,SAAS,CAAC,CAAC,MAAM,CAAC,CAAC;AAEjD,8EAA8E;AAC9E,eAAO,MAAM,mBAAmB,uOAYtB,CAAC;AAeX,qFAAqF;AACrF,eAAO,MAAM,gBAAgB
|
|
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
|
@@ -98,7 +98,6 @@ function isOneOf(set, value) {
|
|
|
98
98
|
export const RUNTIME_LINK_ENV = {
|
|
99
99
|
enabled: "KEI_RUNTIME_ENABLED",
|
|
100
100
|
token: "KEI_RUNTIME_TOKEN",
|
|
101
|
-
legacyToken: "KEI_HARNESS_TOKEN",
|
|
102
101
|
controlPlaneUrl: "KEI_RUNTIME_CONTROL_PLANE_URL",
|
|
103
102
|
proxyPath: "KEI_PROXY_PATH",
|
|
104
103
|
proxySha: "KEI_PROXY_SHA",
|
|
@@ -136,7 +135,6 @@ export const RUNTIME_LINK_CONFIG_ERROR_CODES = [
|
|
|
136
135
|
"beat_timeout",
|
|
137
136
|
"harness_kind",
|
|
138
137
|
"envelope",
|
|
139
|
-
"legacy_token",
|
|
140
138
|
"token_missing",
|
|
141
139
|
"control_plane_url",
|
|
142
140
|
"binary",
|
|
@@ -197,9 +195,6 @@ export function normalizeRuntimeLinkConfig(cfg) {
|
|
|
197
195
|
throw new RuntimeLinkConfigError("envelope", "harness.deploymentEnv");
|
|
198
196
|
}
|
|
199
197
|
if (cfg.enabled) {
|
|
200
|
-
if (cfg.token.env === RUNTIME_LINK_ENV.legacyToken) {
|
|
201
|
-
throw new RuntimeLinkConfigError("legacy_token", "token");
|
|
202
|
-
}
|
|
203
198
|
if (cfg.token.env !== RUNTIME_LINK_ENV.token) {
|
|
204
199
|
throw new RuntimeLinkConfigError("invalid_value", "token");
|
|
205
200
|
}
|
|
@@ -288,9 +283,6 @@ export function runtimeLinkConfigFromEnv(env = process.env, harness = {}) {
|
|
|
288
283
|
cfg.logCount = Number(logCount);
|
|
289
284
|
}
|
|
290
285
|
const hasToken = get(E.token) !== "";
|
|
291
|
-
if (!hasToken && get(E.legacyToken) !== "") {
|
|
292
|
-
throw new RuntimeLinkConfigError("legacy_token", E.legacyToken);
|
|
293
|
-
}
|
|
294
286
|
cfg.enabled = hasToken;
|
|
295
287
|
const enabled = get(E.enabled).toLowerCase();
|
|
296
288
|
if (enabled !== "") {
|
|
@@ -2,6 +2,7 @@ import { type RuntimeLink, type RuntimeLinkConfig } from "./runtimeLink.js";
|
|
|
2
2
|
export interface RuntimeChild {
|
|
3
3
|
readonly stdout: AsyncIterable<Uint8Array | string>;
|
|
4
4
|
readonly exitCode: Promise<number>;
|
|
5
|
+
closeStdin?(): void;
|
|
5
6
|
kill(signal?: "SIGTERM" | "SIGKILL"): void;
|
|
6
7
|
}
|
|
7
8
|
/** Injectable process boundary for tests and embedders. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runtimeLinkRuntime.d.ts","sourceRoot":"","sources":["../../src/kei/runtimeLinkRuntime.ts"],"names":[],"mappings":"AAIA,OAAO,EAYL,KAAK,WAAW,EAChB,KAAK,iBAAiB,EACvB,MAAM,kBAAkB,CAAC;AAE1B,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC,UAAU,GAAG,MAAM,CAAC,CAAC;IACpD,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;IACnC,IAAI,CAAC,MAAM,CAAC,EAAE,SAAS,GAAG,SAAS,GAAG,IAAI,CAAC;CAC5C;AAED,2DAA2D;AAC3D,MAAM,WAAW,mBAAmB;IAClC,KAAK,CACH,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,GAAG,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GACpC,YAAY,CAAC;CACjB;AAED,MAAM,WAAW,kBAAkB;IACjC,GAAG,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC,CAAC;IACnD,YAAY,CAAC,EAAE,mBAAmB,CAAC;IACnC,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5E;AAED,qGAAqG;AACrG,qBAAa,uBAAwB,YAAW,mBAAmB;IACjE,KAAK,CACH,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,GAAG,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GACpC,YAAY;
|
|
1
|
+
{"version":3,"file":"runtimeLinkRuntime.d.ts","sourceRoot":"","sources":["../../src/kei/runtimeLinkRuntime.ts"],"names":[],"mappings":"AAIA,OAAO,EAYL,KAAK,WAAW,EAChB,KAAK,iBAAiB,EACvB,MAAM,kBAAkB,CAAC;AAE1B,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC,UAAU,GAAG,MAAM,CAAC,CAAC;IACpD,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;IACnC,UAAU,CAAC,IAAI,IAAI,CAAC;IACpB,IAAI,CAAC,MAAM,CAAC,EAAE,SAAS,GAAG,SAAS,GAAG,IAAI,CAAC;CAC5C;AAED,2DAA2D;AAC3D,MAAM,WAAW,mBAAmB;IAClC,KAAK,CACH,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,GAAG,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GACpC,YAAY,CAAC;CACjB;AAED,MAAM,WAAW,kBAAkB;IACjC,GAAG,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC,CAAC;IACnD,YAAY,CAAC,EAAE,mBAAmB,CAAC;IACnC,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5E;AAED,qGAAqG;AACrG,qBAAa,uBAAwB,YAAW,mBAAmB;IACjE,KAAK,CACH,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,GAAG,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GACpC,YAAY;CA+BhB;AAWD,wGAAwG;AACxG,wBAAgB,cAAc,CAC5B,MAAM,EAAE,iBAAiB,EACzB,OAAO,GAAE,kBAAuB,GAC/B,WAAW,CAEb"}
|
|
@@ -8,7 +8,7 @@ export class NodeRuntimeChildFactory {
|
|
|
8
8
|
start(command, args, env) {
|
|
9
9
|
const child = spawn(command, [...args], {
|
|
10
10
|
env: { ...env },
|
|
11
|
-
stdio: ["
|
|
11
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
12
12
|
detached: process.platform !== "win32",
|
|
13
13
|
});
|
|
14
14
|
const stdout = child.stdout;
|
|
@@ -19,6 +19,7 @@ export class NodeRuntimeChildFactory {
|
|
|
19
19
|
child.once("close", (code) => resolve(code ?? 1));
|
|
20
20
|
});
|
|
21
21
|
return {
|
|
22
|
+
closeStdin: () => child.stdin?.end(),
|
|
22
23
|
stdout: (async function* () {
|
|
23
24
|
for await (const chunk of stdout)
|
|
24
25
|
yield chunk;
|
|
@@ -184,7 +185,17 @@ class RuntimeLinkImpl {
|
|
|
184
185
|
await this.verifyBinary();
|
|
185
186
|
const env = this.childEnv();
|
|
186
187
|
const factory = this.options.childFactory ?? new NodeRuntimeChildFactory();
|
|
187
|
-
const child = factory.start(this.config.binary.path, [
|
|
188
|
+
const child = factory.start(this.config.binary.path, [
|
|
189
|
+
"runtime",
|
|
190
|
+
"heartbeat",
|
|
191
|
+
"--interval",
|
|
192
|
+
`${this.config.intervalMs / 1000}s`,
|
|
193
|
+
"--timeout",
|
|
194
|
+
`${this.config.beatTimeoutMs / 1000}s`,
|
|
195
|
+
"--output",
|
|
196
|
+
"jsonl",
|
|
197
|
+
"--parent-stdin",
|
|
198
|
+
], env);
|
|
188
199
|
this.child = child;
|
|
189
200
|
let identitySeen = false;
|
|
190
201
|
let heartbeatDeadline = Date.now() + this.config.beatTimeoutMs;
|
|
@@ -243,13 +254,21 @@ class RuntimeLinkImpl {
|
|
|
243
254
|
this.consecutiveFails += ev.droppedAgents;
|
|
244
255
|
this.runId = ev.identity.runId;
|
|
245
256
|
identitySeen = true;
|
|
246
|
-
heartbeatDeadline =
|
|
257
|
+
heartbeatDeadline =
|
|
258
|
+
Date.now() +
|
|
259
|
+
this.config.intervalMs +
|
|
260
|
+
this.config.beatTimeoutMs +
|
|
261
|
+
this.config.graceMs;
|
|
247
262
|
this.transition("connected");
|
|
248
263
|
}
|
|
249
264
|
else if (ev.kind === "beat") {
|
|
250
265
|
this.runId = ev.beat.runId || this.runId;
|
|
251
266
|
this.lastBeatAt = this.options.now?.() ?? new Date();
|
|
252
|
-
heartbeatDeadline =
|
|
267
|
+
heartbeatDeadline =
|
|
268
|
+
Date.now() +
|
|
269
|
+
this.config.intervalMs +
|
|
270
|
+
this.config.beatTimeoutMs +
|
|
271
|
+
this.config.graceMs;
|
|
253
272
|
if (ev.beat.outcome === "ok") {
|
|
254
273
|
this.lastCatalogOkAt = this.lastBeatAt;
|
|
255
274
|
this.consecutiveFails = 0;
|
|
@@ -278,6 +297,7 @@ class RuntimeLinkImpl {
|
|
|
278
297
|
return "runtime_unavailable";
|
|
279
298
|
}
|
|
280
299
|
finally {
|
|
300
|
+
child.closeStdin?.();
|
|
281
301
|
child.kill("SIGTERM");
|
|
282
302
|
this.child = undefined;
|
|
283
303
|
}
|
|
@@ -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"}
|