@claude-flow/cli 3.32.2 → 3.32.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.
- package/.claude/helpers/helpers.manifest.json +3 -3
- package/.claude/helpers/statusline.cjs +38 -38
- package/catalog-manifest.json +2 -2
- package/dist/src/auth/client.d.ts +89 -0
- package/dist/src/auth/client.js +242 -0
- package/dist/src/auth/constants.d.ts +7 -0
- package/dist/src/auth/constants.js +7 -0
- package/dist/src/auth/scopes.d.ts +14 -0
- package/dist/src/auth/scopes.js +21 -0
- package/dist/src/auth/security-bridge.d.ts +36 -0
- package/dist/src/auth/security-bridge.js +42 -0
- package/dist/src/auth/session.d.ts +20 -0
- package/dist/src/auth/session.js +32 -0
- package/dist/src/auth/state.d.ts +19 -0
- package/dist/src/auth/state.js +53 -0
- package/dist/src/auth/types.d.ts +27 -0
- package/dist/src/auth/types.js +11 -0
- package/dist/src/commands/auth.d.ts +15 -0
- package/dist/src/commands/auth.js +244 -0
- package/dist/src/commands/doctor.js +211 -4
- package/dist/src/commands/index.js +2 -0
- package/dist/src/commands/proxy-lifecycle.d.ts +12 -0
- package/dist/src/commands/proxy-lifecycle.js +232 -0
- package/dist/src/commands/proxy.js +92 -4
- package/dist/src/proxy/install.d.ts +29 -0
- package/dist/src/proxy/install.js +135 -0
- package/dist/src/proxy/lifecycle.d.ts +61 -0
- package/dist/src/proxy/lifecycle.js +249 -0
- package/dist/src/proxy/paths.d.ts +34 -0
- package/dist/src/proxy/paths.js +70 -0
- package/dist/src/proxy/release.d.ts +47 -0
- package/dist/src/proxy/release.js +138 -0
- package/dist/src/proxy/token-bridge.d.ts +5 -0
- package/dist/src/proxy/token-bridge.js +61 -0
- package/dist/src/proxy/verify.d.ts +44 -0
- package/dist/src/proxy/verify.js +68 -0
- package/package.json +2 -2
- package/plugins/ruflo-metaharness/scripts/smoke.sh +11 -11
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ruflo proxy install|start|stop|status|logs|update|uninstall` (ADR-307) —
|
|
3
|
+
* the full lifecycle command set, kept in its own file so
|
|
4
|
+
* src/commands/proxy.ts (the ADR-313/314/315 consent subcommands) stays
|
|
5
|
+
* under the repo's 500-line-per-file convention. Merged into one
|
|
6
|
+
* `proxyCommand` in proxy.ts.
|
|
7
|
+
*/
|
|
8
|
+
import type { Command } from '../types.js';
|
|
9
|
+
/** Pinned and reviewed; later upgrades remain explicit commands. */
|
|
10
|
+
export declare const DEFAULT_PROXY_RELEASE = "0.4.0";
|
|
11
|
+
export declare const proxyLifecycleSubcommands: Command[];
|
|
12
|
+
//# sourceMappingURL=proxy-lifecycle.d.ts.map
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ruflo proxy install|start|stop|status|logs|update|uninstall` (ADR-307) —
|
|
3
|
+
* the full lifecycle command set, kept in its own file so
|
|
4
|
+
* src/commands/proxy.ts (the ADR-313/314/315 consent subcommands) stays
|
|
5
|
+
* under the repo's 500-line-per-file convention. Merged into one
|
|
6
|
+
* `proxyCommand` in proxy.ts.
|
|
7
|
+
*/
|
|
8
|
+
import { output } from '../output.js';
|
|
9
|
+
import { hasConsent, recordConsent, revokeConsent } from '../funnel/index.js';
|
|
10
|
+
import { installProxy, uninstallProxy } from '../proxy/install.js';
|
|
11
|
+
import { startForeground, startBackground, stopProxy, getProxyStatus, readProxyLogTail, watchProxyLog, ProxyNotInstalledError, ProxyAlreadyRunningError, } from '../proxy/lifecycle.js';
|
|
12
|
+
import { proxyTokenPath } from '../proxy/paths.js';
|
|
13
|
+
import { removeInjectedToken, startTokenRefreshPump } from '../proxy/token-bridge.js';
|
|
14
|
+
/** Pinned and reviewed; later upgrades remain explicit commands. */
|
|
15
|
+
export const DEFAULT_PROXY_RELEASE = '0.4.0';
|
|
16
|
+
const INSTALL_DISCLOSURE = [
|
|
17
|
+
'Installing the Meta LLM Proxy (ADR-304/307).',
|
|
18
|
+
'',
|
|
19
|
+
'This downloads a separately-released Rust binary (Ed25519-signature and',
|
|
20
|
+
'checksum verified before anything is written to disk) and runs it as a',
|
|
21
|
+
'local process bound to 127.0.0.1 only. The proxy routes to LOCAL',
|
|
22
|
+
'backends by default — no prompt leaves this machine. Cloud routing is a',
|
|
23
|
+
'separate, explicit opt-in (`ruflo proxy config --cloud`), never enabled',
|
|
24
|
+
'by install alone.',
|
|
25
|
+
'',
|
|
26
|
+
'Uninstall anytime: ruflo proxy uninstall',
|
|
27
|
+
].join('\n');
|
|
28
|
+
const installSub = {
|
|
29
|
+
name: 'install',
|
|
30
|
+
description: 'Download, verify, and install the meta-proxy binary (ADR-307)',
|
|
31
|
+
options: [
|
|
32
|
+
// NOT named 'version' — index.ts:107 globally intercepts any --version/-v
|
|
33
|
+
// anywhere in argv (`if (flags.version || flags.V) { showVersion(); return; }`)
|
|
34
|
+
// BEFORE subcommand dispatch, regardless of which command defines it. A
|
|
35
|
+
// subcommand-local --version is silently swallowed by the CLI's own
|
|
36
|
+
// `ruflo --version` handling — confirmed the hard way in E2E testing.
|
|
37
|
+
{ name: 'release', description: `Release version to install (default: ${DEFAULT_PROXY_RELEASE})`, type: 'string' },
|
|
38
|
+
{ name: 'yes', description: 'Skip the confirmation prompt', type: 'boolean', default: false },
|
|
39
|
+
],
|
|
40
|
+
action: async (ctx) => {
|
|
41
|
+
// Resolve the reviewed default before the consent gate. An explicit empty
|
|
42
|
+
// override still fails below without recording a consent receipt.
|
|
43
|
+
const version = typeof ctx.flags.release === 'string' ? ctx.flags.release : DEFAULT_PROXY_RELEASE;
|
|
44
|
+
if (!version) {
|
|
45
|
+
output.printError('ruflo proxy install requires --release <x.y.z> — there is no version-discovery ' +
|
|
46
|
+
'endpoint yet (see the plan doc for the tracked follow-up). Find the latest at ' +
|
|
47
|
+
'the release channel and pass it explicitly.');
|
|
48
|
+
return { success: false, exitCode: 1 };
|
|
49
|
+
}
|
|
50
|
+
if (!hasConsent('proxy-install')) {
|
|
51
|
+
output.writeln(INSTALL_DISCLOSURE);
|
|
52
|
+
output.writeln('');
|
|
53
|
+
const confirmed = Boolean(ctx.flags.yes);
|
|
54
|
+
if (!confirmed) {
|
|
55
|
+
output.writeln(`Re-run with --yes to confirm: ruflo proxy install --yes (installs ${version})`);
|
|
56
|
+
return { success: true, data: { confirmed: false } };
|
|
57
|
+
}
|
|
58
|
+
recordConsent('proxy-install', true, 'proxy-install');
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
const spinner = output.createSpinner({ text: `Installing meta-proxy ${version}...`, spinner: 'dots' });
|
|
62
|
+
spinner.start();
|
|
63
|
+
const result = await installProxy({ version, log: (line) => spinner.setText(line) });
|
|
64
|
+
spinner.succeed(`meta-proxy ${version} installed`);
|
|
65
|
+
output.writeln(` binary: ${result.binaryPath}`);
|
|
66
|
+
output.writeln(` sha256: ${result.sha256}`);
|
|
67
|
+
return { success: true, data: result };
|
|
68
|
+
}
|
|
69
|
+
catch (e) {
|
|
70
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
71
|
+
output.printError('Install failed', message);
|
|
72
|
+
return { success: false, message, exitCode: 1 };
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
const updateSub = {
|
|
77
|
+
name: 'update',
|
|
78
|
+
description: 'Re-verify and replace the installed binary with a specific version (never automatic)',
|
|
79
|
+
options: [{ name: 'release', description: 'Release version to install', type: 'string', required: true }],
|
|
80
|
+
action: async (ctx) => {
|
|
81
|
+
const version = typeof ctx.flags.release === 'string' ? ctx.flags.release : undefined;
|
|
82
|
+
if (!version) {
|
|
83
|
+
output.printError('ruflo proxy update requires --release <x.y.z>');
|
|
84
|
+
return { success: false, exitCode: 1 };
|
|
85
|
+
}
|
|
86
|
+
try {
|
|
87
|
+
const spinner = output.createSpinner({ text: `Updating meta-proxy to ${version}...`, spinner: 'dots' });
|
|
88
|
+
spinner.start();
|
|
89
|
+
const result = await installProxy({ version, log: (line) => spinner.setText(line) });
|
|
90
|
+
spinner.succeed(`meta-proxy updated to ${version}`);
|
|
91
|
+
output.writeln(` binary: ${result.binaryPath}`);
|
|
92
|
+
return { success: true, data: result };
|
|
93
|
+
}
|
|
94
|
+
catch (e) {
|
|
95
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
96
|
+
output.printError('Update failed', message);
|
|
97
|
+
return { success: false, message, exitCode: 1 };
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
const startSub = {
|
|
102
|
+
name: 'start',
|
|
103
|
+
description: 'Start meta-proxy (foreground by default; --service to detach)',
|
|
104
|
+
options: [
|
|
105
|
+
{ name: 'service', description: 'Run detached (background), survives terminal close but not a reboot — full OS-service registration is not yet implemented', type: 'boolean', default: false },
|
|
106
|
+
],
|
|
107
|
+
action: async (ctx) => {
|
|
108
|
+
const service = Boolean(ctx.flags.service);
|
|
109
|
+
try {
|
|
110
|
+
if (service) {
|
|
111
|
+
const { pid } = await startBackground();
|
|
112
|
+
output.printSuccess(`meta-proxy started in the background (pid ${pid})`);
|
|
113
|
+
output.writeln(' Note: survives this terminal closing, but not a reboot — OS-service registration is not yet implemented.');
|
|
114
|
+
output.writeln(' Logs: ruflo proxy logs');
|
|
115
|
+
return { success: true, data: { pid } };
|
|
116
|
+
}
|
|
117
|
+
output.writeln('Starting meta-proxy in the foreground — press Ctrl+C to stop.');
|
|
118
|
+
await startTokenRefreshPump();
|
|
119
|
+
await startForeground(); // never returns normally
|
|
120
|
+
return { success: true };
|
|
121
|
+
}
|
|
122
|
+
catch (e) {
|
|
123
|
+
if (e instanceof ProxyNotInstalledError || e instanceof ProxyAlreadyRunningError) {
|
|
124
|
+
output.printError(e.message);
|
|
125
|
+
return { success: false, message: e.message, exitCode: 1 };
|
|
126
|
+
}
|
|
127
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
128
|
+
output.printError('Failed to start meta-proxy', message);
|
|
129
|
+
return { success: false, message, exitCode: 1 };
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
const superviseSub = {
|
|
134
|
+
name: 'supervise',
|
|
135
|
+
description: 'Internal detached supervisor for token refresh and meta-proxy lifecycle',
|
|
136
|
+
action: async () => {
|
|
137
|
+
await startTokenRefreshPump();
|
|
138
|
+
await startForeground(true);
|
|
139
|
+
return { success: true };
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
const stopSub = {
|
|
143
|
+
name: 'stop',
|
|
144
|
+
description: 'Stop a running meta-proxy process',
|
|
145
|
+
action: async () => {
|
|
146
|
+
const result = await stopProxy();
|
|
147
|
+
removeInjectedToken();
|
|
148
|
+
if (!result.wasRunning) {
|
|
149
|
+
output.writeln('meta-proxy was not running.');
|
|
150
|
+
return { success: true, data: result };
|
|
151
|
+
}
|
|
152
|
+
output.printSuccess(`meta-proxy stopped (was pid ${result.pid})`);
|
|
153
|
+
return { success: true, data: result };
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
const statusSub = {
|
|
157
|
+
name: 'status',
|
|
158
|
+
description: 'Show meta-proxy install + process status',
|
|
159
|
+
options: [{ name: 'json', description: 'Machine-readable output', type: 'boolean', default: false }],
|
|
160
|
+
action: async (ctx) => {
|
|
161
|
+
const status = getProxyStatus();
|
|
162
|
+
if (ctx.flags.json) {
|
|
163
|
+
output.printJson(status);
|
|
164
|
+
return { success: true, data: status };
|
|
165
|
+
}
|
|
166
|
+
output.writeln(`Installed: ${status.installed ? 'yes' : 'no'}`);
|
|
167
|
+
output.writeln(`Running: ${status.running ? `yes (pid ${status.pid})` : 'no'}`);
|
|
168
|
+
if (status.stalePidFile)
|
|
169
|
+
output.writeln(' (a stale PID file was found and will be cleared on next start)');
|
|
170
|
+
if (!status.installed)
|
|
171
|
+
output.writeln(`Run: ruflo proxy install --yes (installs ${DEFAULT_PROXY_RELEASE})`);
|
|
172
|
+
else if (!status.running)
|
|
173
|
+
output.writeln('Run: ruflo proxy start');
|
|
174
|
+
return { success: true, data: status };
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
const logsSub = {
|
|
178
|
+
name: 'logs',
|
|
179
|
+
description: 'Show meta-proxy --service logs',
|
|
180
|
+
options: [
|
|
181
|
+
{ name: 'follow', short: 'f', description: 'Stream new log lines as they arrive', type: 'boolean', default: false },
|
|
182
|
+
],
|
|
183
|
+
action: async (ctx) => {
|
|
184
|
+
if (ctx.flags.follow) {
|
|
185
|
+
output.writeln('Following meta-proxy logs — press Ctrl+C to stop.');
|
|
186
|
+
try {
|
|
187
|
+
const watcher = watchProxyLog((line) => output.writeln(line));
|
|
188
|
+
await new Promise((resolve) => {
|
|
189
|
+
process.on('SIGINT', () => {
|
|
190
|
+
watcher.close();
|
|
191
|
+
resolve();
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
return { success: true };
|
|
195
|
+
}
|
|
196
|
+
catch (e) {
|
|
197
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
198
|
+
output.printError(message);
|
|
199
|
+
return { success: false, message, exitCode: 1 };
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
const tail = readProxyLogTail();
|
|
203
|
+
if (!tail) {
|
|
204
|
+
output.writeln('No log content yet — meta-proxy has never been started in --service mode.');
|
|
205
|
+
return { success: true, data: { empty: true } };
|
|
206
|
+
}
|
|
207
|
+
output.writeln(tail);
|
|
208
|
+
return { success: true };
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
const uninstallSub = {
|
|
212
|
+
name: 'uninstall',
|
|
213
|
+
description: 'Stop the proxy (if running), remove the binary, token, and consent receipt',
|
|
214
|
+
action: async () => {
|
|
215
|
+
const status = getProxyStatus();
|
|
216
|
+
if (status.running) {
|
|
217
|
+
await stopProxy();
|
|
218
|
+
output.writeln('Stopped the running meta-proxy process.');
|
|
219
|
+
}
|
|
220
|
+
const removed = await uninstallProxy();
|
|
221
|
+
const { existsSync, unlinkSync } = await import('node:fs');
|
|
222
|
+
const tokenPath = proxyTokenPath();
|
|
223
|
+
if (existsSync(tokenPath))
|
|
224
|
+
unlinkSync(tokenPath);
|
|
225
|
+
removeInjectedToken();
|
|
226
|
+
revokeConsent('proxy-install', 'proxy-uninstall');
|
|
227
|
+
output.printSuccess(removed ? 'meta-proxy uninstalled.' : 'Nothing was installed — cleaned up any leftover state.');
|
|
228
|
+
return { success: true, data: { removed } };
|
|
229
|
+
},
|
|
230
|
+
};
|
|
231
|
+
export const proxyLifecycleSubcommands = [installSub, updateSub, startSub, superviseSub, stopSub, statusSub, logsSub, uninstallSub];
|
|
232
|
+
//# sourceMappingURL=proxy-lifecycle.js.map
|
|
@@ -23,6 +23,7 @@ import { clearRateLimitStatus, readRateLimitStatus } from '../funnel/rate-limit-
|
|
|
23
23
|
import { clearQuotaLowStatus, readQuotaLowStatus } from '../funnel/power-saver-notifier.js';
|
|
24
24
|
import { getInstalledCliVersion } from '../init/helper-refresh.js';
|
|
25
25
|
import * as path from 'path';
|
|
26
|
+
import { proxyLifecycleSubcommands } from './proxy-lifecycle.js';
|
|
26
27
|
const PROXY_CONFIG_FILE = 'proxy-config.toml';
|
|
27
28
|
/**
|
|
28
29
|
* Minimal hand-rolled TOML writer — this config has exactly one boolean
|
|
@@ -40,12 +41,12 @@ function readProxyConfigRaw() {
|
|
|
40
41
|
return '';
|
|
41
42
|
}
|
|
42
43
|
}
|
|
43
|
-
function
|
|
44
|
+
function writeConfigLine(field, rawValue) {
|
|
44
45
|
const dir = funnelStateDir();
|
|
45
46
|
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
46
47
|
const target = path.join(dir, PROXY_CONFIG_FILE);
|
|
47
48
|
const raw = readProxyConfigRaw();
|
|
48
|
-
const line = `${field} = ${
|
|
49
|
+
const line = `${field} = ${rawValue}`;
|
|
49
50
|
const pattern = new RegExp(`^${field}\\s*=.*$`, 'm');
|
|
50
51
|
let next;
|
|
51
52
|
if (pattern.test(raw)) {
|
|
@@ -58,6 +59,9 @@ function writeConsentMirrorLine(field, value) {
|
|
|
58
59
|
fs.writeFileSync(tmp, next, { encoding: 'utf-8', mode: 0o600 });
|
|
59
60
|
fs.renameSync(tmp, target);
|
|
60
61
|
}
|
|
62
|
+
function writeConsentMirrorLine(field, value) {
|
|
63
|
+
writeConfigLine(field, String(value));
|
|
64
|
+
}
|
|
61
65
|
function writeSponsoredConsentMirror(granted) {
|
|
62
66
|
writeConsentMirrorLine('sponsored_consent_granted', granted);
|
|
63
67
|
}
|
|
@@ -67,6 +71,78 @@ function writePowerSaverConsentMirror(granted) {
|
|
|
67
71
|
function writeTrainingShareConsentMirror(granted) {
|
|
68
72
|
writeConsentMirrorLine('training_share_consent_granted', granted);
|
|
69
73
|
}
|
|
74
|
+
/**
|
|
75
|
+
* `default_data_plane` — the ADR-304 cloud-routing toggle. Values confirmed
|
|
76
|
+
* against meta-proxy's actual `DataPlane` enum (`src/config.rs`,
|
|
77
|
+
* `#[serde(rename_all = "snake_case")]`): "local" | "cloud" | "sponsored" |
|
|
78
|
+
* "passthrough" (the last two are not written by this command — sponsored
|
|
79
|
+
* is ADR-313's own consent flag, passthrough is the proxy's own default).
|
|
80
|
+
*/
|
|
81
|
+
function readDataPlane() {
|
|
82
|
+
const raw = readProxyConfigRaw();
|
|
83
|
+
const match = raw.match(/^default_data_plane\s*=\s*"([^"]*)"/m);
|
|
84
|
+
return match ? match[1] : 'passthrough'; // matches the Rust struct's own default
|
|
85
|
+
}
|
|
86
|
+
function writeDataPlane(plane) {
|
|
87
|
+
writeConfigLine('default_data_plane', `"${plane}"`);
|
|
88
|
+
}
|
|
89
|
+
const CLOUD_ROUTING_DISCLOSURE = [
|
|
90
|
+
'Enabling cloud routing.',
|
|
91
|
+
'',
|
|
92
|
+
'With cloud routing ON, prompts for cloud-tier requests are sent to',
|
|
93
|
+
'api.cognitum.one and forwarded to the selected provider',
|
|
94
|
+
'(Claude / GPT / Gemini / DeepSeek / OpenRouter).',
|
|
95
|
+
'',
|
|
96
|
+
'Requests routed to local backends never leave this machine.',
|
|
97
|
+
'',
|
|
98
|
+
'Disable anytime: ruflo proxy config --local-only',
|
|
99
|
+
].join('\n');
|
|
100
|
+
const configSub = {
|
|
101
|
+
name: 'config',
|
|
102
|
+
description: 'Toggle cloud routing (ADR-304) — local backends only by default',
|
|
103
|
+
options: [
|
|
104
|
+
{ name: 'cloud', description: 'Enable cloud routing (requires cloud-routing consent)', type: 'boolean', default: false },
|
|
105
|
+
{ name: 'local-only', description: 'Disable cloud routing, revert to local-only routing', type: 'boolean', default: false },
|
|
106
|
+
{ name: 'yes', description: 'Skip the confirmation prompt', type: 'boolean', default: false },
|
|
107
|
+
],
|
|
108
|
+
action: async (ctx) => {
|
|
109
|
+
const wantCloud = Boolean(ctx.flags.cloud);
|
|
110
|
+
const wantLocalOnly = Boolean(ctx.flags.localOnly ?? ctx.flags['local-only']);
|
|
111
|
+
if (wantCloud && wantLocalOnly) {
|
|
112
|
+
output.printError('Pass either --cloud or --local-only, not both.');
|
|
113
|
+
return { success: false, exitCode: 1 };
|
|
114
|
+
}
|
|
115
|
+
if (!wantCloud && !wantLocalOnly) {
|
|
116
|
+
const plane = readDataPlane();
|
|
117
|
+
output.writeln(`Current data plane: ${plane}`);
|
|
118
|
+
output.writeln(plane === 'cloud'
|
|
119
|
+
? 'Cloud routing is ON — cloud-tier requests go to api.cognitum.one.'
|
|
120
|
+
: 'Cloud routing is OFF — requests never leave this machine (or use your own Claude subscription on Passthrough).');
|
|
121
|
+
return { success: true, data: { plane } };
|
|
122
|
+
}
|
|
123
|
+
if (wantLocalOnly) {
|
|
124
|
+
writeDataPlane('local');
|
|
125
|
+
revokeConsent('cloud-routing', 'proxy-config-local-only');
|
|
126
|
+
output.printSuccess('Cloud routing disabled — reverted to local-only routing.');
|
|
127
|
+
return { success: true, data: { plane: 'local' } };
|
|
128
|
+
}
|
|
129
|
+
// wantCloud
|
|
130
|
+
if (!hasConsent('cloud-routing')) {
|
|
131
|
+
output.writeln(CLOUD_ROUTING_DISCLOSURE);
|
|
132
|
+
output.writeln('');
|
|
133
|
+
if (!ctx.flags.yes) {
|
|
134
|
+
output.writeln('Re-run with --yes to confirm: ruflo proxy config --cloud --yes');
|
|
135
|
+
return { success: true, data: { confirmed: false } };
|
|
136
|
+
}
|
|
137
|
+
recordConsent('cloud-routing', true, 'proxy-config-cloud');
|
|
138
|
+
}
|
|
139
|
+
writeDataPlane('cloud');
|
|
140
|
+
output.printSuccess('Cloud routing enabled.');
|
|
141
|
+
output.writeln(' Requests routed to local backends still never leave this machine.');
|
|
142
|
+
output.writeln(' Disable anytime: ruflo proxy config --local-only');
|
|
143
|
+
return { success: true, data: { plane: 'cloud' } };
|
|
144
|
+
},
|
|
145
|
+
};
|
|
70
146
|
const SPONSOR_DISCLOSURE = [
|
|
71
147
|
'Enabling sponsored downtime mode.',
|
|
72
148
|
'',
|
|
@@ -292,19 +368,31 @@ const trainingShareStatusSub = {
|
|
|
292
368
|
};
|
|
293
369
|
export const proxyCommand = {
|
|
294
370
|
name: 'proxy',
|
|
295
|
-
description: 'Meta LLM Proxy — sponsored downtime + power saver + training-data sharing (ADR-304/307/313/314/315)',
|
|
371
|
+
description: 'Meta LLM Proxy — install/lifecycle + sponsored downtime + power saver + training-data sharing (ADR-304/307/313/314/315)',
|
|
296
372
|
subcommands: [
|
|
373
|
+
...proxyLifecycleSubcommands,
|
|
374
|
+
configSub,
|
|
297
375
|
sponsorEnableSub, sponsorDisableSub, sponsorStatusSub, sponsorClearSub,
|
|
298
376
|
powerSaverEnableSub, powerSaverDisableSub, powerSaverStatusSub, powerSaverClearSub,
|
|
299
377
|
trainingShareEnableSub, trainingShareDisableSub, trainingShareStatusSub,
|
|
300
378
|
],
|
|
301
379
|
examples: [
|
|
380
|
+
{ command: 'ruflo proxy install --yes', description: 'Install the signed Meta-Proxy v0.4.0 binary' },
|
|
381
|
+
{ command: 'ruflo proxy start', description: 'Start meta-proxy in the foreground' },
|
|
382
|
+
{ command: 'ruflo proxy status', description: 'Show install + process status' },
|
|
383
|
+
{ command: 'ruflo proxy config --cloud --yes', description: 'Enable cloud routing (ADR-304)' },
|
|
384
|
+
{ command: 'ruflo proxy config --local-only', description: 'Revert to local-only routing' },
|
|
302
385
|
{ command: 'ruflo proxy sponsor-status', description: 'Show current sponsored-mode state' },
|
|
303
386
|
{ command: 'ruflo proxy sponsor-enable --yes', description: 'Opt into sponsored downtime capacity' },
|
|
304
387
|
{ command: 'ruflo proxy power-saver-enable --yes', description: 'Opt into power saver mode' },
|
|
305
388
|
{ command: 'ruflo proxy training-share-enable --yes', description: 'Opt into training-data sharing (ADR-315)' },
|
|
306
389
|
],
|
|
307
|
-
action:
|
|
390
|
+
action: async (ctx) => {
|
|
391
|
+
const { getProxyStatus } = await import('../proxy/lifecycle.js');
|
|
392
|
+
const status = getProxyStatus();
|
|
393
|
+
output.writeln(`Installed: ${status.installed ? 'yes' : 'no'}; Running: ${status.running ? `yes (pid ${status.pid})` : 'no'}`);
|
|
394
|
+
return sponsorStatusSub.action(ctx);
|
|
395
|
+
},
|
|
308
396
|
};
|
|
309
397
|
export default proxyCommand;
|
|
310
398
|
//# sourceMappingURL=proxy.js.map
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ruflo proxy install`/`update` orchestration (ADR-307): download -> verify
|
|
3
|
+
* -> extract -> place -> record. The per-user bearer token
|
|
4
|
+
* (`~/.ruflo/proxy-token`) is NOT generated here — confirmed empirically
|
|
5
|
+
* (2026-07-16) that the meta-proxy binary itself creates it on first launch
|
|
6
|
+
* (`load_or_create_token()`), so this module's job ends at a verified binary
|
|
7
|
+
* on disk plus an install manifest doctor can check against.
|
|
8
|
+
*
|
|
9
|
+
* @module proxy/install
|
|
10
|
+
*/
|
|
11
|
+
export declare class ExtractionError extends Error {
|
|
12
|
+
constructor(message: string);
|
|
13
|
+
}
|
|
14
|
+
export interface InstallOptions {
|
|
15
|
+
version: string;
|
|
16
|
+
log?: (line: string) => void;
|
|
17
|
+
}
|
|
18
|
+
export interface InstallResult {
|
|
19
|
+
version: string;
|
|
20
|
+
binaryPath: string;
|
|
21
|
+
sha256: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Full install pipeline. Refuses (throws) on any verification failure —
|
|
25
|
+
* never writes a partially-verified binary into the live install path.
|
|
26
|
+
*/
|
|
27
|
+
export declare function installProxy(opts: InstallOptions): Promise<InstallResult>;
|
|
28
|
+
export declare function uninstallProxy(): Promise<boolean>;
|
|
29
|
+
//# sourceMappingURL=install.d.ts.map
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ruflo proxy install`/`update` orchestration (ADR-307): download -> verify
|
|
3
|
+
* -> extract -> place -> record. The per-user bearer token
|
|
4
|
+
* (`~/.ruflo/proxy-token`) is NOT generated here — confirmed empirically
|
|
5
|
+
* (2026-07-16) that the meta-proxy binary itself creates it on first launch
|
|
6
|
+
* (`load_or_create_token()`), so this module's job ends at a verified binary
|
|
7
|
+
* on disk plus an install manifest doctor can check against.
|
|
8
|
+
*
|
|
9
|
+
* @module proxy/install
|
|
10
|
+
*/
|
|
11
|
+
import * as fs from 'node:fs';
|
|
12
|
+
import * as os from 'node:os';
|
|
13
|
+
import * as path from 'node:path';
|
|
14
|
+
import { fetchReleaseAssets, detectTargetTriple, releaseArchiveExtension, releaseAssetFilename } from './release.js';
|
|
15
|
+
import { verifyRelease, sha256Hex, PROXY_RELEASE_PUBKEY_PEM } from './verify.js';
|
|
16
|
+
import { proxyBinaryPath, proxyInstallManifestPath } from './paths.js';
|
|
17
|
+
export class ExtractionError extends Error {
|
|
18
|
+
constructor(message) {
|
|
19
|
+
super(message);
|
|
20
|
+
this.name = 'ExtractionError';
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function binaryNameInArchive() {
|
|
24
|
+
return process.platform === 'win32' ? 'meta-proxy.exe' : 'meta-proxy';
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Extracts the archive via the OS's own tools — `tar` for `.tar.gz`
|
|
28
|
+
* (present on macOS/Linux/Windows 10+), PowerShell `Expand-Archive`
|
|
29
|
+
* specifically for `.zip` on Windows (not tar's bsdtar zip support — not
|
|
30
|
+
* reliable enough to lean on). Zero new archive-parsing dependency, matching
|
|
31
|
+
* this repo's existing taste for shelling out over adding a parser dep.
|
|
32
|
+
*/
|
|
33
|
+
async function extractArchive(archivePath, extractDir, ext) {
|
|
34
|
+
const { SafeExecutor } = await import('@claude-flow/security');
|
|
35
|
+
fs.mkdirSync(extractDir, { recursive: true });
|
|
36
|
+
if (ext === 'tar.gz') {
|
|
37
|
+
const exec = new SafeExecutor({ allowedCommands: ['tar'], timeout: 60_000 });
|
|
38
|
+
const result = await exec.execute('tar', ['xzf', archivePath, '-C', extractDir]);
|
|
39
|
+
if (result.exitCode !== 0) {
|
|
40
|
+
throw new ExtractionError(`tar extraction failed (exit ${result.exitCode}): ${result.stderr || result.stdout}`);
|
|
41
|
+
}
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
// .zip — PowerShell Expand-Archive, single-quoted literal paths (doubling
|
|
45
|
+
// any embedded single quote per PowerShell string-literal escaping) passed
|
|
46
|
+
// as ONE argv element to -Command. shell:false means no OS shell ever
|
|
47
|
+
// tokenizes this string — only powershell.exe's own parser does.
|
|
48
|
+
const escape = (p) => p.replace(/'/g, "''");
|
|
49
|
+
const command = `Expand-Archive -LiteralPath '${escape(archivePath)}' -DestinationPath '${escape(extractDir)}' -Force`;
|
|
50
|
+
const exec = new SafeExecutor({ allowedCommands: ['powershell', 'powershell.exe'], timeout: 60_000 });
|
|
51
|
+
const result = await exec.execute('powershell', ['-NoProfile', '-NonInteractive', '-Command', command]);
|
|
52
|
+
if (result.exitCode !== 0) {
|
|
53
|
+
throw new ExtractionError(`Expand-Archive failed (exit ${result.exitCode}): ${result.stderr || result.stdout}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Full install pipeline. Refuses (throws) on any verification failure —
|
|
58
|
+
* never writes a partially-verified binary into the live install path.
|
|
59
|
+
*/
|
|
60
|
+
export async function installProxy(opts) {
|
|
61
|
+
const log = opts.log ?? (() => { });
|
|
62
|
+
const triple = detectTargetTriple();
|
|
63
|
+
const archiveFilename = releaseAssetFilename(opts.version, triple);
|
|
64
|
+
const workDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ruflo-proxy-install-'));
|
|
65
|
+
try {
|
|
66
|
+
log(`Fetching meta-proxy ${opts.version} (${triple})...`);
|
|
67
|
+
const assets = await fetchReleaseAssets(opts.version, triple, workDir, log);
|
|
68
|
+
log('Verifying release signature and checksum...');
|
|
69
|
+
const { sha256 } = verifyRelease({
|
|
70
|
+
sumsBytes: assets.sumsBytes,
|
|
71
|
+
sigBase64: assets.sigBase64,
|
|
72
|
+
assetBytes: assets.archiveBytes,
|
|
73
|
+
assetFilename: assets.archiveFilename,
|
|
74
|
+
});
|
|
75
|
+
log(`Verified — sha256 ${sha256.slice(0, 16)}…`);
|
|
76
|
+
// fetchReleaseAssets's dev (gh) path already wrote the archive to workDir
|
|
77
|
+
// under archiveFilename; ensure it's there regardless of source so
|
|
78
|
+
// extraction always has a real file to operate on.
|
|
79
|
+
const archivePath = path.join(workDir, archiveFilename);
|
|
80
|
+
if (!fs.existsSync(archivePath)) {
|
|
81
|
+
fs.writeFileSync(archivePath, assets.archiveBytes);
|
|
82
|
+
}
|
|
83
|
+
const extractDir = path.join(workDir, 'extracted');
|
|
84
|
+
await extractArchive(archivePath, extractDir, releaseArchiveExtension(triple));
|
|
85
|
+
const extractedBinaryPath = path.join(extractDir, binaryNameInArchive());
|
|
86
|
+
if (!fs.existsSync(extractedBinaryPath)) {
|
|
87
|
+
throw new ExtractionError(`archive did not contain the expected binary at its root: ${binaryNameInArchive()}`);
|
|
88
|
+
}
|
|
89
|
+
// Defense in depth: confirm the extracted binary genuinely resolves
|
|
90
|
+
// inside extractDir (catches a symlink swap or similar), even though
|
|
91
|
+
// we only ever read one specific expected relative path, never an
|
|
92
|
+
// archive-listed one (so "zip slip" via arbitrary archive paths isn't
|
|
93
|
+
// reachable here in the first place).
|
|
94
|
+
const { PathValidator } = await import('@claude-flow/security');
|
|
95
|
+
const validator = new PathValidator({ allowedPrefixes: [extractDir] });
|
|
96
|
+
const validation = await validator.validate(extractedBinaryPath);
|
|
97
|
+
if (!validation.isValid) {
|
|
98
|
+
throw new ExtractionError(`extracted binary path failed validation: ${validation.errors.join('; ') || 'unknown'}`);
|
|
99
|
+
}
|
|
100
|
+
const finalPath = proxyBinaryPath();
|
|
101
|
+
fs.mkdirSync(path.dirname(finalPath), { recursive: true, mode: 0o700 });
|
|
102
|
+
const tmp = `${finalPath}.tmp`;
|
|
103
|
+
fs.copyFileSync(extractedBinaryPath, tmp);
|
|
104
|
+
fs.chmodSync(tmp, 0o755);
|
|
105
|
+
fs.renameSync(tmp, finalPath);
|
|
106
|
+
const liveSha = sha256Hex(fs.readFileSync(finalPath));
|
|
107
|
+
const manifest = {
|
|
108
|
+
version: opts.version,
|
|
109
|
+
sha256: liveSha,
|
|
110
|
+
verifiedAt: new Date().toISOString(),
|
|
111
|
+
pubkeyFingerprint: sha256Hex(Buffer.from(PROXY_RELEASE_PUBKEY_PEM)).slice(0, 16),
|
|
112
|
+
};
|
|
113
|
+
fs.mkdirSync(path.dirname(proxyInstallManifestPath()), { recursive: true });
|
|
114
|
+
fs.writeFileSync(proxyInstallManifestPath(), JSON.stringify(manifest, null, 2), { mode: 0o600 });
|
|
115
|
+
log(`meta-proxy ${opts.version} installed at ${finalPath}`);
|
|
116
|
+
return { version: opts.version, binaryPath: finalPath, sha256: liveSha };
|
|
117
|
+
}
|
|
118
|
+
finally {
|
|
119
|
+
fs.rmSync(workDir, { recursive: true, force: true });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
export async function uninstallProxy() {
|
|
123
|
+
const binPath = proxyBinaryPath();
|
|
124
|
+
const existed = fs.existsSync(binPath);
|
|
125
|
+
if (existed)
|
|
126
|
+
fs.unlinkSync(binPath);
|
|
127
|
+
try {
|
|
128
|
+
fs.unlinkSync(proxyInstallManifestPath());
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
/* absent — fine */
|
|
132
|
+
}
|
|
133
|
+
return existed;
|
|
134
|
+
}
|
|
135
|
+
//# sourceMappingURL=install.js.map
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* meta-proxy process lifecycle (ADR-307) — start/stop/status/logs.
|
|
3
|
+
*
|
|
4
|
+
* Adapts daemon.ts's proven pattern (PID file, O_EXCL lockfile for atomic
|
|
5
|
+
* check-then-start, signal-0 liveness, SIGTERM->1000ms->SIGKILL) to a
|
|
6
|
+
* native binary instead of a forked Node process. The binary itself takes
|
|
7
|
+
* no CLI flags — confirmed empirically (2026-07-16): `meta-proxy.exe` has
|
|
8
|
+
* no `--version`/`--help`, and any invocation just starts the server reading
|
|
9
|
+
* its own config file — so `spawn()` here passes zero arguments, always.
|
|
10
|
+
*
|
|
11
|
+
* Foreground `start` (the ADR-307 default) uses `stdio: 'inherit'` and
|
|
12
|
+
* blocks directly — simplest and safest, no log-file redirection needed.
|
|
13
|
+
* `start --service` needs REAL file-descriptor redirection
|
|
14
|
+
* (`stdio: ['ignore', fd, fd]`) + `detached: true` + `unref()` via
|
|
15
|
+
* `child_process.spawn()` directly — `SafeExecutor.executeStreaming()`
|
|
16
|
+
* buffers output in-process, which is wrong for a process meant to outlive
|
|
17
|
+
* the `ruflo` invocation that started it.
|
|
18
|
+
*
|
|
19
|
+
* @module proxy/lifecycle
|
|
20
|
+
*/
|
|
21
|
+
import * as fs from 'node:fs';
|
|
22
|
+
export declare class ProxyNotInstalledError extends Error {
|
|
23
|
+
constructor();
|
|
24
|
+
}
|
|
25
|
+
export declare class ProxyAlreadyRunningError extends Error {
|
|
26
|
+
readonly pid: number;
|
|
27
|
+
constructor(pid: number);
|
|
28
|
+
}
|
|
29
|
+
export interface ProxyStatus {
|
|
30
|
+
installed: boolean;
|
|
31
|
+
running: boolean;
|
|
32
|
+
pid: number | null;
|
|
33
|
+
stalePidFile: boolean;
|
|
34
|
+
}
|
|
35
|
+
export declare function getProxyStatus(): ProxyStatus;
|
|
36
|
+
/**
|
|
37
|
+
* Foreground start (ADR-307 default) — blocks the caller until the process
|
|
38
|
+
* exits or is interrupted. `stdio: 'inherit'` passes the proxy's own output
|
|
39
|
+
* straight through to the terminal; signals (Ctrl+C) propagate naturally to
|
|
40
|
+
* the child, no manual forwarding needed.
|
|
41
|
+
*/
|
|
42
|
+
export declare function startForeground(supervised?: boolean): Promise<never>;
|
|
43
|
+
/**
|
|
44
|
+
* Background/`--service` start — detaches so the process outlives this
|
|
45
|
+
* `ruflo` invocation, redirecting stdout/stderr to a real log file (not
|
|
46
|
+
* buffered in-process). Returns once the child's PID is confirmed written,
|
|
47
|
+
* without waiting for the process to exit.
|
|
48
|
+
*/
|
|
49
|
+
export declare function startBackground(): Promise<{
|
|
50
|
+
pid: number;
|
|
51
|
+
}>;
|
|
52
|
+
export interface StopResult {
|
|
53
|
+
wasRunning: boolean;
|
|
54
|
+
pid: number | null;
|
|
55
|
+
}
|
|
56
|
+
/** SIGTERM -> 1000ms -> SIGKILL if still alive, mirroring daemon.ts's killBackgroundDaemon. */
|
|
57
|
+
export declare function stopProxy(): Promise<StopResult>;
|
|
58
|
+
export declare function readProxyLogTail(maxBytes?: number): string;
|
|
59
|
+
/** Streams new log lines as they're appended, starting from the current end of file. */
|
|
60
|
+
export declare function watchProxyLog(onLine: (line: string) => void): fs.FSWatcher;
|
|
61
|
+
//# sourceMappingURL=lifecycle.d.ts.map
|