claude-flow 3.32.2 → 3.32.8
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/statusline.cjs +38 -38
- package/.claude-plugin/scripts/ruflo-hook.cjs +2 -2
- package/.claude-plugin/scripts/ruflo-hook.sh +17 -17
- package/package.json +1 -1
- package/v3/@claude-flow/cli/catalog-manifest.json +2 -2
- package/v3/@claude-flow/cli/dist/src/auth/client.d.ts +89 -0
- package/v3/@claude-flow/cli/dist/src/auth/client.js +242 -0
- package/v3/@claude-flow/cli/dist/src/auth/constants.d.ts +7 -0
- package/v3/@claude-flow/cli/dist/src/auth/constants.js +7 -0
- package/v3/@claude-flow/cli/dist/src/auth/scopes.d.ts +14 -0
- package/v3/@claude-flow/cli/dist/src/auth/scopes.js +21 -0
- package/v3/@claude-flow/cli/dist/src/auth/security-bridge.d.ts +36 -0
- package/v3/@claude-flow/cli/dist/src/auth/security-bridge.js +42 -0
- package/v3/@claude-flow/cli/dist/src/auth/session.d.ts +20 -0
- package/v3/@claude-flow/cli/dist/src/auth/session.js +32 -0
- package/v3/@claude-flow/cli/dist/src/auth/state.d.ts +19 -0
- package/v3/@claude-flow/cli/dist/src/auth/state.js +53 -0
- package/v3/@claude-flow/cli/dist/src/auth/types.d.ts +27 -0
- package/v3/@claude-flow/cli/dist/src/auth/types.js +11 -0
- package/v3/@claude-flow/cli/dist/src/commands/auth.d.ts +15 -0
- package/v3/@claude-flow/cli/dist/src/commands/auth.js +244 -0
- package/v3/@claude-flow/cli/dist/src/commands/doctor.js +211 -4
- package/v3/@claude-flow/cli/dist/src/commands/index.js +2 -0
- package/v3/@claude-flow/cli/dist/src/commands/proxy-lifecycle.d.ts +20 -0
- package/v3/@claude-flow/cli/dist/src/commands/proxy-lifecycle.js +277 -0
- package/v3/@claude-flow/cli/dist/src/commands/proxy.js +97 -4
- package/v3/@claude-flow/cli/dist/src/mcp-tools/memory-tools.js +11 -3
- package/v3/@claude-flow/cli/dist/src/proxy/install.d.ts +29 -0
- package/v3/@claude-flow/cli/dist/src/proxy/install.js +135 -0
- package/v3/@claude-flow/cli/dist/src/proxy/lifecycle.d.ts +61 -0
- package/v3/@claude-flow/cli/dist/src/proxy/lifecycle.js +249 -0
- package/v3/@claude-flow/cli/dist/src/proxy/paths.d.ts +34 -0
- package/v3/@claude-flow/cli/dist/src/proxy/paths.js +70 -0
- package/v3/@claude-flow/cli/dist/src/proxy/release.d.ts +47 -0
- package/v3/@claude-flow/cli/dist/src/proxy/release.js +138 -0
- package/v3/@claude-flow/cli/dist/src/proxy/token-bridge.d.ts +5 -0
- package/v3/@claude-flow/cli/dist/src/proxy/token-bridge.js +61 -0
- package/v3/@claude-flow/cli/dist/src/proxy/verify.d.ts +44 -0
- package/v3/@claude-flow/cli/dist/src/proxy/verify.js +68 -0
- package/v3/@claude-flow/cli/package.json +3 -3
- package/.claude/.proven-config-version +0 -1
- package/.claude/proven-config.json +0 -42
|
@@ -23,6 +23,8 @@ 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, printProxyConsoleGuidance } from './proxy-lifecycle.js';
|
|
27
|
+
import { getProxyStatus } from '../proxy/lifecycle.js';
|
|
26
28
|
const PROXY_CONFIG_FILE = 'proxy-config.toml';
|
|
27
29
|
/**
|
|
28
30
|
* Minimal hand-rolled TOML writer — this config has exactly one boolean
|
|
@@ -40,12 +42,12 @@ function readProxyConfigRaw() {
|
|
|
40
42
|
return '';
|
|
41
43
|
}
|
|
42
44
|
}
|
|
43
|
-
function
|
|
45
|
+
function writeConfigLine(field, rawValue) {
|
|
44
46
|
const dir = funnelStateDir();
|
|
45
47
|
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
46
48
|
const target = path.join(dir, PROXY_CONFIG_FILE);
|
|
47
49
|
const raw = readProxyConfigRaw();
|
|
48
|
-
const line = `${field} = ${
|
|
50
|
+
const line = `${field} = ${rawValue}`;
|
|
49
51
|
const pattern = new RegExp(`^${field}\\s*=.*$`, 'm');
|
|
50
52
|
let next;
|
|
51
53
|
if (pattern.test(raw)) {
|
|
@@ -58,6 +60,9 @@ function writeConsentMirrorLine(field, value) {
|
|
|
58
60
|
fs.writeFileSync(tmp, next, { encoding: 'utf-8', mode: 0o600 });
|
|
59
61
|
fs.renameSync(tmp, target);
|
|
60
62
|
}
|
|
63
|
+
function writeConsentMirrorLine(field, value) {
|
|
64
|
+
writeConfigLine(field, String(value));
|
|
65
|
+
}
|
|
61
66
|
function writeSponsoredConsentMirror(granted) {
|
|
62
67
|
writeConsentMirrorLine('sponsored_consent_granted', granted);
|
|
63
68
|
}
|
|
@@ -67,6 +72,78 @@ function writePowerSaverConsentMirror(granted) {
|
|
|
67
72
|
function writeTrainingShareConsentMirror(granted) {
|
|
68
73
|
writeConsentMirrorLine('training_share_consent_granted', granted);
|
|
69
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* `default_data_plane` — the ADR-304 cloud-routing toggle. Values confirmed
|
|
77
|
+
* against meta-proxy's actual `DataPlane` enum (`src/config.rs`,
|
|
78
|
+
* `#[serde(rename_all = "snake_case")]`): "local" | "cloud" | "sponsored" |
|
|
79
|
+
* "passthrough" (the last two are not written by this command — sponsored
|
|
80
|
+
* is ADR-313's own consent flag, passthrough is the proxy's own default).
|
|
81
|
+
*/
|
|
82
|
+
function readDataPlane() {
|
|
83
|
+
const raw = readProxyConfigRaw();
|
|
84
|
+
const match = raw.match(/^default_data_plane\s*=\s*"([^"]*)"/m);
|
|
85
|
+
return match ? match[1] : 'passthrough'; // matches the Rust struct's own default
|
|
86
|
+
}
|
|
87
|
+
function writeDataPlane(plane) {
|
|
88
|
+
writeConfigLine('default_data_plane', `"${plane}"`);
|
|
89
|
+
}
|
|
90
|
+
const CLOUD_ROUTING_DISCLOSURE = [
|
|
91
|
+
'Enabling cloud routing.',
|
|
92
|
+
'',
|
|
93
|
+
'With cloud routing ON, prompts for cloud-tier requests are sent to',
|
|
94
|
+
'api.cognitum.one and forwarded to the selected provider',
|
|
95
|
+
'(Claude / GPT / Gemini / DeepSeek / OpenRouter).',
|
|
96
|
+
'',
|
|
97
|
+
'Requests routed to local backends never leave this machine.',
|
|
98
|
+
'',
|
|
99
|
+
'Disable anytime: ruflo proxy config --local-only',
|
|
100
|
+
].join('\n');
|
|
101
|
+
const configSub = {
|
|
102
|
+
name: 'config',
|
|
103
|
+
description: 'Toggle cloud routing (ADR-304) — local backends only by default',
|
|
104
|
+
options: [
|
|
105
|
+
{ name: 'cloud', description: 'Enable cloud routing (requires cloud-routing consent)', type: 'boolean', default: false },
|
|
106
|
+
{ name: 'local-only', description: 'Disable cloud routing, revert to local-only routing', type: 'boolean', default: false },
|
|
107
|
+
{ name: 'yes', description: 'Skip the confirmation prompt', type: 'boolean', default: false },
|
|
108
|
+
],
|
|
109
|
+
action: async (ctx) => {
|
|
110
|
+
const wantCloud = Boolean(ctx.flags.cloud);
|
|
111
|
+
const wantLocalOnly = Boolean(ctx.flags.localOnly ?? ctx.flags['local-only']);
|
|
112
|
+
if (wantCloud && wantLocalOnly) {
|
|
113
|
+
output.printError('Pass either --cloud or --local-only, not both.');
|
|
114
|
+
return { success: false, exitCode: 1 };
|
|
115
|
+
}
|
|
116
|
+
if (!wantCloud && !wantLocalOnly) {
|
|
117
|
+
const plane = readDataPlane();
|
|
118
|
+
output.writeln(`Current data plane: ${plane}`);
|
|
119
|
+
output.writeln(plane === 'cloud'
|
|
120
|
+
? 'Cloud routing is ON — cloud-tier requests go to api.cognitum.one.'
|
|
121
|
+
: 'Cloud routing is OFF — requests never leave this machine (or use your own Claude subscription on Passthrough).');
|
|
122
|
+
return { success: true, data: { plane } };
|
|
123
|
+
}
|
|
124
|
+
if (wantLocalOnly) {
|
|
125
|
+
writeDataPlane('local');
|
|
126
|
+
revokeConsent('cloud-routing', 'proxy-config-local-only');
|
|
127
|
+
output.printSuccess('Cloud routing disabled — reverted to local-only routing.');
|
|
128
|
+
return { success: true, data: { plane: 'local' } };
|
|
129
|
+
}
|
|
130
|
+
// wantCloud
|
|
131
|
+
if (!hasConsent('cloud-routing')) {
|
|
132
|
+
output.writeln(CLOUD_ROUTING_DISCLOSURE);
|
|
133
|
+
output.writeln('');
|
|
134
|
+
if (!ctx.flags.yes) {
|
|
135
|
+
output.writeln('Re-run with --yes to confirm: ruflo proxy config --cloud --yes');
|
|
136
|
+
return { success: true, data: { confirmed: false } };
|
|
137
|
+
}
|
|
138
|
+
recordConsent('cloud-routing', true, 'proxy-config-cloud');
|
|
139
|
+
}
|
|
140
|
+
writeDataPlane('cloud');
|
|
141
|
+
output.printSuccess('Cloud routing enabled.');
|
|
142
|
+
output.writeln(' Requests routed to local backends still never leave this machine.');
|
|
143
|
+
output.writeln(' Disable anytime: ruflo proxy config --local-only');
|
|
144
|
+
return { success: true, data: { plane: 'cloud' } };
|
|
145
|
+
},
|
|
146
|
+
};
|
|
70
147
|
const SPONSOR_DISCLOSURE = [
|
|
71
148
|
'Enabling sponsored downtime mode.',
|
|
72
149
|
'',
|
|
@@ -292,19 +369,35 @@ const trainingShareStatusSub = {
|
|
|
292
369
|
};
|
|
293
370
|
export const proxyCommand = {
|
|
294
371
|
name: 'proxy',
|
|
295
|
-
description: 'Meta LLM Proxy — sponsored downtime + power saver + training-data sharing (ADR-304/307/313/314/315)',
|
|
372
|
+
description: 'Meta LLM Proxy — install/lifecycle + sponsored downtime + power saver + training-data sharing (ADR-304/307/313/314/315)',
|
|
296
373
|
subcommands: [
|
|
374
|
+
...proxyLifecycleSubcommands,
|
|
375
|
+
configSub,
|
|
297
376
|
sponsorEnableSub, sponsorDisableSub, sponsorStatusSub, sponsorClearSub,
|
|
298
377
|
powerSaverEnableSub, powerSaverDisableSub, powerSaverStatusSub, powerSaverClearSub,
|
|
299
378
|
trainingShareEnableSub, trainingShareDisableSub, trainingShareStatusSub,
|
|
300
379
|
],
|
|
301
380
|
examples: [
|
|
381
|
+
{ command: 'ruflo proxy install --yes', description: 'Install the signed Meta-Proxy v0.4.0 binary' },
|
|
382
|
+
{ command: 'ruflo proxy start', description: 'Start meta-proxy in the foreground' },
|
|
383
|
+
{ command: 'ruflo proxy status', description: 'Show install + process status' },
|
|
384
|
+
{ command: 'ruflo proxy config --cloud --yes', description: 'Enable cloud routing (ADR-304)' },
|
|
385
|
+
{ command: 'ruflo proxy config --local-only', description: 'Revert to local-only routing' },
|
|
302
386
|
{ command: 'ruflo proxy sponsor-status', description: 'Show current sponsored-mode state' },
|
|
303
387
|
{ command: 'ruflo proxy sponsor-enable --yes', description: 'Opt into sponsored downtime capacity' },
|
|
304
388
|
{ command: 'ruflo proxy power-saver-enable --yes', description: 'Opt into power saver mode' },
|
|
305
389
|
{ command: 'ruflo proxy training-share-enable --yes', description: 'Opt into training-data sharing (ADR-315)' },
|
|
306
390
|
],
|
|
307
|
-
action:
|
|
391
|
+
action: async () => {
|
|
392
|
+
const status = getProxyStatus();
|
|
393
|
+
output.writeln('Meta Proxy');
|
|
394
|
+
output.writeln(` Installation: ${status.installed ? 'ready' : 'not installed'}`);
|
|
395
|
+
output.writeln(` Process: ${status.running ? `running (pid ${status.pid})` : 'not running'}`);
|
|
396
|
+
if (status.stalePidFile)
|
|
397
|
+
output.writeln(' (a stale PID file was found and will be cleared on next start)');
|
|
398
|
+
printProxyConsoleGuidance(status);
|
|
399
|
+
return { success: true, data: status };
|
|
400
|
+
},
|
|
308
401
|
};
|
|
309
402
|
export default proxyCommand;
|
|
310
403
|
//# sourceMappingURL=proxy.js.map
|
|
@@ -389,7 +389,7 @@ export const memoryTools = [
|
|
|
389
389
|
type: 'object',
|
|
390
390
|
properties: {
|
|
391
391
|
query: { type: 'string', description: 'Search query (semantic similarity)' },
|
|
392
|
-
namespace: { type: 'string', description: 'Namespace to search (default:
|
|
392
|
+
namespace: { type: 'string', description: 'Namespace to search (default: all namespaces — omit to search across every namespace)' },
|
|
393
393
|
limit: { type: 'number', description: 'Maximum results (default: 10)' },
|
|
394
394
|
threshold: { type: 'number', description: 'Minimum similarity threshold 0-1 (default: 0.3)' },
|
|
395
395
|
smart: { type: 'boolean', description: 'Enable SmartRetrieval pipeline — query expansion, RRF fusion, recency boost, MMR diversity (default: false)' },
|
|
@@ -400,10 +400,18 @@ export const memoryTools = [
|
|
|
400
400
|
await ensureInitialized();
|
|
401
401
|
const { searchEntries } = await getMemoryFunctions();
|
|
402
402
|
const query = input.query;
|
|
403
|
-
|
|
403
|
+
// #2646 (3rd occurrence of #1123/#1131 shape): do NOT coerce an omitted
|
|
404
|
+
// namespace to the literal string 'default' here. Both searchEntries()
|
|
405
|
+
// and bridgeSearchEntries() already resolve an omitted/undefined
|
|
406
|
+
// namespace to 'all' (fan out across every namespace) — but 'default'
|
|
407
|
+
// is truthy, so passing it defeats that fallback and silently scopes
|
|
408
|
+
// the search to a namespace that is usually empty. Leave namespace
|
|
409
|
+
// undefined when not provided so the underlying `|| 'all'` fallback
|
|
410
|
+
// in the search layer actually fires.
|
|
411
|
+
const namespace = input.namespace;
|
|
404
412
|
const limit = input.limit ?? 10;
|
|
405
413
|
const threshold = input.threshold ?? 0.3;
|
|
406
|
-
validateMemoryInput(undefined, undefined, query);
|
|
414
|
+
validateMemoryInput(undefined, undefined, query, namespace);
|
|
407
415
|
const startTime = performance.now();
|
|
408
416
|
try {
|
|
409
417
|
// #1846: feature-detect smartSearch on the resolved memory package.
|
|
@@ -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
|
|
@@ -0,0 +1,249 @@
|
|
|
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 { spawn } from 'node:child_process';
|
|
22
|
+
import * as fs from 'node:fs';
|
|
23
|
+
import { proxyBinaryPath, proxyPidFilePath, proxyLockFilePath, proxyLogFilePath } from './paths.js';
|
|
24
|
+
export class ProxyNotInstalledError extends Error {
|
|
25
|
+
constructor() {
|
|
26
|
+
super('meta-proxy is not installed. Run: ruflo proxy install');
|
|
27
|
+
this.name = 'ProxyNotInstalledError';
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export class ProxyAlreadyRunningError extends Error {
|
|
31
|
+
pid;
|
|
32
|
+
constructor(pid) {
|
|
33
|
+
super(`meta-proxy is already running (pid ${pid}). Stop it first with: ruflo proxy stop`);
|
|
34
|
+
this.pid = pid;
|
|
35
|
+
this.name = 'ProxyAlreadyRunningError';
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function requireBinary() {
|
|
39
|
+
const bin = proxyBinaryPath();
|
|
40
|
+
if (!fs.existsSync(bin))
|
|
41
|
+
throw new ProxyNotInstalledError();
|
|
42
|
+
return bin;
|
|
43
|
+
}
|
|
44
|
+
/** Signal-0 liveness probe — cross-platform in Node, throws if the process is dead. */
|
|
45
|
+
function isProcessRunning(pid) {
|
|
46
|
+
try {
|
|
47
|
+
process.kill(pid, 0);
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
export function getProxyStatus() {
|
|
55
|
+
const installed = fs.existsSync(proxyBinaryPath());
|
|
56
|
+
const pidPath = proxyPidFilePath();
|
|
57
|
+
if (!fs.existsSync(pidPath)) {
|
|
58
|
+
return { installed, running: false, pid: null, stalePidFile: false };
|
|
59
|
+
}
|
|
60
|
+
const raw = fs.readFileSync(pidPath, 'utf-8').trim();
|
|
61
|
+
const pid = parseInt(raw, 10);
|
|
62
|
+
if (!Number.isFinite(pid)) {
|
|
63
|
+
return { installed, running: false, pid: null, stalePidFile: true };
|
|
64
|
+
}
|
|
65
|
+
const running = isProcessRunning(pid);
|
|
66
|
+
return { installed, running, pid: running ? pid : null, stalePidFile: !running };
|
|
67
|
+
}
|
|
68
|
+
function writePidFile(pid) {
|
|
69
|
+
fs.writeFileSync(proxyPidFilePath(), String(pid), 'utf-8');
|
|
70
|
+
}
|
|
71
|
+
function clearStalePidFile() {
|
|
72
|
+
try {
|
|
73
|
+
fs.unlinkSync(proxyPidFilePath());
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
// already absent
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Atomic check-then-start via an O_EXCL lockfile — the same fix daemon.ts
|
|
81
|
+
* applies for the identical race (#2407/#2484: without this, N concurrent
|
|
82
|
+
* `start` calls all see "not running" and all spawn their own process).
|
|
83
|
+
* Returns the lock fd to release once the PID file has been written (or on
|
|
84
|
+
* any early-return/throw), never before.
|
|
85
|
+
*/
|
|
86
|
+
function acquireStartLock() {
|
|
87
|
+
const lockFile = proxyLockFilePath();
|
|
88
|
+
try {
|
|
89
|
+
return fs.openSync(lockFile, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY);
|
|
90
|
+
}
|
|
91
|
+
catch (e) {
|
|
92
|
+
if (e.code === 'EEXIST')
|
|
93
|
+
return null; // another start is mid-spawn
|
|
94
|
+
throw e;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
function releaseStartLock(fd) {
|
|
98
|
+
if (fd === null)
|
|
99
|
+
return;
|
|
100
|
+
try {
|
|
101
|
+
fs.closeSync(fd);
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
/* ignore */
|
|
105
|
+
}
|
|
106
|
+
try {
|
|
107
|
+
fs.unlinkSync(proxyLockFilePath());
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
/* ignore */
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Foreground start (ADR-307 default) — blocks the caller until the process
|
|
115
|
+
* exits or is interrupted. `stdio: 'inherit'` passes the proxy's own output
|
|
116
|
+
* straight through to the terminal; signals (Ctrl+C) propagate naturally to
|
|
117
|
+
* the child, no manual forwarding needed.
|
|
118
|
+
*/
|
|
119
|
+
export async function startForeground(supervised = false) {
|
|
120
|
+
const bin = requireBinary();
|
|
121
|
+
const status = getProxyStatus();
|
|
122
|
+
// In service mode startBackground has already written this supervisor's
|
|
123
|
+
// PID. Treating it as a competing proxy makes the supervisor immediately
|
|
124
|
+
// exit before it can spawn meta-proxy.
|
|
125
|
+
if (!supervised && status.running && status.pid)
|
|
126
|
+
throw new ProxyAlreadyRunningError(status.pid);
|
|
127
|
+
if (status.stalePidFile)
|
|
128
|
+
clearStalePidFile();
|
|
129
|
+
const child = spawn(bin, [], { stdio: 'inherit', windowsHide: false });
|
|
130
|
+
if (!supervised && child.pid)
|
|
131
|
+
writePidFile(child.pid);
|
|
132
|
+
const cleanup = () => clearStalePidFile();
|
|
133
|
+
process.on('exit', cleanup);
|
|
134
|
+
if (supervised) {
|
|
135
|
+
const forwardSignal = (signal) => {
|
|
136
|
+
if (!child.killed)
|
|
137
|
+
child.kill(signal);
|
|
138
|
+
};
|
|
139
|
+
process.once('SIGTERM', () => forwardSignal('SIGTERM'));
|
|
140
|
+
process.once('SIGINT', () => forwardSignal('SIGINT'));
|
|
141
|
+
}
|
|
142
|
+
await new Promise((resolve) => {
|
|
143
|
+
child.on('exit', () => {
|
|
144
|
+
cleanup();
|
|
145
|
+
resolve();
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
process.exit(0);
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Background/`--service` start — detaches so the process outlives this
|
|
152
|
+
* `ruflo` invocation, redirecting stdout/stderr to a real log file (not
|
|
153
|
+
* buffered in-process). Returns once the child's PID is confirmed written,
|
|
154
|
+
* without waiting for the process to exit.
|
|
155
|
+
*/
|
|
156
|
+
export async function startBackground() {
|
|
157
|
+
requireBinary();
|
|
158
|
+
const status = getProxyStatus();
|
|
159
|
+
if (status.running && status.pid)
|
|
160
|
+
throw new ProxyAlreadyRunningError(status.pid);
|
|
161
|
+
const lockFd = acquireStartLock();
|
|
162
|
+
try {
|
|
163
|
+
// Re-check under the lock — the dedup above raced a concurrent starter.
|
|
164
|
+
const rechecked = getProxyStatus();
|
|
165
|
+
if (rechecked.running && rechecked.pid)
|
|
166
|
+
throw new ProxyAlreadyRunningError(rechecked.pid);
|
|
167
|
+
if (rechecked.stalePidFile)
|
|
168
|
+
clearStalePidFile();
|
|
169
|
+
const logFd = fs.openSync(proxyLogFilePath(), 'a');
|
|
170
|
+
const cliEntry = process.argv[1];
|
|
171
|
+
if (!cliEntry)
|
|
172
|
+
throw new Error('cannot locate the ruflo CLI entrypoint for supervised service mode');
|
|
173
|
+
const child = spawn(process.execPath, [cliEntry, 'proxy', 'supervise'], {
|
|
174
|
+
stdio: ['ignore', logFd, logFd],
|
|
175
|
+
detached: true,
|
|
176
|
+
windowsHide: true,
|
|
177
|
+
});
|
|
178
|
+
fs.closeSync(logFd); // the child holds its own duplicated fd; safe to close ours
|
|
179
|
+
if (!child.pid)
|
|
180
|
+
throw new Error('ruflo proxy supervisor failed to spawn — no PID returned');
|
|
181
|
+
writePidFile(child.pid);
|
|
182
|
+
child.unref();
|
|
183
|
+
return { pid: child.pid };
|
|
184
|
+
}
|
|
185
|
+
finally {
|
|
186
|
+
releaseStartLock(lockFd);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
/** SIGTERM -> 1000ms -> SIGKILL if still alive, mirroring daemon.ts's killBackgroundDaemon. */
|
|
190
|
+
export async function stopProxy() {
|
|
191
|
+
const status = getProxyStatus();
|
|
192
|
+
if (!status.running || !status.pid) {
|
|
193
|
+
if (status.stalePidFile)
|
|
194
|
+
clearStalePidFile();
|
|
195
|
+
return { wasRunning: false, pid: null };
|
|
196
|
+
}
|
|
197
|
+
const pid = status.pid;
|
|
198
|
+
process.kill(pid, 'SIGTERM');
|
|
199
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
200
|
+
if (isProcessRunning(pid)) {
|
|
201
|
+
process.kill(pid, 'SIGKILL');
|
|
202
|
+
}
|
|
203
|
+
clearStalePidFile();
|
|
204
|
+
return { wasRunning: true, pid };
|
|
205
|
+
}
|
|
206
|
+
const TAIL_BYTES = 64 * 1024; // bounded read — a long-running proxy's log grows over weeks
|
|
207
|
+
export function readProxyLogTail(maxBytes = TAIL_BYTES) {
|
|
208
|
+
const logPath = proxyLogFilePath();
|
|
209
|
+
if (!fs.existsSync(logPath))
|
|
210
|
+
return '';
|
|
211
|
+
const { size } = fs.statSync(logPath);
|
|
212
|
+
const start = Math.max(0, size - maxBytes);
|
|
213
|
+
const fd = fs.openSync(logPath, 'r');
|
|
214
|
+
try {
|
|
215
|
+
const buf = Buffer.alloc(size - start);
|
|
216
|
+
fs.readSync(fd, buf, 0, buf.length, start);
|
|
217
|
+
return buf.toString('utf-8');
|
|
218
|
+
}
|
|
219
|
+
finally {
|
|
220
|
+
fs.closeSync(fd);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
/** Streams new log lines as they're appended, starting from the current end of file. */
|
|
224
|
+
export function watchProxyLog(onLine) {
|
|
225
|
+
const logPath = proxyLogFilePath();
|
|
226
|
+
if (!fs.existsSync(logPath)) {
|
|
227
|
+
throw new Error('no log file yet — meta-proxy has never been started in --service mode');
|
|
228
|
+
}
|
|
229
|
+
let offset = fs.statSync(logPath).size;
|
|
230
|
+
return fs.watch(logPath, () => {
|
|
231
|
+
const { size } = fs.statSync(logPath);
|
|
232
|
+
if (size <= offset)
|
|
233
|
+
return;
|
|
234
|
+
const fd = fs.openSync(logPath, 'r');
|
|
235
|
+
try {
|
|
236
|
+
const buf = Buffer.alloc(size - offset);
|
|
237
|
+
fs.readSync(fd, buf, 0, buf.length, offset);
|
|
238
|
+
offset = size;
|
|
239
|
+
for (const line of buf.toString('utf-8').split('\n')) {
|
|
240
|
+
if (line.length > 0)
|
|
241
|
+
onLine(line);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
finally {
|
|
245
|
+
fs.closeSync(fd);
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
//# sourceMappingURL=lifecycle.js.map
|