@openape/cli 0.1.0 ā 0.1.2
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/dist/commands/install-proxy.js +188 -0
- package/dist/commands/install-sudo.js +86 -0
- package/dist/index.js +42 -0
- package/dist/utils.js +57 -0
- package/package.json +8 -5
- package/src/commands/install-proxy.ts +0 -205
- package/src/commands/install-sudo.ts +0 -102
- package/src/index.ts +0 -47
- package/src/utils.ts +0 -63
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { execSync } from 'node:child_process';
|
|
3
|
+
import { ask, closePrompt, requireRoot, ensureDir, writeSecureFile, isMacOS, isLinux, run } from '../utils.js';
|
|
4
|
+
const CONFIG_DIR = '/etc/openape-proxy';
|
|
5
|
+
const CONFIG_PATH = `${CONFIG_DIR}/config.toml`;
|
|
6
|
+
const LAUNCHD_LABEL = 'ai.openape.proxy';
|
|
7
|
+
const LAUNCHD_PLIST = `/Library/LaunchDaemons/${LAUNCHD_LABEL}.plist`;
|
|
8
|
+
const SYSTEMD_UNIT = '/etc/systemd/system/openape-proxy.service';
|
|
9
|
+
export async function installProxy() {
|
|
10
|
+
requireRoot();
|
|
11
|
+
console.log('\nš¾ OpenApe Proxy Installer\n');
|
|
12
|
+
// Check if bun is available
|
|
13
|
+
try {
|
|
14
|
+
run('which bun');
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
throw new Error('Bun is required but not found. Install it: https://bun.sh');
|
|
18
|
+
}
|
|
19
|
+
// Check if already installed
|
|
20
|
+
if (existsSync(CONFIG_PATH)) {
|
|
21
|
+
const overwrite = await ask('Config already exists. Overwrite?', 'n');
|
|
22
|
+
if (overwrite.toLowerCase() !== 'y') {
|
|
23
|
+
console.log('Aborted.');
|
|
24
|
+
closePrompt();
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
// Gather config
|
|
29
|
+
const idpUrl = await ask('IdP URL', 'https://id.test.openape.at');
|
|
30
|
+
const agentEmail = await ask('Agent email');
|
|
31
|
+
const listen = await ask('Listen address', '127.0.0.1:9090');
|
|
32
|
+
const defaultAction = await ask('Default action (block/request/request-async)', 'block');
|
|
33
|
+
const auditLog = await ask('Audit log path', '/var/log/openape-proxy/audit.log');
|
|
34
|
+
console.log('\nConfiguring rules...');
|
|
35
|
+
console.log('(You can edit the config later at ' + CONFIG_PATH + ')\n');
|
|
36
|
+
// Generate config
|
|
37
|
+
const config = `# OpenApe Proxy Configuration
|
|
38
|
+
# Generated by: openape install-proxy
|
|
39
|
+
# Edit this file to add/remove rules. Restart the service after changes.
|
|
40
|
+
|
|
41
|
+
[proxy]
|
|
42
|
+
listen = "${listen}"
|
|
43
|
+
idp_url = "${idpUrl}"
|
|
44
|
+
agent_email = "${agentEmail}"
|
|
45
|
+
default_action = "${defaultAction}"
|
|
46
|
+
audit_log = "${auditLog}"
|
|
47
|
+
|
|
48
|
+
# Example rules ā customize these for your use case:
|
|
49
|
+
|
|
50
|
+
# [[allow]]
|
|
51
|
+
# domain = "api.github.com"
|
|
52
|
+
# methods = ["GET"]
|
|
53
|
+
# note = "GitHub API read-only"
|
|
54
|
+
|
|
55
|
+
# [[deny]]
|
|
56
|
+
# domain = "*.malware.example.com"
|
|
57
|
+
# note = "Known bad domain"
|
|
58
|
+
|
|
59
|
+
# [[grant_required]]
|
|
60
|
+
# domain = "api.github.com"
|
|
61
|
+
# methods = ["POST", "PUT", "DELETE"]
|
|
62
|
+
# grant_type = "allow_once"
|
|
63
|
+
# note = "GitHub API write operations need approval"
|
|
64
|
+
`;
|
|
65
|
+
// Write config
|
|
66
|
+
ensureDir(CONFIG_DIR, 0o700);
|
|
67
|
+
writeSecureFile(CONFIG_PATH, config);
|
|
68
|
+
console.log(`ā
Config written to ${CONFIG_PATH}`);
|
|
69
|
+
// Create audit log directory
|
|
70
|
+
const auditDir = auditLog.substring(0, auditLog.lastIndexOf('/'));
|
|
71
|
+
if (auditDir) {
|
|
72
|
+
ensureDir(auditDir, 0o755);
|
|
73
|
+
console.log(`ā
Audit log directory created: ${auditDir}`);
|
|
74
|
+
}
|
|
75
|
+
// Install proxy source
|
|
76
|
+
const installDir = '/opt/openape-proxy';
|
|
77
|
+
ensureDir(installDir, 0o755);
|
|
78
|
+
// Clone or update proxy source
|
|
79
|
+
if (existsSync(`${installDir}/package.json`)) {
|
|
80
|
+
console.log('Updating proxy source...');
|
|
81
|
+
execSync(`cd ${installDir} && git pull`, { stdio: 'inherit' });
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
console.log('Downloading proxy source...');
|
|
85
|
+
execSync(`git clone https://github.com/openape-ai/proxy.git ${installDir}`, { stdio: 'inherit' });
|
|
86
|
+
}
|
|
87
|
+
// Install dependencies
|
|
88
|
+
console.log('Installing dependencies...');
|
|
89
|
+
execSync(`cd ${installDir} && bun install`, { stdio: 'inherit' });
|
|
90
|
+
// Set up system service
|
|
91
|
+
if (isMacOS()) {
|
|
92
|
+
await setupLaunchd(installDir);
|
|
93
|
+
}
|
|
94
|
+
else if (isLinux()) {
|
|
95
|
+
await setupSystemd(installDir);
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
console.log('\nā ļø Unsupported platform for service setup. Start manually:');
|
|
99
|
+
console.log(` cd ${installDir} && bun run src/index.ts --config ${CONFIG_PATH}`);
|
|
100
|
+
}
|
|
101
|
+
closePrompt();
|
|
102
|
+
console.log('\nš OpenApe Proxy installed!\n');
|
|
103
|
+
console.log(` Config: ${CONFIG_PATH}`);
|
|
104
|
+
console.log(` Proxy: ${installDir}`);
|
|
105
|
+
console.log(` Audit log: ${auditLog}`);
|
|
106
|
+
console.log(` Listening: ${listen}`);
|
|
107
|
+
console.log(`\n Edit ${CONFIG_PATH} to configure rules.`);
|
|
108
|
+
console.log(` The proxy must be restarted after config changes.\n`);
|
|
109
|
+
// Quick test
|
|
110
|
+
try {
|
|
111
|
+
const port = listen.split(':')[1] || '9090';
|
|
112
|
+
const host = listen.split(':')[0] || '127.0.0.1';
|
|
113
|
+
const res = execSync(`curl -s http://${host}:${port}/healthz`, { timeout: 3000, encoding: 'utf-8' });
|
|
114
|
+
console.log(` Health check: ${res}`);
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
console.log(' ā ļø Health check failed ā service may still be starting.');
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
async function setupLaunchd(installDir) {
|
|
121
|
+
const bunPath = run('which bun');
|
|
122
|
+
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
123
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
124
|
+
<plist version="1.0">
|
|
125
|
+
<dict>
|
|
126
|
+
<key>Label</key>
|
|
127
|
+
<string>${LAUNCHD_LABEL}</string>
|
|
128
|
+
<key>ProgramArguments</key>
|
|
129
|
+
<array>
|
|
130
|
+
<string>${bunPath}</string>
|
|
131
|
+
<string>run</string>
|
|
132
|
+
<string>${installDir}/src/index.ts</string>
|
|
133
|
+
<string>--config</string>
|
|
134
|
+
<string>${CONFIG_PATH}</string>
|
|
135
|
+
</array>
|
|
136
|
+
<key>RunAtLoad</key>
|
|
137
|
+
<true/>
|
|
138
|
+
<key>KeepAlive</key>
|
|
139
|
+
<true/>
|
|
140
|
+
<key>StandardOutPath</key>
|
|
141
|
+
<string>/var/log/openape-proxy/stdout.log</string>
|
|
142
|
+
<key>StandardErrorPath</key>
|
|
143
|
+
<string>/var/log/openape-proxy/stderr.log</string>
|
|
144
|
+
<key>WorkingDirectory</key>
|
|
145
|
+
<string>${installDir}</string>
|
|
146
|
+
</dict>
|
|
147
|
+
</plist>`;
|
|
148
|
+
writeSecureFile(LAUNCHD_PLIST, plist, 0o644);
|
|
149
|
+
console.log(`ā
launchd service created: ${LAUNCHD_LABEL}`);
|
|
150
|
+
// Stop if already running
|
|
151
|
+
try {
|
|
152
|
+
execSync(`launchctl unload ${LAUNCHD_PLIST}`, { stdio: 'ignore' });
|
|
153
|
+
}
|
|
154
|
+
catch { }
|
|
155
|
+
execSync(`launchctl load ${LAUNCHD_PLIST}`);
|
|
156
|
+
console.log('ā
Service started');
|
|
157
|
+
}
|
|
158
|
+
async function setupSystemd(installDir) {
|
|
159
|
+
const bunPath = run('which bun');
|
|
160
|
+
const unit = `[Unit]
|
|
161
|
+
Description=OpenApe HTTP Proxy
|
|
162
|
+
After=network.target
|
|
163
|
+
|
|
164
|
+
[Service]
|
|
165
|
+
Type=simple
|
|
166
|
+
ExecStart=${bunPath} run ${installDir}/src/index.ts --config ${CONFIG_PATH}
|
|
167
|
+
WorkingDirectory=${installDir}
|
|
168
|
+
Restart=on-failure
|
|
169
|
+
RestartSec=5
|
|
170
|
+
StandardOutput=journal
|
|
171
|
+
StandardError=journal
|
|
172
|
+
|
|
173
|
+
# Security hardening
|
|
174
|
+
NoNewPrivileges=true
|
|
175
|
+
ProtectSystem=strict
|
|
176
|
+
ProtectHome=true
|
|
177
|
+
ReadOnlyPaths=/
|
|
178
|
+
ReadWritePaths=/var/log/openape-proxy
|
|
179
|
+
|
|
180
|
+
[Install]
|
|
181
|
+
WantedBy=multi-user.target
|
|
182
|
+
`;
|
|
183
|
+
writeSecureFile(SYSTEMD_UNIT, unit, 0o644);
|
|
184
|
+
execSync('systemctl daemon-reload');
|
|
185
|
+
execSync('systemctl enable openape-proxy');
|
|
186
|
+
execSync('systemctl start openape-proxy');
|
|
187
|
+
console.log('ā
systemd service created and started: openape-proxy');
|
|
188
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { execSync } from 'node:child_process';
|
|
3
|
+
import { ask, closePrompt, requireRoot, ensureDir, writeSecureFile, run } from '../utils.js';
|
|
4
|
+
const CONFIG_DIR = '/etc/apes';
|
|
5
|
+
const CONFIG_PATH = `${CONFIG_DIR}/config.toml`;
|
|
6
|
+
const BIN_PATH = '/usr/local/bin/apes';
|
|
7
|
+
export async function installSudo() {
|
|
8
|
+
requireRoot();
|
|
9
|
+
console.log('\nš¾ OpenApe Sudo (apes) Installer\n');
|
|
10
|
+
// Check if already installed
|
|
11
|
+
if (existsSync(BIN_PATH)) {
|
|
12
|
+
const overwrite = await ask('apes is already installed. Reinstall?', 'n');
|
|
13
|
+
if (overwrite.toLowerCase() !== 'y') {
|
|
14
|
+
console.log('Aborted.');
|
|
15
|
+
closePrompt();
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
// Check for Rust toolchain
|
|
20
|
+
let hasCargo = false;
|
|
21
|
+
try {
|
|
22
|
+
run('cargo --version');
|
|
23
|
+
hasCargo = true;
|
|
24
|
+
}
|
|
25
|
+
catch { }
|
|
26
|
+
if (!hasCargo) {
|
|
27
|
+
console.log('ā ļø Rust/Cargo not found. Checking for pre-built binary...');
|
|
28
|
+
// TODO: download pre-built binary from GitHub releases
|
|
29
|
+
throw new Error('Rust toolchain required to build apes. Install it: https://rustup.rs\n' +
|
|
30
|
+
'Pre-built binaries will be available in a future release.');
|
|
31
|
+
}
|
|
32
|
+
// Build from source
|
|
33
|
+
const buildDir = '/tmp/openape-sudo-build';
|
|
34
|
+
if (existsSync(`${buildDir}/.git`)) {
|
|
35
|
+
console.log('Updating source...');
|
|
36
|
+
execSync(`cd ${buildDir} && git pull`, { stdio: 'inherit' });
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
console.log('Downloading source...');
|
|
40
|
+
execSync(`rm -rf ${buildDir}`, { stdio: 'ignore' });
|
|
41
|
+
execSync(`git clone https://github.com/openape-ai/sudo.git ${buildDir}`, { stdio: 'inherit' });
|
|
42
|
+
}
|
|
43
|
+
console.log('Building apes (release mode)...');
|
|
44
|
+
execSync(`cd ${buildDir} && cargo build --release`, { stdio: 'inherit' });
|
|
45
|
+
// Install binary with setuid
|
|
46
|
+
execSync(`cp ${buildDir}/target/release/apes ${BIN_PATH}`);
|
|
47
|
+
execSync(`chown root:wheel ${BIN_PATH}`);
|
|
48
|
+
execSync(`chmod u+s ${BIN_PATH}`);
|
|
49
|
+
console.log(`ā
apes installed at ${BIN_PATH} (setuid root)`);
|
|
50
|
+
// Config setup
|
|
51
|
+
if (existsSync(CONFIG_PATH)) {
|
|
52
|
+
const overwriteConfig = await ask('Config already exists. Overwrite?', 'n');
|
|
53
|
+
if (overwriteConfig.toLowerCase() !== 'y') {
|
|
54
|
+
closePrompt();
|
|
55
|
+
console.log('\nš apes binary updated! Existing config preserved.\n');
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const serverUrl = await ask('IdP URL', 'https://id.test.openape.at');
|
|
60
|
+
const timeoutSecs = await ask('Poll timeout (seconds)', '300');
|
|
61
|
+
const intervalSecs = await ask('Poll interval (seconds)', '2');
|
|
62
|
+
const config = `# OpenApe Sudo (apes) Configuration
|
|
63
|
+
# Generated by: openape install-sudo
|
|
64
|
+
|
|
65
|
+
server_url = "${serverUrl}"
|
|
66
|
+
|
|
67
|
+
# Agent identity is determined by the key passed via --key flag.
|
|
68
|
+
# Each user provides their own key: apes --key ~/.ssh/id_ed25519 -- <command>
|
|
69
|
+
|
|
70
|
+
[poll]
|
|
71
|
+
interval_secs = ${intervalSecs}
|
|
72
|
+
timeout_secs = ${timeoutSecs}
|
|
73
|
+
`;
|
|
74
|
+
ensureDir(CONFIG_DIR, 0o700);
|
|
75
|
+
writeSecureFile(CONFIG_PATH, config);
|
|
76
|
+
console.log(`ā
Config written to ${CONFIG_PATH}`);
|
|
77
|
+
closePrompt();
|
|
78
|
+
console.log('\nš apes installed!\n');
|
|
79
|
+
console.log(` Binary: ${BIN_PATH} (setuid root)`);
|
|
80
|
+
console.log(` Config: ${CONFIG_PATH}`);
|
|
81
|
+
console.log('\n Next steps:');
|
|
82
|
+
console.log(' 1. Register this agent on the IdP:');
|
|
83
|
+
console.log(` sudo apes enroll --server ${serverUrl} --agent-email <email> --agent-name <name> --key <path>`);
|
|
84
|
+
console.log(' 2. Ask your admin to approve the enrollment');
|
|
85
|
+
console.log(' 3. Use it: apes --key ~/.ssh/id_ed25519 --reason "why" -- <command>\n');
|
|
86
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { installProxy } from './commands/install-proxy.js';
|
|
3
|
+
import { installSudo } from './commands/install-sudo.js';
|
|
4
|
+
const command = process.argv[2];
|
|
5
|
+
const HELP = `
|
|
6
|
+
OpenApe CLI ā install and manage OpenApe components
|
|
7
|
+
|
|
8
|
+
Usage: openape <command> [options]
|
|
9
|
+
|
|
10
|
+
Commands:
|
|
11
|
+
install-proxy Install and configure the OpenApe HTTP proxy
|
|
12
|
+
install-sudo Install and configure apes (privilege elevation)
|
|
13
|
+
help Show this help message
|
|
14
|
+
|
|
15
|
+
Examples:
|
|
16
|
+
sudo openape install-proxy
|
|
17
|
+
sudo openape install-sudo
|
|
18
|
+
`;
|
|
19
|
+
async function main() {
|
|
20
|
+
switch (command) {
|
|
21
|
+
case 'install-proxy':
|
|
22
|
+
await installProxy();
|
|
23
|
+
break;
|
|
24
|
+
case 'install-sudo':
|
|
25
|
+
await installSudo();
|
|
26
|
+
break;
|
|
27
|
+
case 'help':
|
|
28
|
+
case '--help':
|
|
29
|
+
case '-h':
|
|
30
|
+
case undefined:
|
|
31
|
+
console.log(HELP);
|
|
32
|
+
process.exit(0);
|
|
33
|
+
default:
|
|
34
|
+
console.error(`Unknown command: ${command}`);
|
|
35
|
+
console.log(HELP);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
main().catch((err) => {
|
|
40
|
+
console.error(`\nā ${err.message}`);
|
|
41
|
+
process.exit(1);
|
|
42
|
+
});
|
package/dist/utils.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { createInterface } from 'node:readline';
|
|
2
|
+
import { execSync } from 'node:child_process';
|
|
3
|
+
import { existsSync, mkdirSync, writeFileSync, chmodSync, chownSync } from 'node:fs';
|
|
4
|
+
import { platform } from 'node:os';
|
|
5
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
6
|
+
export function ask(question, defaultValue) {
|
|
7
|
+
const suffix = defaultValue ? ` [${defaultValue}]` : '';
|
|
8
|
+
return new Promise((resolve) => {
|
|
9
|
+
rl.question(`${question}${suffix}: `, (answer) => {
|
|
10
|
+
resolve(answer.trim() || defaultValue || '');
|
|
11
|
+
});
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
export function closePrompt() {
|
|
15
|
+
rl.close();
|
|
16
|
+
}
|
|
17
|
+
export function requireRoot() {
|
|
18
|
+
if (process.getuid?.() !== 0) {
|
|
19
|
+
throw new Error('This command must be run as root (use sudo)');
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export function ensureDir(path, mode = 0o755) {
|
|
23
|
+
if (!existsSync(path)) {
|
|
24
|
+
mkdirSync(path, { recursive: true });
|
|
25
|
+
}
|
|
26
|
+
chmodSync(path, mode);
|
|
27
|
+
}
|
|
28
|
+
export function writeSecureFile(path, content, mode = 0o600) {
|
|
29
|
+
writeFileSync(path, content, 'utf-8');
|
|
30
|
+
chmodSync(path, mode);
|
|
31
|
+
// Ensure root ownership
|
|
32
|
+
try {
|
|
33
|
+
chownSync(path, 0, 0);
|
|
34
|
+
}
|
|
35
|
+
catch { }
|
|
36
|
+
}
|
|
37
|
+
export function run(cmd) {
|
|
38
|
+
return execSync(cmd, { encoding: 'utf-8' }).trim();
|
|
39
|
+
}
|
|
40
|
+
export function isMacOS() {
|
|
41
|
+
return platform() === 'darwin';
|
|
42
|
+
}
|
|
43
|
+
export function isLinux() {
|
|
44
|
+
return platform() === 'linux';
|
|
45
|
+
}
|
|
46
|
+
export function serviceExists(name) {
|
|
47
|
+
if (isMacOS()) {
|
|
48
|
+
return existsSync(`/Library/LaunchDaemons/${name}.plist`);
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
execSync(`systemctl cat ${name}`, { stdio: 'ignore' });
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
}
|
package/package.json
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openape/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "OpenApe CLI ā install and manage OpenApe components (proxy, sudo, auth)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"openape": "
|
|
7
|
+
"openape": "dist/index.js"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
|
-
"
|
|
10
|
+
"build": "tsc",
|
|
11
|
+
"prepublishOnly": "tsc",
|
|
12
|
+
"start": "node dist/index.js",
|
|
13
|
+
"dev": "bun run src/index.ts",
|
|
11
14
|
"typecheck": "tsc --noEmit"
|
|
12
15
|
},
|
|
13
16
|
"keywords": [
|
|
@@ -26,7 +29,7 @@
|
|
|
26
29
|
"license": "MIT",
|
|
27
30
|
"repository": {
|
|
28
31
|
"type": "git",
|
|
29
|
-
"url": "https://github.com/openape-ai/cli.git"
|
|
32
|
+
"url": "git+https://github.com/openape-ai/cli.git"
|
|
30
33
|
},
|
|
31
34
|
"homepage": "https://openape.at",
|
|
32
35
|
"bugs": {
|
|
@@ -43,7 +46,7 @@
|
|
|
43
46
|
"node": ">=18"
|
|
44
47
|
},
|
|
45
48
|
"files": [
|
|
46
|
-
"
|
|
49
|
+
"dist/",
|
|
47
50
|
"README.md",
|
|
48
51
|
"LICENSE"
|
|
49
52
|
]
|
|
@@ -1,205 +0,0 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs'
|
|
2
|
-
import { execSync } from 'node:child_process'
|
|
3
|
-
import { ask, closePrompt, requireRoot, ensureDir, writeSecureFile, isMacOS, isLinux, run } from '../utils.js'
|
|
4
|
-
|
|
5
|
-
const CONFIG_DIR = '/etc/openape-proxy'
|
|
6
|
-
const CONFIG_PATH = `${CONFIG_DIR}/config.toml`
|
|
7
|
-
const LAUNCHD_LABEL = 'ai.openape.proxy'
|
|
8
|
-
const LAUNCHD_PLIST = `/Library/LaunchDaemons/${LAUNCHD_LABEL}.plist`
|
|
9
|
-
const SYSTEMD_UNIT = '/etc/systemd/system/openape-proxy.service'
|
|
10
|
-
|
|
11
|
-
export async function installProxy(): Promise<void> {
|
|
12
|
-
requireRoot()
|
|
13
|
-
|
|
14
|
-
console.log('\nš¾ OpenApe Proxy Installer\n')
|
|
15
|
-
|
|
16
|
-
// Check if bun is available
|
|
17
|
-
try {
|
|
18
|
-
run('which bun')
|
|
19
|
-
} catch {
|
|
20
|
-
throw new Error('Bun is required but not found. Install it: https://bun.sh')
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// Check if already installed
|
|
24
|
-
if (existsSync(CONFIG_PATH)) {
|
|
25
|
-
const overwrite = await ask('Config already exists. Overwrite?', 'n')
|
|
26
|
-
if (overwrite.toLowerCase() !== 'y') {
|
|
27
|
-
console.log('Aborted.')
|
|
28
|
-
closePrompt()
|
|
29
|
-
return
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// Gather config
|
|
34
|
-
const idpUrl = await ask('IdP URL', 'https://id.test.openape.at')
|
|
35
|
-
const agentEmail = await ask('Agent email')
|
|
36
|
-
const listen = await ask('Listen address', '127.0.0.1:9090')
|
|
37
|
-
const defaultAction = await ask('Default action (block/request/request-async)', 'block')
|
|
38
|
-
const auditLog = await ask('Audit log path', '/var/log/openape-proxy/audit.log')
|
|
39
|
-
|
|
40
|
-
console.log('\nConfiguring rules...')
|
|
41
|
-
console.log('(You can edit the config later at ' + CONFIG_PATH + ')\n')
|
|
42
|
-
|
|
43
|
-
// Generate config
|
|
44
|
-
const config = `# OpenApe Proxy Configuration
|
|
45
|
-
# Generated by: openape install-proxy
|
|
46
|
-
# Edit this file to add/remove rules. Restart the service after changes.
|
|
47
|
-
|
|
48
|
-
[proxy]
|
|
49
|
-
listen = "${listen}"
|
|
50
|
-
idp_url = "${idpUrl}"
|
|
51
|
-
agent_email = "${agentEmail}"
|
|
52
|
-
default_action = "${defaultAction}"
|
|
53
|
-
audit_log = "${auditLog}"
|
|
54
|
-
|
|
55
|
-
# Example rules ā customize these for your use case:
|
|
56
|
-
|
|
57
|
-
# [[allow]]
|
|
58
|
-
# domain = "api.github.com"
|
|
59
|
-
# methods = ["GET"]
|
|
60
|
-
# note = "GitHub API read-only"
|
|
61
|
-
|
|
62
|
-
# [[deny]]
|
|
63
|
-
# domain = "*.malware.example.com"
|
|
64
|
-
# note = "Known bad domain"
|
|
65
|
-
|
|
66
|
-
# [[grant_required]]
|
|
67
|
-
# domain = "api.github.com"
|
|
68
|
-
# methods = ["POST", "PUT", "DELETE"]
|
|
69
|
-
# grant_type = "allow_once"
|
|
70
|
-
# note = "GitHub API write operations need approval"
|
|
71
|
-
`
|
|
72
|
-
|
|
73
|
-
// Write config
|
|
74
|
-
ensureDir(CONFIG_DIR, 0o700)
|
|
75
|
-
writeSecureFile(CONFIG_PATH, config)
|
|
76
|
-
console.log(`ā
Config written to ${CONFIG_PATH}`)
|
|
77
|
-
|
|
78
|
-
// Create audit log directory
|
|
79
|
-
const auditDir = auditLog.substring(0, auditLog.lastIndexOf('/'))
|
|
80
|
-
if (auditDir) {
|
|
81
|
-
ensureDir(auditDir, 0o755)
|
|
82
|
-
console.log(`ā
Audit log directory created: ${auditDir}`)
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// Install proxy source
|
|
86
|
-
const installDir = '/opt/openape-proxy'
|
|
87
|
-
ensureDir(installDir, 0o755)
|
|
88
|
-
|
|
89
|
-
// Clone or update proxy source
|
|
90
|
-
if (existsSync(`${installDir}/package.json`)) {
|
|
91
|
-
console.log('Updating proxy source...')
|
|
92
|
-
execSync(`cd ${installDir} && git pull`, { stdio: 'inherit' })
|
|
93
|
-
} else {
|
|
94
|
-
console.log('Downloading proxy source...')
|
|
95
|
-
execSync(`git clone https://github.com/openape-ai/proxy.git ${installDir}`, { stdio: 'inherit' })
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// Install dependencies
|
|
99
|
-
console.log('Installing dependencies...')
|
|
100
|
-
execSync(`cd ${installDir} && bun install`, { stdio: 'inherit' })
|
|
101
|
-
|
|
102
|
-
// Set up system service
|
|
103
|
-
if (isMacOS()) {
|
|
104
|
-
await setupLaunchd(installDir)
|
|
105
|
-
} else if (isLinux()) {
|
|
106
|
-
await setupSystemd(installDir)
|
|
107
|
-
} else {
|
|
108
|
-
console.log('\nā ļø Unsupported platform for service setup. Start manually:')
|
|
109
|
-
console.log(` cd ${installDir} && bun run src/index.ts --config ${CONFIG_PATH}`)
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
closePrompt()
|
|
113
|
-
|
|
114
|
-
console.log('\nš OpenApe Proxy installed!\n')
|
|
115
|
-
console.log(` Config: ${CONFIG_PATH}`)
|
|
116
|
-
console.log(` Proxy: ${installDir}`)
|
|
117
|
-
console.log(` Audit log: ${auditLog}`)
|
|
118
|
-
console.log(` Listening: ${listen}`)
|
|
119
|
-
console.log(`\n Edit ${CONFIG_PATH} to configure rules.`)
|
|
120
|
-
console.log(` The proxy must be restarted after config changes.\n`)
|
|
121
|
-
|
|
122
|
-
// Quick test
|
|
123
|
-
try {
|
|
124
|
-
const port = listen.split(':')[1] || '9090'
|
|
125
|
-
const host = listen.split(':')[0] || '127.0.0.1'
|
|
126
|
-
const res = execSync(`curl -s http://${host}:${port}/healthz`, { timeout: 3000, encoding: 'utf-8' })
|
|
127
|
-
console.log(` Health check: ${res}`)
|
|
128
|
-
} catch {
|
|
129
|
-
console.log(' ā ļø Health check failed ā service may still be starting.')
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
async function setupLaunchd(installDir: string): Promise<void> {
|
|
134
|
-
const bunPath = run('which bun')
|
|
135
|
-
|
|
136
|
-
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
137
|
-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
138
|
-
<plist version="1.0">
|
|
139
|
-
<dict>
|
|
140
|
-
<key>Label</key>
|
|
141
|
-
<string>${LAUNCHD_LABEL}</string>
|
|
142
|
-
<key>ProgramArguments</key>
|
|
143
|
-
<array>
|
|
144
|
-
<string>${bunPath}</string>
|
|
145
|
-
<string>run</string>
|
|
146
|
-
<string>${installDir}/src/index.ts</string>
|
|
147
|
-
<string>--config</string>
|
|
148
|
-
<string>${CONFIG_PATH}</string>
|
|
149
|
-
</array>
|
|
150
|
-
<key>RunAtLoad</key>
|
|
151
|
-
<true/>
|
|
152
|
-
<key>KeepAlive</key>
|
|
153
|
-
<true/>
|
|
154
|
-
<key>StandardOutPath</key>
|
|
155
|
-
<string>/var/log/openape-proxy/stdout.log</string>
|
|
156
|
-
<key>StandardErrorPath</key>
|
|
157
|
-
<string>/var/log/openape-proxy/stderr.log</string>
|
|
158
|
-
<key>WorkingDirectory</key>
|
|
159
|
-
<string>${installDir}</string>
|
|
160
|
-
</dict>
|
|
161
|
-
</plist>`
|
|
162
|
-
|
|
163
|
-
writeSecureFile(LAUNCHD_PLIST, plist, 0o644)
|
|
164
|
-
console.log(`ā
launchd service created: ${LAUNCHD_LABEL}`)
|
|
165
|
-
|
|
166
|
-
// Stop if already running
|
|
167
|
-
try { execSync(`launchctl unload ${LAUNCHD_PLIST}`, { stdio: 'ignore' }) } catch {}
|
|
168
|
-
|
|
169
|
-
execSync(`launchctl load ${LAUNCHD_PLIST}`)
|
|
170
|
-
console.log('ā
Service started')
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
async function setupSystemd(installDir: string): Promise<void> {
|
|
174
|
-
const bunPath = run('which bun')
|
|
175
|
-
|
|
176
|
-
const unit = `[Unit]
|
|
177
|
-
Description=OpenApe HTTP Proxy
|
|
178
|
-
After=network.target
|
|
179
|
-
|
|
180
|
-
[Service]
|
|
181
|
-
Type=simple
|
|
182
|
-
ExecStart=${bunPath} run ${installDir}/src/index.ts --config ${CONFIG_PATH}
|
|
183
|
-
WorkingDirectory=${installDir}
|
|
184
|
-
Restart=on-failure
|
|
185
|
-
RestartSec=5
|
|
186
|
-
StandardOutput=journal
|
|
187
|
-
StandardError=journal
|
|
188
|
-
|
|
189
|
-
# Security hardening
|
|
190
|
-
NoNewPrivileges=true
|
|
191
|
-
ProtectSystem=strict
|
|
192
|
-
ProtectHome=true
|
|
193
|
-
ReadOnlyPaths=/
|
|
194
|
-
ReadWritePaths=/var/log/openape-proxy
|
|
195
|
-
|
|
196
|
-
[Install]
|
|
197
|
-
WantedBy=multi-user.target
|
|
198
|
-
`
|
|
199
|
-
|
|
200
|
-
writeSecureFile(SYSTEMD_UNIT, unit, 0o644)
|
|
201
|
-
execSync('systemctl daemon-reload')
|
|
202
|
-
execSync('systemctl enable openape-proxy')
|
|
203
|
-
execSync('systemctl start openape-proxy')
|
|
204
|
-
console.log('ā
systemd service created and started: openape-proxy')
|
|
205
|
-
}
|
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs'
|
|
2
|
-
import { execSync } from 'node:child_process'
|
|
3
|
-
import { arch, platform } from 'node:os'
|
|
4
|
-
import { ask, closePrompt, requireRoot, ensureDir, writeSecureFile, run } from '../utils.js'
|
|
5
|
-
|
|
6
|
-
const CONFIG_DIR = '/etc/apes'
|
|
7
|
-
const CONFIG_PATH = `${CONFIG_DIR}/config.toml`
|
|
8
|
-
const BIN_PATH = '/usr/local/bin/apes'
|
|
9
|
-
|
|
10
|
-
export async function installSudo(): Promise<void> {
|
|
11
|
-
requireRoot()
|
|
12
|
-
|
|
13
|
-
console.log('\nš¾ OpenApe Sudo (apes) Installer\n')
|
|
14
|
-
|
|
15
|
-
// Check if already installed
|
|
16
|
-
if (existsSync(BIN_PATH)) {
|
|
17
|
-
const overwrite = await ask('apes is already installed. Reinstall?', 'n')
|
|
18
|
-
if (overwrite.toLowerCase() !== 'y') {
|
|
19
|
-
console.log('Aborted.')
|
|
20
|
-
closePrompt()
|
|
21
|
-
return
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// Check for Rust toolchain
|
|
26
|
-
let hasCargo = false
|
|
27
|
-
try {
|
|
28
|
-
run('cargo --version')
|
|
29
|
-
hasCargo = true
|
|
30
|
-
} catch {}
|
|
31
|
-
|
|
32
|
-
if (!hasCargo) {
|
|
33
|
-
console.log('ā ļø Rust/Cargo not found. Checking for pre-built binary...')
|
|
34
|
-
// TODO: download pre-built binary from GitHub releases
|
|
35
|
-
throw new Error(
|
|
36
|
-
'Rust toolchain required to build apes. Install it: https://rustup.rs\n' +
|
|
37
|
-
'Pre-built binaries will be available in a future release.'
|
|
38
|
-
)
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// Build from source
|
|
42
|
-
const buildDir = '/tmp/openape-sudo-build'
|
|
43
|
-
if (existsSync(`${buildDir}/.git`)) {
|
|
44
|
-
console.log('Updating source...')
|
|
45
|
-
execSync(`cd ${buildDir} && git pull`, { stdio: 'inherit' })
|
|
46
|
-
} else {
|
|
47
|
-
console.log('Downloading source...')
|
|
48
|
-
execSync(`rm -rf ${buildDir}`, { stdio: 'ignore' })
|
|
49
|
-
execSync(`git clone https://github.com/openape-ai/sudo.git ${buildDir}`, { stdio: 'inherit' })
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
console.log('Building apes (release mode)...')
|
|
53
|
-
execSync(`cd ${buildDir} && cargo build --release`, { stdio: 'inherit' })
|
|
54
|
-
|
|
55
|
-
// Install binary with setuid
|
|
56
|
-
execSync(`cp ${buildDir}/target/release/apes ${BIN_PATH}`)
|
|
57
|
-
execSync(`chown root:wheel ${BIN_PATH}`)
|
|
58
|
-
execSync(`chmod u+s ${BIN_PATH}`)
|
|
59
|
-
console.log(`ā
apes installed at ${BIN_PATH} (setuid root)`)
|
|
60
|
-
|
|
61
|
-
// Config setup
|
|
62
|
-
if (existsSync(CONFIG_PATH)) {
|
|
63
|
-
const overwriteConfig = await ask('Config already exists. Overwrite?', 'n')
|
|
64
|
-
if (overwriteConfig.toLowerCase() !== 'y') {
|
|
65
|
-
closePrompt()
|
|
66
|
-
console.log('\nš apes binary updated! Existing config preserved.\n')
|
|
67
|
-
return
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const serverUrl = await ask('IdP URL', 'https://id.test.openape.at')
|
|
72
|
-
const timeoutSecs = await ask('Poll timeout (seconds)', '300')
|
|
73
|
-
const intervalSecs = await ask('Poll interval (seconds)', '2')
|
|
74
|
-
|
|
75
|
-
const config = `# OpenApe Sudo (apes) Configuration
|
|
76
|
-
# Generated by: openape install-sudo
|
|
77
|
-
|
|
78
|
-
server_url = "${serverUrl}"
|
|
79
|
-
|
|
80
|
-
# Agent identity is determined by the key passed via --key flag.
|
|
81
|
-
# Each user provides their own key: apes --key ~/.ssh/id_ed25519 -- <command>
|
|
82
|
-
|
|
83
|
-
[poll]
|
|
84
|
-
interval_secs = ${intervalSecs}
|
|
85
|
-
timeout_secs = ${timeoutSecs}
|
|
86
|
-
`
|
|
87
|
-
|
|
88
|
-
ensureDir(CONFIG_DIR, 0o700)
|
|
89
|
-
writeSecureFile(CONFIG_PATH, config)
|
|
90
|
-
console.log(`ā
Config written to ${CONFIG_PATH}`)
|
|
91
|
-
|
|
92
|
-
closePrompt()
|
|
93
|
-
|
|
94
|
-
console.log('\nš apes installed!\n')
|
|
95
|
-
console.log(` Binary: ${BIN_PATH} (setuid root)`)
|
|
96
|
-
console.log(` Config: ${CONFIG_PATH}`)
|
|
97
|
-
console.log('\n Next steps:')
|
|
98
|
-
console.log(' 1. Register this agent on the IdP:')
|
|
99
|
-
console.log(` sudo apes enroll --server ${serverUrl} --agent-email <email> --agent-name <name> --key <path>`)
|
|
100
|
-
console.log(' 2. Ask your admin to approve the enrollment')
|
|
101
|
-
console.log(' 3. Use it: apes --key ~/.ssh/id_ed25519 --reason "why" -- <command>\n')
|
|
102
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
|
-
import { parseArgs } from 'node:util'
|
|
3
|
-
import { installProxy } from './commands/install-proxy.js'
|
|
4
|
-
import { installSudo } from './commands/install-sudo.js'
|
|
5
|
-
|
|
6
|
-
const command = process.argv[2]
|
|
7
|
-
|
|
8
|
-
const HELP = `
|
|
9
|
-
OpenApe CLI ā install and manage OpenApe components
|
|
10
|
-
|
|
11
|
-
Usage: openape <command> [options]
|
|
12
|
-
|
|
13
|
-
Commands:
|
|
14
|
-
install-proxy Install and configure the OpenApe HTTP proxy
|
|
15
|
-
install-sudo Install and configure apes (privilege elevation)
|
|
16
|
-
help Show this help message
|
|
17
|
-
|
|
18
|
-
Examples:
|
|
19
|
-
sudo openape install-proxy
|
|
20
|
-
sudo openape install-sudo
|
|
21
|
-
`
|
|
22
|
-
|
|
23
|
-
async function main() {
|
|
24
|
-
switch (command) {
|
|
25
|
-
case 'install-proxy':
|
|
26
|
-
await installProxy()
|
|
27
|
-
break
|
|
28
|
-
case 'install-sudo':
|
|
29
|
-
await installSudo()
|
|
30
|
-
break
|
|
31
|
-
case 'help':
|
|
32
|
-
case '--help':
|
|
33
|
-
case '-h':
|
|
34
|
-
case undefined:
|
|
35
|
-
console.log(HELP)
|
|
36
|
-
process.exit(0)
|
|
37
|
-
default:
|
|
38
|
-
console.error(`Unknown command: ${command}`)
|
|
39
|
-
console.log(HELP)
|
|
40
|
-
process.exit(1)
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
main().catch((err) => {
|
|
45
|
-
console.error(`\nā ${err.message}`)
|
|
46
|
-
process.exit(1)
|
|
47
|
-
})
|
package/src/utils.ts
DELETED
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
import { createInterface } from 'node:readline'
|
|
2
|
-
import { execSync } from 'node:child_process'
|
|
3
|
-
import { existsSync, mkdirSync, writeFileSync, chmodSync, chownSync } from 'node:fs'
|
|
4
|
-
import { platform } from 'node:os'
|
|
5
|
-
|
|
6
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout })
|
|
7
|
-
|
|
8
|
-
export function ask(question: string, defaultValue?: string): Promise<string> {
|
|
9
|
-
const suffix = defaultValue ? ` [${defaultValue}]` : ''
|
|
10
|
-
return new Promise((resolve) => {
|
|
11
|
-
rl.question(`${question}${suffix}: `, (answer) => {
|
|
12
|
-
resolve(answer.trim() || defaultValue || '')
|
|
13
|
-
})
|
|
14
|
-
})
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function closePrompt(): void {
|
|
18
|
-
rl.close()
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function requireRoot(): void {
|
|
22
|
-
if (process.getuid?.() !== 0) {
|
|
23
|
-
throw new Error('This command must be run as root (use sudo)')
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function ensureDir(path: string, mode: number = 0o755): void {
|
|
28
|
-
if (!existsSync(path)) {
|
|
29
|
-
mkdirSync(path, { recursive: true })
|
|
30
|
-
}
|
|
31
|
-
chmodSync(path, mode)
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export function writeSecureFile(path: string, content: string, mode: number = 0o600): void {
|
|
35
|
-
writeFileSync(path, content, 'utf-8')
|
|
36
|
-
chmodSync(path, mode)
|
|
37
|
-
// Ensure root ownership
|
|
38
|
-
try { chownSync(path, 0, 0) } catch {}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export function run(cmd: string): string {
|
|
42
|
-
return execSync(cmd, { encoding: 'utf-8' }).trim()
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export function isMacOS(): boolean {
|
|
46
|
-
return platform() === 'darwin'
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export function isLinux(): boolean {
|
|
50
|
-
return platform() === 'linux'
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export function serviceExists(name: string): boolean {
|
|
54
|
-
if (isMacOS()) {
|
|
55
|
-
return existsSync(`/Library/LaunchDaemons/${name}.plist`)
|
|
56
|
-
}
|
|
57
|
-
try {
|
|
58
|
-
execSync(`systemctl cat ${name}`, { stdio: 'ignore' })
|
|
59
|
-
return true
|
|
60
|
-
} catch {
|
|
61
|
-
return false
|
|
62
|
-
}
|
|
63
|
-
}
|