@qilin-agent/cli 0.0.0-stage → 3.1.4

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.
@@ -0,0 +1,252 @@
1
+ import { a as isTenantIdentity } from "./tenant-runtime-ChD53FKq.js";
2
+ import { resolveQilinHome } from "@qilin-agent/home-paths";
3
+ import { mkdir, rm, stat } from "node:fs/promises";
4
+ import { dirname, join, resolve } from "node:path";
5
+ import { createInterface } from "node:readline/promises";
6
+ import { Writable } from "node:stream";
7
+ import { AccountStore, MIN_PASSWORD_LENGTH, accountsFilePath, isValidEmail, isValidUsername, normalizeEmail, normalizeUsername } from "@qilin-agent/accounts-local";
8
+ //#region lib/types/tenants-roster.js
9
+ /**
10
+ * `qilin tenants` roster operations: the account file every tenant runtime on
11
+ * this Harness home shares, and the tenant directory each account owns.
12
+ *
13
+ * A tenant runtime admits exactly the account it serves, so a new account
14
+ * cannot be created through a tenant's own Web surface (its registration is
15
+ * closed). These commands are how a deployment adds, re-keys, and retires the
16
+ * accounts its tenant runtimes serve.
17
+ * @module @qilin-agent/cli/tenants-roster
18
+ */
19
+ /** Absolute tenant directory one account owns. */
20
+ function tenantDirectory(home, accountId) {
21
+ return join(home, "tenants", accountId);
22
+ }
23
+ /** Whether a path exists. */
24
+ async function exists(path) {
25
+ return await stat(path).then(() => true, () => false);
26
+ }
27
+ /** Open the shared home's roster. */
28
+ async function roster(home) {
29
+ return await AccountStore.open(accountsFilePath(home));
30
+ }
31
+ /**
32
+ * Every account of one shared home with the tenant directory it owns.
33
+ * @param home - absolute shared Harness home.
34
+ * @returns one entry per account, oldest first.
35
+ */
36
+ async function rosterEntries(home) {
37
+ const store = await roster(home);
38
+ return await Promise.all(store.all().map(async (account) => ({
39
+ id: account.id,
40
+ username: account.username,
41
+ email: account.email,
42
+ createdAt: account.createdAt,
43
+ tenantDirectory: tenantDirectory(home, account.id),
44
+ tenantDataPresent: await exists(tenantDirectory(home, account.id))
45
+ })));
46
+ }
47
+ /**
48
+ * Create one account in the shared roster.
49
+ * @param home - absolute shared Harness home.
50
+ * @param request - the login name, address, password, and administrator seat.
51
+ * @returns the created account.
52
+ * @throws Error when the name or address is refused or already taken, or when
53
+ * the deployment already has an administrator.
54
+ */
55
+ async function addAccount(home, request) {
56
+ const username = normalizeUsername(request.username);
57
+ if (!isValidUsername(username)) throw new Error(`qilin tenants: '${request.username}' is not an acceptable username: 3-32 characters of a-z, 0-9, '_', '.', or '-', starting with a letter or digit`);
58
+ if (request.password.length < MIN_PASSWORD_LENGTH) throw new Error(`qilin tenants: a password needs at least ${String(MIN_PASSWORD_LENGTH)} characters`);
59
+ const store = await roster(home);
60
+ if (store.byUsername(username) !== void 0) throw new Error(`qilin tenants: the account ${username} already exists`);
61
+ let email = null;
62
+ if (request.email !== void 0 && request.email.trim() !== "") {
63
+ email = normalizeEmail(request.email);
64
+ if (!isValidEmail(email)) throw new Error(`qilin tenants: '${request.email}' is not an acceptable email address`);
65
+ if (store.byEmail(email) !== void 0) throw new Error(`qilin tenants: the address ${email} already belongs to an account`);
66
+ }
67
+ return await store.add({
68
+ username,
69
+ email
70
+ }, request.password, { admin: request.admin === true });
71
+ }
72
+ /**
73
+ * Replace one account's password, retiring the sessions the previous one issued.
74
+ * @param home - absolute shared Harness home.
75
+ * @param username - the account's login name, normalized before lookup.
76
+ * @param password - the replacement password.
77
+ * @returns the stored successor.
78
+ * @throws Error when no account has that name or the password is too short.
79
+ */
80
+ async function setAccountPassword(home, username, password) {
81
+ if (password.length < MIN_PASSWORD_LENGTH) throw new Error(`qilin tenants: a password needs at least ${String(MIN_PASSWORD_LENGTH)} characters`);
82
+ const store = await roster(home);
83
+ const account = store.byUsername(normalizeUsername(username));
84
+ if (account === void 0) throw new Error(`qilin tenants: no account is named ${normalizeUsername(username)}`);
85
+ return await store.setPassword(account.id, password);
86
+ }
87
+ /**
88
+ * Remove one account from the shared roster.
89
+ *
90
+ * A running tenant runtime keeps serving the account until it is reclaimed or
91
+ * restarted; a runtime started afterwards fails loud, because the overlay
92
+ * admits an account the roster no longer holds.
93
+ * @param home - absolute shared Harness home.
94
+ * @param username - the account's login name, normalized before lookup.
95
+ * @param purgeData - also remove that account's tenant directory.
96
+ * @returns the removed account and, when asked, the removed directory.
97
+ * @throws Error when no account has that name, or when the tenant directory is not where it belongs.
98
+ */
99
+ async function removeAccount(home, username, purgeData) {
100
+ const store = await roster(home);
101
+ const account = store.byUsername(normalizeUsername(username));
102
+ if (account === void 0) throw new Error(`qilin tenants: no account is named ${normalizeUsername(username)}`);
103
+ const directory = tenantDirectory(home, account.id);
104
+ const resolved = resolve(directory);
105
+ if (purgeData && (!isTenantIdentity(account.id) || dirname(resolved) !== resolve(join(home, "tenants")))) throw new Error(`qilin tenants: refusing to remove ${resolved}, which is not this account's tenant directory`);
106
+ const removed = await store.remove(account.id);
107
+ if (!purgeData || !await exists(directory)) return {
108
+ account: removed,
109
+ purgedDirectory: void 0
110
+ };
111
+ await rm(directory, {
112
+ recursive: true,
113
+ force: true
114
+ });
115
+ return {
116
+ account: removed,
117
+ purgedDirectory: directory
118
+ };
119
+ }
120
+ /**
121
+ * Read one password from standard input without echoing it.
122
+ * @param prompt - the text shown on standard error before reading.
123
+ * @returns the entered line, without its terminator.
124
+ * @throws Error when standard input is not a terminal, so an unattended run cannot wait for input.
125
+ */
126
+ async function promptPassword(prompt) {
127
+ if (!process.stdin.isTTY) throw new Error("qilin tenants: standard input is not a terminal; pipe the password in and pass --password-stdin");
128
+ const silent = new Writable({ write(_chunk, _encoding, done) {
129
+ done();
130
+ } });
131
+ const prompt_ = createInterface({
132
+ input: process.stdin,
133
+ output: silent,
134
+ terminal: true
135
+ });
136
+ try {
137
+ process.stderr.write(prompt);
138
+ const entered = await prompt_.question("");
139
+ process.stderr.write("\n");
140
+ return entered;
141
+ } finally {
142
+ prompt_.close();
143
+ }
144
+ }
145
+ /**
146
+ * Read the first line of standard input.
147
+ * @returns the line, without its terminator.
148
+ */
149
+ async function readStandardInput() {
150
+ const chunks = [];
151
+ for await (const chunk of process.stdin) chunks.push(Buffer.from(chunk));
152
+ return (Buffer.concat(chunks).toString("utf8").split("\n")[0] ?? "").replace(/\r$/u, "");
153
+ }
154
+ /**
155
+ * Read the password one mutation applies.
156
+ * @param source - whether to prompt or read standard input.
157
+ * @param label - the account being changed, named in the confirmation prompt.
158
+ * @returns the password.
159
+ * @throws Error when an interactive entry is not confirmed, or when standard input is a terminal without a prompt.
160
+ */
161
+ async function readPassword(source, label) {
162
+ if (source.kind === "stdin") return await readStandardInput();
163
+ const entered = await promptPassword(`Password for ${label}: `);
164
+ if (entered !== await promptPassword("Repeat password: ")) throw new Error("qilin tenants: the two passwords differ");
165
+ return entered;
166
+ }
167
+ /** One roster row as the table prints it. */
168
+ function formatRow(entry) {
169
+ const created = new Date(entry.createdAt).toISOString().slice(0, 10);
170
+ const data = entry.tenantDataPresent ? "yes" : "no";
171
+ return `${entry.username.padEnd(20)} ${entry.id} ${created} ${data.padEnd(3)} ${entry.email ?? "-"}`;
172
+ }
173
+ /**
174
+ * Print the roster of one shared home.
175
+ * @param home - absolute shared Harness home.
176
+ * @param write - where each line goes.
177
+ * @returns the printed entries.
178
+ */
179
+ async function printRoster(home, write) {
180
+ const entries = await rosterEntries(home);
181
+ if (entries.length === 0) {
182
+ write(`qilin tenants: ${home} has no accounts yet; create one with: qilin tenants add <username>`);
183
+ return entries;
184
+ }
185
+ write("USERNAME ID CREATED DATA EMAIL");
186
+ for (const entry of entries) write(formatRow(entry));
187
+ write(`qilin tenants: ${String(entries.length)} account(s) in ${accountsFilePath(home)}`);
188
+ return entries;
189
+ }
190
+ /**
191
+ * Create the tenant directory one account owns, so its first runtime starts in
192
+ * a directory that exists even before a browser visits it.
193
+ * @param home - absolute shared Harness home.
194
+ * @param accountId - the account whose directory to create.
195
+ * @returns the absolute directory.
196
+ */
197
+ async function ensureTenantDirectory(home, accountId) {
198
+ const directory = tenantDirectory(home, accountId);
199
+ await mkdir(directory, { recursive: true });
200
+ return directory;
201
+ }
202
+ /**
203
+ * Run one roster subcommand over this Harness home and print what it did.
204
+ *
205
+ * Failures are reported as a message and a non-zero exit code rather than a
206
+ * stack trace: every refusal here is something the operator can correct.
207
+ * @param request - the roster subcommand and its arguments.
208
+ * @returns the process exit code: 0 when the request was carried out, 1 when it was refused.
209
+ */
210
+ async function runTenantsRoster(request) {
211
+ const home = resolveQilinHome();
212
+ try {
213
+ switch (request.command) {
214
+ case "accounts":
215
+ await printRoster(home, (line) => {
216
+ process.stdout.write(`${line}\n`);
217
+ });
218
+ return 0;
219
+ case "add": {
220
+ const password = await readPassword(request.password, request.username);
221
+ const account = await addAccount(home, {
222
+ username: request.username,
223
+ email: request.email,
224
+ password,
225
+ admin: request.admin
226
+ });
227
+ const directory = await ensureTenantDirectory(home, account.id);
228
+ process.stdout.write(`qilin tenants: created ${account.username} (${account.id}); its tenant directory is ${directory}\n`);
229
+ if (account.admin === true) process.stdout.write(`qilin tenants: ${account.username} is this deployment's administrator and reads every account's usage ledger\n`);
230
+ return 0;
231
+ }
232
+ case "passwd": {
233
+ const password = await readPassword(request.password, request.username);
234
+ const account = await setAccountPassword(home, request.username, password);
235
+ process.stdout.write(`qilin tenants: replaced the password of ${account.username}; sessions it issued no longer verify\n`);
236
+ return 0;
237
+ }
238
+ case "remove": {
239
+ const removed = await removeAccount(home, request.username, request.purgeData);
240
+ const kept = removed.purgedDirectory === void 0 ? `its Sessions stay in ${tenantDirectory(home, removed.account.id)}` : `its tenant directory ${removed.purgedDirectory} is gone`;
241
+ process.stdout.write(`qilin tenants: retired ${removed.account.username} (${removed.account.id}); ${kept}\n`);
242
+ return 0;
243
+ }
244
+ default: throw new Error(`qilin tenants: unhandled roster subcommand ${JSON.stringify(request)}`);
245
+ }
246
+ } catch (error) {
247
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
248
+ return 1;
249
+ }
250
+ }
251
+ //#endregion
252
+ export { runTenantsRoster };
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Commander adapter for the `qilin` command line.
3
+ *
4
+ * The launcher parses only what it owns — which profile to boot, which extra
5
+ * patch overlays to apply, and the config dumps — and hands **everything after
6
+ * its own flags** to the booted tree verbatim, where injected app plugins parse
7
+ * their own flag families and print their own `--help` (see
8
+ * `@qilin-agent/cmdline`). Launcher flags therefore come first: the first
9
+ * token this parser does not recognize starts the inner arguments, so
10
+ * `qilin --profile tui --resume abc` boots the tui profile with `--resume abc`,
11
+ * and `qilin --profile web -h` prints the web app's help, not this one's.
12
+ *
13
+ * `qilin <name>` abbreviates `qilin --profile <name>`; `plugin` manages a profile's
14
+ * plugin dependencies by forwarding to pnpm.
15
+ * @module @qilin-agent/cli/args
16
+ */
17
+ import type { RosterRequest } from './tenants-roster.ts';
18
+ /** Boot a named profile and hand it the invocation's inner arguments. */
19
+ interface ProfileInvocation {
20
+ mode: 'profile';
21
+ profile: string;
22
+ /** Shipped template used once to initialize a missing profile. */
23
+ fromDefaultProfile?: string | undefined;
24
+ /** Extra patch-list overlays applied after the profile's own layer, in argv order. */
25
+ patches: string[];
26
+ /** Everything after the launcher's own flags, verbatim, for injected app plugins. */
27
+ args: string[];
28
+ }
29
+ /** Print a composed profile tree and exit without booting. */
30
+ interface DumpConfigInvocation {
31
+ mode: 'dump-config';
32
+ profile: string;
33
+ /** Shipped template used once to initialize a missing profile. */
34
+ fromDefaultProfile?: string | undefined;
35
+ /** Omit the profile's user layer and --patch overlays; print bundle layers only. */
36
+ defaultOnly: boolean;
37
+ patches: string[];
38
+ }
39
+ /** Manage a profile's plugins: forward `args` to pnpm inside the profile directory. */
40
+ interface PluginInvocation {
41
+ mode: 'plugin';
42
+ profile: string;
43
+ /** Raw pnpm arguments, verbatim. */
44
+ args: string[];
45
+ }
46
+ /** What one `qilin tenants` invocation asks for. */
47
+ type TenantsRequest = RosterRequest;
48
+ /** Serve one isolated Web runtime per account, or manage this home's roster. */
49
+ interface TenantsInvocation {
50
+ mode: 'tenants';
51
+ /** The validated subcommand and the options it takes. */
52
+ request: TenantsRequest;
53
+ }
54
+ /** The resolved `qilin` invocation. Help, version, and errors exit inside {@link parseQilinArgs}. */
55
+ export type QilinInvocation = ProfileInvocation | DumpConfigInvocation | PluginInvocation | TenantsInvocation;
56
+ /**
57
+ * Resolve argv into one invocation, or print and exit for help, version, or an
58
+ * error.
59
+ * @param argv - arguments after the Node binary and script.
60
+ * @param version - version string printed by `--version`.
61
+ * @returns the resolved invocation.
62
+ */
63
+ export declare function parseQilinArgs(argv: readonly string[], version: string): QilinInvocation;
64
+ export {};
65
+ //# sourceMappingURL=args.d.ts.map
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Command-line entry for qilin.
4
+ * @module @qilin-agent/cli/bin
5
+ */
6
+ /**
7
+ * Run the public qilin command-line interface.
8
+ * @returns a promise that settles when the selected command mode finishes.
9
+ */
10
+ export declare function runCli(): Promise<void>;
11
+ //# sourceMappingURL=bin.d.ts.map
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Config-dump entry for `qilin --profile <name> --dump-config`: compose the
3
+ * profile's patch layers through the include plugin's patch algorithm without
4
+ * booting or evaluating `!!js`, with one source layer per bundle, the
5
+ * profile's own patch file, and each `--patch` overlay.
6
+ * @module @qilin-agent/cli/dump-config
7
+ */
8
+ /**
9
+ * Print a profile composition with comments naming each source file and patch layer.
10
+ * @param profile - the profile name.
11
+ * @param defaultOnly - omit the profile's user layer and `--patch` overlays
12
+ * (the recovery diagnostic for a broken `cordis.patch.yml`, which is then
13
+ * never parsed).
14
+ * @param patches - `--patch` overlay paths, in argv order.
15
+ * @param fromDefaultProfile - shipped template used once to initialize a missing profile.
16
+ */
17
+ export declare function runDumpConfig(profile: string, defaultOnly: boolean, patches: readonly string[], fromDefaultProfile?: string): void;
18
+ //# sourceMappingURL=dump-config.d.ts.map
@@ -0,0 +1,29 @@
1
+ /**
2
+ * `qilin plugin [--profile <name>] <args...>` — profile plugin management.
3
+ *
4
+ * `list` and `doctor` are the launcher's own verbs and never mutate anything:
5
+ * `list` prints the profile's bundle layers, and `doctor` reports one plugin
6
+ * package's DSH-era compatibility. `version-exemptions`, `allow-version`, and
7
+ * `revoke-version` read and write the profile's own `compatibility.json` under
8
+ * its file lock and never become pnpm arguments. Every other argument list
9
+ * forwards to pnpm in the profile directory: refuse the packages it names that
10
+ * the running runtime version rejects, initialize the profile on first use, run
11
+ * `pnpm <args...>`, then reconcile the `qilin.profile.bundles` layer list
12
+ * against the installed state (a dependency resolving to a package that
13
+ * declares `qilin.bundle` joins the layer stack; a removed or bundle-less
14
+ * dependency leaves it). Reconciling by installed state, not by dependency
15
+ * diff, means `update` activates a package that gained its `qilin.bundle`
16
+ * declaration in a newer version. A profile that installed an upstream DSH-era
17
+ * engine package is refused before any layer changes, with the removal command
18
+ * in the diagnostic.
19
+ * @module @qilin-agent/cli/plugin
20
+ */
21
+ /**
22
+ * Run one `qilin plugin` invocation: report or list, grant or revoke one
23
+ * exact-version exemption, or forward to pnpm and reconcile.
24
+ * @param profile - the profile name.
25
+ * @param args - the subcommand and its arguments, or pnpm arguments with relative path specs anchored to the invoking directory.
26
+ * @returns the process exit code, or 1 when a report, an exemption, or the reconcile refused the profile.
27
+ */
28
+ export declare function runPlugin(profile: string, args: readonly string[]): Promise<number>;
29
+ //# sourceMappingURL=plugin.d.ts.map
@@ -0,0 +1,20 @@
1
+ /** Bounded, escalating process shutdown for the long-lived CLI surfaces. */
2
+ /** Maximum grace allowed for the application tree to dispose before process exit. */
3
+ export declare const PROCESS_SHUTDOWN_TIMEOUT_MS = 5000;
4
+ /** Process-exit controller shared by normal completion and Unix signal handlers. */
5
+ export interface ProcessShutdown {
6
+ /** Start or join graceful disposal before allowing natural completion with `code`. */
7
+ shutdown(code: number): Promise<void>;
8
+ /** Start graceful disposal followed by exit, or force exit when shutdown is already running. */
9
+ interrupt(code: number): void;
10
+ }
11
+ /**
12
+ * Create one process-exit controller around an application disposer.
13
+ * @param dispose - Whole-application teardown that resolves at quiescence.
14
+ * @param forceExit - Function that exits the process immediately, replaceable by tests.
15
+ * @param complete - Function that records the natural completion code, replaceable by tests.
16
+ * @param timeoutMs - Grace before forced exit, replaceable by tests.
17
+ * @returns A controller whose normal calls coalesce and whose repeated signal call escalates.
18
+ */
19
+ export declare function createProcessShutdown(dispose: () => Promise<void>, forceExit?: (code: number) => void, complete?: (code: number) => void, timeoutMs?: number): ProcessShutdown;
20
+ //# sourceMappingURL=process-shutdown.d.ts.map
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Shared profile boot for every `qilin` surface: resolve the profile, stack its
3
+ * patch layers (bundle layers in `qilin.profile.bundles` order, the profile's
4
+ * own `cordis.patch.yml`, `--patch` overlays, the telemetry switch), mount the
5
+ * tree over the profile's empty root config, and wire fail-loud plus bounded shutdown.
6
+ *
7
+ * App flags are not the launcher's business: the invocation's inner arguments
8
+ * are provided to the tree through `ctx.cmdlineArgs`, where any injected app
9
+ * plugin may read the same immutable snapshot.
10
+ * @module @qilin-agent/cli/profile-boot
11
+ */
12
+ import { type Context } from '@qilin-agent/kylin';
13
+ import { type ProfileContext, type Profile, type ProfileResolutionMode } from '@qilin-agent/app-boot';
14
+ import { type LaunchEnvironmentSnapshot } from '@qilin-agent/launch-environment';
15
+ import { type ProcessShutdown } from './process-shutdown.ts';
16
+ /**
17
+ * The home-level user patch layer (`$QILIN_HOME/cordis.patch.yml`), applied
18
+ * over every profile's own layer. Resolved per call, not at module load:
19
+ * `$QILIN_HOME` may be set by the test or launcher after import.
20
+ * @returns the absolute patch-file path.
21
+ */
22
+ export declare function homePatchPath(): string;
23
+ /** Absolute path of this qilin installation's package.json (both anchors: src/ and lib/ sit one level under apps/cli). */
24
+ export declare const INSTALL_ANCHOR: string;
25
+ /** Root config filename inside a profile directory. */
26
+ export declare const PROFILE_ROOT_FILENAME = "cordis.yml";
27
+ /**
28
+ * Initialize a missing profile from one shipped template. This copies only
29
+ * the template's bundle list; local state from the
30
+ * same-named shipped profile is not read, and no inheritance metadata is
31
+ * persisted. Shipped profile names are reserved, and the target directory is
32
+ * claimed exclusively so existing or concurrent state is never reused.
33
+ * @param name - the new profile name.
34
+ * @param fromDefaultProfile - shipped profile template to copy.
35
+ * @param home - Harness home containing the profile directory.
36
+ * @throws when the template is unknown, the target name is shipped, or the target directory exists.
37
+ */
38
+ export declare function initializeProfileFromDefault(name: string, fromDefaultProfile: string, home?: string): void;
39
+ /**
40
+ * Load a resolved profile for `name` and (re)write the empty root config. The
41
+ * root is always rewritten: the whole composition is patch layers, and the
42
+ * vendored Loader's tree write-back (a plugin self-disposing persists the
43
+ * current tree) can bake composed rows into this file — which would duplicate
44
+ * every bundle insert on the next boot. The file exists on disk only because
45
+ * the Loader needs a real include root to anchor `baseUrl` at the profile
46
+ * directory (the config dump anchors on the same file, so both compose over
47
+ * the identical base).
48
+ * @param name - the profile name.
49
+ * @param userLayer - `false` skips parsing `cordis.patch.yml` (the default dump).
50
+ * @param fromDefaultProfile - shipped template used once to initialize a missing profile.
51
+ * @returns the loaded profile.
52
+ * @throws when explicit initialization names an unknown template or an existing profile.
53
+ */
54
+ export declare function prepareProfile(name: string, userLayer?: boolean, fromDefaultProfile?: string): Profile;
55
+ /** An application-owned profile and its independent installation fallback. */
56
+ export interface ResolvedProfileRuntime {
57
+ /** Profile already loaded from the application's own directory. */
58
+ profile: Profile;
59
+ /** Absolute package.json path of the application's qilin installation. */
60
+ installAnchor: string;
61
+ }
62
+ /** Options for {@link runProfile}. */
63
+ export interface RunProfileOptions {
64
+ /** This run's frozen environment snapshot, provided before any entry mounts. */
65
+ environment: LaunchEnvironmentSnapshot;
66
+ /** The profile name to boot. */
67
+ profile: string;
68
+ /** Loaded application profile; bypasses named profile initialization when supplied. */
69
+ resolvedProfile?: ResolvedProfileRuntime | undefined;
70
+ /** Shipped template used once to initialize a missing profile. */
71
+ fromDefaultProfile?: string | undefined;
72
+ /** `--patch` overlay paths, in argv order. */
73
+ patchFiles: readonly string[];
74
+ /** The invocation's inner arguments, handed to the tree through `ctx.cmdlineArgs`. */
75
+ args: readonly string[];
76
+ /** Application-owned package runtime, scoped to plugin package operations. */
77
+ packageManager?: ProfileContext['packageManager'];
78
+ /** Module fallback backend; defaults to runtime. Plain Node callers may override it; pkg executables always use runtime. */
79
+ resolutionMode?: ProfileResolutionMode;
80
+ }
81
+ /**
82
+ * Boot one profile invocation end to end and leave process lifetime to the
83
+ * mounted plugins (or to a one-shot runner the composition mounts).
84
+ * @param options - environment snapshot, profile name, overlays, and the booted app's own arguments.
85
+ * @returns the settled root context and the shutdown controller.
86
+ * @throws after disposing startup resources; cleanup failures retain the original error.
87
+ */
88
+ export declare function runProfile(options: RunProfileOptions): Promise<{
89
+ ctx: Context;
90
+ shutdown: ProcessShutdown;
91
+ }>;
92
+ //# sourceMappingURL=profile-boot.d.ts.map
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Host fallback for the Web Scheduler API global.
3
+ * @module @qilin-agent/cli/scheduler-yield
4
+ */
5
+ /**
6
+ * Install `globalThis.scheduler.yield` unless the host already defines a
7
+ * `scheduler`.
8
+ *
9
+ * The fallback resolves on the next `setImmediate` — the API's "yield to the
10
+ * event loop once, then continue" semantic — and a host-provided `scheduler`
11
+ * is left untouched. Call it before importing anything that may read the
12
+ * global; `bin.ts` calls it at the top of the entry, ahead of every mode
13
+ * module. Rationale: [the host-shim Agent
14
+ * Note](../../../.agents/notes/implemented/architecture/2026-10-07-web-scheduler-yield-host-shim.md).
15
+ *
16
+ * @returns whether this call installed the fallback.
17
+ */
18
+ export declare function installSchedulerYield(): boolean;
19
+ //# sourceMappingURL=scheduler-yield.d.ts.map
@@ -0,0 +1,19 @@
1
+ /** Save original startup diagnostics while keeping the terminal report concise. */
2
+ import type { StartupError } from '@qilin-agent/app-boot';
3
+ /** Launcher-owned context; no environment values or plugin configurations are collected. */
4
+ interface StartupDiagnosticContext {
5
+ home: string;
6
+ version: string;
7
+ profile: string;
8
+ }
9
+ /**
10
+ * Print the startup summary and save a private, uniquely named report under QILIN_HOME/logs.
11
+ * Failed writes print the complete report to stderr instead of claiming a saved path.
12
+ * @param error - startup audit failure retaining plugin metadata and original errors.
13
+ * @param context - resolved Harness home, application version, and selected profile.
14
+ * @param write - terminal output sink; awaited before returning, defaults to stderr.
15
+ * @returns after saving or printing the report and completing terminal writes.
16
+ */
17
+ export declare function reportStartupFailure(error: StartupError, context: StartupDiagnosticContext, write?: (text: string) => void | Promise<void>): Promise<void>;
18
+ export {};
19
+ //# sourceMappingURL=startup-diagnostics.d.ts.map
@@ -0,0 +1,101 @@
1
+ /**
2
+ * The hub's transport hop into one tenant runtime: an HTTP request forwarded
3
+ * with that runtime's own authority, and an upgraded socket piped the same way.
4
+ *
5
+ * The hop changes transport facts only. It rewrites `Host` to the tenant's
6
+ * authority — the tenant's session cookie is named after the authority it was
7
+ * issued for, so one browser can hold one cookie per tenant on one origin —
8
+ * and rewrites `Origin` to that same authority after checking the browser's
9
+ * own Origin names the hub, which keeps the tenant's same-origin fence as
10
+ * strong as it is without a proxy. Everything else, including `Range`,
11
+ * `Set-Cookie`, `Location`, and partial-content answers, crosses unchanged.
12
+ *
13
+ * The hop decides no access: the tenant runtime's own account gate answers
14
+ * every request, so a forged routing cookie reaches a runtime that does not
15
+ * recognize the caller.
16
+ * @module @qilin-agent/cli/tenant-proxy
17
+ */
18
+ import { type IncomingHttpHeaders, type IncomingMessage, type ServerResponse } from 'node:http';
19
+ import { Readable, type Duplex } from 'node:stream';
20
+ /** One request the hub forwards. */
21
+ export interface ProxyRequestOptions {
22
+ /** Origin the runtime serves, such as `http://127.0.0.1:41234`. */
23
+ readonly origin: string;
24
+ /** Request method, verbatim. */
25
+ readonly method: string;
26
+ /** Path and query to request upstream, exactly the path the browser asked for. */
27
+ readonly path: string;
28
+ /** Browser headers, verbatim. */
29
+ readonly headers: IncomingHttpHeaders;
30
+ /** Browser authority that reached the hub, checked against the Origin fence. */
31
+ readonly hubAuthority: string;
32
+ /** Incoming body, when the request carries one. */
33
+ readonly body?: Readable | undefined;
34
+ }
35
+ /** One upstream answer, still streaming. */
36
+ export interface ProxyAnswer {
37
+ /** Status code the runtime answered. */
38
+ readonly status: number;
39
+ /** Answer headers, hop-by-hop entries removed. */
40
+ readonly headers: IncomingHttpHeaders;
41
+ /** Answer body. */
42
+ readonly stream: Readable;
43
+ }
44
+ /**
45
+ * Whether a browser-labeled request may be forwarded at all.
46
+ *
47
+ * Over plain HTTP a read carries neither Origin nor Fetch-Metadata, so an
48
+ * absent Origin is accepted and the tenant's own Host fence still binds the
49
+ * request. A present Origin must name the hub exactly, and an explicit
50
+ * cross-site marker is refused: the hub is the outermost hop, so this is the
51
+ * only place those markers can be read before the tenant's authority replaces
52
+ * them.
53
+ * @param headers - browser headers, verbatim.
54
+ * @param hubAuthority - authority the browser used to reach the hub.
55
+ * @returns true when the request may be forwarded.
56
+ */
57
+ export declare function hubAcceptsOrigin(headers: IncomingHttpHeaders, hubAuthority: string): boolean;
58
+ /**
59
+ * Headers to forward upstream: the browser's own, minus connection-scoped
60
+ * entries, with the authority-bearing ones rewritten to the tenant.
61
+ * @param headers - browser headers, verbatim.
62
+ * @param tenantAuthority - the runtime's own `host:port`.
63
+ * @param deploymentAuthority - authority the browser used to reach the hub, and
64
+ * therefore the one the runtime names its sessions for; empty when the request
65
+ * reached a runtime directly, which then keeps its own authority as the name.
66
+ * Required rather than optional because every hop of this deployment knows it,
67
+ * and an upgrade that forgot it authenticates against a cookie no browser holds.
68
+ * @returns the forwarded header set.
69
+ */
70
+ export declare function forwardedHeaders(headers: IncomingHttpHeaders, tenantAuthority: string, deploymentAuthority: string): IncomingHttpHeaders;
71
+ /**
72
+ * Send one request to a tenant runtime and hand back its streaming answer.
73
+ *
74
+ * The hub's own origin fence runs here, in the operation that would forward the
75
+ * request, so no caller can forward a browser request whose Origin names
76
+ * another site.
77
+ * @param options - the runtime, the path, headers, and body to forward.
78
+ * @returns the runtime's status, headers, and body, or the refusal.
79
+ * @throws Error when the runtime is unreachable or refuses the connection.
80
+ */
81
+ export declare function proxyRequest(options: ProxyRequestOptions): Promise<ProxyAnswer>;
82
+ /**
83
+ * Write one upstream answer to the browser, streaming the body.
84
+ * @param answer - the runtime's status, headers, and body.
85
+ * @param response - the browser's response.
86
+ */
87
+ export declare function relayAnswer(answer: ProxyAnswer, response: ServerResponse): void;
88
+ /**
89
+ * Pipe one upgraded browser socket to the same path on a tenant runtime, with
90
+ * the tenant's authority in place of the hub's.
91
+ *
92
+ * The upgrade is piped, never terminated: the hub holds no copy of the frames,
93
+ * so the tenant's own mux and its trust fence stay in charge of the stream.
94
+ * @param request - the browser's upgrade request.
95
+ * @param socket - the browser's socket.
96
+ * @param head - bytes already read from the browser.
97
+ * @param target - origin of the runtime the socket belongs to.
98
+ * @param headers - already-forwarded headers, with the tenant's authority set.
99
+ */
100
+ export declare function relayUpgrade(request: IncomingMessage, socket: Duplex, head: Buffer, target: string, headers: IncomingHttpHeaders): void;
101
+ //# sourceMappingURL=tenant-proxy.d.ts.map