@lamalibre/portlama-agent 1.0.21 → 1.0.23
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/package.json +1 -1
- package/src/commands/panel.js +59 -40
- package/src/commands/plugin.js +23 -159
- package/src/commands/update.js +30 -0
- package/src/index.js +1 -1
- package/src/lib/agent-plugin-router.js +141 -0
- package/src/lib/agent-plugins.js +358 -0
- package/src/lib/keychain.js +0 -169
- package/src/lib/local-plugin-host.js +7 -2
- package/src/lib/local-plugins.js +2 -1
- package/src/lib/panel-api-routes.js +130 -0
- package/src/lib/panel-server.js +63 -2
- package/src/lib/panel-service.js +3 -1
package/package.json
CHANGED
package/src/commands/panel.js
CHANGED
|
@@ -24,9 +24,9 @@ const DEFAULT_PANEL_PORT = 9393;
|
|
|
24
24
|
|
|
25
25
|
/**
|
|
26
26
|
* @param {string[]} args
|
|
27
|
-
* @param {{ label: string }} options
|
|
27
|
+
* @param {{ label: string, json?: boolean }} options
|
|
28
28
|
*/
|
|
29
|
-
export async function runPanel(args, { label }) {
|
|
29
|
+
export async function runPanel(args, { label, json: globalJson = false }) {
|
|
30
30
|
assertSupportedPlatform();
|
|
31
31
|
|
|
32
32
|
const config = await loadAgentConfig(label);
|
|
@@ -35,9 +35,10 @@ export async function runPanel(args, { label }) {
|
|
|
35
35
|
process.exit(1);
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
const isJson = args.includes('--json');
|
|
38
|
+
const isJson = globalJson || args.includes('--json');
|
|
39
39
|
const isEnable = args.includes('--enable');
|
|
40
40
|
const isDisable = args.includes('--disable');
|
|
41
|
+
const isLocalOnly = args.includes('--local-only');
|
|
41
42
|
const isStatus = args.includes('--status') || (!isEnable && !isDisable);
|
|
42
43
|
|
|
43
44
|
// Parse --port flag
|
|
@@ -51,24 +52,24 @@ export async function runPanel(args, { label }) {
|
|
|
51
52
|
}
|
|
52
53
|
|
|
53
54
|
if (isEnable) {
|
|
54
|
-
await enablePanel(label, config, port, isJson);
|
|
55
|
+
await enablePanel(label, config, port, isJson, isLocalOnly);
|
|
55
56
|
} else if (isDisable) {
|
|
56
|
-
await disablePanel(label, config, isJson);
|
|
57
|
+
await disablePanel(label, config, isJson, isLocalOnly);
|
|
57
58
|
} else if (isStatus) {
|
|
58
59
|
await showStatus(label, config, isJson);
|
|
59
60
|
}
|
|
60
61
|
}
|
|
61
62
|
|
|
62
|
-
async function enablePanel(label, config, port, isJson) {
|
|
63
|
+
async function enablePanel(label, config, port, isJson, localOnly = false) {
|
|
63
64
|
// 1. Check if already enabled
|
|
64
65
|
const loaded = await isPanelLoaded(label);
|
|
65
66
|
if (loaded) {
|
|
66
67
|
if (isJson) {
|
|
67
|
-
console.log(JSON.stringify({
|
|
68
|
+
console.log(JSON.stringify({ ok: true, alreadyRunning: true, port: config.panelPort || port }));
|
|
68
69
|
} else {
|
|
69
70
|
console.log(chalk.yellow('Panel service is already running.'));
|
|
70
71
|
}
|
|
71
|
-
|
|
72
|
+
return;
|
|
72
73
|
}
|
|
73
74
|
|
|
74
75
|
// 2. Generate and write service config
|
|
@@ -78,7 +79,24 @@ async function enablePanel(label, config, port, isJson) {
|
|
|
78
79
|
// 3. Start the panel service
|
|
79
80
|
await loadPanelService(label);
|
|
80
81
|
|
|
81
|
-
// 4.
|
|
82
|
+
// 4. Save panel config
|
|
83
|
+
config.panelPort = port;
|
|
84
|
+
config.panelEnabled = true;
|
|
85
|
+
config.updatedAt = new Date().toISOString();
|
|
86
|
+
await saveAgentConfig(label, config);
|
|
87
|
+
|
|
88
|
+
// 5. If local-only, skip tunnel exposure and chisel restart
|
|
89
|
+
if (localOnly) {
|
|
90
|
+
if (isJson) {
|
|
91
|
+
console.log(JSON.stringify({ ok: true, port }));
|
|
92
|
+
} else {
|
|
93
|
+
console.log(chalk.green('\nAgent panel started locally.'));
|
|
94
|
+
console.log(` Port: ${chalk.cyan(port)}`);
|
|
95
|
+
}
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 6. Create the tunnel on the panel server
|
|
82
100
|
let tunnel;
|
|
83
101
|
try {
|
|
84
102
|
const result = await exposePanelTunnel(config, port);
|
|
@@ -87,6 +105,9 @@ async function enablePanel(label, config, port, isJson) {
|
|
|
87
105
|
// Rollback: stop the panel service we just started
|
|
88
106
|
await unloadPanelService(label);
|
|
89
107
|
await removePanelServiceConfig(label);
|
|
108
|
+
config.panelEnabled = false;
|
|
109
|
+
delete config.panelPort;
|
|
110
|
+
await saveAgentConfig(label, config);
|
|
90
111
|
const msg = err instanceof Error ? err.message : 'Unknown error';
|
|
91
112
|
if (isJson) {
|
|
92
113
|
console.log(JSON.stringify({ error: msg }));
|
|
@@ -96,13 +117,7 @@ async function enablePanel(label, config, port, isJson) {
|
|
|
96
117
|
process.exit(1);
|
|
97
118
|
}
|
|
98
119
|
|
|
99
|
-
//
|
|
100
|
-
config.panelPort = port;
|
|
101
|
-
config.panelEnabled = true;
|
|
102
|
-
config.updatedAt = new Date().toISOString();
|
|
103
|
-
await saveAgentConfig(label, config);
|
|
104
|
-
|
|
105
|
-
// 6. Update the agent's chisel service (needs new tunnel mapping)
|
|
120
|
+
// 7. Update the agent's chisel service (needs new tunnel mapping)
|
|
106
121
|
try {
|
|
107
122
|
const { fetchAgentConfig } = await import('../lib/panel-api.js');
|
|
108
123
|
const { generateServiceConfig, writeServiceConfigFile } =
|
|
@@ -132,14 +147,16 @@ async function enablePanel(label, config, port, isJson) {
|
|
|
132
147
|
}
|
|
133
148
|
}
|
|
134
149
|
|
|
135
|
-
async function disablePanel(label, config, isJson) {
|
|
136
|
-
// 1. Retract the tunnel
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
150
|
+
async function disablePanel(label, config, isJson, localOnly = false) {
|
|
151
|
+
// 1. Retract the tunnel (skip in local-only mode)
|
|
152
|
+
if (!localOnly) {
|
|
153
|
+
try {
|
|
154
|
+
await retractPanelTunnel(config);
|
|
155
|
+
} catch (err) {
|
|
156
|
+
if (!isJson) {
|
|
157
|
+
const msg = err instanceof Error ? err.message : 'Unknown error';
|
|
158
|
+
console.log(chalk.yellow(`Warning: Could not retract panel tunnel: ${msg}`));
|
|
159
|
+
}
|
|
143
160
|
}
|
|
144
161
|
}
|
|
145
162
|
|
|
@@ -149,21 +166,23 @@ async function disablePanel(label, config, isJson) {
|
|
|
149
166
|
// 3. Remove service config
|
|
150
167
|
await removePanelServiceConfig(label);
|
|
151
168
|
|
|
152
|
-
// 4. Update chisel (remove the panel tunnel mapping)
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
169
|
+
// 4. Update chisel (remove the panel tunnel mapping) — skip in local-only mode
|
|
170
|
+
if (!localOnly) {
|
|
171
|
+
try {
|
|
172
|
+
const { fetchAgentConfig } = await import('../lib/panel-api.js');
|
|
173
|
+
const { generateServiceConfig, writeServiceConfigFile } =
|
|
174
|
+
await import('../lib/service-config.js');
|
|
175
|
+
const { unloadAgent, loadAgent } = await import('../lib/service.js');
|
|
176
|
+
const agentConfig = await fetchAgentConfig(config);
|
|
177
|
+
const serviceContent = generateServiceConfig(agentConfig.chiselArgs, label);
|
|
178
|
+
await writeServiceConfigFile(serviceContent, label);
|
|
179
|
+
await unloadAgent(label);
|
|
180
|
+
await loadAgent(label);
|
|
181
|
+
} catch (err) {
|
|
182
|
+
if (!isJson) {
|
|
183
|
+
const msg = err instanceof Error ? err.message : 'Unknown error';
|
|
184
|
+
console.log(chalk.yellow(`Warning: Could not restart chisel: ${msg}`));
|
|
185
|
+
}
|
|
167
186
|
}
|
|
168
187
|
}
|
|
169
188
|
|
|
@@ -176,7 +195,7 @@ async function disablePanel(label, config, isJson) {
|
|
|
176
195
|
if (isJson) {
|
|
177
196
|
console.log(JSON.stringify({ ok: true }));
|
|
178
197
|
} else {
|
|
179
|
-
console.log(chalk.green('Agent panel retracted.'));
|
|
198
|
+
console.log(chalk.green(localOnly ? 'Agent panel stopped.' : 'Agent panel retracted.'));
|
|
180
199
|
}
|
|
181
200
|
}
|
|
182
201
|
|
package/src/commands/plugin.js
CHANGED
|
@@ -1,46 +1,12 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
import {
|
|
3
|
-
import { readFile, writeFile, rename, open, mkdir, rm } from 'node:fs/promises';
|
|
4
|
-
import { createRequire } from 'node:module';
|
|
5
|
-
import path from 'node:path';
|
|
6
|
-
import { assertSupportedPlatform, agentDataDir, agentPluginsFile, agentPluginsDir } from '../lib/platform.js';
|
|
2
|
+
import { assertSupportedPlatform } from '../lib/platform.js';
|
|
7
3
|
import { validateLabel } from '../lib/registry.js';
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
try {
|
|
15
|
-
const raw = await readFile(agentPluginsFile(label), 'utf-8');
|
|
16
|
-
const parsed = JSON.parse(raw);
|
|
17
|
-
return Array.isArray(parsed.plugins) ? parsed : { plugins: [] };
|
|
18
|
-
} catch (err) {
|
|
19
|
-
if (err.code === 'ENOENT') {
|
|
20
|
-
return { plugins: [] };
|
|
21
|
-
}
|
|
22
|
-
throw new Error(`Failed to read local plugin registry: ${err.message}`);
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Write the local plugin registry atomically.
|
|
28
|
-
* @param {string} label - Agent label
|
|
29
|
-
* @param {object} data
|
|
30
|
-
*/
|
|
31
|
-
async function writeLocalPlugins(label, data) {
|
|
32
|
-
const pluginsFile = agentPluginsFile(label);
|
|
33
|
-
const tmpPath = `${pluginsFile}.tmp`;
|
|
34
|
-
|
|
35
|
-
const content = JSON.stringify(data, null, 2) + '\n';
|
|
36
|
-
await writeFile(tmpPath, content, { encoding: 'utf-8', mode: 0o600 });
|
|
37
|
-
|
|
38
|
-
const fd = await open(tmpPath, 'r');
|
|
39
|
-
await fd.sync();
|
|
40
|
-
await fd.close();
|
|
41
|
-
|
|
42
|
-
await rename(tmpPath, pluginsFile);
|
|
43
|
-
}
|
|
4
|
+
import {
|
|
5
|
+
readAgentPluginRegistry,
|
|
6
|
+
installAgentPlugin,
|
|
7
|
+
uninstallAgentPlugin,
|
|
8
|
+
updateAgentPlugin,
|
|
9
|
+
} from '../lib/agent-plugins.js';
|
|
44
10
|
|
|
45
11
|
/**
|
|
46
12
|
* Install a plugin locally on the agent.
|
|
@@ -48,71 +14,14 @@ async function writeLocalPlugins(label, data) {
|
|
|
48
14
|
* @param {string} packageName
|
|
49
15
|
*/
|
|
50
16
|
async function installLocal(label, packageName) {
|
|
51
|
-
if (!packageName.startsWith('@lamalibre/')) {
|
|
52
|
-
console.error(chalk.red(' Only @lamalibre/ scoped packages are allowed'));
|
|
53
|
-
process.exit(1);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const registry = await readLocalPlugins(label);
|
|
57
|
-
const existing = registry.plugins.find((p) => p.packageName === packageName);
|
|
58
|
-
if (existing) {
|
|
59
|
-
console.log(chalk.yellow(` Plugin "${packageName}" is already installed`));
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
17
|
console.log(chalk.cyan(` Installing ${packageName}...`));
|
|
64
|
-
|
|
65
|
-
// Ensure agent dir has a package.json so npm installs locally
|
|
66
|
-
const agentDir = agentDataDir(label);
|
|
67
|
-
await mkdir(agentDir, { recursive: true, mode: 0o700 });
|
|
68
|
-
const agentPkgJson = path.join(agentDir, 'package.json');
|
|
69
|
-
try {
|
|
70
|
-
await readFile(agentPkgJson, 'utf-8');
|
|
71
|
-
} catch (err) {
|
|
72
|
-
if (err.code === 'ENOENT') {
|
|
73
|
-
await writeFile(agentPkgJson, '{"private":true}\n', { encoding: 'utf-8', mode: 0o600 });
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
18
|
try {
|
|
78
|
-
await
|
|
19
|
+
const entry = await installAgentPlugin(label, packageName);
|
|
20
|
+
console.log(chalk.green(` Plugin "${entry.name}" installed`));
|
|
79
21
|
} catch (err) {
|
|
80
|
-
console.error(chalk.red(`
|
|
81
|
-
process.exit(1);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// Read the plugin manifest
|
|
85
|
-
let manifest;
|
|
86
|
-
try {
|
|
87
|
-
const require = createRequire(path.join(agentDir, '/'));
|
|
88
|
-
const manifestPath = require.resolve(`${packageName}/portlama-plugin.json`);
|
|
89
|
-
const manifestRaw = await readFile(manifestPath, 'utf-8');
|
|
90
|
-
manifest = JSON.parse(manifestRaw);
|
|
91
|
-
} catch {
|
|
92
|
-
console.log(chalk.yellow(' Warning: No portlama-plugin.json found — registering with package name only'));
|
|
93
|
-
manifest = { name: packageName.replace('@lamalibre/', ''), version: 'unknown' };
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Validate manifest name to prevent path traversal
|
|
97
|
-
if (!/^[a-z0-9-]+$/.test(manifest.name)) {
|
|
98
|
-
console.error(chalk.red(` Invalid plugin name: "${manifest.name}"`));
|
|
22
|
+
console.error(chalk.red(` ${err.message}`));
|
|
99
23
|
process.exit(1);
|
|
100
24
|
}
|
|
101
|
-
|
|
102
|
-
// Create local plugin directory
|
|
103
|
-
const pluginDir = path.join(agentPluginsDir(label), manifest.name);
|
|
104
|
-
await mkdir(pluginDir, { recursive: true, mode: 0o700 });
|
|
105
|
-
|
|
106
|
-
registry.plugins.push({
|
|
107
|
-
name: manifest.name,
|
|
108
|
-
packageName,
|
|
109
|
-
version: manifest.version,
|
|
110
|
-
installedAt: new Date().toISOString(),
|
|
111
|
-
status: 'installed',
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
await writeLocalPlugins(label, registry);
|
|
115
|
-
console.log(chalk.green(` Plugin "${manifest.name}" installed`));
|
|
116
25
|
}
|
|
117
26
|
|
|
118
27
|
/**
|
|
@@ -121,38 +30,24 @@ async function installLocal(label, packageName) {
|
|
|
121
30
|
* @param {string} nameOrPackage
|
|
122
31
|
*/
|
|
123
32
|
async function uninstallLocal(label, nameOrPackage) {
|
|
124
|
-
|
|
125
|
-
const
|
|
33
|
+
// Resolve name from registry if a package name was given
|
|
34
|
+
const registry = await readAgentPluginRegistry(label);
|
|
35
|
+
const plugin = registry.plugins.find(
|
|
126
36
|
(p) => p.name === nameOrPackage || p.packageName === nameOrPackage,
|
|
127
37
|
);
|
|
128
|
-
|
|
129
|
-
if (index === -1) {
|
|
38
|
+
if (!plugin) {
|
|
130
39
|
console.error(chalk.red(` Plugin "${nameOrPackage}" not found`));
|
|
131
40
|
process.exit(1);
|
|
132
41
|
}
|
|
133
42
|
|
|
134
|
-
const plugin = registry.plugins[index];
|
|
135
|
-
|
|
136
|
-
if (!plugin.packageName.startsWith('@lamalibre/')) {
|
|
137
|
-
console.error(chalk.red(' Registry corruption: invalid package scope'));
|
|
138
|
-
process.exit(1);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
43
|
console.log(chalk.cyan(` Uninstalling ${plugin.packageName}...`));
|
|
142
|
-
|
|
143
44
|
try {
|
|
144
|
-
await
|
|
45
|
+
await uninstallAgentPlugin(label, plugin.name);
|
|
46
|
+
console.log(chalk.green(` Plugin "${plugin.name}" uninstalled`));
|
|
145
47
|
} catch (err) {
|
|
146
|
-
console.
|
|
48
|
+
console.error(chalk.red(` ${err.message}`));
|
|
49
|
+
process.exit(1);
|
|
147
50
|
}
|
|
148
|
-
|
|
149
|
-
// Remove local plugin directory
|
|
150
|
-
const pluginDir = path.join(agentPluginsDir(label), plugin.name);
|
|
151
|
-
await rm(pluginDir, { recursive: true, force: true }).catch(() => {});
|
|
152
|
-
|
|
153
|
-
registry.plugins.splice(index, 1);
|
|
154
|
-
await writeLocalPlugins(label, registry);
|
|
155
|
-
console.log(chalk.green(` Plugin "${plugin.name}" uninstalled`));
|
|
156
51
|
}
|
|
157
52
|
|
|
158
53
|
/**
|
|
@@ -161,45 +56,14 @@ async function uninstallLocal(label, nameOrPackage) {
|
|
|
161
56
|
* @param {string} nameOrPackage
|
|
162
57
|
*/
|
|
163
58
|
async function updateLocal(label, nameOrPackage) {
|
|
164
|
-
|
|
165
|
-
const plugin = registry.plugins.find(
|
|
166
|
-
(p) => p.name === nameOrPackage || p.packageName === nameOrPackage,
|
|
167
|
-
);
|
|
168
|
-
|
|
169
|
-
if (!plugin) {
|
|
170
|
-
console.error(chalk.red(` Plugin "${nameOrPackage}" not found`));
|
|
171
|
-
process.exit(1);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
if (!plugin.packageName.startsWith('@lamalibre/')) {
|
|
175
|
-
console.error(chalk.red(' Registry corruption: invalid package scope'));
|
|
176
|
-
process.exit(1);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
console.log(chalk.cyan(` Updating ${plugin.packageName}...`));
|
|
180
|
-
|
|
181
|
-
const agentDir = agentDataDir(label);
|
|
59
|
+
console.log(chalk.cyan(` Updating ${nameOrPackage}...`));
|
|
182
60
|
try {
|
|
183
|
-
|
|
61
|
+
const plugin = await updateAgentPlugin(label, nameOrPackage);
|
|
62
|
+
console.log(chalk.green(` Plugin "${plugin.name}" updated to v${plugin.version}`));
|
|
184
63
|
} catch (err) {
|
|
185
|
-
console.error(chalk.red(`
|
|
64
|
+
console.error(chalk.red(` ${err.message}`));
|
|
186
65
|
process.exit(1);
|
|
187
66
|
}
|
|
188
|
-
|
|
189
|
-
// Re-read manifest to capture the updated version
|
|
190
|
-
try {
|
|
191
|
-
const require = createRequire(path.join(agentDir, '/'));
|
|
192
|
-
const manifestPath = require.resolve(`${plugin.packageName}/portlama-plugin.json`);
|
|
193
|
-
const manifestRaw = await readFile(manifestPath, 'utf-8');
|
|
194
|
-
const manifest = JSON.parse(manifestRaw);
|
|
195
|
-
plugin.version = manifest.version || plugin.version;
|
|
196
|
-
} catch {
|
|
197
|
-
// Manifest may not exist — keep existing version
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
plugin.updatedAt = new Date().toISOString();
|
|
201
|
-
await writeLocalPlugins(label, registry);
|
|
202
|
-
console.log(chalk.green(` Plugin "${plugin.name}" updated`));
|
|
203
67
|
}
|
|
204
68
|
|
|
205
69
|
/**
|
|
@@ -207,7 +71,7 @@ async function updateLocal(label, nameOrPackage) {
|
|
|
207
71
|
* @param {string} label - Agent label
|
|
208
72
|
*/
|
|
209
73
|
async function showStatus(label) {
|
|
210
|
-
const registry = await
|
|
74
|
+
const registry = await readAgentPluginRegistry(label);
|
|
211
75
|
const b = chalk.bold;
|
|
212
76
|
const c = chalk.cyan;
|
|
213
77
|
const d = chalk.dim;
|
package/src/commands/update.js
CHANGED
|
@@ -84,6 +84,36 @@ export async function runUpdate({ label }) {
|
|
|
84
84
|
});
|
|
85
85
|
},
|
|
86
86
|
},
|
|
87
|
+
{
|
|
88
|
+
title: 'Reporting installed plugins',
|
|
89
|
+
task: async (_ctx, task) => {
|
|
90
|
+
const { readAgentPluginRegistry } = await import('../lib/agent-plugins.js');
|
|
91
|
+
const registry = await readAgentPluginRegistry(label);
|
|
92
|
+
const enabledPlugins = registry.plugins.filter((p) => p.status === 'enabled');
|
|
93
|
+
if (enabledPlugins.length === 0) {
|
|
94
|
+
task.skip('No enabled plugins');
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const pluginReport = enabledPlugins.map((p) => ({
|
|
98
|
+
name: p.name,
|
|
99
|
+
version: p.version,
|
|
100
|
+
capabilities: p.capabilities || [],
|
|
101
|
+
}));
|
|
102
|
+
try {
|
|
103
|
+
const { curlAuthenticatedJson } = await import('../lib/panel-api.js');
|
|
104
|
+
await curlAuthenticatedJson(config, [
|
|
105
|
+
'-X', 'POST',
|
|
106
|
+
'-H', 'Content-Type: application/json',
|
|
107
|
+
'-d', JSON.stringify({ plugins: pluginReport }),
|
|
108
|
+
`${config.panelUrl}/api/agents/plugins/report`,
|
|
109
|
+
]);
|
|
110
|
+
task.output = `Reported ${enabledPlugins.length} plugin(s)`;
|
|
111
|
+
} catch {
|
|
112
|
+
task.skip('Server does not support plugin reporting yet');
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
rendererOptions: { persistentOutput: true },
|
|
116
|
+
},
|
|
87
117
|
],
|
|
88
118
|
{
|
|
89
119
|
renderer: 'default',
|
package/src/index.js
CHANGED
|
@@ -163,7 +163,7 @@ export async function main() {
|
|
|
163
163
|
const { runPanel } = await import('./commands/panel.js');
|
|
164
164
|
const { resolveLabel } = await import('./lib/registry.js');
|
|
165
165
|
const resolved = await resolveLabel(label);
|
|
166
|
-
await runPanel(args.slice(1), { label: resolved });
|
|
166
|
+
await runPanel(args.slice(1), { label: resolved, json });
|
|
167
167
|
break;
|
|
168
168
|
}
|
|
169
169
|
case 'list': {
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent plugin router — mounts enabled plugin server routes and serves
|
|
3
|
+
* panel bundles on the agent panel server.
|
|
4
|
+
*
|
|
5
|
+
* Registered as a Fastify plugin under '/api/plugins' prefix in panel-server.js.
|
|
6
|
+
* mTLS validation is handled by the parent panel server (panel-server.js),
|
|
7
|
+
* so no additional auth guard is needed here.
|
|
8
|
+
*
|
|
9
|
+
* Combines patterns from:
|
|
10
|
+
* - plugin-router.js (server-side two-level encapsulation)
|
|
11
|
+
* - local-plugin-host.js (CJS/ESM plugin loading)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { readFile } from 'node:fs/promises';
|
|
15
|
+
import { createRequire } from 'node:module';
|
|
16
|
+
import path from 'node:path';
|
|
17
|
+
import { readAgentPluginRegistry } from './agent-plugins.js';
|
|
18
|
+
import { agentDataDir, agentPluginsDir } from './platform.js';
|
|
19
|
+
|
|
20
|
+
// Reserved prefixes that are never plugin route names.
|
|
21
|
+
const RESERVED_PREFIXES = new Set(['install']);
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Fastify plugin that mounts agent plugin routes and panel bundles.
|
|
25
|
+
*
|
|
26
|
+
* @param {import('fastify').FastifyInstance} fastify
|
|
27
|
+
* @param {{ label: string }} opts
|
|
28
|
+
*/
|
|
29
|
+
export default async function agentPluginRouter(fastify, { label }) {
|
|
30
|
+
const registry = await readAgentPluginRegistry(label);
|
|
31
|
+
const dir = agentDataDir(label);
|
|
32
|
+
const pluginsDir = agentPluginsDir(label);
|
|
33
|
+
|
|
34
|
+
for (const plugin of registry.plugins) {
|
|
35
|
+
if (plugin.status !== 'enabled') continue;
|
|
36
|
+
|
|
37
|
+
const pluginName = plugin.name;
|
|
38
|
+
|
|
39
|
+
if (plugin.packages?.server) {
|
|
40
|
+
// Defense-in-depth: verify scope at load time
|
|
41
|
+
if (!plugin.packages.server.startsWith('@lamalibre/')) {
|
|
42
|
+
fastify.log.error(
|
|
43
|
+
{ plugin: pluginName },
|
|
44
|
+
'Plugin server package scope violation — skipping',
|
|
45
|
+
);
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
const require = createRequire(path.join(dir, '/'));
|
|
51
|
+
const modulePath = require.resolve(plugin.packages.server);
|
|
52
|
+
let serverModule;
|
|
53
|
+
try {
|
|
54
|
+
// Try require first (CJS packages)
|
|
55
|
+
serverModule = require(plugin.packages.server);
|
|
56
|
+
} catch {
|
|
57
|
+
// Fall back to dynamic import (ESM packages)
|
|
58
|
+
serverModule = await import(modulePath);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Resolve the Fastify plugin function from the module.
|
|
62
|
+
let pluginFn = serverModule.default || serverModule;
|
|
63
|
+
if (typeof pluginFn !== 'function' && typeof serverModule.buildPlugin === 'function') {
|
|
64
|
+
pluginFn = serverModule.buildPlugin();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (typeof pluginFn === 'function') {
|
|
68
|
+
const pluginDir = path.join(pluginsDir, pluginName) + '/';
|
|
69
|
+
|
|
70
|
+
// Two-level encapsulation: outer scope isolates the plugin code from
|
|
71
|
+
// the router's hooks, preventing plugins from overriding auth or
|
|
72
|
+
// adding hooks above their encapsulation boundary.
|
|
73
|
+
await fastify.register(async function pluginScope(outer) {
|
|
74
|
+
await outer.register(pluginFn, {
|
|
75
|
+
pluginDir,
|
|
76
|
+
logger: fastify.log,
|
|
77
|
+
});
|
|
78
|
+
}, { prefix: `/${pluginName}` });
|
|
79
|
+
fastify.log.info({ plugin: pluginName }, 'Agent plugin routes mounted');
|
|
80
|
+
}
|
|
81
|
+
} catch (err) {
|
|
82
|
+
fastify.log.error(
|
|
83
|
+
{ plugin: pluginName, err: err.message },
|
|
84
|
+
'Failed to mount agent plugin routes',
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Serve plugin panel bundle
|
|
90
|
+
if (plugin.packages?.server) {
|
|
91
|
+
const serverPkg = plugin.packages.server;
|
|
92
|
+
fastify.get(`/${pluginName}/panel.js`, async (_request, reply) => {
|
|
93
|
+
try {
|
|
94
|
+
if (!serverPkg.startsWith('@lamalibre/')) {
|
|
95
|
+
return reply.code(403).send({ error: 'Plugin server package scope violation' });
|
|
96
|
+
}
|
|
97
|
+
const require = createRequire(path.join(dir, '/'));
|
|
98
|
+
const panelPath = require.resolve(`${serverPkg}/panel.js`);
|
|
99
|
+
const content = await readFile(panelPath, 'utf-8');
|
|
100
|
+
return reply
|
|
101
|
+
.header('Content-Type', 'application/javascript')
|
|
102
|
+
.header('Cache-Control', 'public, max-age=3600')
|
|
103
|
+
.send(content);
|
|
104
|
+
} catch {
|
|
105
|
+
return reply.code(404).send({ error: 'Plugin panel bundle not found' });
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// --- Disabled plugin catch-all ---
|
|
112
|
+
|
|
113
|
+
let cachedDisabledPlugins = new Set();
|
|
114
|
+
let cacheExpiry = 0;
|
|
115
|
+
|
|
116
|
+
async function getDisabledPlugins() {
|
|
117
|
+
const now = Date.now();
|
|
118
|
+
if (now < cacheExpiry) return cachedDisabledPlugins;
|
|
119
|
+
|
|
120
|
+
const currentRegistry = await readAgentPluginRegistry(label);
|
|
121
|
+
cachedDisabledPlugins = new Set(
|
|
122
|
+
currentRegistry.plugins.filter((p) => p.status !== 'enabled').map((p) => p.name),
|
|
123
|
+
);
|
|
124
|
+
cacheExpiry = now + 5000;
|
|
125
|
+
return cachedDisabledPlugins;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
fastify.addHook('onRequest', async (request, reply) => {
|
|
129
|
+
// Match /<pluginName>/... under the /api/plugins prefix
|
|
130
|
+
const match = request.url.match(/^\/([a-z0-9-]+)(\/|$)/);
|
|
131
|
+
if (!match) return;
|
|
132
|
+
|
|
133
|
+
const name = match[1];
|
|
134
|
+
if (RESERVED_PREFIXES.has(name)) return;
|
|
135
|
+
|
|
136
|
+
const disabled = await getDisabledPlugins();
|
|
137
|
+
if (disabled.has(name)) {
|
|
138
|
+
return reply.code(503).send({ error: `Plugin "${name}" is disabled` });
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
}
|