@fusionkit/cli 0.1.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/cli.d.ts +8 -0
- package/dist/cli.js +34 -0
- package/dist/commands/ensemble-gateway.d.ts +2 -0
- package/dist/commands/ensemble-gateway.js +114 -0
- package/dist/commands/ensemble-records.d.ts +33 -0
- package/dist/commands/ensemble-records.js +207 -0
- package/dist/commands/ensemble.d.ts +2 -0
- package/dist/commands/ensemble.js +254 -0
- package/dist/commands/fusion.d.ts +2 -0
- package/dist/commands/fusion.js +112 -0
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.js +24 -0
- package/dist/commands/lifecycle.d.ts +2 -0
- package/dist/commands/lifecycle.js +124 -0
- package/dist/commands/local.d.ts +2 -0
- package/dist/commands/local.js +25 -0
- package/dist/commands/plane.d.ts +2 -0
- package/dist/commands/plane.js +30 -0
- package/dist/commands/run.d.ts +2 -0
- package/dist/commands/run.js +149 -0
- package/dist/commands/runner.d.ts +2 -0
- package/dist/commands/runner.js +33 -0
- package/dist/commands/secrets.d.ts +2 -0
- package/dist/commands/secrets.js +21 -0
- package/dist/config.d.ts +30 -0
- package/dist/config.js +69 -0
- package/dist/fusion-quickstart.d.ts +182 -0
- package/dist/fusion-quickstart.js +673 -0
- package/dist/gateway.d.ts +63 -0
- package/dist/gateway.js +304 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +28 -0
- package/dist/local.d.ts +40 -0
- package/dist/local.js +144 -0
- package/dist/render.d.ts +7 -0
- package/dist/render.js +131 -0
- package/dist/shared/errors.d.ts +6 -0
- package/dist/shared/errors.js +9 -0
- package/dist/shared/options.d.ts +24 -0
- package/dist/shared/options.js +106 -0
- package/dist/shared/plane.d.ts +13 -0
- package/dist/shared/plane.js +46 -0
- package/dist/shared/preflight.d.ts +15 -0
- package/dist/shared/preflight.js +48 -0
- package/dist/shared/proc.d.ts +41 -0
- package/dist/shared/proc.js +122 -0
- package/dist/test/cli.test.d.ts +1 -0
- package/dist/test/cli.test.js +867 -0
- package/dist/test/e2e.test.d.ts +1 -0
- package/dist/test/e2e.test.js +250 -0
- package/dist/test/fusion-quickstart.test.d.ts +1 -0
- package/dist/test/fusion-quickstart.test.js +189 -0
- package/dist/test/gateway-e2e.test.d.ts +1 -0
- package/dist/test/gateway-e2e.test.js +606 -0
- package/dist/test/handoff.test.d.ts +1 -0
- package/dist/test/handoff.test.js +212 -0
- package/dist/test/local.test.d.ts +1 -0
- package/dist/test/local.test.js +39 -0
- package/dist/test/proc.test.d.ts +1 -0
- package/dist/test/proc.test.js +22 -0
- package/package.json +48 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { FUSION_TOOLS, pickTool, runFusion } from "../fusion-quickstart.js";
|
|
3
|
+
import { collect, parseFusionTool, parseIdValue, parsePanelModelSpec, parsePort } from "../shared/options.js";
|
|
4
|
+
/** Attach the panel/gateway flags shared by `fusion` and the per-tool launchers. */
|
|
5
|
+
function applyFusionOptions(command) {
|
|
6
|
+
return command
|
|
7
|
+
.option("--model <spec>", "panel model ID=MODEL or ID=PROVIDER:MODEL (repeatable)", collect)
|
|
8
|
+
.option("--models <spec>", "alias of --model", collect)
|
|
9
|
+
.option("--model-endpoint <spec>", "pre-running OpenAI-compatible endpoint ID=URL (repeatable)", collect)
|
|
10
|
+
.option("--key-env <spec>", "env var holding a model's API key ID=ENV (repeatable)", collect)
|
|
11
|
+
.option("--judge-model <model>", "model used for judge synthesis")
|
|
12
|
+
.option("--synthesis-url <url>", "pre-running fusionkit serve for synthesis")
|
|
13
|
+
.option("--fusionkit-dir <dir>", "local FusionKit checkout (dev override for the uvx synthesizer)")
|
|
14
|
+
.option("--repo <dir>", "coding workspace the panel fuses over")
|
|
15
|
+
.option("--cursor-kit-dir <dir>", "built Cursorkit checkout for the cursor tool")
|
|
16
|
+
.option("--local", "use the local MLX panel trio instead of the default cloud panel")
|
|
17
|
+
.option("--observe", "boot the local scope dashboard and stream live trace events")
|
|
18
|
+
.option("--auth-token <token>", "require a bearer token on the gateway")
|
|
19
|
+
.option("--port <n>", "gateway port (default: ephemeral)")
|
|
20
|
+
.allowUnknownOption()
|
|
21
|
+
.passThroughOptions();
|
|
22
|
+
}
|
|
23
|
+
/** Build the `RunFusionOptions` shared by every entrypoint from parsed flags. */
|
|
24
|
+
function resolveOptions(opts) {
|
|
25
|
+
const options = {};
|
|
26
|
+
const keyEnvs = {};
|
|
27
|
+
for (const spec of opts.keyEnv ?? []) {
|
|
28
|
+
const { id, value } = parseIdValue("--key-env", spec);
|
|
29
|
+
keyEnvs[id] = value;
|
|
30
|
+
}
|
|
31
|
+
if (opts.judgeModel !== undefined)
|
|
32
|
+
options.judgeModel = opts.judgeModel;
|
|
33
|
+
if (opts.synthesisUrl !== undefined)
|
|
34
|
+
options.synthesisUrl = opts.synthesisUrl;
|
|
35
|
+
if (opts.fusionkitDir !== undefined)
|
|
36
|
+
options.fusionkitDir = resolve(opts.fusionkitDir);
|
|
37
|
+
if (opts.repo !== undefined)
|
|
38
|
+
options.repo = resolve(opts.repo);
|
|
39
|
+
if (opts.cursorKitDir !== undefined)
|
|
40
|
+
options.cursorKitDir = resolve(opts.cursorKitDir);
|
|
41
|
+
if (opts.local === true)
|
|
42
|
+
options.local = true;
|
|
43
|
+
if (opts.observe === true)
|
|
44
|
+
options.observe = true;
|
|
45
|
+
if (opts.authToken !== undefined)
|
|
46
|
+
options.authToken = opts.authToken;
|
|
47
|
+
if (opts.port !== undefined)
|
|
48
|
+
options.port = parsePort(opts.port, 0);
|
|
49
|
+
const fusionkitDirEnv = process.env.FUSIONKIT_DIR ?? process.env.WARRANT_FUSIONKIT_DIR;
|
|
50
|
+
if (options.fusionkitDir === undefined && fusionkitDirEnv !== undefined) {
|
|
51
|
+
options.fusionkitDir = resolve(fusionkitDirEnv);
|
|
52
|
+
}
|
|
53
|
+
const modelSpecs = [...(opts.model ?? []), ...(opts.models ?? [])];
|
|
54
|
+
if (modelSpecs.length > 0) {
|
|
55
|
+
options.models = modelSpecs.map((spec) => parsePanelModelSpec(spec, keyEnvs));
|
|
56
|
+
}
|
|
57
|
+
const endpointSpecs = opts.modelEndpoint ?? [];
|
|
58
|
+
if (endpointSpecs.length > 0) {
|
|
59
|
+
const endpoints = {};
|
|
60
|
+
for (const spec of endpointSpecs) {
|
|
61
|
+
const { id, value } = parseIdValue("--model-endpoint", spec);
|
|
62
|
+
endpoints[id] = value;
|
|
63
|
+
}
|
|
64
|
+
options.endpoints = endpoints;
|
|
65
|
+
// Pre-running endpoints define the panel; ignore any --model specs.
|
|
66
|
+
options.models = Object.keys(endpoints).map((id) => ({
|
|
67
|
+
id,
|
|
68
|
+
model: id,
|
|
69
|
+
provider: "openai-compatible"
|
|
70
|
+
}));
|
|
71
|
+
}
|
|
72
|
+
return options;
|
|
73
|
+
}
|
|
74
|
+
export function registerFusion(program) {
|
|
75
|
+
// Generic `fusion [tool]` — keeps the original surface and interactive pick.
|
|
76
|
+
applyFusionOptions(program
|
|
77
|
+
.command("fusion")
|
|
78
|
+
.description("one command: real model fusion backs a coding agent")
|
|
79
|
+
.argument("[tool]", `${FUSION_TOOLS.join(" | ")} (omit on a TTY to pick interactively)`)
|
|
80
|
+
.argument("[args...]", "arguments forwarded to the tool")
|
|
81
|
+
.option("--tool <tool>", `coding agent to launch (${FUSION_TOOLS.join(" | ")})`))
|
|
82
|
+
.addHelpText("after", "\nfusionkit's own flags must precede the tool name; everything after the tool is forwarded to it.")
|
|
83
|
+
.action(async (positionalTool, args, opts) => {
|
|
84
|
+
let tool = opts.tool ? parseFusionTool(opts.tool) : undefined;
|
|
85
|
+
let toolArgs = [...args];
|
|
86
|
+
if (positionalTool !== undefined) {
|
|
87
|
+
if (tool === undefined && FUSION_TOOLS.includes(positionalTool)) {
|
|
88
|
+
tool = positionalTool;
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
toolArgs = [positionalTool, ...toolArgs];
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const options = resolveOptions(opts);
|
|
95
|
+
const resolvedTool = tool ?? (process.stdin.isTTY ? await pickTool() : "codex");
|
|
96
|
+
const code = await runFusion(resolvedTool, toolArgs, options);
|
|
97
|
+
process.exit(code);
|
|
98
|
+
});
|
|
99
|
+
// Top-level shortcuts: `fusionkit codex`, `fusionkit claude`, etc.
|
|
100
|
+
for (const tool of FUSION_TOOLS) {
|
|
101
|
+
applyFusionOptions(program
|
|
102
|
+
.command(tool)
|
|
103
|
+
.description(`real model fusion backs ${tool === "serve" ? "any tool (prints setup snippets)" : tool}`)
|
|
104
|
+
.argument("[args...]", `arguments forwarded to ${tool}`))
|
|
105
|
+
.addHelpText("after", `\nfusionkit's own flags must precede any ${tool} args; everything after is forwarded to ${tool}.`)
|
|
106
|
+
.action(async (args, opts) => {
|
|
107
|
+
const options = resolveOptions(opts);
|
|
108
|
+
const code = await runFusion(tool, args, options);
|
|
109
|
+
process.exit(code);
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { initHome } from "../config.js";
|
|
3
|
+
import { resolveDir } from "../shared/plane.js";
|
|
4
|
+
export function registerInit(program) {
|
|
5
|
+
program
|
|
6
|
+
.command("init")
|
|
7
|
+
.description("initialize org keys, config, policy")
|
|
8
|
+
.option("--port <n>", "plane port")
|
|
9
|
+
.option("--host <host>", "plane bind host")
|
|
10
|
+
.option("--plane-url <url>", "public plane URL for clients and runners")
|
|
11
|
+
.action((opts) => {
|
|
12
|
+
const dir = resolveDir(program.opts().dir);
|
|
13
|
+
const home = initHome(dir, {
|
|
14
|
+
...(opts.port ? { port: Number(opts.port) } : {}),
|
|
15
|
+
...(opts.host ? { host: opts.host } : {}),
|
|
16
|
+
...(opts.planeUrl ? { planeUrl: opts.planeUrl } : {})
|
|
17
|
+
});
|
|
18
|
+
console.log(`initialized warrant home at ${home.dir}`);
|
|
19
|
+
console.log(`plane url: ${home.config.planeUrl}`);
|
|
20
|
+
console.log(`policy: ${join(home.dir, "policy.json")}`);
|
|
21
|
+
console.log(`enroll token (for runners): ${home.config.enrollToken}`);
|
|
22
|
+
console.log(`admin token (for the control panel): ${home.config.adminToken}`);
|
|
23
|
+
});
|
|
24
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { verifyReceiptBundle } from "@fusionkit/protocol";
|
|
4
|
+
import { pullRun } from "@fusionkit/workspace";
|
|
5
|
+
import { loadHome } from "../config.js";
|
|
6
|
+
import { renderReceipt, renderRunList } from "../render.js";
|
|
7
|
+
import { clientFor, resolveDir, waitForTerminal } from "../shared/plane.js";
|
|
8
|
+
export function registerLifecycle(program) {
|
|
9
|
+
const dirOf = () => resolveDir(program.opts().dir);
|
|
10
|
+
program
|
|
11
|
+
.command("runs")
|
|
12
|
+
.description("list runs")
|
|
13
|
+
.action(async () => {
|
|
14
|
+
const { runs } = await clientFor(dirOf()).listRuns();
|
|
15
|
+
console.log(renderRunList(runs));
|
|
16
|
+
});
|
|
17
|
+
program
|
|
18
|
+
.command("approve <runId>")
|
|
19
|
+
.description("grant required consent")
|
|
20
|
+
.action(async (runId) => {
|
|
21
|
+
const dir = dirOf();
|
|
22
|
+
const home = loadHome(dir);
|
|
23
|
+
const result = await clientFor(dir).approve(runId, {
|
|
24
|
+
kind: "human",
|
|
25
|
+
id: home.config.requestedBy
|
|
26
|
+
});
|
|
27
|
+
console.log(`run ${result.runId} [${result.status}]`);
|
|
28
|
+
});
|
|
29
|
+
program
|
|
30
|
+
.command("cancel <runId>")
|
|
31
|
+
.description("cancel an unclaimed run")
|
|
32
|
+
.action(async (runId) => {
|
|
33
|
+
const dir = dirOf();
|
|
34
|
+
const home = loadHome(dir);
|
|
35
|
+
const result = await clientFor(dir).cancel(runId, {
|
|
36
|
+
kind: "human",
|
|
37
|
+
id: home.config.requestedBy
|
|
38
|
+
});
|
|
39
|
+
console.log(`run ${result.runId} [${result.status}]`);
|
|
40
|
+
});
|
|
41
|
+
program
|
|
42
|
+
.command("watch <runId>")
|
|
43
|
+
.description("stream run status")
|
|
44
|
+
.action(async (runId) => {
|
|
45
|
+
const status = await waitForTerminal(clientFor(dirOf()), runId, (s) => console.log(s));
|
|
46
|
+
console.log(`final: ${status}`);
|
|
47
|
+
});
|
|
48
|
+
program
|
|
49
|
+
.command("receipt <runId>")
|
|
50
|
+
.description("one screen, five questions")
|
|
51
|
+
.action(async (runId) => {
|
|
52
|
+
console.log(renderReceipt(await clientFor(dirOf()).getBundle(runId)));
|
|
53
|
+
});
|
|
54
|
+
program
|
|
55
|
+
.command("bundle <runId>")
|
|
56
|
+
.description("save offline-verifiable bundle")
|
|
57
|
+
.option("--out <file>", "output path")
|
|
58
|
+
.action(async (runId, opts) => {
|
|
59
|
+
const bundle = await clientFor(dirOf()).getBundle(runId);
|
|
60
|
+
const out = opts.out ?? `${runId}.bundle.json`;
|
|
61
|
+
writeFileSync(out, JSON.stringify(bundle, null, 2));
|
|
62
|
+
console.log(`bundle written to ${out}`);
|
|
63
|
+
});
|
|
64
|
+
program
|
|
65
|
+
.command("verify <file>")
|
|
66
|
+
.description("verify a bundle offline")
|
|
67
|
+
.action((file) => {
|
|
68
|
+
const bundle = JSON.parse(readFileSync(file, "utf8"));
|
|
69
|
+
const result = verifyReceiptBundle(bundle);
|
|
70
|
+
if (result.ok) {
|
|
71
|
+
console.log("VERIFIED: signatures, event chain, and linkage all check out");
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
console.error("VERIFICATION FAILED:");
|
|
75
|
+
for (const problem of result.problems)
|
|
76
|
+
console.error(` - ${problem}`);
|
|
77
|
+
process.exit(1);
|
|
78
|
+
});
|
|
79
|
+
program
|
|
80
|
+
.command("pull <runId>")
|
|
81
|
+
.description("divergence-safe pull of results")
|
|
82
|
+
.option("--repo <dir>", "workspace repository", ".")
|
|
83
|
+
.action(async (runId, opts) => {
|
|
84
|
+
const client = clientFor(dirOf());
|
|
85
|
+
const bundle = await client.getBundle(runId);
|
|
86
|
+
const diffHash = bundle.receipt.workspaceOut.diffHash;
|
|
87
|
+
if (!diffHash) {
|
|
88
|
+
console.log("run produced no workspace changes; nothing to pull");
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const diff = await client.getBlob(diffHash);
|
|
92
|
+
const result = pullRun(resolve(opts.repo), runId, bundle.contract.workspace.baseRef, diff);
|
|
93
|
+
switch (result.mode) {
|
|
94
|
+
case "applied":
|
|
95
|
+
console.log("applied run output to the working tree (clean fast path)");
|
|
96
|
+
break;
|
|
97
|
+
case "branch":
|
|
98
|
+
console.log(`local workspace diverged from the contract base; results are on branch ${result.branch}`);
|
|
99
|
+
break;
|
|
100
|
+
case "empty":
|
|
101
|
+
console.log("run produced no workspace changes; nothing to pull");
|
|
102
|
+
break;
|
|
103
|
+
default: {
|
|
104
|
+
const exhausted = result;
|
|
105
|
+
throw new Error(`unreachable: ${String(exhausted)}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
program
|
|
110
|
+
.command("export")
|
|
111
|
+
.description("audit JSONL export")
|
|
112
|
+
.option("--since <iso>", "only export events at or after this timestamp")
|
|
113
|
+
.action(async (opts) => {
|
|
114
|
+
process.stdout.write(await clientFor(dirOf()).exportJsonl(opts.since));
|
|
115
|
+
});
|
|
116
|
+
program
|
|
117
|
+
.command("ui")
|
|
118
|
+
.description("control panel URL and login token")
|
|
119
|
+
.action(() => {
|
|
120
|
+
const home = loadHome(dirOf());
|
|
121
|
+
console.log(`control panel: ${home.config.planeUrl}/ui/`);
|
|
122
|
+
console.log(`login token: ${home.config.adminToken}`);
|
|
123
|
+
});
|
|
124
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { LOCAL_TOOLS, runLocal } from "../local.js";
|
|
2
|
+
import { fail } from "../shared/errors.js";
|
|
3
|
+
export function registerLocal(program) {
|
|
4
|
+
program
|
|
5
|
+
.command("local")
|
|
6
|
+
.description("back a vendor agent with a local model")
|
|
7
|
+
.argument("[tool]", `${LOCAL_TOOLS.join(" | ")}`)
|
|
8
|
+
.argument("[args...]", "arguments forwarded to the tool")
|
|
9
|
+
.option("--public-url <url>", "public tunnel URL for Cursor (or WARRANT_PUBLIC_URL)")
|
|
10
|
+
.option("--auth-token <token>", "require a bearer token on the gateway")
|
|
11
|
+
.allowUnknownOption()
|
|
12
|
+
.passThroughOptions()
|
|
13
|
+
.addHelpText("after", "\nwarrant's own flags must precede the tool name; everything after the tool is forwarded to it.")
|
|
14
|
+
.action(async (tool, args, opts) => {
|
|
15
|
+
if (tool === undefined || !LOCAL_TOOLS.includes(tool)) {
|
|
16
|
+
fail(`usage: warrant local <${LOCAL_TOOLS.join(" | ")}> [args...]`);
|
|
17
|
+
}
|
|
18
|
+
const options = {
|
|
19
|
+
...(opts.publicUrl !== undefined ? { publicUrl: opts.publicUrl } : {}),
|
|
20
|
+
...(opts.authToken !== undefined ? { authToken: opts.authToken } : {})
|
|
21
|
+
};
|
|
22
|
+
const code = await runLocal(tool, args, options);
|
|
23
|
+
process.exit(code);
|
|
24
|
+
});
|
|
25
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { Plane, startPlaneServer } from "@fusionkit/plane";
|
|
3
|
+
import { loadHome, secretStoreFor } from "../config.js";
|
|
4
|
+
import { resolveDir } from "../shared/plane.js";
|
|
5
|
+
export function registerPlane(program) {
|
|
6
|
+
const plane = program.command("plane").description("control plane + control panel");
|
|
7
|
+
plane
|
|
8
|
+
.command("start")
|
|
9
|
+
.description("start the control plane + control panel")
|
|
10
|
+
.option("--port <n>", "bind port")
|
|
11
|
+
.option("--host <host>", "bind host")
|
|
12
|
+
.action(async (opts) => {
|
|
13
|
+
const dir = resolveDir(program.opts().dir);
|
|
14
|
+
const home = loadHome(dir);
|
|
15
|
+
const planeInstance = new Plane({
|
|
16
|
+
dataDir: join(dir, "data"),
|
|
17
|
+
policy: home.policy,
|
|
18
|
+
planePrivateKeyPem: home.planePrivateKeyPem,
|
|
19
|
+
planePublicKeyPem: home.planePublicKeyPem,
|
|
20
|
+
adminToken: home.config.adminToken,
|
|
21
|
+
enrollToken: home.config.enrollToken,
|
|
22
|
+
secretStore: secretStoreFor(home)
|
|
23
|
+
});
|
|
24
|
+
const port = opts.port ? Number(opts.port) : home.config.port;
|
|
25
|
+
const host = opts.host ?? home.config.host;
|
|
26
|
+
const started = await startPlaneServer(planeInstance, { port, host });
|
|
27
|
+
console.log(`warrant plane listening on http://${started.host}:${started.port}`);
|
|
28
|
+
console.log(`control panel: http://${started.host}:${started.port}/ui/`);
|
|
29
|
+
});
|
|
30
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { agents, handoff, targets } from "@fusionkit/handoff";
|
|
4
|
+
import { AGENT_KINDS } from "@fusionkit/protocol";
|
|
5
|
+
import { captureWorkspace } from "@fusionkit/workspace";
|
|
6
|
+
import { loadHome } from "../config.js";
|
|
7
|
+
import { renderDisclosure, renderReceipt, renderTrace } from "../render.js";
|
|
8
|
+
import { fail } from "../shared/errors.js";
|
|
9
|
+
import { collect, isolationFlag } from "../shared/options.js";
|
|
10
|
+
import { CONTINUE_WAIT_TIMEOUT_MS, clientFor, resolveDir, waitForTerminal } from "../shared/plane.js";
|
|
11
|
+
function agentSpecFor(kind) {
|
|
12
|
+
switch (kind) {
|
|
13
|
+
case "claude-code":
|
|
14
|
+
return agents.claudeCode();
|
|
15
|
+
case "codex":
|
|
16
|
+
return agents.codex();
|
|
17
|
+
case "pi":
|
|
18
|
+
return agents.pi();
|
|
19
|
+
case "mock":
|
|
20
|
+
return agents.mock();
|
|
21
|
+
case "command":
|
|
22
|
+
return agents.command();
|
|
23
|
+
default:
|
|
24
|
+
fail(`unknown agent kind "${kind}" (expected ${AGENT_KINDS.join(" | ")})`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function addRunOptions(cmd) {
|
|
28
|
+
return cmd
|
|
29
|
+
.option("--agent <kind>", `agent kind (${AGENT_KINDS.join(" | ")})`)
|
|
30
|
+
.option("--pool <pool>", "runner pool", "default")
|
|
31
|
+
.option("--secret <name>", "release a secret into the session (repeatable)", collect)
|
|
32
|
+
.option("--allow-host <host>", "allow egress to host (repeatable)", collect)
|
|
33
|
+
.option("--allow-untracked <glob>", "include untracked files matching glob (repeatable)", collect)
|
|
34
|
+
.option("--repo <dir>", "workspace repository", ".")
|
|
35
|
+
.option("--isolation <tier>", "session isolation: process | hermetic | vercel-sandbox")
|
|
36
|
+
.option("--dry-run", "show what would move; move nothing")
|
|
37
|
+
.option("--no-watch", "do not wait for completion");
|
|
38
|
+
}
|
|
39
|
+
export function registerRun(program) {
|
|
40
|
+
addRunOptions(program
|
|
41
|
+
.command("run")
|
|
42
|
+
.description("request a governed run")
|
|
43
|
+
.argument("[task...]", "task prompt")).action(async (task, opts) => {
|
|
44
|
+
const dir = resolveDir(program.opts().dir);
|
|
45
|
+
if (!opts.agent)
|
|
46
|
+
fail(`--agent is required (${AGENT_KINDS.join(" | ")})`);
|
|
47
|
+
const prompt = task.join(" ").trim();
|
|
48
|
+
if (!prompt)
|
|
49
|
+
fail("a task prompt is required");
|
|
50
|
+
const home = loadHome(dir);
|
|
51
|
+
const client = clientFor(dir);
|
|
52
|
+
const repoDir = resolve(opts.repo);
|
|
53
|
+
const captured = captureWorkspace(repoDir, {
|
|
54
|
+
allowUntracked: opts.allowUntracked ?? []
|
|
55
|
+
});
|
|
56
|
+
const isolation = isolationFlag(opts.isolation);
|
|
57
|
+
const request = {
|
|
58
|
+
requestedBy: { kind: "human", id: home.config.requestedBy },
|
|
59
|
+
agentKind: opts.agent,
|
|
60
|
+
prompt,
|
|
61
|
+
pool: opts.pool,
|
|
62
|
+
secretNames: opts.secret ?? [],
|
|
63
|
+
workspace: captured.manifest,
|
|
64
|
+
network: {
|
|
65
|
+
defaultDeny: home.policy.network.defaultDeny,
|
|
66
|
+
allowHosts: opts.allowHost ?? []
|
|
67
|
+
},
|
|
68
|
+
budget: {},
|
|
69
|
+
disclosure: "minimal-context",
|
|
70
|
+
...(isolation ? { isolation } : {})
|
|
71
|
+
};
|
|
72
|
+
if (opts.dryRun) {
|
|
73
|
+
const report = await client.dryRun(request);
|
|
74
|
+
console.log(renderDisclosure(report));
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
await client.putBlob(captured.bundle);
|
|
78
|
+
if (captured.dirtyDiff)
|
|
79
|
+
await client.putBlob(captured.dirtyDiff);
|
|
80
|
+
for (const file of captured.untracked)
|
|
81
|
+
await client.putBlob(file.content);
|
|
82
|
+
const created = await client.requestRun(request);
|
|
83
|
+
console.log(`run ${created.runId} [${created.status}]`);
|
|
84
|
+
if (!opts.watch)
|
|
85
|
+
return;
|
|
86
|
+
const status = await waitForTerminal(client, created.runId, (s) => console.log(` ${s}`));
|
|
87
|
+
if (status === "completed" || status === "failed") {
|
|
88
|
+
const bundle = await client.getBundle(created.runId);
|
|
89
|
+
console.log("");
|
|
90
|
+
console.log(renderReceipt(bundle));
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
addRunOptions(program
|
|
94
|
+
.command("continue")
|
|
95
|
+
.description("hand local work to a governed runner")
|
|
96
|
+
.argument("[task...]", "task prompt"))
|
|
97
|
+
.option("--transcript <file>", "carry a session transcript as semantic state")
|
|
98
|
+
.option("--reason <text>", "why the runtime boundary changes")
|
|
99
|
+
.action(async (task, opts) => {
|
|
100
|
+
const dir = resolveDir(program.opts().dir);
|
|
101
|
+
if (!opts.agent)
|
|
102
|
+
fail(`--agent is required (${AGENT_KINDS.join(" | ")})`);
|
|
103
|
+
const prompt = task.join(" ").trim();
|
|
104
|
+
if (!prompt)
|
|
105
|
+
fail("a task prompt is required");
|
|
106
|
+
const home = loadHome(dir);
|
|
107
|
+
const repoDir = resolve(opts.repo);
|
|
108
|
+
const target = targets.pool(opts.pool);
|
|
109
|
+
const transcript = opts.transcript ? readFileSync(opts.transcript, "utf8") : undefined;
|
|
110
|
+
const h = handoff({
|
|
111
|
+
workspace: repoDir,
|
|
112
|
+
plane: { url: home.config.planeUrl, adminToken: home.config.adminToken },
|
|
113
|
+
actor: { kind: "human", id: home.config.requestedBy },
|
|
114
|
+
agent: agentSpecFor(opts.agent),
|
|
115
|
+
secrets: opts.secret ?? [],
|
|
116
|
+
allowHosts: opts.allowHost ?? [],
|
|
117
|
+
allowUntracked: opts.allowUntracked ?? []
|
|
118
|
+
});
|
|
119
|
+
const isolation = isolationFlag(opts.isolation);
|
|
120
|
+
const continueOptions = {
|
|
121
|
+
task: prompt,
|
|
122
|
+
...(opts.reason ? { reason: opts.reason } : {}),
|
|
123
|
+
...(transcript !== undefined ? { transcript } : {}),
|
|
124
|
+
...(isolation ? { session: isolation } : {})
|
|
125
|
+
};
|
|
126
|
+
if (opts.dryRun) {
|
|
127
|
+
const { report } = await h.dryRun(target, continueOptions);
|
|
128
|
+
console.log(renderDisclosure(report));
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
const run = await h.continueIn(target, continueOptions);
|
|
132
|
+
console.log(`continuation ${run.envelope.envelopeId} → ${target.id} as run ${run.runId}`);
|
|
133
|
+
if (!opts.watch)
|
|
134
|
+
return;
|
|
135
|
+
const outcome = await run.wait({ timeoutMs: CONTINUE_WAIT_TIMEOUT_MS });
|
|
136
|
+
if (outcome.status === "awaiting_approval") {
|
|
137
|
+
console.log(`awaiting approval (${outcome.consentRequirements.join("; ")}) — run: warrant approve ${run.runId}`);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
console.log("");
|
|
141
|
+
console.log(renderTrace(h.trace()));
|
|
142
|
+
if (outcome.status === "completed" || outcome.status === "failed") {
|
|
143
|
+
console.log("");
|
|
144
|
+
console.log(renderReceipt(await run.receipt()));
|
|
145
|
+
console.log("");
|
|
146
|
+
console.log(`pull results: warrant pull ${run.runId}`);
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { Runner } from "@fusionkit/runner";
|
|
3
|
+
import { loadHome } from "../config.js";
|
|
4
|
+
import { resolveDir } from "../shared/plane.js";
|
|
5
|
+
export function registerRunner(program) {
|
|
6
|
+
const runner = program.command("runner").description("outbound-only execution runner");
|
|
7
|
+
runner
|
|
8
|
+
.command("start")
|
|
9
|
+
.description("start an outbound-only runner")
|
|
10
|
+
.option("--pool <pool>", "runner pool", "default")
|
|
11
|
+
.option("--plane <url>", "plane URL")
|
|
12
|
+
.option("--enroll-token <token>", "enrollment token")
|
|
13
|
+
.option("--data-dir <dir>", "runner data directory")
|
|
14
|
+
.action(async (opts) => {
|
|
15
|
+
const dir = resolveDir(program.opts().dir);
|
|
16
|
+
let planeUrl = opts.plane;
|
|
17
|
+
let enrollToken = opts.enrollToken;
|
|
18
|
+
if (!planeUrl || !enrollToken) {
|
|
19
|
+
const home = loadHome(dir);
|
|
20
|
+
planeUrl = planeUrl ?? home.config.planeUrl;
|
|
21
|
+
enrollToken = enrollToken ?? home.config.enrollToken;
|
|
22
|
+
}
|
|
23
|
+
const instance = new Runner({
|
|
24
|
+
planeUrl,
|
|
25
|
+
pool: opts.pool,
|
|
26
|
+
dataDir: resolve(opts.dataDir ?? ".warrant-runner"),
|
|
27
|
+
enrollToken
|
|
28
|
+
});
|
|
29
|
+
const identity = await instance.ensureEnrolled();
|
|
30
|
+
console.log(`runner ${identity.runnerId} polling pool "${identity.pool}" (outbound-only)`);
|
|
31
|
+
await instance.start();
|
|
32
|
+
});
|
|
33
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { loadHome, secretStoreFor } from "../config.js";
|
|
2
|
+
import { resolveDir } from "../shared/plane.js";
|
|
3
|
+
export function registerSecrets(program) {
|
|
4
|
+
const secrets = program.command("secrets").description("org secret store");
|
|
5
|
+
secrets
|
|
6
|
+
.command("set <name> <value>")
|
|
7
|
+
.description("store a secret in the org store")
|
|
8
|
+
.action((name, value) => {
|
|
9
|
+
const home = loadHome(resolveDir(program.opts().dir));
|
|
10
|
+
secretStoreFor(home).set(name, value);
|
|
11
|
+
console.log(`secret "${name}" stored (value encrypted at rest)`);
|
|
12
|
+
});
|
|
13
|
+
secrets
|
|
14
|
+
.command("list")
|
|
15
|
+
.description("list stored secret names")
|
|
16
|
+
.action(() => {
|
|
17
|
+
const home = loadHome(resolveDir(program.opts().dir));
|
|
18
|
+
const names = secretStoreFor(home).names();
|
|
19
|
+
console.log(names.length > 0 ? names.join("\n") : "no secrets stored");
|
|
20
|
+
});
|
|
21
|
+
}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { SecretStore } from "@fusionkit/plane";
|
|
2
|
+
import type { MasterKey } from "@fusionkit/plane";
|
|
3
|
+
import type { Policy } from "@fusionkit/protocol";
|
|
4
|
+
export type CliConfig = {
|
|
5
|
+
version: "warrant.config.v2";
|
|
6
|
+
planeUrl: string;
|
|
7
|
+
port: number;
|
|
8
|
+
/** Bind address for `plane start`. Loopback by default. */
|
|
9
|
+
host: string;
|
|
10
|
+
adminToken: string;
|
|
11
|
+
enrollToken: string;
|
|
12
|
+
requestedBy: string;
|
|
13
|
+
};
|
|
14
|
+
export type WarrantHome = {
|
|
15
|
+
dir: string;
|
|
16
|
+
config: CliConfig;
|
|
17
|
+
policy: Policy;
|
|
18
|
+
master: MasterKey;
|
|
19
|
+
planePublicKeyPem: string;
|
|
20
|
+
planePrivateKeyPem: string;
|
|
21
|
+
};
|
|
22
|
+
export type InitOptions = {
|
|
23
|
+
port?: number;
|
|
24
|
+
host?: string;
|
|
25
|
+
/** Public URL clients and runners should use to reach the plane. */
|
|
26
|
+
planeUrl?: string;
|
|
27
|
+
};
|
|
28
|
+
export declare function initHome(dir: string, options?: InitOptions): WarrantHome;
|
|
29
|
+
export declare function loadHome(dir: string): WarrantHome;
|
|
30
|
+
export declare function secretStoreFor(home: WarrantHome): SecretStore;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { defaultPolicy, FileKeyProvider, resolveMasterKey, SecretStore } from "@fusionkit/plane";
|
|
5
|
+
/** Defaults for `warrant init`; flags (--port/--host) override them. */
|
|
6
|
+
const DEFAULT_PLANE_PORT = 7172;
|
|
7
|
+
const DEFAULT_PLANE_HOST = "127.0.0.1";
|
|
8
|
+
function masterKeyPath(dir) {
|
|
9
|
+
return join(dir, "master.key");
|
|
10
|
+
}
|
|
11
|
+
function keyProvider(dir, master) {
|
|
12
|
+
return new FileKeyProvider(master, join(dir, "keys", "plane.pub.pem"), join(dir, "keys", "plane.key.enc"));
|
|
13
|
+
}
|
|
14
|
+
export function initHome(dir, options = {}) {
|
|
15
|
+
mkdirSync(join(dir, "keys"), { recursive: true });
|
|
16
|
+
const configPath = join(dir, "config.json");
|
|
17
|
+
if (existsSync(configPath)) {
|
|
18
|
+
throw new Error(`already initialized: ${configPath} exists`);
|
|
19
|
+
}
|
|
20
|
+
const port = options.port ?? DEFAULT_PLANE_PORT;
|
|
21
|
+
const host = options.host ?? DEFAULT_PLANE_HOST;
|
|
22
|
+
const config = {
|
|
23
|
+
version: "warrant.config.v2",
|
|
24
|
+
planeUrl: options.planeUrl ?? `http://${DEFAULT_PLANE_HOST}:${port}`,
|
|
25
|
+
port,
|
|
26
|
+
host,
|
|
27
|
+
adminToken: randomBytes(32).toString("base64url"),
|
|
28
|
+
enrollToken: randomBytes(32).toString("base64url"),
|
|
29
|
+
requestedBy: process.env.USER ?? "operator"
|
|
30
|
+
};
|
|
31
|
+
// Master key: WARRANT_MASTER_KEY if set, otherwise a generated 0600 key
|
|
32
|
+
// file. Config holds no key material, so config alone decrypts nothing.
|
|
33
|
+
const master = resolveMasterKey(masterKeyPath(dir), { createIfMissing: true });
|
|
34
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 0o600 });
|
|
35
|
+
const policy = defaultPolicy();
|
|
36
|
+
writeFileSync(join(dir, "policy.json"), JSON.stringify(policy, null, 2));
|
|
37
|
+
const pair = keyProvider(dir, master).ensure();
|
|
38
|
+
return {
|
|
39
|
+
dir,
|
|
40
|
+
config,
|
|
41
|
+
policy,
|
|
42
|
+
master,
|
|
43
|
+
planePublicKeyPem: pair.publicKeyPem,
|
|
44
|
+
planePrivateKeyPem: pair.privateKeyPem
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
export function loadHome(dir) {
|
|
48
|
+
const configPath = join(dir, "config.json");
|
|
49
|
+
if (!existsSync(configPath)) {
|
|
50
|
+
throw new Error(`not initialized: run "warrant init" first (missing ${configPath})`);
|
|
51
|
+
}
|
|
52
|
+
const config = JSON.parse(readFileSync(configPath, "utf8"));
|
|
53
|
+
if (!config.host)
|
|
54
|
+
config.host = "127.0.0.1";
|
|
55
|
+
const policy = JSON.parse(readFileSync(join(dir, "policy.json"), "utf8"));
|
|
56
|
+
const master = resolveMasterKey(masterKeyPath(dir));
|
|
57
|
+
const pair = keyProvider(dir, master).getOrgKeyPair();
|
|
58
|
+
return {
|
|
59
|
+
dir,
|
|
60
|
+
config,
|
|
61
|
+
policy,
|
|
62
|
+
master,
|
|
63
|
+
planePublicKeyPem: pair.publicKeyPem,
|
|
64
|
+
planePrivateKeyPem: pair.privateKeyPem
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
export function secretStoreFor(home) {
|
|
68
|
+
return new SecretStore(join(home.dir, "secrets.enc"), home.master);
|
|
69
|
+
}
|