@kavrosai/cli 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +144 -0
- package/bin/kavros.mjs +345 -0
- package/lib/args.mjs +84 -0
- package/lib/bundle.mjs +142 -0
- package/lib/explain.mjs +123 -0
- package/package.json +40 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kavros OS
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# @kavrosai/cli
|
|
2
|
+
|
|
3
|
+
Language-agnostic developer CLI for Kavros workload egress.
|
|
4
|
+
|
|
5
|
+
## Requirements
|
|
6
|
+
|
|
7
|
+
- Node.js 18 or newer
|
|
8
|
+
- A registered Kavros workload
|
|
9
|
+
- Network access to the workload's data-plane endpoint
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install --global @kavrosai/cli
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Or run it without a global install:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npx @kavrosai/cli doctor
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Configure
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
export KAVROS_API_URL="https://<data-plane-host>/api/agent/egress"
|
|
27
|
+
export KAVROS_API_KEY="<workload-api-key>"
|
|
28
|
+
export KAVROS_AGENT_ID="<workload-id>"
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Commands
|
|
32
|
+
|
|
33
|
+
Check connectivity and workload configuration:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
kavros doctor
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Per-command options and examples:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
kavros help <command>
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Send a policy-bound request:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
kavros request \
|
|
49
|
+
--action post_data \
|
|
50
|
+
--target "https://api.example.com/v1/run" \
|
|
51
|
+
--content '{"input":"hello"}' \
|
|
52
|
+
--header 'content-type:application/json' \
|
|
53
|
+
--json
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Fetch an approved URL:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
kavros request --action get --target "https://docs.example.com" --json
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
The CLI sends credentials as headers and supports the same JSON contract used by
|
|
63
|
+
native clients. It is useful for smoke tests, scripts, jobs, and applications
|
|
64
|
+
that do not yet have a native Kavros SDK.
|
|
65
|
+
|
|
66
|
+
Forwarded headers are evaluated against the workload policy like any other
|
|
67
|
+
request content; the CLI warns when a header looks like credentials
|
|
68
|
+
(`Authorization`, `Cookie`, or `x-kavros-*`).
|
|
69
|
+
|
|
70
|
+
### When a request is blocked
|
|
71
|
+
|
|
72
|
+
A block is a governed outcome, not a malfunction. The CLI prints the policy
|
|
73
|
+
reason plus a plain-language explanation: what happened, whether the content
|
|
74
|
+
reached the outside world (blocked requests never do), and what to do next.
|
|
75
|
+
The same explanation is available standalone:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
kavros explain 403 --body '{"error":"Blocked by Kavros","reason":"…"}'
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Verify a signed workflow bundle locally
|
|
82
|
+
|
|
83
|
+
Before importing a workflow bundle exported from another deployment, verify its
|
|
84
|
+
Ed25519 signature against the trusted public key (`CP_POLICY_PUBLIC_KEY` from
|
|
85
|
+
provisioning) — the same check the import route performs, run on your machine,
|
|
86
|
+
often offline:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
kavros verify-bundle workflow.kavros-bundle.json \
|
|
90
|
+
--public-key "$CP_POLICY_PUBLIC_KEY"
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
The key defaults to `KAVROS_POLICY_PUBLIC_KEY`, then `CP_POLICY_PUBLIC_KEY`,
|
|
94
|
+
from the environment. Output names the workflow, revision, origin deployment,
|
|
95
|
+
pinned capabilities, and a fingerprint of the key that verified the signature —
|
|
96
|
+
suitable for pasting into a change record. A tampered or foreign-signed bundle
|
|
97
|
+
exits non-zero with instructions not to import.
|
|
98
|
+
|
|
99
|
+
## Development
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
npm test
|
|
103
|
+
node bin/kavros.mjs --help
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
The CLI has no runtime dependencies and supports Node.js 18 and newer. The
|
|
107
|
+
printed version is read from `package.json` at runtime.
|
|
108
|
+
|
|
109
|
+
## Releasing
|
|
110
|
+
|
|
111
|
+
Releases use npm Trusted Publishing (GitHub Actions OIDC) — no stored npm
|
|
112
|
+
token, and every published package carries a sigstore provenance attestation.
|
|
113
|
+
|
|
114
|
+
One-time setup on npmjs.com under the package's **Settings → Trusted
|
|
115
|
+
Publisher**:
|
|
116
|
+
|
|
117
|
+
- Provider: GitHub Actions
|
|
118
|
+
- Organization or user: `Kavrosai`
|
|
119
|
+
- Repository: `cli`
|
|
120
|
+
- Workflow filename: `publish.yml`
|
|
121
|
+
- Allowed action: npm publish
|
|
122
|
+
|
|
123
|
+
To release a new version, update `version` in `package.json`, commit, then push
|
|
124
|
+
a matching tag — the workflow rejects a tag that does not match:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
git tag v0.2.3
|
|
128
|
+
git push origin v0.2.3
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Fallback until Trusted Publishing is configured: add an npm granular access
|
|
132
|
+
token with publish permission for `@kavrosai/cli` and **bypass 2FA** enabled
|
|
133
|
+
as the `NPM_TOKEN` Actions secret. The workflow then publishes without
|
|
134
|
+
provenance (`EOTP` failures in Actions mean 2FA bypass was not enabled).
|
|
135
|
+
Provenance requires a public source repository, so Trusted Publishing is the
|
|
136
|
+
long-term path. Remove the `NPM_TOKEN` secret once a tagged release has
|
|
137
|
+
published through OIDC.
|
|
138
|
+
|
|
139
|
+
npm does not allow re-publishing an existing package version; if a tag was
|
|
140
|
+
already published successfully, bump the version instead of retrying.
|
|
141
|
+
|
|
142
|
+
## License
|
|
143
|
+
|
|
144
|
+
[MIT](LICENSE)
|
package/bin/kavros.mjs
ADDED
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import process from "node:process";
|
|
4
|
+
import { readFileSync } from "node:fs";
|
|
5
|
+
import {
|
|
6
|
+
config,
|
|
7
|
+
parseArgs,
|
|
8
|
+
parseContent,
|
|
9
|
+
parseHeaders,
|
|
10
|
+
requireConfig,
|
|
11
|
+
sensitiveHeaders,
|
|
12
|
+
} from "../lib/args.mjs";
|
|
13
|
+
import { verifyBundle } from "../lib/bundle.mjs";
|
|
14
|
+
import { explainFailure } from "../lib/explain.mjs";
|
|
15
|
+
import { readFile } from "node:fs/promises";
|
|
16
|
+
|
|
17
|
+
const { version: VERSION } = JSON.parse(
|
|
18
|
+
readFileSync(new URL("../package.json", import.meta.url), "utf8"),
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
function usage() {
|
|
22
|
+
console.log(`Kavros CLI ${VERSION}
|
|
23
|
+
|
|
24
|
+
Language-agnostic tools for protected workload egress.
|
|
25
|
+
|
|
26
|
+
Usage:
|
|
27
|
+
kavros request --action <action> --target <url> [--content <json|string>]
|
|
28
|
+
kavros doctor
|
|
29
|
+
kavros verify-bundle <file> [--public-key <base64>]
|
|
30
|
+
kavros explain <http-status> [--body <json>]
|
|
31
|
+
kavros help <command>
|
|
32
|
+
kavros --version
|
|
33
|
+
kavros --help
|
|
34
|
+
|
|
35
|
+
Commands:
|
|
36
|
+
request Send a policy-bound request through the data plane
|
|
37
|
+
doctor Check connectivity and workload configuration
|
|
38
|
+
verify-bundle Verify a signed workflow bundle's Ed25519 signature locally
|
|
39
|
+
explain Plain-language meaning of a governed-request failure
|
|
40
|
+
|
|
41
|
+
Environment:
|
|
42
|
+
KAVROS_API_URL Data-plane egress URL
|
|
43
|
+
KAVROS_API_KEY Workload API key
|
|
44
|
+
KAVROS_AGENT_ID Workload ID
|
|
45
|
+
KAVROS_POLICY_PUBLIC_KEY Trusted public key for verify-bundle (optional)
|
|
46
|
+
|
|
47
|
+
Run 'kavros help <command>' for command-specific options and examples.
|
|
48
|
+
|
|
49
|
+
Examples:
|
|
50
|
+
kavros doctor
|
|
51
|
+
kavros request --action post_data --target https://api.example.com/v1/run \\
|
|
52
|
+
--content '{"input":"hello"}'
|
|
53
|
+
kavros verify-bundle workflow.kavros-bundle.json
|
|
54
|
+
`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function commandHelp(command) {
|
|
58
|
+
const pages = {
|
|
59
|
+
request: `kavros request — send a policy-bound request through the data plane
|
|
60
|
+
|
|
61
|
+
The request is evaluated against the workload's signed policy: identity,
|
|
62
|
+
allowlist, DLP, and metering. A block is a governed outcome, not an error —
|
|
63
|
+
use \`kavros explain\` or the failure footer for what to do next.
|
|
64
|
+
|
|
65
|
+
Required:
|
|
66
|
+
--action <action> e.g. get or post_data (per the workload's policy)
|
|
67
|
+
--target <url> the approved upstream URL
|
|
68
|
+
|
|
69
|
+
Options:
|
|
70
|
+
--content <json|string> request body
|
|
71
|
+
--header <name:value> forward a header (repeatable; credential-like
|
|
72
|
+
headers warn)
|
|
73
|
+
--json formatted JSON output
|
|
74
|
+
--timeout <seconds> request timeout (default: 120)
|
|
75
|
+
|
|
76
|
+
Environment: KAVROS_API_URL, KAVROS_API_KEY, KAVROS_AGENT_ID
|
|
77
|
+
|
|
78
|
+
Examples:
|
|
79
|
+
kavros request --action get --target https://docs.example.com --json
|
|
80
|
+
kavros request --action post_data --target https://api.example.com/v1/run \\
|
|
81
|
+
--content '{"input":"hello"}'
|
|
82
|
+
`,
|
|
83
|
+
doctor: `kavros doctor — check connectivity and workload configuration
|
|
84
|
+
|
|
85
|
+
Verifies that the data plane is reachable and the configured workload
|
|
86
|
+
identity is accepted. Requires the same environment as request.
|
|
87
|
+
|
|
88
|
+
Environment: KAVROS_API_URL, KAVROS_API_KEY, KAVROS_AGENT_ID
|
|
89
|
+
|
|
90
|
+
Example:
|
|
91
|
+
kavros doctor
|
|
92
|
+
`,
|
|
93
|
+
"verify-bundle": `kavros verify-bundle — verify a signed workflow bundle locally
|
|
94
|
+
|
|
95
|
+
Checks the Ed25519 signature on a bundle exported from a control plane
|
|
96
|
+
against a trusted public key. This is the same verification the import
|
|
97
|
+
route performs, run on your machine — useful before importing a bundle
|
|
98
|
+
and for security review. Works offline; no deployment connection needed.
|
|
99
|
+
|
|
100
|
+
Usage:
|
|
101
|
+
kavros verify-bundle <file> [--public-key <base64>]
|
|
102
|
+
|
|
103
|
+
Options:
|
|
104
|
+
--public-key <base64> Trusted public key (SPKI DER or raw32 base64).
|
|
105
|
+
Falls back to KAVROS_POLICY_PUBLIC_KEY, then
|
|
106
|
+
CP_POLICY_PUBLIC_KEY.
|
|
107
|
+
--json Print the verification result as JSON
|
|
108
|
+
|
|
109
|
+
The key to trust is the CP_POLICY_PUBLIC_KEY value provisioning shares
|
|
110
|
+
between the exporting and importing deployments.
|
|
111
|
+
|
|
112
|
+
Examples:
|
|
113
|
+
kavros verify-bundle workflow.kavros-bundle.json
|
|
114
|
+
kavros verify-bundle bundle.json --public-key "MCowBQYDK2VwAyEA…"
|
|
115
|
+
`,
|
|
116
|
+
explain: `kavros explain — plain-language meaning of a governed failure
|
|
117
|
+
|
|
118
|
+
Turns a data-plane status/body into what happened, whether egress was
|
|
119
|
+
reached, and what to do next. Also prints automatically when a request
|
|
120
|
+
fails (unless --json is set).
|
|
121
|
+
|
|
122
|
+
Usage:
|
|
123
|
+
kavros explain <http-status> [--body '<json>']
|
|
124
|
+
|
|
125
|
+
Examples:
|
|
126
|
+
kavros explain 403 --body '{"error":"Blocked by Kavros","reason":"DLP rule matched"}'
|
|
127
|
+
kavros explain 429
|
|
128
|
+
`,
|
|
129
|
+
};
|
|
130
|
+
const page = pages[command];
|
|
131
|
+
if (!page) {
|
|
132
|
+
console.error(`kavros: no help for "${command}". Commands: request, doctor, verify-bundle, explain.`);
|
|
133
|
+
process.exitCode = 2;
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
console.log(page);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function fail(message, code = 2) {
|
|
140
|
+
console.error(`kavros: ${message}`);
|
|
141
|
+
process.exitCode = code;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function fetchWithTimeout(url, init, timeoutSeconds) {
|
|
145
|
+
const controller = new AbortController();
|
|
146
|
+
const timeout = setTimeout(() => controller.abort(), timeoutSeconds * 1000);
|
|
147
|
+
try {
|
|
148
|
+
return await fetch(url, { ...init, signal: controller.signal });
|
|
149
|
+
} finally {
|
|
150
|
+
clearTimeout(timeout);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function readBody(response) {
|
|
155
|
+
const text = await response.text();
|
|
156
|
+
if (!text) return null;
|
|
157
|
+
try {
|
|
158
|
+
return JSON.parse(text);
|
|
159
|
+
} catch {
|
|
160
|
+
return text;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function printBody(body, formatted) {
|
|
165
|
+
if (typeof body === "string") console.log(body);
|
|
166
|
+
else console.log(JSON.stringify(body ?? {}, null, formatted ? 2 : 0));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async function request(options) {
|
|
170
|
+
const values = requireConfig();
|
|
171
|
+
if (!options.action) throw new Error("request requires --action");
|
|
172
|
+
if (!options.target) throw new Error("request requires --target");
|
|
173
|
+
|
|
174
|
+
const upstreamHeaders = parseHeaders(options.headers);
|
|
175
|
+
for (const name of sensitiveHeaders(upstreamHeaders)) {
|
|
176
|
+
console.error(`kavros: warning: forwarding sensitive header "${name}" to the upstream target; omit it unless the target requires it`);
|
|
177
|
+
}
|
|
178
|
+
const headers = {
|
|
179
|
+
Authorization: `Bearer ${values.apiKey}`,
|
|
180
|
+
"X-Kavros-Agent-ID": values.agentId,
|
|
181
|
+
"Content-Type": "application/json",
|
|
182
|
+
};
|
|
183
|
+
const payload = {
|
|
184
|
+
action: options.action,
|
|
185
|
+
target: options.target,
|
|
186
|
+
content: parseContent(options.content),
|
|
187
|
+
headers: upstreamHeaders,
|
|
188
|
+
};
|
|
189
|
+
const timeoutSeconds = Number(options.timeout ?? 120);
|
|
190
|
+
if (!Number.isFinite(timeoutSeconds) || timeoutSeconds <= 0) throw new Error("--timeout must be a positive number");
|
|
191
|
+
|
|
192
|
+
let response;
|
|
193
|
+
try {
|
|
194
|
+
response = await fetchWithTimeout(values.url, {
|
|
195
|
+
method: "POST",
|
|
196
|
+
headers,
|
|
197
|
+
body: JSON.stringify(payload),
|
|
198
|
+
}, timeoutSeconds);
|
|
199
|
+
} catch (error) {
|
|
200
|
+
throw new Error(`data-plane request failed: ${error.name === "AbortError" ? "timed out" : error.message}`);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const body = await readBody(response);
|
|
204
|
+
if (!response.ok) {
|
|
205
|
+
printBody(body, true);
|
|
206
|
+
if (!options.json) {
|
|
207
|
+
const explanation = explainFailure({ status: response.status, body });
|
|
208
|
+
console.error(`\nWhat happened: ${explanation.headline}`);
|
|
209
|
+
console.error(`Egress: ${explanation.egress}`);
|
|
210
|
+
console.error("Next steps:");
|
|
211
|
+
for (const step of explanation.next) console.error(` - ${step}`);
|
|
212
|
+
}
|
|
213
|
+
process.exitCode = 1;
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
printBody(body, options.json);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async function verifyBundleCommand(options) {
|
|
220
|
+
const file = options._[1];
|
|
221
|
+
if (!file) throw new Error("verify-bundle requires a bundle file path");
|
|
222
|
+
let raw;
|
|
223
|
+
try {
|
|
224
|
+
raw = await readFile(file, "utf8");
|
|
225
|
+
} catch {
|
|
226
|
+
throw new Error(`cannot read bundle file: ${file}`);
|
|
227
|
+
}
|
|
228
|
+
let signed;
|
|
229
|
+
try {
|
|
230
|
+
signed = JSON.parse(raw);
|
|
231
|
+
} catch {
|
|
232
|
+
throw new Error("the bundle file is not valid JSON");
|
|
233
|
+
}
|
|
234
|
+
const publicKeyBase64 = options["public-key"];
|
|
235
|
+
const result = verifyBundle(signed, { publicKey: publicKeyBase64 });
|
|
236
|
+
if (options.json) {
|
|
237
|
+
console.log(JSON.stringify(result, null, 2));
|
|
238
|
+
} else if (result.ok) {
|
|
239
|
+
const s = result.summary;
|
|
240
|
+
console.log(`Signature valid (Ed25519, key ${s.signature_key_fingerprint}).`);
|
|
241
|
+
console.log(`Workflow: ${s.workflow} (revision ${s.revision ?? "?"})`);
|
|
242
|
+
console.log(`Exported: ${s.exported_at ?? "unknown"}${s.origin_deployment ? ` from deployment ${s.origin_deployment}` : ""}`);
|
|
243
|
+
if (s.capabilities.length === 0) {
|
|
244
|
+
console.log("Capabilities: none");
|
|
245
|
+
} else {
|
|
246
|
+
console.log("Capabilities:");
|
|
247
|
+
for (const c of s.capabilities) {
|
|
248
|
+
const columns = c.approved_columns > 0 ? `, ${c.approved_columns} approved column(s)` : "";
|
|
249
|
+
console.log(` - ${c.key} v${c.version ?? "?"}${c.source ? ` (${c.source}${columns})` : ""}`);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
console.log("Safe to import: the bundle matches the deployment key you trust.");
|
|
253
|
+
} else {
|
|
254
|
+
console.error(`Signature verification FAILED: ${result.reason}`);
|
|
255
|
+
console.error("Do not import this bundle. Re-export from the source deployment or confirm the trusted key with the sender.");
|
|
256
|
+
process.exitCode = 1;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function explainCommand(options) {
|
|
261
|
+
const status = Number(options._[1]);
|
|
262
|
+
if (!Number.isInteger(status) || status < 100) throw new Error("explain requires an HTTP status, e.g. kavros explain 403");
|
|
263
|
+
let body;
|
|
264
|
+
if (options.body) {
|
|
265
|
+
try {
|
|
266
|
+
body = JSON.parse(options.body);
|
|
267
|
+
} catch {
|
|
268
|
+
body = options.body;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
const explanation = explainFailure({ status, body });
|
|
272
|
+
console.log(`What happened: ${explanation.headline}`);
|
|
273
|
+
console.log(`Egress: ${explanation.egress}`);
|
|
274
|
+
console.log("Next steps:");
|
|
275
|
+
for (const step of explanation.next) console.log(` - ${step}`);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
async function doctor() {
|
|
279
|
+
const values = config();
|
|
280
|
+
const missing = [];
|
|
281
|
+
if (!values.url) missing.push("KAVROS_API_URL");
|
|
282
|
+
if (!values.apiKey) missing.push("KAVROS_API_KEY");
|
|
283
|
+
if (!values.agentId) missing.push("KAVROS_AGENT_ID");
|
|
284
|
+
if (missing.length > 0) {
|
|
285
|
+
console.error(`Missing environment variables: ${missing.join(", ")}`);
|
|
286
|
+
process.exitCode = 1;
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
let response;
|
|
291
|
+
try {
|
|
292
|
+
const healthUrl = new URL(values.url);
|
|
293
|
+
healthUrl.pathname = "/health";
|
|
294
|
+
healthUrl.search = "";
|
|
295
|
+
response = await fetchWithTimeout(healthUrl, {
|
|
296
|
+
headers: {
|
|
297
|
+
Authorization: `Bearer ${values.apiKey}`,
|
|
298
|
+
"X-Kavros-Agent-ID": values.agentId,
|
|
299
|
+
},
|
|
300
|
+
}, 10);
|
|
301
|
+
} catch (error) {
|
|
302
|
+
console.error(`Data plane unreachable: ${error.message}`);
|
|
303
|
+
process.exitCode = 1;
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const body = await readBody(response);
|
|
308
|
+
if (!response.ok) {
|
|
309
|
+
console.error(`Data plane health check failed (HTTP ${response.status})`);
|
|
310
|
+
printBody(body, true);
|
|
311
|
+
process.exitCode = 1;
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
console.log(`Data plane reachable${body?.build_sha ? ` (build ${body.build_sha})` : ""}.`);
|
|
315
|
+
console.log(`Workload identity configured: ${values.agentId}`);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
async function main() {
|
|
319
|
+
try {
|
|
320
|
+
const options = parseArgs(process.argv.slice(2));
|
|
321
|
+
if (options.version) {
|
|
322
|
+
console.log(VERSION);
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
if (options.help || options._[0] === undefined) {
|
|
326
|
+
usage();
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
const [command] = options._;
|
|
330
|
+
if (options.help && command && command !== "help") {
|
|
331
|
+
commandHelp(command);
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
if (command === "request") await request(options);
|
|
335
|
+
else if (command === "doctor") await doctor();
|
|
336
|
+
else if (command === "verify-bundle") await verifyBundleCommand(options);
|
|
337
|
+
else if (command === "explain") explainCommand(options);
|
|
338
|
+
else if (command === "help" && options._[1]) commandHelp(options._[1]);
|
|
339
|
+
else throw new Error(`unknown command ${command ?? ""}; run kavros --help`);
|
|
340
|
+
} catch (error) {
|
|
341
|
+
fail(error.message);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
await main();
|
package/lib/args.mjs
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// Pure argument and configuration helpers for the Kavros CLI. Kept free of
|
|
2
|
+
// I/O so the test suite can exercise them without network or environment setup.
|
|
3
|
+
|
|
4
|
+
export function parseArgs(argv) {
|
|
5
|
+
const options = { _: [] };
|
|
6
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
7
|
+
const value = argv[index];
|
|
8
|
+
if (!value.startsWith("-")) {
|
|
9
|
+
options._.push(value);
|
|
10
|
+
continue;
|
|
11
|
+
}
|
|
12
|
+
if (value === "--help" || value === "-h") options.help = true;
|
|
13
|
+
else if (value === "--version" || value === "-v") options.version = true;
|
|
14
|
+
else if (value === "--json") options.json = true;
|
|
15
|
+
else if (value === "--action" || value === "--target" || value === "--content" || value === "--header" || value === "--timeout" || value === "--public-key" || value === "--body") {
|
|
16
|
+
const next = argv[index + 1];
|
|
17
|
+
if (!next || next.startsWith("-")) throw new Error(`${value} requires a value`);
|
|
18
|
+
index += 1;
|
|
19
|
+
if (value === "--header") {
|
|
20
|
+
options.headers ??= [];
|
|
21
|
+
options.headers.push(next);
|
|
22
|
+
} else {
|
|
23
|
+
options[value.slice(2)] = next;
|
|
24
|
+
}
|
|
25
|
+
} else {
|
|
26
|
+
throw new Error(`unknown option: ${value}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return options;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function config() {
|
|
33
|
+
return {
|
|
34
|
+
url: process.env.KAVROS_API_URL,
|
|
35
|
+
apiKey: process.env.KAVROS_API_KEY,
|
|
36
|
+
agentId: process.env.KAVROS_AGENT_ID,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function requireConfig(values = config()) {
|
|
41
|
+
const missing = missingConfig(values);
|
|
42
|
+
if (missing.length > 0) {
|
|
43
|
+
throw new Error(`missing ${missing.join(", ")}; configure the workload environment first`);
|
|
44
|
+
}
|
|
45
|
+
return values;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Environment names that are unset, in config order — for the explain path. */
|
|
49
|
+
export function missingConfig(values = config()) {
|
|
50
|
+
return Object.entries(values)
|
|
51
|
+
.filter(([, value]) => !value)
|
|
52
|
+
.map(([key]) => key === "url" ? "KAVROS_API_URL" : key === "apiKey" ? "KAVROS_API_KEY" : "KAVROS_AGENT_ID");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function parseHeaders(values = []) {
|
|
56
|
+
const headers = {};
|
|
57
|
+
for (const value of values) {
|
|
58
|
+
const separator = value.indexOf(":");
|
|
59
|
+
if (separator < 1) throw new Error(`invalid --header value ${JSON.stringify(value)}; use name:value`);
|
|
60
|
+
headers[value.slice(0, separator).trim()] = value.slice(separator + 1).trim();
|
|
61
|
+
}
|
|
62
|
+
return headers;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function parseContent(value) {
|
|
66
|
+
if (value === undefined) return "";
|
|
67
|
+
try {
|
|
68
|
+
return JSON.stringify(JSON.parse(value));
|
|
69
|
+
} catch {
|
|
70
|
+
return value;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Headers that should never be blindly forwarded to the upstream target:
|
|
75
|
+
// workload credentials (x-kavros-*), end-user credentials, and the Host
|
|
76
|
+
// header, which the upstream connection must derive itself.
|
|
77
|
+
const SENSITIVE_HEADER_NAMES = new Set(["authorization", "cookie", "host"]);
|
|
78
|
+
|
|
79
|
+
export function sensitiveHeaders(headers) {
|
|
80
|
+
return Object.keys(headers).filter((name) => {
|
|
81
|
+
const lower = name.toLowerCase();
|
|
82
|
+
return SENSITIVE_HEADER_NAMES.has(lower) || lower.startsWith("x-kavros-");
|
|
83
|
+
});
|
|
84
|
+
}
|
package/lib/bundle.mjs
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// Signed workflow-bundle verification for the Kavros CLI. Pure functions,
|
|
2
|
+
// no dependencies beyond Node's crypto — mirrors the control plane's
|
|
3
|
+
// lib/workflowBundle.ts (canonical JSON + Ed25519 over the canonical bytes)
|
|
4
|
+
// so "trust us" becomes "run kavros verify-bundle" locally, including on
|
|
5
|
+
// machines that never talk to a deployment.
|
|
6
|
+
|
|
7
|
+
import crypto from "node:crypto";
|
|
8
|
+
|
|
9
|
+
const BUNDLE_FORMAT = "kavros-workflow-bundle";
|
|
10
|
+
const BUNDLE_FORMAT_VERSION = 2;
|
|
11
|
+
|
|
12
|
+
/** Deterministic JSON with recursively sorted keys — byte-identical to the
|
|
13
|
+
* control plane's canonical form, which is what the signature covers. */
|
|
14
|
+
export function canonicalJson(value) {
|
|
15
|
+
const seen = new WeakSet();
|
|
16
|
+
const walk = (node) => {
|
|
17
|
+
if (node === null || typeof node !== "object") return node;
|
|
18
|
+
if (Array.isArray(node)) return node.map(walk);
|
|
19
|
+
if (seen.has(node)) throw new Error("Cannot canonicalize a bundle with reference cycles.");
|
|
20
|
+
seen.add(node);
|
|
21
|
+
const out = {};
|
|
22
|
+
for (const key of Object.keys(node).sort()) {
|
|
23
|
+
out[key] = walk(node[key]);
|
|
24
|
+
}
|
|
25
|
+
return out;
|
|
26
|
+
};
|
|
27
|
+
return JSON.stringify(walk(value));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Verify a signed bundle against a trusted public key.
|
|
32
|
+
*
|
|
33
|
+
* @param {object} signed Parsed bundle file: { bundle, signature, algorithm? }
|
|
34
|
+
* @param {object} options
|
|
35
|
+
* @param {string} [options.publicKey] Base64 SPKI DER or raw32 Ed25519 key
|
|
36
|
+
* (the CP_POLICY_PUBLIC_KEY value provisioning shares between deployments).
|
|
37
|
+
* Falls back to KAVROS_POLICY_PUBLIC_KEY / CP_POLICY_PUBLIC_KEY env vars.
|
|
38
|
+
* @param {string} [options.expectedWorkflow] When set, fails if the bundle is
|
|
39
|
+
* for a different workflow name (guards against swapping files).
|
|
40
|
+
* @returns {{ ok: boolean, reason?: string, summary?: object }}
|
|
41
|
+
*/
|
|
42
|
+
export function verifyBundle(signed, options = {}) {
|
|
43
|
+
if (!signed || typeof signed !== "object") {
|
|
44
|
+
return { ok: false, reason: "Not a signed bundle file — expected a JSON object." };
|
|
45
|
+
}
|
|
46
|
+
if (signed.algorithm !== "ed25519") {
|
|
47
|
+
return {
|
|
48
|
+
ok: false,
|
|
49
|
+
reason: `Unsupported signature algorithm: ${signed.algorithm ?? "none"}. Kavros bundles are Ed25519-signed; this file may not be a bundle export.`,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
const publicKeyBase64 = options.publicKey ?? process.env.KAVROS_POLICY_PUBLIC_KEY ?? process.env.CP_POLICY_PUBLIC_KEY;
|
|
53
|
+
if (!publicKeyBase64) {
|
|
54
|
+
return {
|
|
55
|
+
ok: false,
|
|
56
|
+
reason: "No trusted public key supplied. Pass --public-key <base64> or set KAVROS_POLICY_PUBLIC_KEY (the CP_POLICY_PUBLIC_KEY value from provisioning).",
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
const bundle = signed.bundle;
|
|
60
|
+
if (
|
|
61
|
+
!bundle ||
|
|
62
|
+
typeof bundle !== "object" ||
|
|
63
|
+
bundle.format !== BUNDLE_FORMAT ||
|
|
64
|
+
bundle.format_version !== BUNDLE_FORMAT_VERSION ||
|
|
65
|
+
!bundle.workflow ||
|
|
66
|
+
typeof bundle.workflow.name !== "string"
|
|
67
|
+
) {
|
|
68
|
+
return { ok: false, reason: "Not a valid Kavros workflow bundle (missing format markers or workflow name)." };
|
|
69
|
+
}
|
|
70
|
+
if (typeof signed.signature !== "string" || signed.signature.length === 0) {
|
|
71
|
+
return { ok: false, reason: "The bundle file has no signature." };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
let publicKey;
|
|
75
|
+
try {
|
|
76
|
+
const der = Buffer.from(publicKeyBase64, "base64");
|
|
77
|
+
try {
|
|
78
|
+
publicKey = crypto.createPublicKey({ key: der, format: "der", type: "spki" });
|
|
79
|
+
} catch {
|
|
80
|
+
if (der.length !== 32) {
|
|
81
|
+
return { ok: false, reason: `The supplied public key is neither SPKI DER nor raw32 Ed25519 (${der.length} bytes).` };
|
|
82
|
+
}
|
|
83
|
+
publicKey = crypto.createPublicKey({
|
|
84
|
+
key: { kty: "OKP", crv: "Ed25519", x: der.toString("base64url") },
|
|
85
|
+
format: "jwk",
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
} catch {
|
|
89
|
+
return { ok: false, reason: "The supplied public key could not be parsed." };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
let canonical;
|
|
93
|
+
try {
|
|
94
|
+
canonical = canonicalJson(bundle);
|
|
95
|
+
} catch (error) {
|
|
96
|
+
return { ok: false, reason: error instanceof Error ? error.message : "Unserializable bundle." };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
let ok = false;
|
|
100
|
+
try {
|
|
101
|
+
ok = crypto.verify(null, Buffer.from(canonical, "utf8"), publicKey, Buffer.from(signed.signature, "base64"));
|
|
102
|
+
} catch {
|
|
103
|
+
ok = false;
|
|
104
|
+
}
|
|
105
|
+
if (!ok) {
|
|
106
|
+
return {
|
|
107
|
+
ok: false,
|
|
108
|
+
reason: "Signature verification failed — the bundle was modified after signing or was signed by a different deployment.",
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (options.expectedWorkflow && bundle.workflow.name !== options.expectedWorkflow) {
|
|
113
|
+
return {
|
|
114
|
+
ok: false,
|
|
115
|
+
reason: `Signature is valid, but this bundle is for workflow "${bundle.workflow.name}", not "${options.expectedWorkflow}".`,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const capabilities = Array.isArray(bundle.capabilities) ? bundle.capabilities : [];
|
|
120
|
+
return {
|
|
121
|
+
ok: true,
|
|
122
|
+
summary: {
|
|
123
|
+
workflow: bundle.workflow.name,
|
|
124
|
+
revision: bundle.workflow.revision,
|
|
125
|
+
exported_at: bundle.exported_at,
|
|
126
|
+
origin_deployment: bundle.origin?.deployment_id ?? null,
|
|
127
|
+
capabilities: capabilities.map((c) => ({
|
|
128
|
+
key: c.key,
|
|
129
|
+
version: c.version,
|
|
130
|
+
source: c.source,
|
|
131
|
+
approved_columns: Array.isArray(c.approved_columns) ? c.approved_columns.length : 0,
|
|
132
|
+
})),
|
|
133
|
+
signature_key_fingerprint: fingerprint(publicKey),
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Short SHA-256 fingerprint of the verifying public key, for audit notes. */
|
|
139
|
+
export function fingerprint(publicKey) {
|
|
140
|
+
const der = publicKey.export({ format: "der", type: "spki" });
|
|
141
|
+
return `sha256:${crypto.createHash("sha256").update(der).digest("hex").slice(0, 16)}`;
|
|
142
|
+
}
|
package/lib/explain.mjs
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// Plain-language error explanation for CLI failures — the plan's rule that a
|
|
2
|
+
// failure sentence must say what happened, whether egress was reached, and
|
|
3
|
+
// what to do next. Pure: maps known API/error payloads to human guidance.
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Build an operator-readable explanation for a failed governed request.
|
|
7
|
+
*
|
|
8
|
+
* @param {object} params
|
|
9
|
+
* @param {number} [params.status] HTTP status of the data-plane reply
|
|
10
|
+
* @param {object|string|null} [params.body] Parsed response body (or text)
|
|
11
|
+
* @param {string[]} [params.configMissing] Missing env var names, if config failed
|
|
12
|
+
* @returns {{ headline: string, egress: string, next: string[] }}
|
|
13
|
+
*/
|
|
14
|
+
export function explainFailure({ status, body, configMissing = [] } = {}) {
|
|
15
|
+
if (configMissing.length > 0) {
|
|
16
|
+
return {
|
|
17
|
+
headline: "This shell has no workload credentials configured.",
|
|
18
|
+
egress: "Nothing was sent — no egress was attempted.",
|
|
19
|
+
next: [
|
|
20
|
+
"Export the three variables from your workload's provisioning page:",
|
|
21
|
+
" export KAVROS_API_URL=\"https://<data-plane-host>/api/agent/egress\"",
|
|
22
|
+
" export KAVROS_API_KEY=\"<workload-api-key>\"",
|
|
23
|
+
" export KAVROS_AGENT_ID=\"<workload-id>\"",
|
|
24
|
+
"Then run `kavros doctor` to confirm connectivity.",
|
|
25
|
+
],
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const reason =
|
|
30
|
+
typeof body === "object" && body !== null
|
|
31
|
+
? body.reason ?? body.error ?? null
|
|
32
|
+
: typeof body === "string" && body
|
|
33
|
+
? body
|
|
34
|
+
: null;
|
|
35
|
+
const blocked = typeof body === "object" && body !== null && body.error === "Blocked by Kavros";
|
|
36
|
+
|
|
37
|
+
if (blocked) {
|
|
38
|
+
return {
|
|
39
|
+
headline: `Blocked by policy${reason ? ` — ${reason}` : "."}`,
|
|
40
|
+
egress: "The request was stopped at the data plane. Nothing reached the target, and blocked content is never forwarded or stored.",
|
|
41
|
+
next: [
|
|
42
|
+
"Read the reason above: it names the violated rule (DLP match, unapproved target, or identity state).",
|
|
43
|
+
"If the target is legitimate, ask your admin to add it to the workload's signed policy — do not bypass the data plane.",
|
|
44
|
+
"Run `kavros doctor` to rule out an identity problem (halted or revoked workloads also block).",
|
|
45
|
+
],
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (status === 401 || status === 403) {
|
|
50
|
+
return {
|
|
51
|
+
headline: "Your workload identity was rejected.",
|
|
52
|
+
egress: "The data plane refused the credentials before any policy evaluation — nothing was sent onward.",
|
|
53
|
+
next: [
|
|
54
|
+
"Check that KAVROS_API_KEY and KAVROS_AGENT_ID belong to the same registered workload.",
|
|
55
|
+
"The workload may be halted or revoked — an admin can check its state in the Agent Registry.",
|
|
56
|
+
"If the key was rotated, fetch the current key from the control plane and re-export it.",
|
|
57
|
+
],
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (status === 404) {
|
|
62
|
+
return {
|
|
63
|
+
headline: "The data plane does not recognize this workload or route.",
|
|
64
|
+
egress: "Nothing was forwarded — the request stopped at the data plane.",
|
|
65
|
+
next: [
|
|
66
|
+
"Confirm KAVROS_API_URL points at the data-plane egress endpoint (…/api/agent/egress), not the control plane UI.",
|
|
67
|
+
"Confirm KAVROS_AGENT_ID matches a workload registered on this deployment.",
|
|
68
|
+
],
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (status === 429) {
|
|
73
|
+
return {
|
|
74
|
+
headline: "Rate or quota limit reached.",
|
|
75
|
+
egress: "The request was refused at the data plane; nothing was sent onward.",
|
|
76
|
+
next: [
|
|
77
|
+
"Wait for the current window to reset, or ask your admin to raise the workload's quota.",
|
|
78
|
+
"Repeated 429s are visible in Usage — an admin can identify which workload is consuming the budget.",
|
|
79
|
+
],
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (status === 502 || status === 504) {
|
|
84
|
+
return {
|
|
85
|
+
headline: "The approved upstream target could not be reached.",
|
|
86
|
+
egress: "Policy checks passed, but the target itself failed or timed out after egress was attempted.",
|
|
87
|
+
next: [
|
|
88
|
+
"This is a target-side failure, not a policy denial — retry or check the target service's health.",
|
|
89
|
+
"If it persists, verify the target URL in the workload's policy matches a healthy endpoint.",
|
|
90
|
+
],
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (status === 500 || status === 503) {
|
|
95
|
+
return {
|
|
96
|
+
headline: "The data plane reported an internal error.",
|
|
97
|
+
egress: "The request failed inside the data plane; depending on the stage, egress may not have been attempted.",
|
|
98
|
+
next: [
|
|
99
|
+
"Retry once, then check the deployment's health page or contact the operator.",
|
|
100
|
+
"Include the request timestamp in any report — data-plane logs are audit-linked.",
|
|
101
|
+
],
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (status !== undefined) {
|
|
106
|
+
return {
|
|
107
|
+
headline: `The data plane returned HTTP ${status}.`,
|
|
108
|
+
egress: status >= 500 ? "A server-side failure occurred." : "The request was refused before egress.",
|
|
109
|
+
next: ["Run `kavros doctor` to verify connectivity and identity, then retry."],
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
headline: reason ? `Request failed — ${reason}` : "Request failed.",
|
|
115
|
+
egress: "The failure occurred before a verdict was reached; treat the target as unreached.",
|
|
116
|
+
next: ["Run `kavros doctor` to verify connectivity and identity, then retry."],
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** One-line summary for scripts/CI: headline only, no advice. */
|
|
121
|
+
export function explainOneLine(explanation) {
|
|
122
|
+
return explanation.headline;
|
|
123
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kavrosai/cli",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Language-agnostic CLI for Kavros workload egress and diagnostics",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"kavros": "bin/kavros.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin/",
|
|
11
|
+
"lib/",
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"test": "node --test"
|
|
17
|
+
},
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=18"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"kavros",
|
|
23
|
+
"ai",
|
|
24
|
+
"llm",
|
|
25
|
+
"agents",
|
|
26
|
+
"security",
|
|
27
|
+
"egress",
|
|
28
|
+
"dlp",
|
|
29
|
+
"policy"
|
|
30
|
+
],
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "git+https://github.com/Kavrosai/cli.git"
|
|
35
|
+
},
|
|
36
|
+
"bugs": {
|
|
37
|
+
"url": "https://github.com/Kavrosai/cli/issues"
|
|
38
|
+
},
|
|
39
|
+
"homepage": "https://kavros.xyz"
|
|
40
|
+
}
|