celestea-agent 2.7.1
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/dist/args.d.ts +38 -0
- package/dist/args.js +94 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +11 -0
- package/dist/main.d.ts +3 -0
- package/dist/main.js +44 -0
- package/dist/open-browser.d.ts +41 -0
- package/dist/open-browser.js +47 -0
- package/dist/paths.d.ts +21 -0
- package/dist/paths.js +33 -0
- package/dist/sandbox-note.d.ts +17 -0
- package/dist/sandbox-note.js +32 -0
- package/dist/web.d.ts +28 -0
- package/dist/web.js +58 -0
- package/package.json +34 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mcd0LUO
|
|
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/dist/args.d.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `celestea` CLI argument parsing (H) — pure and unit-testable.
|
|
3
|
+
*
|
|
4
|
+
* Supported:
|
|
5
|
+
* celestea web [--port N] [--bind ADDR] [--no-open] [--token SECRET]
|
|
6
|
+
* celestea --version | -v
|
|
7
|
+
* celestea --help | -h
|
|
8
|
+
*
|
|
9
|
+
* Unknown flags/commands are an explicit error (never silently ignored), so a
|
|
10
|
+
* typo cannot start a server on the wrong port.
|
|
11
|
+
*/
|
|
12
|
+
export declare const DEFAULT_PORT = 3777;
|
|
13
|
+
export declare const DEFAULT_BIND = "127.0.0.1";
|
|
14
|
+
export type CliCommand = "web" | "version" | "help";
|
|
15
|
+
export interface WebOptions {
|
|
16
|
+
port: number;
|
|
17
|
+
bind: string;
|
|
18
|
+
open: boolean;
|
|
19
|
+
/** H-security: bearer token; `undefined` = fall back to CELESTEA_AUTH_TOKEN. */
|
|
20
|
+
token?: string;
|
|
21
|
+
}
|
|
22
|
+
export type ParseResult = {
|
|
23
|
+
ok: true;
|
|
24
|
+
command: "web";
|
|
25
|
+
options: WebOptions;
|
|
26
|
+
} | {
|
|
27
|
+
ok: true;
|
|
28
|
+
command: "version";
|
|
29
|
+
} | {
|
|
30
|
+
ok: true;
|
|
31
|
+
command: "help";
|
|
32
|
+
} | {
|
|
33
|
+
ok: false;
|
|
34
|
+
error: string;
|
|
35
|
+
};
|
|
36
|
+
/** The argv after the program name, e.g. `["web", "--port", "8080"]`. */
|
|
37
|
+
export declare function parseArgs(argv: readonly string[]): ParseResult;
|
|
38
|
+
export declare const HELP_TEXT: string;
|
package/dist/args.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `celestea` CLI argument parsing (H) — pure and unit-testable.
|
|
3
|
+
*
|
|
4
|
+
* Supported:
|
|
5
|
+
* celestea web [--port N] [--bind ADDR] [--no-open] [--token SECRET]
|
|
6
|
+
* celestea --version | -v
|
|
7
|
+
* celestea --help | -h
|
|
8
|
+
*
|
|
9
|
+
* Unknown flags/commands are an explicit error (never silently ignored), so a
|
|
10
|
+
* typo cannot start a server on the wrong port.
|
|
11
|
+
*/
|
|
12
|
+
export const DEFAULT_PORT = 3777;
|
|
13
|
+
export const DEFAULT_BIND = "127.0.0.1";
|
|
14
|
+
/** Parse `--port N` / `--port=N`; a non-integer or out-of-range port is an error. */
|
|
15
|
+
function parsePort(raw) {
|
|
16
|
+
const port = Number.parseInt(raw, 10);
|
|
17
|
+
if (!Number.isInteger(port) || port < 0 || port > 65535 || String(port) !== raw.trim()) {
|
|
18
|
+
return { error: `--port must be an integer between 0 and 65535 (got ${JSON.stringify(raw)})` };
|
|
19
|
+
}
|
|
20
|
+
return { port };
|
|
21
|
+
}
|
|
22
|
+
/** The argv after the program name, e.g. `["web", "--port", "8080"]`. */
|
|
23
|
+
export function parseArgs(argv) {
|
|
24
|
+
const rest = [...argv];
|
|
25
|
+
const first = rest.shift();
|
|
26
|
+
if (first === undefined || first === "--help" || first === "-h")
|
|
27
|
+
return { ok: true, command: "help" };
|
|
28
|
+
if (first === "--version" || first === "-v")
|
|
29
|
+
return { ok: true, command: "version" };
|
|
30
|
+
if (first !== "web")
|
|
31
|
+
return { ok: false, error: `unknown command ${JSON.stringify(first)} (expected "web")` };
|
|
32
|
+
const options = { port: DEFAULT_PORT, bind: DEFAULT_BIND, open: true };
|
|
33
|
+
while (rest.length > 0) {
|
|
34
|
+
const flag = rest.shift();
|
|
35
|
+
if (flag === "--no-open") {
|
|
36
|
+
options.open = false;
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (flag === "--port" || flag.startsWith("--port=")) {
|
|
40
|
+
const raw = flag.includes("=") ? flag.slice("--port=".length) : rest.shift();
|
|
41
|
+
if (raw === undefined)
|
|
42
|
+
return { ok: false, error: "--port requires a value" };
|
|
43
|
+
const parsed = parsePort(raw);
|
|
44
|
+
if ("error" in parsed)
|
|
45
|
+
return { ok: false, error: parsed.error };
|
|
46
|
+
options.port = parsed.port;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (flag === "--bind" || flag.startsWith("--bind=")) {
|
|
50
|
+
const raw = flag.includes("=") ? flag.slice("--bind=".length) : rest.shift();
|
|
51
|
+
if (raw === undefined || raw.trim() === "")
|
|
52
|
+
return { ok: false, error: "--bind requires a non-empty value" };
|
|
53
|
+
options.bind = raw.trim();
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (flag === "--token" || flag.startsWith("--token=")) {
|
|
57
|
+
const raw = flag.includes("=") ? flag.slice("--token=".length) : rest.shift();
|
|
58
|
+
if (raw === undefined || raw.trim() === "")
|
|
59
|
+
return { ok: false, error: "--token requires a non-empty value" };
|
|
60
|
+
options.token = raw.trim();
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
return { ok: false, error: `unknown option ${JSON.stringify(flag)}` };
|
|
64
|
+
}
|
|
65
|
+
return { ok: true, command: "web", options };
|
|
66
|
+
}
|
|
67
|
+
export const HELP_TEXT = [
|
|
68
|
+
"celestea — Celestea Studio agent (web UI + HTTP API)",
|
|
69
|
+
"",
|
|
70
|
+
"Usage:",
|
|
71
|
+
" celestea web [--port N] [--bind ADDR] [--no-open] [--token SECRET]",
|
|
72
|
+
" celestea --version",
|
|
73
|
+
" celestea --help",
|
|
74
|
+
"",
|
|
75
|
+
"Options:",
|
|
76
|
+
` --port N HTTP port (default ${DEFAULT_PORT}; 0 = an ephemeral free port)`,
|
|
77
|
+
` --bind ADDR Bind address (default ${DEFAULT_BIND}).`,
|
|
78
|
+
" --no-open Do not open the browser",
|
|
79
|
+
" --token SEC Require this bearer token on every /api/* request except",
|
|
80
|
+
" /api/health (also read from CELESTEA_AUTH_TOKEN).",
|
|
81
|
+
" Browser: open http://<host>:<port>/auth/token?token=<SEC> once",
|
|
82
|
+
" to set the HttpOnly session cookie, then use the UI normally.",
|
|
83
|
+
"",
|
|
84
|
+
"WARNING — a non-loopback --bind (0.0.0.0 / :: / a public IP) is NOT just",
|
|
85
|
+
"'the web UI': it exposes FULL unauthenticated control of this machine:",
|
|
86
|
+
" · POST /api/exec runs arbitrary shell commands as this user;",
|
|
87
|
+
" · GET /api/fs/list reads any directory;",
|
|
88
|
+
" · every agent endpoint (turn, tools, sessions) is reachable.",
|
|
89
|
+
"Such a bind is REFUSED unless a token is configured. Prefer --bind 127.0.0.1",
|
|
90
|
+
"behind nginx (which owns the browser login gate).",
|
|
91
|
+
"",
|
|
92
|
+
"Data root: $CELESTEA_HOME (else $XDG_DATA_HOME/celestea, else ~/.celestea).",
|
|
93
|
+
"Set the model + API key in <data root>/providers.json, or via CELESTEA_API_KEY.",
|
|
94
|
+
].join("\n");
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `celestea-agent` — the published CLI + cross-platform launch helpers (H).
|
|
3
|
+
*
|
|
4
|
+
* The executable is `dist/main.js` (bin: `celestea`); the pure helpers are
|
|
5
|
+
* exported so the platform branches are unit-tested without a real browser.
|
|
6
|
+
*/
|
|
7
|
+
export * from "./args.js";
|
|
8
|
+
export * from "./open-browser.js";
|
|
9
|
+
export * from "./paths.js";
|
|
10
|
+
export * from "./sandbox-note.js";
|
|
11
|
+
export * from "./web.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `celestea-agent` — the published CLI + cross-platform launch helpers (H).
|
|
3
|
+
*
|
|
4
|
+
* The executable is `dist/main.js` (bin: `celestea`); the pure helpers are
|
|
5
|
+
* exported so the platform branches are unit-tested without a real browser.
|
|
6
|
+
*/
|
|
7
|
+
export * from "./args.js";
|
|
8
|
+
export * from "./open-browser.js";
|
|
9
|
+
export * from "./paths.js";
|
|
10
|
+
export * from "./sandbox-note.js";
|
|
11
|
+
export * from "./web.js";
|
package/dist/main.d.ts
ADDED
package/dist/main.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* `celestea` — the published CLI entry (H).
|
|
4
|
+
*
|
|
5
|
+
* `celestea web` boots the studio HTTP server (shared with the source entry via
|
|
6
|
+
* `@celestea/studio`); `--version`/`--help` are pure. A parse error exits 2
|
|
7
|
+
* with the usage line, never a silent start.
|
|
8
|
+
*/
|
|
9
|
+
import { readFileSync } from "node:fs";
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
11
|
+
import { HELP_TEXT, parseArgs } from "./args.js";
|
|
12
|
+
import { runWeb } from "./web.js";
|
|
13
|
+
/** The installed package's own version (a release tag, never a git describe). */
|
|
14
|
+
export function readVersion(from = import.meta.url) {
|
|
15
|
+
try {
|
|
16
|
+
const path = fileURLToPath(new URL("../package.json", from));
|
|
17
|
+
const doc = JSON.parse(readFileSync(path, "utf8"));
|
|
18
|
+
return typeof doc.version === "string" ? doc.version : "0.0.0";
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return "0.0.0";
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
const parsed = parseArgs(process.argv.slice(2));
|
|
25
|
+
if (!parsed.ok) {
|
|
26
|
+
console.error("celestea: " + parsed.error);
|
|
27
|
+
console.error("Run 'celestea --help' for usage.");
|
|
28
|
+
process.exit(2);
|
|
29
|
+
}
|
|
30
|
+
if (parsed.command === "help") {
|
|
31
|
+
console.log(HELP_TEXT);
|
|
32
|
+
}
|
|
33
|
+
else if (parsed.command === "version") {
|
|
34
|
+
console.log(readVersion());
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
try {
|
|
38
|
+
runWeb(parsed.options);
|
|
39
|
+
}
|
|
40
|
+
catch (e) {
|
|
41
|
+
console.error("[celestea] FATAL: " + (e instanceof Error ? e.message : String(e)));
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Open the operator's browser at `url` (H) — the cross-platform seam.
|
|
3
|
+
*
|
|
4
|
+
* Reuses the W885 platform primitives (`isWindows`, `whichInPath`) instead of
|
|
5
|
+
* hard-coding a per-OS branch, and takes `platform` / `env` / `spawn` as
|
|
6
|
+
* arguments so the Windows path is unit-testable on this Linux host (the host
|
|
7
|
+
* is never asked).
|
|
8
|
+
*
|
|
9
|
+
* Command per platform:
|
|
10
|
+
* win32 -> `cmd /d /s /c start "" <url>` (start is a cmd builtin; the empty
|
|
11
|
+
* title argument keeps a quoted URL from being read as a title)
|
|
12
|
+
* darwin -> `open <url>`
|
|
13
|
+
* other -> `xdg-open <url>` (the freedesktop opener; absent on a headless
|
|
14
|
+
* box, which is reported, never a crash)
|
|
15
|
+
*/
|
|
16
|
+
export interface OpenBrowserInput {
|
|
17
|
+
platform?: NodeJS.Platform | string;
|
|
18
|
+
env?: Record<string, string | undefined>;
|
|
19
|
+
/** Injected launcher (tests capture argv; default: `node:child_process`). */
|
|
20
|
+
spawn?: (command: string, args: readonly string[], detached: boolean) => void;
|
|
21
|
+
/** Injected existence check for the opener binary (tests). */
|
|
22
|
+
which?: (bin: string) => string | null;
|
|
23
|
+
}
|
|
24
|
+
export type OpenBrowserResult = {
|
|
25
|
+
opened: true;
|
|
26
|
+
command: string;
|
|
27
|
+
args: readonly string[];
|
|
28
|
+
} | {
|
|
29
|
+
opened: false;
|
|
30
|
+
reason: string;
|
|
31
|
+
};
|
|
32
|
+
/** The opener command + argv for `url` on `platform` (pure). */
|
|
33
|
+
export declare function openerFor(url: string, platform: string): {
|
|
34
|
+
command: string;
|
|
35
|
+
args: readonly string[];
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Open `url`. Never throws: a missing opener (headless Linux, minimal
|
|
39
|
+
* container) returns `{opened:false}` so the caller can print the URL instead.
|
|
40
|
+
*/
|
|
41
|
+
export declare function openBrowser(url: string, input?: OpenBrowserInput): OpenBrowserResult;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Open the operator's browser at `url` (H) — the cross-platform seam.
|
|
3
|
+
*
|
|
4
|
+
* Reuses the W885 platform primitives (`isWindows`, `whichInPath`) instead of
|
|
5
|
+
* hard-coding a per-OS branch, and takes `platform` / `env` / `spawn` as
|
|
6
|
+
* arguments so the Windows path is unit-testable on this Linux host (the host
|
|
7
|
+
* is never asked).
|
|
8
|
+
*
|
|
9
|
+
* Command per platform:
|
|
10
|
+
* win32 -> `cmd /d /s /c start "" <url>` (start is a cmd builtin; the empty
|
|
11
|
+
* title argument keeps a quoted URL from being read as a title)
|
|
12
|
+
* darwin -> `open <url>`
|
|
13
|
+
* other -> `xdg-open <url>` (the freedesktop opener; absent on a headless
|
|
14
|
+
* box, which is reported, never a crash)
|
|
15
|
+
*/
|
|
16
|
+
import { isWindows, whichInPath } from "@celestea/tools";
|
|
17
|
+
/** The opener command + argv for `url` on `platform` (pure). */
|
|
18
|
+
export function openerFor(url, platform) {
|
|
19
|
+
if (isWindows(platform))
|
|
20
|
+
return { command: "cmd", args: ["/d", "/s", "/c", "start", "", url] };
|
|
21
|
+
if (platform === "darwin")
|
|
22
|
+
return { command: "open", args: [url] };
|
|
23
|
+
return { command: "xdg-open", args: [url] };
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Open `url`. Never throws: a missing opener (headless Linux, minimal
|
|
27
|
+
* container) returns `{opened:false}` so the caller can print the URL instead.
|
|
28
|
+
*/
|
|
29
|
+
export function openBrowser(url, input = {}) {
|
|
30
|
+
const platform = input.platform ?? process.platform;
|
|
31
|
+
const env = input.env ?? process.env;
|
|
32
|
+
const { command, args } = openerFor(url, platform);
|
|
33
|
+
const which = input.which ?? ((bin) => whichInPath(bin, platform, env));
|
|
34
|
+
if (which(command) === null)
|
|
35
|
+
return { opened: false, reason: `${command} not found on PATH` };
|
|
36
|
+
const launch = input.spawn ??
|
|
37
|
+
((cmd, argv, detached) => {
|
|
38
|
+
// Lazy import keeps this module pure for the unit tests that inject spawn.
|
|
39
|
+
void import("node:child_process").then(({ spawn }) => {
|
|
40
|
+
const child = spawn(cmd, [...argv], { detached, stdio: "ignore" });
|
|
41
|
+
child.on("error", () => { });
|
|
42
|
+
child.unref();
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
launch(command, args, true);
|
|
46
|
+
return { opened: true, command, args };
|
|
47
|
+
}
|
package/dist/paths.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI path/guidance resolution (H) — the cross-platform data root.
|
|
3
|
+
*
|
|
4
|
+
* The CLI never hard-codes `/var/lib` or a checkout path: the data root comes
|
|
5
|
+
* from `celesteaHome()` (W880: `$CELESTEA_HOME` -> XDG -> `~/.celestea` ->
|
|
6
|
+
* `%USERPROFILE%\.celestea`), and every file under it is derived from there.
|
|
7
|
+
*/
|
|
8
|
+
export interface CliPaths {
|
|
9
|
+
home: string;
|
|
10
|
+
workspacesFile: string;
|
|
11
|
+
providersFile: string;
|
|
12
|
+
promptsFile: string;
|
|
13
|
+
}
|
|
14
|
+
/** Every path the studio reads, rooted at the cross-platform data home. */
|
|
15
|
+
export declare function cliPaths(env?: NodeJS.ProcessEnv, home?: string): CliPaths;
|
|
16
|
+
/**
|
|
17
|
+
* The first-run guidance lines. `hasApiKey` is the secret-free fact the engine
|
|
18
|
+
* view already computed; when it is false the operator gets an actionable
|
|
19
|
+
* message instead of a silent failure at the first turn.
|
|
20
|
+
*/
|
|
21
|
+
export declare function firstRunGuidance(paths: CliPaths, hasApiKey: boolean): string[];
|
package/dist/paths.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI path/guidance resolution (H) — the cross-platform data root.
|
|
3
|
+
*
|
|
4
|
+
* The CLI never hard-codes `/var/lib` or a checkout path: the data root comes
|
|
5
|
+
* from `celesteaHome()` (W880: `$CELESTEA_HOME` -> XDG -> `~/.celestea` ->
|
|
6
|
+
* `%USERPROFILE%\.celestea`), and every file under it is derived from there.
|
|
7
|
+
*/
|
|
8
|
+
import { celesteaHome } from "@celestea/core";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
/** Every path the studio reads, rooted at the cross-platform data home. */
|
|
11
|
+
export function cliPaths(env = process.env, home = celesteaHome({ env })) {
|
|
12
|
+
return {
|
|
13
|
+
home,
|
|
14
|
+
workspacesFile: join(home, "workspaces.json"),
|
|
15
|
+
providersFile: join(home, "providers.json"),
|
|
16
|
+
promptsFile: join(home, "prompts.json"),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The first-run guidance lines. `hasApiKey` is the secret-free fact the engine
|
|
21
|
+
* view already computed; when it is false the operator gets an actionable
|
|
22
|
+
* message instead of a silent failure at the first turn.
|
|
23
|
+
*/
|
|
24
|
+
export function firstRunGuidance(paths, hasApiKey) {
|
|
25
|
+
if (hasApiKey)
|
|
26
|
+
return [];
|
|
27
|
+
return [
|
|
28
|
+
"no model API key is configured — the UI will start, but a turn cannot run yet.",
|
|
29
|
+
` 1. create ${paths.providersFile} (mode 0600) with a provider row, or`,
|
|
30
|
+
" 2. export CELESTEA_API_KEY=<your key> (and optionally CELESTEA_BASE_URL / CELESTEA_MODEL).",
|
|
31
|
+
" See GET /api/providers for the expected shape; the key is never logged.",
|
|
32
|
+
];
|
|
33
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The startup sandbox note (H) — say the security posture out loud.
|
|
3
|
+
*
|
|
4
|
+
* `selectSandboxDetailed` is the SAME policy the engine composes per session, so
|
|
5
|
+
* the CLI reports the provider that will actually run commands. On Windows (no
|
|
6
|
+
* bwrap) or a Linux host without bwrap this is the userspace provider, and the
|
|
7
|
+
* note states the degradation and the `CELESTEA_SANDBOX_FALLBACK` value instead
|
|
8
|
+
* of pretending isolation is on.
|
|
9
|
+
*/
|
|
10
|
+
import { type HostProbe } from "@celestea/tools";
|
|
11
|
+
export interface SandboxNoteInput {
|
|
12
|
+
env?: NodeJS.ProcessEnv;
|
|
13
|
+
/** Injected host probe (tests); default: the memoized real probe. */
|
|
14
|
+
probe?: HostProbe;
|
|
15
|
+
}
|
|
16
|
+
/** One human line describing the sandbox decision (never throws on `fail`). */
|
|
17
|
+
export declare function sandboxStartupNote(input?: SandboxNoteInput): string;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The startup sandbox note (H) — say the security posture out loud.
|
|
3
|
+
*
|
|
4
|
+
* `selectSandboxDetailed` is the SAME policy the engine composes per session, so
|
|
5
|
+
* the CLI reports the provider that will actually run commands. On Windows (no
|
|
6
|
+
* bwrap) or a Linux host without bwrap this is the userspace provider, and the
|
|
7
|
+
* note states the degradation and the `CELESTEA_SANDBOX_FALLBACK` value instead
|
|
8
|
+
* of pretending isolation is on.
|
|
9
|
+
*/
|
|
10
|
+
import { ENV_SANDBOX_FALLBACK, fallbackMode, selectSandboxDetailed } from "@celestea/tools";
|
|
11
|
+
/** One human line describing the sandbox decision (never throws on `fail`). */
|
|
12
|
+
export function sandboxStartupNote(input = {}) {
|
|
13
|
+
const env = input.env ?? process.env;
|
|
14
|
+
let mode;
|
|
15
|
+
try {
|
|
16
|
+
mode = fallbackMode(env);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return `sandbox: invalid ${ENV_SANDBOX_FALLBACK} — refusing to guess (see the docs)`;
|
|
20
|
+
}
|
|
21
|
+
let selection;
|
|
22
|
+
try {
|
|
23
|
+
selection = selectSandboxDetailed({ env, ...(input.probe === undefined ? {} : { probe: input.probe }) });
|
|
24
|
+
}
|
|
25
|
+
catch (e) {
|
|
26
|
+
return `sandbox: refusing to execute — ${e instanceof Error ? e.message : String(e)}`;
|
|
27
|
+
}
|
|
28
|
+
if (selection.provider === "bwrap" && !selection.degraded) {
|
|
29
|
+
return "sandbox: bwrap (namespaces + private tmp; network isolated unless granted)";
|
|
30
|
+
}
|
|
31
|
+
return `sandbox: DEGRADED to userspace — ${selection.reason ?? "bwrap unavailable"} (${ENV_SANDBOX_FALLBACK}=${mode})`;
|
|
32
|
+
}
|
package/dist/web.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `celestea web` (H) — start the studio and (by default) open the browser.
|
|
3
|
+
*
|
|
4
|
+
* The HTTP server itself is `startStudioServer` from `@celestea/studio`, so
|
|
5
|
+
* this command owns only: the data-root paths, the startup banner (sandbox +
|
|
6
|
+
* first-run guidance), the browser hand-off, and the signal wiring.
|
|
7
|
+
*/
|
|
8
|
+
import { type StudioServerHandle, type StudioServerOptions } from "@celestea/studio";
|
|
9
|
+
import type { WebOptions } from "./args.js";
|
|
10
|
+
import { openBrowser } from "./open-browser.js";
|
|
11
|
+
export interface RunWebDeps {
|
|
12
|
+
env?: NodeJS.ProcessEnv;
|
|
13
|
+
/** Injected server start (tests); default: the real `startStudioServer`. */
|
|
14
|
+
start?: (options: StudioServerOptions) => StudioServerHandle;
|
|
15
|
+
/** Injected opener (tests); default: the real `openBrowser`. */
|
|
16
|
+
open?: typeof openBrowser;
|
|
17
|
+
/** Injected contract gate (tests); default: the real one. */
|
|
18
|
+
verify?: () => void;
|
|
19
|
+
/** Injected signal registrar (tests); default: `process.on`. */
|
|
20
|
+
onSignal?: (signal: NodeJS.Signals, handler: () => void) => void;
|
|
21
|
+
}
|
|
22
|
+
export interface RunWebResult {
|
|
23
|
+
handle: StudioServerHandle;
|
|
24
|
+
url: string;
|
|
25
|
+
guidance: string[];
|
|
26
|
+
}
|
|
27
|
+
/** Start the server; returns the handle plus the lines already printed. */
|
|
28
|
+
export declare function runWeb(options: WebOptions, deps?: RunWebDeps): RunWebResult;
|
package/dist/web.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `celestea web` (H) — start the studio and (by default) open the browser.
|
|
3
|
+
*
|
|
4
|
+
* The HTTP server itself is `startStudioServer` from `@celestea/studio`, so
|
|
5
|
+
* this command owns only: the data-root paths, the startup banner (sandbox +
|
|
6
|
+
* first-run guidance), the browser hand-off, and the signal wiring.
|
|
7
|
+
*/
|
|
8
|
+
import { verifyContractsAtStartup } from "@celestea/core";
|
|
9
|
+
import { isLoopbackBind, loadStudioConfig, startStudioServer } from "@celestea/studio";
|
|
10
|
+
import { openBrowser } from "./open-browser.js";
|
|
11
|
+
import { cliPaths, firstRunGuidance } from "./paths.js";
|
|
12
|
+
import { sandboxStartupNote } from "./sandbox-note.js";
|
|
13
|
+
/** Start the server; returns the handle plus the lines already printed. */
|
|
14
|
+
export function runWeb(options, deps = {}) {
|
|
15
|
+
const env = deps.env ?? process.env;
|
|
16
|
+
const paths = cliPaths(env);
|
|
17
|
+
const verify = deps.verify ?? verifyContractsAtStartup;
|
|
18
|
+
verify();
|
|
19
|
+
const config = loadStudioConfig({
|
|
20
|
+
env,
|
|
21
|
+
paths: { workspacesFile: paths.workspacesFile, providersFile: paths.providersFile, promptsFile: paths.promptsFile },
|
|
22
|
+
// H-security: --token wins; else CELESTEA_AUTH_TOKEN. A non-loopback bind
|
|
23
|
+
// with neither is refused inside startStudioServer (never a silent hole).
|
|
24
|
+
...(options.token === undefined ? {} : { authToken: options.token }),
|
|
25
|
+
});
|
|
26
|
+
const start = deps.start ?? startStudioServer;
|
|
27
|
+
const handle = start({ port: options.port, hostname: options.bind, config, env });
|
|
28
|
+
const url = `http://${options.bind === "0.0.0.0" ? "127.0.0.1" : options.bind}:${handle.port}/`;
|
|
29
|
+
console.log("[celestea] data root: " + paths.home);
|
|
30
|
+
console.log("[celestea] " + sandboxStartupNote({ env }));
|
|
31
|
+
if (config.authToken !== null) {
|
|
32
|
+
console.log("[celestea] api token: required on every /api/* request except /api/health");
|
|
33
|
+
// Never echo the secret: print the bootstrap URL with a placeholder.
|
|
34
|
+
const shown = options.bind === "0.0.0.0" || options.bind === "::" ? "127.0.0.1" : options.bind;
|
|
35
|
+
console.log(`[celestea] browser: open http://${shown}:${handle.port}/auth/token?token=<your-token> once to sign in`);
|
|
36
|
+
if (!isLoopbackBind(options.bind)) {
|
|
37
|
+
console.log("[celestea] WARNING: non-loopback + plain HTTP — the token and its cookie travel in cleartext; put TLS (nginx) in front for anything beyond a trusted network");
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
// The composed engine injects the providers.json key into this env in memory;
|
|
41
|
+
// reading the env AFTER start therefore reflects both sources, without ever
|
|
42
|
+
// printing the secret.
|
|
43
|
+
const hasApiKey = (env[config.apiKeyEnv] ?? "").trim() !== "";
|
|
44
|
+
const guidance = firstRunGuidance(paths, hasApiKey);
|
|
45
|
+
for (const line of guidance)
|
|
46
|
+
console.log("[celestea] " + line);
|
|
47
|
+
if (options.open) {
|
|
48
|
+
const opened = (deps.open ?? openBrowser)(url);
|
|
49
|
+
console.log(opened.opened ? `[celestea] opened ${url}` : `[celestea] could not open a browser (${opened.reason}); open ${url}`);
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
console.log("[celestea] open " + url);
|
|
53
|
+
}
|
|
54
|
+
const onSignal = deps.onSignal ?? ((signal, handler) => void process.on(signal, handler));
|
|
55
|
+
for (const signal of ["SIGINT", "SIGTERM"])
|
|
56
|
+
onSignal(signal, () => void handle.stop(signal));
|
|
57
|
+
return { handle, url, guidance };
|
|
58
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "celestea-agent",
|
|
3
|
+
"version": "2.7.1",
|
|
4
|
+
"private": false,
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"description": "Celestea Studio agent — web UI + HTTP API, one command",
|
|
8
|
+
"bin": {
|
|
9
|
+
"celestea": "./dist/main.js"
|
|
10
|
+
},
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"default": "./dist/index.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"main": "./dist/index.js",
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"files": [
|
|
20
|
+
"dist"
|
|
21
|
+
],
|
|
22
|
+
"publishConfig": {
|
|
23
|
+
"access": "public"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@celestea/studio": "2.7.1",
|
|
27
|
+
"@celestea/core": "2.7.1",
|
|
28
|
+
"@celestea/tools": "2.7.1"
|
|
29
|
+
},
|
|
30
|
+
"scripts": {
|
|
31
|
+
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
32
|
+
"build": "tsc -p tsconfig.build.json && node ../../scripts/make-bin-executable.mjs dist/main.js"
|
|
33
|
+
}
|
|
34
|
+
}
|