@seekrit/cli 1.2.2 → 1.4.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/dist/index.js +148 -10
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -9,6 +9,7 @@ import { createInterface } from "node:readline";
|
|
|
9
9
|
import { Writable } from "node:stream";
|
|
10
10
|
import { createHash } from "node:crypto";
|
|
11
11
|
import { fileURLToPath } from "node:url";
|
|
12
|
+
import { request } from "node:http";
|
|
12
13
|
/** Default lifetime of a published bundle (7 days), in seconds. */
|
|
13
14
|
const POLICY_DEFAULT_TTL_SECONDS = 10080 * 60;
|
|
14
15
|
/** Bounds on a bundle's lifetime: an hour at the short end, 90 days at the long. */
|
|
@@ -5051,7 +5052,7 @@ async function createAgentTaskToken() {
|
|
|
5051
5052
|
}
|
|
5052
5053
|
//#endregion
|
|
5053
5054
|
//#region package.json
|
|
5054
|
-
var version = "1.
|
|
5055
|
+
var version = "1.4.0";
|
|
5055
5056
|
//#endregion
|
|
5056
5057
|
//#region ../../packages/api-client/src/index.ts
|
|
5057
5058
|
var SeekritApiError = class extends Error {
|
|
@@ -11510,9 +11511,20 @@ function renderProxyConfig(plan) {
|
|
|
11510
11511
|
out.push("# agent. Requires SEEKRIT_PROXY_CONTROL_TOKEN in the environment, and that");
|
|
11511
11512
|
out.push("# token must not be readable by the agents.");
|
|
11512
11513
|
out.push("[control]");
|
|
11513
|
-
out.push(`
|
|
11514
|
+
if (plan.control.socket) out.push(`socket = ${tomlString(plan.control.socket)}`);
|
|
11515
|
+
else if (plan.control.listen) out.push(`listen = ${tomlString(plan.control.listen)}`);
|
|
11514
11516
|
out.push(`ttl = ${tomlString(plan.control.ttl)}`);
|
|
11515
11517
|
out.push(`max_ttl = ${tomlString(plan.control.maxTtl)}`);
|
|
11518
|
+
if (plan.control.socket) {
|
|
11519
|
+
out.push("# Only this program may mint tickets and decide held requests. Without it,");
|
|
11520
|
+
out.push("# any process running as your user and holding the control token can.");
|
|
11521
|
+
out.push("# allow_binary = [\"/usr/local/bin/your-orchestrator\"]");
|
|
11522
|
+
out.push("# allow_sha256 = [\"<sha256 of that binary>\"]");
|
|
11523
|
+
} else {
|
|
11524
|
+
out.push("# A TCP control listener carries no peer credentials, so the token is the");
|
|
11525
|
+
out.push("# only gate — and an agent spawned by the orchestrator inherits it. Use a");
|
|
11526
|
+
out.push("# Unix socket (--control /run/seekrit/control.sock) to attest the caller.");
|
|
11527
|
+
}
|
|
11516
11528
|
out.push("");
|
|
11517
11529
|
}
|
|
11518
11530
|
if (plan.tasks) {
|
|
@@ -12165,7 +12177,7 @@ function collect$2(value, acc) {
|
|
|
12165
12177
|
* release-please-config.json), so the pin follows the crate without anyone
|
|
12166
12178
|
* remembering to move it.
|
|
12167
12179
|
*/
|
|
12168
|
-
const PROXY_VERSION = "0.
|
|
12180
|
+
const PROXY_VERSION = "0.12.0";
|
|
12169
12181
|
const BIN = "seekrit-proxy";
|
|
12170
12182
|
/**
|
|
12171
12183
|
* Host → Rust target triple.
|
|
@@ -12201,7 +12213,7 @@ function versionPrefix(version) {
|
|
|
12201
12213
|
return version.startsWith("v") ? version : `v${version}`;
|
|
12202
12214
|
}
|
|
12203
12215
|
function resolveVersion(explicit) {
|
|
12204
|
-
return explicit ?? process.env.SEEKRIT_PROXY_VERSION ?? "0.
|
|
12216
|
+
return explicit ?? process.env.SEEKRIT_PROXY_VERSION ?? "0.12.0";
|
|
12205
12217
|
}
|
|
12206
12218
|
function resolveBaseUrl(explicit) {
|
|
12207
12219
|
return (explicit ?? process.env.SEEKRIT_PROXY_BASE_URL ?? "https://proxy.seekrit.dev").replace(/\/+$/, "");
|
|
@@ -12328,6 +12340,93 @@ function signalNumber(signal) {
|
|
|
12328
12340
|
* — and none of them changes what the proxy does. The generated config is the
|
|
12329
12341
|
* same config; the fetched binary is the released binary, checksum-verified.
|
|
12330
12342
|
*/
|
|
12343
|
+
/**
|
|
12344
|
+
* Where `seekrit proxy approvals` looks by default.
|
|
12345
|
+
*
|
|
12346
|
+
* The control listener is loopback-bound by design, so the only interesting
|
|
12347
|
+
* variable is the port — and one URL is easier to override than a host/port pair.
|
|
12348
|
+
*/
|
|
12349
|
+
const DEFAULT_CONTROL_URL = "http://127.0.0.1:9090";
|
|
12350
|
+
/** Header the control listener authenticates with (mirrors `src/tickets.rs`). */
|
|
12351
|
+
const CONTROL_TOKEN_HEADER = "x-seekrit-control-token";
|
|
12352
|
+
/**
|
|
12353
|
+
* Talk to a running proxy's control listener.
|
|
12354
|
+
*
|
|
12355
|
+
* The token comes from the environment and never from a flag: it is what decides
|
|
12356
|
+
* whether a held request goes through, and a flag would put it in shell history
|
|
12357
|
+
* next to the operation it is guarding.
|
|
12358
|
+
*/
|
|
12359
|
+
async function controlRequest(base, path, decision) {
|
|
12360
|
+
const token = process.env.SEEKRIT_PROXY_CONTROL_TOKEN;
|
|
12361
|
+
if (!token) fail("SEEKRIT_PROXY_CONTROL_TOKEN is not set — it is the same token the proxy was started with, and without it the listener will not answer");
|
|
12362
|
+
const headers = {
|
|
12363
|
+
[CONTROL_TOKEN_HEADER]: token,
|
|
12364
|
+
...decision ? { "content-type": "application/json" } : {}
|
|
12365
|
+
};
|
|
12366
|
+
const method = decision ? "POST" : "GET";
|
|
12367
|
+
const body = decision ? JSON.stringify({ decision }) : void 0;
|
|
12368
|
+
let status;
|
|
12369
|
+
let text;
|
|
12370
|
+
try {
|
|
12371
|
+
({status, text} = isSocketPath(base) ? await socketRequest(socketPathOf(base), path, method, headers, body) : await fetchRequest(base, path, method, headers, body));
|
|
12372
|
+
} catch (e) {
|
|
12373
|
+
fail(`could not reach the proxy's control listener at ${base}: ${e.message}\nIs the proxy running, and does its config have a [control] block?`);
|
|
12374
|
+
throw e;
|
|
12375
|
+
}
|
|
12376
|
+
if (status < 200 || status >= 300) fail(`${status} from the control listener: ${text.trim()}`);
|
|
12377
|
+
return JSON.parse(text);
|
|
12378
|
+
}
|
|
12379
|
+
/** Is this `--control` value a Unix socket rather than a URL? */
|
|
12380
|
+
function isSocketPath(value) {
|
|
12381
|
+
return value.startsWith("unix:") || value.startsWith("/") || value.startsWith(".");
|
|
12382
|
+
}
|
|
12383
|
+
/** The filesystem path inside a `--control` value. */
|
|
12384
|
+
function socketPathOf(value) {
|
|
12385
|
+
return value.startsWith("unix:") ? value.slice(5).replace(/^\/\//, "") : value;
|
|
12386
|
+
}
|
|
12387
|
+
async function fetchRequest(base, path, method, headers, body) {
|
|
12388
|
+
const resp = await fetch(new URL(path, base), {
|
|
12389
|
+
method,
|
|
12390
|
+
headers,
|
|
12391
|
+
...body ? { body } : {}
|
|
12392
|
+
});
|
|
12393
|
+
return {
|
|
12394
|
+
status: resp.status,
|
|
12395
|
+
text: await resp.text()
|
|
12396
|
+
};
|
|
12397
|
+
}
|
|
12398
|
+
/**
|
|
12399
|
+
* The same request over a Unix socket.
|
|
12400
|
+
*
|
|
12401
|
+
* `fetch` cannot dial one without a custom dispatcher, and `node:http` can with
|
|
12402
|
+
* no dependency at all — which matters for a CLI shipped over npm.
|
|
12403
|
+
*/
|
|
12404
|
+
function socketRequest(socketPath, path, method, headers, body) {
|
|
12405
|
+
return new Promise((resolve, reject) => {
|
|
12406
|
+
const req = request({
|
|
12407
|
+
socketPath,
|
|
12408
|
+
path,
|
|
12409
|
+
method,
|
|
12410
|
+
headers: {
|
|
12411
|
+
...headers,
|
|
12412
|
+
...body ? { "content-length": Buffer.byteLength(body) } : {}
|
|
12413
|
+
}
|
|
12414
|
+
}, (res) => {
|
|
12415
|
+
let text = "";
|
|
12416
|
+
res.setEncoding("utf8");
|
|
12417
|
+
res.on("data", (chunk) => {
|
|
12418
|
+
text += chunk;
|
|
12419
|
+
});
|
|
12420
|
+
res.on("end", () => resolve({
|
|
12421
|
+
status: res.statusCode ?? 0,
|
|
12422
|
+
text
|
|
12423
|
+
}));
|
|
12424
|
+
});
|
|
12425
|
+
req.on("error", reject);
|
|
12426
|
+
if (body) req.write(body);
|
|
12427
|
+
req.end();
|
|
12428
|
+
});
|
|
12429
|
+
}
|
|
12331
12430
|
const DEFAULT_CONFIG = "seekrit-proxy.toml";
|
|
12332
12431
|
/**
|
|
12333
12432
|
* The exact grammar `seekrit_cache::parse_duration` accepts: a positive integer,
|
|
@@ -12350,7 +12449,8 @@ function planOptions(options) {
|
|
|
12350
12449
|
const listen = options.listen ?? PLAN_DEFAULTS.listen;
|
|
12351
12450
|
const forwardListen = options.forwardListen ?? PLAN_DEFAULTS.forwardListen;
|
|
12352
12451
|
if (mode === "both" && listen === forwardListen) fail(`--listen and --forward-listen cannot both be ${listen} — the two planes need separate ports`);
|
|
12353
|
-
|
|
12452
|
+
const controlIsSocket = options.control !== void 0 && isSocketPath(options.control);
|
|
12453
|
+
if (options.control && !controlIsSocket && (options.control === listen || options.control === forwardListen)) fail(`--control cannot share an address with a data plane (${options.control})`);
|
|
12354
12454
|
const refresh = duration(options.refresh, "--refresh");
|
|
12355
12455
|
return {
|
|
12356
12456
|
mode,
|
|
@@ -12362,7 +12462,7 @@ function planOptions(options) {
|
|
|
12362
12462
|
...options.cache || options.cacheMaxAge ? { cacheMaxAge: duration(options.cacheMaxAge, "--cache-max-age") ?? "24h" } : {},
|
|
12363
12463
|
...refresh ? { secretsRefresh: refresh } : {},
|
|
12364
12464
|
...options.control ? { control: {
|
|
12365
|
-
listen: options.control,
|
|
12465
|
+
...controlIsSocket ? { socket: options.control } : { listen: options.control },
|
|
12366
12466
|
ttl: "1h",
|
|
12367
12467
|
maxTtl: "12h"
|
|
12368
12468
|
} } : {},
|
|
@@ -12486,7 +12586,7 @@ async function buildPlan(options) {
|
|
|
12486
12586
|
}
|
|
12487
12587
|
/** Add the generation flags to a command, so `init` and `run` stay in step. */
|
|
12488
12588
|
function withGenerateOptions(cmd) {
|
|
12489
|
-
return cmd.option("--preset <name>", "gateway preset (repeatable; see `seekrit proxy presets`)", (value, acc = []) => [...acc, value]).option("--host <host[=SECRET,…]>", "ad-hoc rule: bare hostname, optionally the secrets it may receive (repeatable)", (value, acc = []) => [...acc, value]).option("--base-url <url>", "upstream base URL for an OpenAI-compatible gateway").option("--secret <NAME>", "override a preset's secret name").option("--prefix <path>", "override a preset's route prefix").option("--agent <slug>", "take the rules from published agent policy (server mode)").option("--agents <slug>", "additional identities this proxy may serve", (v, a = []) => [...a, v]).option("--org <slug>", "organization (for --agent)").option("--mode <reverse|forward|both>", "which data plane(s) to configure", "reverse").option("--listen <addr>", `reverse-proxy address (default: ${PLAN_DEFAULTS.listen})`).option("--forward-listen <addr>", `forward-proxy address (default: ${PLAN_DEFAULTS.forwardListen})`).option("--unmatched <tunnel|deny>", "what to do with an unruled host in forward mode").option("--ca-cert <path>", "interception CA certificate path (forward mode)").option("--ca-key <path>", "interception CA key path (forward mode)").option("--cache", "add a [cache] block so the proxy can start during an outage").option("--cache-max-age <dur>", "how stale a cached resolve may be (implies --cache)").option("--refresh <dur>", "re-resolve/re-fetch interval").option("--control <addr>", "add a [control] listener for per-agent session tickets").option("--tasks", "add a [tasks] block so this proxy honours runs dispatched with `seekrit agents dispatch`").option("--tasks-cache-ttl <dur>", "how long an introspected task is reused (implies --tasks)").option("--activity", "add an [activity] block so this proxy reports aggregate decisions for `seekrit agents review`").option("--activity-interval <dur>", "how often counts are flushed (implies --activity)");
|
|
12589
|
+
return cmd.option("--preset <name>", "gateway preset (repeatable; see `seekrit proxy presets`)", (value, acc = []) => [...acc, value]).option("--host <host[=SECRET,…]>", "ad-hoc rule: bare hostname, optionally the secrets it may receive (repeatable)", (value, acc = []) => [...acc, value]).option("--base-url <url>", "upstream base URL for an OpenAI-compatible gateway").option("--secret <NAME>", "override a preset's secret name").option("--prefix <path>", "override a preset's route prefix").option("--agent <slug>", "take the rules from published agent policy (server mode)").option("--agents <slug>", "additional identities this proxy may serve", (v, a = []) => [...a, v]).option("--org <slug>", "organization (for --agent)").option("--mode <reverse|forward|both>", "which data plane(s) to configure", "reverse").option("--listen <addr>", `reverse-proxy address (default: ${PLAN_DEFAULTS.listen})`).option("--forward-listen <addr>", `forward-proxy address (default: ${PLAN_DEFAULTS.forwardListen})`).option("--unmatched <tunnel|deny>", "what to do with an unruled host in forward mode").option("--ca-cert <path>", "interception CA certificate path (forward mode)").option("--ca-key <path>", "interception CA key path (forward mode)").option("--cache", "add a [cache] block so the proxy can start during an outage").option("--cache-max-age <dur>", "how stale a cached resolve may be (implies --cache)").option("--refresh <dur>", "re-resolve/re-fetch interval").option("--control <addr|path>", "add a [control] listener for per-agent session tickets; a path makes it a Unix socket, which is the only transport whose caller can be attested").option("--tasks", "add a [tasks] block so this proxy honours runs dispatched with `seekrit agents dispatch`").option("--tasks-cache-ttl <dur>", "how long an introspected task is reused (implies --tasks)").option("--activity", "add an [activity] block so this proxy reports aggregate decisions for `seekrit agents review`").option("--activity-interval <dur>", "how often counts are flushed (implies --activity)");
|
|
12490
12590
|
}
|
|
12491
12591
|
function registerProxyCommands(program) {
|
|
12492
12592
|
const proxy = program.command("proxy").description("run and configure the agent egress proxy (`seekrit proxy --help`)");
|
|
@@ -12553,10 +12653,10 @@ Next:
|
|
|
12553
12653
|
});
|
|
12554
12654
|
console.log(path);
|
|
12555
12655
|
const { target } = detectTarget();
|
|
12556
|
-
process.stderr.write(`seekrit-proxy ${versionPrefix(options.proxyVersion ?? "0.
|
|
12656
|
+
process.stderr.write(`seekrit-proxy ${versionPrefix(options.proxyVersion ?? "0.12.0")} (${target})\n`);
|
|
12557
12657
|
});
|
|
12558
12658
|
proxy.command("where").description("show which binary `seekrit proxy run` would use, without fetching it").option("--proxy-version <version>", `version to report (default: ${PROXY_VERSION})`).option("--json", "machine-readable output").action((options) => {
|
|
12559
|
-
const version = options.proxyVersion ?? process.env.SEEKRIT_PROXY_VERSION ?? "0.
|
|
12659
|
+
const version = options.proxyVersion ?? process.env.SEEKRIT_PROXY_VERSION ?? "0.12.0";
|
|
12560
12660
|
const override = process.env.SEEKRIT_PROXY_BIN;
|
|
12561
12661
|
const { target, exe } = detectTarget();
|
|
12562
12662
|
const path = override ?? proxyBinaryPath(version, target, exe);
|
|
@@ -12572,12 +12672,50 @@ Next:
|
|
|
12572
12672
|
process.stderr.write(`${info.version} · ${info.target} · ${info.source}\n`);
|
|
12573
12673
|
});
|
|
12574
12674
|
});
|
|
12675
|
+
proxy.command("approvals").description("list requests a running proxy is holding for approval").option("--control <url|path>", "control listener URL, or a Unix socket path", DEFAULT_CONTROL_URL).option("--json", "machine-readable output").action(async (options) => {
|
|
12676
|
+
const { pending } = await controlRequest(options.control, "/approvals");
|
|
12677
|
+
emit(options, { pending }, () => {
|
|
12678
|
+
printTable(pending, [
|
|
12679
|
+
col("ID", (p) => p.id),
|
|
12680
|
+
col("OPERATION", (p) => p.operation),
|
|
12681
|
+
col("REQUEST", (p) => `${p.method} ${p.host}${p.path}`),
|
|
12682
|
+
col("CARRIES", (p) => p.secrets.join(", ") || "no credential"),
|
|
12683
|
+
col("EXPIRES", (p) => `${p.expires_in_seconds}s`)
|
|
12684
|
+
], "nothing is waiting");
|
|
12685
|
+
});
|
|
12686
|
+
});
|
|
12687
|
+
for (const [verb, decision, summary] of [
|
|
12688
|
+
[
|
|
12689
|
+
"approve",
|
|
12690
|
+
"approve",
|
|
12691
|
+
"let a held request through"
|
|
12692
|
+
],
|
|
12693
|
+
[
|
|
12694
|
+
"deny",
|
|
12695
|
+
"deny",
|
|
12696
|
+
"refuse a held request"
|
|
12697
|
+
],
|
|
12698
|
+
[
|
|
12699
|
+
"always",
|
|
12700
|
+
"always",
|
|
12701
|
+
"let it through and stop asking about that host and method for this run"
|
|
12702
|
+
]
|
|
12703
|
+
]) proxy.command(`${verb} <id>`).description(summary).option("--control <url|path>", "control listener URL, or a Unix socket path", DEFAULT_CONTROL_URL).option("--json", "machine-readable output").action(async (id, options) => {
|
|
12704
|
+
const result = await controlRequest(options.control, `/approvals/${encodeURIComponent(id)}`, decision);
|
|
12705
|
+
emit(options, {
|
|
12706
|
+
id,
|
|
12707
|
+
decision,
|
|
12708
|
+
...result
|
|
12709
|
+
}, () => {
|
|
12710
|
+
process.stderr.write(`${id}: ${decision}\n`);
|
|
12711
|
+
});
|
|
12712
|
+
});
|
|
12575
12713
|
withGenerateOptions(proxy.command("compose").description("print a docker compose sidecar snippet for a generated config")).option("--service <name>", "compose service name for the proxy", COMPOSE_DEFAULTS.service).option("--workload <name>", "compose service name for your workload", COMPOSE_DEFAULTS.workload).option("--image <ref>", "image to pin (default: seekritdev/proxy:<version>)").option("--publish", "also publish the proxy's ports to the host").action(async (options) => {
|
|
12576
12714
|
const plan = await buildPlan(options);
|
|
12577
12715
|
process.stdout.write(renderComposeSnippet(plan, {
|
|
12578
12716
|
service: options.service,
|
|
12579
12717
|
workload: options.workload,
|
|
12580
|
-
image: options.image ?? `seekritdev/proxy:0.
|
|
12718
|
+
image: options.image ?? `seekritdev/proxy:0.12.0`,
|
|
12581
12719
|
publish: Boolean(options.publish)
|
|
12582
12720
|
}));
|
|
12583
12721
|
});
|