@openape/cli 0.1.3 → 0.2.0
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/README.md +38 -29
- package/dist/commands/install-proxy.js +43 -76
- package/dist/commands/install-sudo.js +99 -56
- package/dist/index.js +27 -16
- package/dist/utils.js +23 -24
- package/package.json +1 -1
- package/dist/commands/install-proxy-apes.js +0 -135
package/README.md
CHANGED
|
@@ -1,52 +1,61 @@
|
|
|
1
1
|
# @openape/cli
|
|
2
2
|
|
|
3
|
-
Install and manage OpenApe components.
|
|
3
|
+
Install and manage OpenApe components — proxy, sudo (`apes`), and more.
|
|
4
4
|
|
|
5
5
|
## Usage
|
|
6
6
|
|
|
7
|
+
### With traditional sudo
|
|
8
|
+
|
|
7
9
|
```bash
|
|
8
|
-
|
|
9
|
-
|
|
10
|
+
sudo npx @openape/cli install-proxy \
|
|
11
|
+
--idp-url https://id.openape.at \
|
|
12
|
+
--agent-email bot@example.com
|
|
10
13
|
|
|
11
|
-
|
|
12
|
-
|
|
14
|
+
sudo npx @openape/cli install-sudo \
|
|
15
|
+
--idp-url https://id.openape.at
|
|
13
16
|
```
|
|
14
17
|
|
|
15
|
-
|
|
18
|
+
### With apes (agent-friendly, grant-approved)
|
|
16
19
|
|
|
17
|
-
|
|
20
|
+
```bash
|
|
21
|
+
apes -- npx @openape/cli install-proxy \
|
|
22
|
+
--idp-url https://id.openape.at \
|
|
23
|
+
--agent-email bot@example.com
|
|
24
|
+
```
|
|
18
25
|
|
|
19
|
-
|
|
26
|
+
The CLI itself has no knowledge of `apes` — it just writes files and sets up services. `apes` wraps it for privilege elevation with human-approved grants.
|
|
20
27
|
|
|
21
|
-
|
|
22
|
-
- Downloads proxy source to `/opt/openape-proxy`
|
|
23
|
-
- Sets up launchd (macOS) or systemd (Linux) service
|
|
24
|
-
- Starts the proxy
|
|
28
|
+
## Commands
|
|
25
29
|
|
|
26
|
-
|
|
27
|
-
The config is **not accessible to agents** — only root can modify it.
|
|
30
|
+
### `install-proxy`
|
|
28
31
|
|
|
29
|
-
|
|
32
|
+
Installs the [OpenApe HTTP Proxy](https://github.com/openape-ai/proxy).
|
|
30
33
|
|
|
31
|
-
|
|
34
|
+
| Flag | Required | Default | Description |
|
|
35
|
+
|------|----------|---------|-------------|
|
|
36
|
+
| `--idp-url` | ✅ | — | Identity Provider URL |
|
|
37
|
+
| `--agent-email` | ✅ | — | Agent email identity |
|
|
38
|
+
| `--listen` | | `127.0.0.1:9090` | Listen address |
|
|
39
|
+
| `--default-action` | | `block` | `block`, `request`, or `request-async` |
|
|
40
|
+
| `--audit-log` | | `/var/log/openape-proxy/audit.log` | Audit log path |
|
|
41
|
+
| `--force` | | | Overwrite existing config |
|
|
32
42
|
|
|
33
|
-
-
|
|
34
|
-
- Installs to `/usr/local/bin/apes` with setuid bit
|
|
35
|
-
- Creates `/etc/apes/config.toml` (root-only, `600`)
|
|
43
|
+
### `install-sudo`
|
|
36
44
|
|
|
37
|
-
|
|
45
|
+
Installs [apes](https://github.com/openape-ai/sudo) (OpenApe privilege elevation). Requires Rust toolchain.
|
|
38
46
|
|
|
39
|
-
|
|
47
|
+
| Flag | Required | Default | Description |
|
|
48
|
+
|------|----------|---------|-------------|
|
|
49
|
+
| `--idp-url` | ✅ | — | Identity Provider URL |
|
|
50
|
+
| `--poll-timeout` | | `300` | Grant poll timeout (seconds) |
|
|
51
|
+
| `--poll-interval` | | `2` | Grant poll interval (seconds) |
|
|
52
|
+
| `--force` | | | Overwrite existing binary/config |
|
|
40
53
|
|
|
41
|
-
|
|
42
|
-
This is by design — the security boundary between agent and config must be
|
|
43
|
-
enforced by the OS file permission system.
|
|
54
|
+
## Requirements
|
|
44
55
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
| `/etc/apes/config.toml` | root | `600` | apes config |
|
|
49
|
-
| `/usr/local/bin/apes` | root | `4755` (setuid) | Privilege elevation |
|
|
56
|
+
- **install-proxy**: [Bun](https://bun.sh) runtime
|
|
57
|
+
- **install-sudo**: [Rust](https://rustup.rs) toolchain
|
|
58
|
+
- Root privileges (via `sudo` or `apes`)
|
|
50
59
|
|
|
51
60
|
## License
|
|
52
61
|
|
|
@@ -1,42 +1,36 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
2
|
import { execSync } from 'node:child_process';
|
|
3
|
-
import {
|
|
3
|
+
import { requireRoot, ensureDir, writeSecureFile, isMacOS, isLinux, run, parseFlags, requireFlag } from '../utils.js';
|
|
4
4
|
const CONFIG_DIR = '/etc/openape-proxy';
|
|
5
5
|
const CONFIG_PATH = `${CONFIG_DIR}/config.toml`;
|
|
6
|
+
const INSTALL_DIR = '/opt/openape-proxy';
|
|
6
7
|
const LAUNCHD_LABEL = 'ai.openape.proxy';
|
|
7
8
|
const LAUNCHD_PLIST = `/Library/LaunchDaemons/${LAUNCHD_LABEL}.plist`;
|
|
8
9
|
const SYSTEMD_UNIT = '/etc/systemd/system/openape-proxy.service';
|
|
9
|
-
export async function installProxy() {
|
|
10
|
+
export async function installProxy(args) {
|
|
10
11
|
requireRoot();
|
|
12
|
+
const flags = parseFlags(args);
|
|
13
|
+
const idpUrl = requireFlag(flags, 'idp-url');
|
|
14
|
+
const agentEmail = requireFlag(flags, 'agent-email');
|
|
15
|
+
const listen = flags['listen'] || '127.0.0.1:9090';
|
|
16
|
+
const defaultAction = flags['default-action'] || 'block';
|
|
17
|
+
const auditLog = flags['audit-log'] || '/var/log/openape-proxy/audit.log';
|
|
18
|
+
const force = flags['force'] === true;
|
|
11
19
|
console.log('\n🐾 OpenApe Proxy Installer\n');
|
|
12
|
-
// Check
|
|
20
|
+
// Check bun
|
|
13
21
|
try {
|
|
14
22
|
run('which bun');
|
|
15
23
|
}
|
|
16
24
|
catch {
|
|
17
25
|
throw new Error('Bun is required but not found. Install it: https://bun.sh');
|
|
18
26
|
}
|
|
19
|
-
// Check
|
|
20
|
-
if (existsSync(CONFIG_PATH)) {
|
|
21
|
-
|
|
22
|
-
if (overwrite.toLowerCase() !== 'y') {
|
|
23
|
-
console.log('Aborted.');
|
|
24
|
-
closePrompt();
|
|
25
|
-
return;
|
|
26
|
-
}
|
|
27
|
+
// Check existing config
|
|
28
|
+
if (existsSync(CONFIG_PATH) && !force) {
|
|
29
|
+
throw new Error(`Config already exists at ${CONFIG_PATH}. Use --force to overwrite.`);
|
|
27
30
|
}
|
|
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
31
|
// Generate config
|
|
37
32
|
const config = `# OpenApe Proxy Configuration
|
|
38
33
|
# Generated by: openape install-proxy
|
|
39
|
-
# Edit this file to add/remove rules. Restart the service after changes.
|
|
40
34
|
|
|
41
35
|
[proxy]
|
|
42
36
|
listen = "${listen}"
|
|
@@ -45,7 +39,7 @@ agent_email = "${agentEmail}"
|
|
|
45
39
|
default_action = "${defaultAction}"
|
|
46
40
|
audit_log = "${auditLog}"
|
|
47
41
|
|
|
48
|
-
#
|
|
42
|
+
# Rules — customize for your use case:
|
|
49
43
|
|
|
50
44
|
# [[allow]]
|
|
51
45
|
# domain = "api.github.com"
|
|
@@ -60,64 +54,44 @@ audit_log = "${auditLog}"
|
|
|
60
54
|
# domain = "api.github.com"
|
|
61
55
|
# methods = ["POST", "PUT", "DELETE"]
|
|
62
56
|
# grant_type = "allow_once"
|
|
63
|
-
# note = "GitHub API
|
|
57
|
+
# note = "GitHub API writes need approval"
|
|
64
58
|
`;
|
|
65
59
|
// Write config
|
|
66
60
|
ensureDir(CONFIG_DIR, 0o700);
|
|
67
61
|
writeSecureFile(CONFIG_PATH, config);
|
|
68
|
-
console.log(`✅ Config
|
|
69
|
-
//
|
|
62
|
+
console.log(`✅ Config: ${CONFIG_PATH}`);
|
|
63
|
+
// Audit log dir
|
|
70
64
|
const auditDir = auditLog.substring(0, auditLog.lastIndexOf('/'));
|
|
71
65
|
if (auditDir) {
|
|
72
66
|
ensureDir(auditDir, 0o755);
|
|
73
|
-
console.log(`✅ Audit log
|
|
67
|
+
console.log(`✅ Audit log dir: ${auditDir}`);
|
|
74
68
|
}
|
|
75
|
-
// Install proxy
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
console.log('Updating proxy source...');
|
|
81
|
-
execSync(`cd ${installDir} && git pull`, { stdio: 'inherit' });
|
|
69
|
+
// Install proxy via npm/bun (no git needed)
|
|
70
|
+
ensureDir(INSTALL_DIR, 0o755);
|
|
71
|
+
console.log('Installing @openape/proxy via bun...');
|
|
72
|
+
if (!existsSync(`${INSTALL_DIR}/package.json`)) {
|
|
73
|
+
execSync(`cd ${INSTALL_DIR} && bun init -y`, { stdio: 'pipe' });
|
|
82
74
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
}
|
|
87
|
-
// Install dependencies
|
|
88
|
-
console.log('Installing dependencies...');
|
|
89
|
-
execSync(`cd ${installDir} && bun install`, { stdio: 'inherit' });
|
|
90
|
-
// Set up system service
|
|
75
|
+
execSync(`cd ${INSTALL_DIR} && bun add @openape/proxy`, { stdio: 'inherit' });
|
|
76
|
+
console.log(`✅ Proxy installed: ${INSTALL_DIR}`);
|
|
77
|
+
// System service
|
|
91
78
|
if (isMacOS()) {
|
|
92
|
-
|
|
79
|
+
setupLaunchd();
|
|
93
80
|
}
|
|
94
81
|
else if (isLinux()) {
|
|
95
|
-
|
|
82
|
+
setupSystemd();
|
|
96
83
|
}
|
|
97
84
|
else {
|
|
98
|
-
console.log(
|
|
99
|
-
console.log(` cd ${installDir} && bun run src/index.ts --config ${CONFIG_PATH}`);
|
|
85
|
+
console.log(`\n⚠️ Manual start: cd ${INSTALL_DIR} && bunx openape-proxy --config ${CONFIG_PATH}`);
|
|
100
86
|
}
|
|
101
|
-
closePrompt();
|
|
102
87
|
console.log('\n🎉 OpenApe Proxy installed!\n');
|
|
103
|
-
console.log(` Config:
|
|
104
|
-
console.log(`
|
|
105
|
-
console.log(`
|
|
106
|
-
console.log(`
|
|
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
|
-
}
|
|
88
|
+
console.log(` Config: ${CONFIG_PATH}`);
|
|
89
|
+
console.log(` Source: ${INSTALL_DIR}`);
|
|
90
|
+
console.log(` Listen: ${listen}`);
|
|
91
|
+
console.log(` Audit: ${auditLog}`);
|
|
92
|
+
console.log(`\n Edit ${CONFIG_PATH} to configure rules, then restart the service.\n`);
|
|
119
93
|
}
|
|
120
|
-
|
|
94
|
+
function setupLaunchd() {
|
|
121
95
|
const bunPath = run('which bun');
|
|
122
96
|
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
123
97
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
@@ -129,7 +103,7 @@ async function setupLaunchd(installDir) {
|
|
|
129
103
|
<array>
|
|
130
104
|
<string>${bunPath}</string>
|
|
131
105
|
<string>run</string>
|
|
132
|
-
<string>${
|
|
106
|
+
<string>${INSTALL_DIR}/node_modules/@openape/proxy/src/index.ts</string>
|
|
133
107
|
<string>--config</string>
|
|
134
108
|
<string>${CONFIG_PATH}</string>
|
|
135
109
|
</array>
|
|
@@ -142,20 +116,18 @@ async function setupLaunchd(installDir) {
|
|
|
142
116
|
<key>StandardErrorPath</key>
|
|
143
117
|
<string>/var/log/openape-proxy/stderr.log</string>
|
|
144
118
|
<key>WorkingDirectory</key>
|
|
145
|
-
<string>${
|
|
119
|
+
<string>${INSTALL_DIR}</string>
|
|
146
120
|
</dict>
|
|
147
121
|
</plist>`;
|
|
148
122
|
writeSecureFile(LAUNCHD_PLIST, plist, 0o644);
|
|
149
|
-
console.log(`✅ launchd service created: ${LAUNCHD_LABEL}`);
|
|
150
|
-
// Stop if already running
|
|
151
123
|
try {
|
|
152
124
|
execSync(`launchctl unload ${LAUNCHD_PLIST}`, { stdio: 'ignore' });
|
|
153
125
|
}
|
|
154
126
|
catch { }
|
|
155
127
|
execSync(`launchctl load ${LAUNCHD_PLIST}`);
|
|
156
|
-
console.log(
|
|
128
|
+
console.log(`✅ launchd service: ${LAUNCHD_LABEL}`);
|
|
157
129
|
}
|
|
158
|
-
|
|
130
|
+
function setupSystemd() {
|
|
159
131
|
const bunPath = run('which bun');
|
|
160
132
|
const unit = `[Unit]
|
|
161
133
|
Description=OpenApe HTTP Proxy
|
|
@@ -163,18 +135,13 @@ After=network.target
|
|
|
163
135
|
|
|
164
136
|
[Service]
|
|
165
137
|
Type=simple
|
|
166
|
-
ExecStart=${bunPath} run ${
|
|
167
|
-
WorkingDirectory=${
|
|
138
|
+
ExecStart=${bunPath} run ${INSTALL_DIR}/node_modules/@openape/proxy/src/index.ts --config ${CONFIG_PATH}
|
|
139
|
+
WorkingDirectory=${INSTALL_DIR}
|
|
168
140
|
Restart=on-failure
|
|
169
141
|
RestartSec=5
|
|
170
|
-
StandardOutput=journal
|
|
171
|
-
StandardError=journal
|
|
172
|
-
|
|
173
|
-
# Security hardening
|
|
174
142
|
NoNewPrivileges=true
|
|
175
143
|
ProtectSystem=strict
|
|
176
144
|
ProtectHome=true
|
|
177
|
-
ReadOnlyPaths=/
|
|
178
145
|
ReadWritePaths=/var/log/openape-proxy
|
|
179
146
|
|
|
180
147
|
[Install]
|
|
@@ -184,5 +151,5 @@ WantedBy=multi-user.target
|
|
|
184
151
|
execSync('systemctl daemon-reload');
|
|
185
152
|
execSync('systemctl enable openape-proxy');
|
|
186
153
|
execSync('systemctl start openape-proxy');
|
|
187
|
-
console.log('✅ systemd service
|
|
154
|
+
console.log('✅ systemd service: openape-proxy');
|
|
188
155
|
}
|
|
@@ -1,44 +1,123 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
2
|
import { execSync } from 'node:child_process';
|
|
3
|
-
import {
|
|
3
|
+
import { arch, platform } from 'node:os';
|
|
4
|
+
import { requireRoot, ensureDir, writeSecureFile, run, parseFlags, requireFlag } from '../utils.js';
|
|
4
5
|
const CONFIG_DIR = '/etc/apes';
|
|
5
6
|
const CONFIG_PATH = `${CONFIG_DIR}/config.toml`;
|
|
6
7
|
const BIN_PATH = '/usr/local/bin/apes';
|
|
7
|
-
|
|
8
|
+
const REPO = 'openape-ai/sudo';
|
|
9
|
+
export async function installSudo(args) {
|
|
8
10
|
requireRoot();
|
|
11
|
+
const flags = parseFlags(args);
|
|
12
|
+
const idpUrl = requireFlag(flags, 'idp-url');
|
|
13
|
+
const pollTimeout = flags['poll-timeout'] || '300';
|
|
14
|
+
const pollInterval = flags['poll-interval'] || '2';
|
|
15
|
+
const force = flags['force'] === true;
|
|
9
16
|
console.log('\n🐾 OpenApe Sudo (apes) Installer\n');
|
|
10
|
-
// Check
|
|
11
|
-
if (existsSync(BIN_PATH)) {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
+
// Check existing binary
|
|
18
|
+
if (existsSync(BIN_PATH) && !force) {
|
|
19
|
+
throw new Error(`apes already installed at ${BIN_PATH}. Use --force to reinstall.`);
|
|
20
|
+
}
|
|
21
|
+
// Try GitHub Release first, fall back to source build
|
|
22
|
+
const installed = await tryGitHubRelease();
|
|
23
|
+
if (!installed) {
|
|
24
|
+
console.log('No pre-built binary available. Building from source...');
|
|
25
|
+
buildFromSource();
|
|
26
|
+
}
|
|
27
|
+
// Config
|
|
28
|
+
if (existsSync(CONFIG_PATH) && !force) {
|
|
29
|
+
console.log(`ℹ️ Config preserved: ${CONFIG_PATH}`);
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
const config = `# OpenApe Sudo (apes) Configuration
|
|
33
|
+
# Generated by: openape install-sudo
|
|
34
|
+
|
|
35
|
+
server_url = "${idpUrl}"
|
|
36
|
+
|
|
37
|
+
# Agent identity is determined by the key passed via --key flag.
|
|
38
|
+
# Each user provides their own key: apes --key ~/.ssh/id_ed25519 -- <command>
|
|
39
|
+
|
|
40
|
+
[poll]
|
|
41
|
+
interval_secs = ${pollInterval}
|
|
42
|
+
timeout_secs = ${pollTimeout}
|
|
43
|
+
`;
|
|
44
|
+
ensureDir(CONFIG_DIR, 0o700);
|
|
45
|
+
writeSecureFile(CONFIG_PATH, config);
|
|
46
|
+
console.log(`✅ Config: ${CONFIG_PATH}`);
|
|
47
|
+
}
|
|
48
|
+
console.log('\n🎉 apes installed!\n');
|
|
49
|
+
console.log(` Binary: ${BIN_PATH} (setuid root)`);
|
|
50
|
+
console.log(` Config: ${CONFIG_PATH}`);
|
|
51
|
+
console.log('\n Next steps:');
|
|
52
|
+
console.log(` 1. Enroll: apes enroll --server ${idpUrl} --agent-email <email> --agent-name <name> --key <path>`);
|
|
53
|
+
console.log(' 2. Admin approves enrollment');
|
|
54
|
+
console.log(' 3. Use: apes --key ~/.ssh/id_ed25519 --reason "why" -- <command>\n');
|
|
55
|
+
}
|
|
56
|
+
function getAssetName() {
|
|
57
|
+
const os = platform();
|
|
58
|
+
const cpu = arch();
|
|
59
|
+
const osMap = { darwin: 'apple-darwin', linux: 'unknown-linux-gnu' };
|
|
60
|
+
const archMap = { arm64: 'aarch64', x64: 'x86_64' };
|
|
61
|
+
const osStr = osMap[os];
|
|
62
|
+
const archStr = archMap[cpu];
|
|
63
|
+
if (!osStr || !archStr) {
|
|
64
|
+
throw new Error(`Unsupported platform: ${os}-${cpu}`);
|
|
65
|
+
}
|
|
66
|
+
return `apes-${archStr}-${osStr}`;
|
|
67
|
+
}
|
|
68
|
+
async function tryGitHubRelease() {
|
|
69
|
+
try {
|
|
70
|
+
// Check if gh CLI is available
|
|
71
|
+
run('which gh');
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
console.log('GitHub CLI (gh) not found — skipping release download.');
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
const assetName = getAssetName();
|
|
79
|
+
console.log(`Looking for pre-built binary: ${assetName}...`);
|
|
80
|
+
// Download latest release asset
|
|
81
|
+
const tmpBin = '/tmp/apes-download';
|
|
82
|
+
execSync(`gh release download --repo ${REPO} --pattern "${assetName}" --output ${tmpBin} --clobber`, {
|
|
83
|
+
stdio: 'pipe',
|
|
84
|
+
timeout: 30000,
|
|
85
|
+
});
|
|
86
|
+
if (!existsSync(tmpBin)) {
|
|
87
|
+
return false;
|
|
17
88
|
}
|
|
89
|
+
// Install
|
|
90
|
+
execSync(`cp ${tmpBin} ${BIN_PATH}`);
|
|
91
|
+
execSync(`chown root:wheel ${BIN_PATH}`);
|
|
92
|
+
execSync(`chmod u+s ${BIN_PATH}`);
|
|
93
|
+
execSync(`rm -f ${tmpBin}`);
|
|
94
|
+
console.log(`✅ Binary (pre-built): ${BIN_PATH} (setuid root)`);
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
console.log('No pre-built binary found for this platform.');
|
|
99
|
+
return false;
|
|
18
100
|
}
|
|
19
|
-
|
|
20
|
-
|
|
101
|
+
}
|
|
102
|
+
function buildFromSource() {
|
|
103
|
+
// Check Rust toolchain
|
|
21
104
|
try {
|
|
22
105
|
run('cargo --version');
|
|
23
|
-
hasCargo = true;
|
|
24
106
|
}
|
|
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
|
|
107
|
+
catch {
|
|
29
108
|
throw new Error('Rust toolchain required to build apes. Install it: https://rustup.rs\n' +
|
|
30
|
-
'
|
|
109
|
+
'Or create a GitHub Release with pre-built binaries.');
|
|
31
110
|
}
|
|
32
|
-
// Build from source
|
|
33
111
|
const buildDir = '/tmp/openape-sudo-build';
|
|
34
112
|
if (existsSync(`${buildDir}/.git`)) {
|
|
35
113
|
console.log('Updating source...');
|
|
114
|
+
execSync(`git config --global --add safe.directory ${buildDir}`, { stdio: 'ignore' });
|
|
36
115
|
execSync(`cd ${buildDir} && git pull`, { stdio: 'inherit' });
|
|
37
116
|
}
|
|
38
117
|
else {
|
|
39
118
|
console.log('Downloading source...');
|
|
40
119
|
execSync(`rm -rf ${buildDir}`, { stdio: 'ignore' });
|
|
41
|
-
execSync(`git clone https://github.com
|
|
120
|
+
execSync(`git clone https://github.com/${REPO}.git ${buildDir}`, { stdio: 'inherit' });
|
|
42
121
|
}
|
|
43
122
|
console.log('Building apes (release mode)...');
|
|
44
123
|
execSync(`cd ${buildDir} && cargo build --release`, { stdio: 'inherit' });
|
|
@@ -46,41 +125,5 @@ export async function installSudo() {
|
|
|
46
125
|
execSync(`cp ${buildDir}/target/release/apes ${BIN_PATH}`);
|
|
47
126
|
execSync(`chown root:wheel ${BIN_PATH}`);
|
|
48
127
|
execSync(`chmod u+s ${BIN_PATH}`);
|
|
49
|
-
console.log(`✅
|
|
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');
|
|
128
|
+
console.log(`✅ Binary (built): ${BIN_PATH} (setuid root)`);
|
|
86
129
|
}
|
package/dist/index.js
CHANGED
|
@@ -1,36 +1,47 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { installProxy } from './commands/install-proxy.js';
|
|
3
|
-
import { installProxyApes } from './commands/install-proxy-apes.js';
|
|
4
3
|
import { installSudo } from './commands/install-sudo.js';
|
|
5
4
|
const command = process.argv[2];
|
|
5
|
+
const args = process.argv.slice(3);
|
|
6
6
|
const HELP = `
|
|
7
7
|
OpenApe CLI — install and manage OpenApe components
|
|
8
8
|
|
|
9
|
-
Usage: openape <command> [
|
|
9
|
+
Usage: openape <command> [flags]
|
|
10
10
|
|
|
11
11
|
Commands:
|
|
12
|
-
install-proxy
|
|
13
|
-
install-
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
install-proxy Install the OpenApe HTTP proxy
|
|
13
|
+
install-sudo Install apes privilege elevation binary
|
|
14
|
+
|
|
15
|
+
Elevation:
|
|
16
|
+
sudo openape install-proxy ... # traditional sudo
|
|
17
|
+
apes -- npx @openape/cli install-proxy ... # agent-friendly, grant-approved
|
|
18
|
+
|
|
19
|
+
install-proxy flags:
|
|
20
|
+
--idp-url <url> IdP URL (required)
|
|
21
|
+
--agent-email <email> Agent email (required)
|
|
22
|
+
--listen <addr> Listen address (default: 127.0.0.1:9090)
|
|
23
|
+
--default-action <act> Default action: block|request|request-async (default: block)
|
|
24
|
+
--audit-log <path> Audit log path (default: /var/log/openape-proxy/audit.log)
|
|
25
|
+
--force Overwrite existing config
|
|
26
|
+
|
|
27
|
+
install-sudo flags:
|
|
28
|
+
--idp-url <url> IdP URL (required)
|
|
29
|
+
--poll-timeout <secs> Poll timeout in seconds (default: 300)
|
|
30
|
+
--poll-interval <secs> Poll interval in seconds (default: 2)
|
|
31
|
+
--force Overwrite existing config/binary
|
|
16
32
|
|
|
17
33
|
Examples:
|
|
18
|
-
sudo openape install-proxy
|
|
19
|
-
openape install-proxy --
|
|
20
|
-
sudo openape install-sudo
|
|
34
|
+
sudo openape install-proxy --idp-url https://id.openape.at --agent-email bot@example.com
|
|
35
|
+
apes -- npx @openape/cli install-proxy --idp-url https://id.openape.at --agent-email bot@example.com
|
|
36
|
+
sudo openape install-sudo --idp-url https://id.openape.at
|
|
21
37
|
`;
|
|
22
38
|
async function main() {
|
|
23
39
|
switch (command) {
|
|
24
40
|
case 'install-proxy':
|
|
25
|
-
|
|
26
|
-
await installProxyApes();
|
|
27
|
-
}
|
|
28
|
-
else {
|
|
29
|
-
await installProxy();
|
|
30
|
-
}
|
|
41
|
+
await installProxy(args);
|
|
31
42
|
break;
|
|
32
43
|
case 'install-sudo':
|
|
33
|
-
await installSudo();
|
|
44
|
+
await installSudo(args);
|
|
34
45
|
break;
|
|
35
46
|
case 'help':
|
|
36
47
|
case '--help':
|
package/dist/utils.js
CHANGED
|
@@ -1,22 +1,9 @@
|
|
|
1
|
-
import { createInterface } from 'node:readline';
|
|
2
1
|
import { execSync } from 'node:child_process';
|
|
3
2
|
import { existsSync, mkdirSync, writeFileSync, chmodSync, chownSync } from 'node:fs';
|
|
4
3
|
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
4
|
export function requireRoot() {
|
|
18
5
|
if (process.getuid?.() !== 0) {
|
|
19
|
-
throw new Error('This command must be run as root
|
|
6
|
+
throw new Error('This command must be run as root. Use: sudo npx @openape/cli ... or: apes -- npx @openape/cli ...');
|
|
20
7
|
}
|
|
21
8
|
}
|
|
22
9
|
export function ensureDir(path, mode = 0o755) {
|
|
@@ -28,7 +15,6 @@ export function ensureDir(path, mode = 0o755) {
|
|
|
28
15
|
export function writeSecureFile(path, content, mode = 0o600) {
|
|
29
16
|
writeFileSync(path, content, 'utf-8');
|
|
30
17
|
chmodSync(path, mode);
|
|
31
|
-
// Ensure root ownership
|
|
32
18
|
try {
|
|
33
19
|
chownSync(path, 0, 0);
|
|
34
20
|
}
|
|
@@ -43,15 +29,28 @@ export function isMacOS() {
|
|
|
43
29
|
export function isLinux() {
|
|
44
30
|
return platform() === 'linux';
|
|
45
31
|
}
|
|
46
|
-
export function
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
32
|
+
export function parseFlags(args) {
|
|
33
|
+
const flags = {};
|
|
34
|
+
for (let i = 0; i < args.length; i++) {
|
|
35
|
+
const arg = args[i];
|
|
36
|
+
if (arg.startsWith('--')) {
|
|
37
|
+
const key = arg.slice(2);
|
|
38
|
+
const next = args[i + 1];
|
|
39
|
+
if (next && !next.startsWith('--')) {
|
|
40
|
+
flags[key] = next;
|
|
41
|
+
i++;
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
flags[key] = true;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
53
47
|
}
|
|
54
|
-
|
|
55
|
-
|
|
48
|
+
return flags;
|
|
49
|
+
}
|
|
50
|
+
export function requireFlag(flags, name) {
|
|
51
|
+
const val = flags[name];
|
|
52
|
+
if (!val || val === true) {
|
|
53
|
+
throw new Error(`Missing required flag: --${name}`);
|
|
56
54
|
}
|
|
55
|
+
return val;
|
|
57
56
|
}
|
package/package.json
CHANGED
|
@@ -1,135 +0,0 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs';
|
|
2
|
-
import { execSync } from 'node:child_process';
|
|
3
|
-
import { writeFileSync } from 'node:fs';
|
|
4
|
-
import { ask, closePrompt, isMacOS } from '../utils.js';
|
|
5
|
-
const CONFIG_DIR = '/etc/openape-proxy';
|
|
6
|
-
const CONFIG_PATH = `${CONFIG_DIR}/config.toml`;
|
|
7
|
-
const INSTALL_DIR = '/opt/openape-proxy';
|
|
8
|
-
const LAUNCHD_LABEL = 'ai.openape.proxy';
|
|
9
|
-
const LAUNCHD_PLIST = `/Library/LaunchDaemons/${LAUNCHD_LABEL}.plist`;
|
|
10
|
-
function apes(reason, cmd) {
|
|
11
|
-
// Find apes key — prefer user's SSH key
|
|
12
|
-
const keyPaths = [
|
|
13
|
-
`${process.env.HOME}/.ssh/id_ed25519`,
|
|
14
|
-
`${process.env.HOME}/.ssh/id_ecdsa`,
|
|
15
|
-
`${process.env.HOME}/.ssh/id_rsa`,
|
|
16
|
-
];
|
|
17
|
-
const keyPath = keyPaths.find(p => existsSync(p));
|
|
18
|
-
if (!keyPath) {
|
|
19
|
-
throw new Error('No SSH key found. apes needs --key to authenticate.');
|
|
20
|
-
}
|
|
21
|
-
console.log(`\n🔐 Requesting grant: ${reason}`);
|
|
22
|
-
execSync(`apes --key ${keyPath} --reason "${reason}" -- ${cmd}`, {
|
|
23
|
-
stdio: 'inherit',
|
|
24
|
-
});
|
|
25
|
-
}
|
|
26
|
-
export async function installProxyApes() {
|
|
27
|
-
// Check apes is installed
|
|
28
|
-
try {
|
|
29
|
-
execSync('which apes', { stdio: 'ignore' });
|
|
30
|
-
}
|
|
31
|
-
catch {
|
|
32
|
-
throw new Error('apes is not installed. Run: sudo npx @openape/cli install-sudo');
|
|
33
|
-
}
|
|
34
|
-
console.log('\n🐾 OpenApe Proxy Installer (via apes)\n');
|
|
35
|
-
console.log('This installer uses apes for privilege elevation.');
|
|
36
|
-
console.log('Each privileged step requires approval from your admin.\n');
|
|
37
|
-
// Gather config interactively (no root needed)
|
|
38
|
-
const idpUrl = await ask('IdP URL', 'https://id.test.openape.at');
|
|
39
|
-
const agentEmail = await ask('Agent email');
|
|
40
|
-
const listen = await ask('Listen address', '127.0.0.1:9090');
|
|
41
|
-
const defaultAction = await ask('Default action (block/request/request-async)', 'block');
|
|
42
|
-
const auditLog = await ask('Audit log path', '/var/log/openape-proxy/audit.log');
|
|
43
|
-
closePrompt();
|
|
44
|
-
// Generate config locally in /tmp
|
|
45
|
-
const config = `# OpenApe Proxy Configuration
|
|
46
|
-
# Generated by: openape install-proxy --via-apes
|
|
47
|
-
|
|
48
|
-
[proxy]
|
|
49
|
-
listen = "${listen}"
|
|
50
|
-
idp_url = "${idpUrl}"
|
|
51
|
-
agent_email = "${agentEmail}"
|
|
52
|
-
default_action = "${defaultAction}"
|
|
53
|
-
audit_log = "${auditLog}"
|
|
54
|
-
|
|
55
|
-
# Add your rules below:
|
|
56
|
-
|
|
57
|
-
# [[allow]]
|
|
58
|
-
# domain = "api.github.com"
|
|
59
|
-
# methods = ["GET"]
|
|
60
|
-
# note = "GitHub read-only"
|
|
61
|
-
|
|
62
|
-
# [[deny]]
|
|
63
|
-
# domain = "*.malware.example.com"
|
|
64
|
-
# note = "Blocked"
|
|
65
|
-
|
|
66
|
-
# [[grant_required]]
|
|
67
|
-
# domain = "api.github.com"
|
|
68
|
-
# methods = ["POST", "PUT", "DELETE"]
|
|
69
|
-
# grant_type = "allow_once"
|
|
70
|
-
# note = "Needs approval"
|
|
71
|
-
`;
|
|
72
|
-
const tmpConfig = '/tmp/openape-proxy-config.toml';
|
|
73
|
-
writeFileSync(tmpConfig, config);
|
|
74
|
-
console.log(`\n📝 Config prepared at ${tmpConfig}`);
|
|
75
|
-
// Step 1: Create config directory
|
|
76
|
-
apes('Create OpenApe Proxy config directory', `mkdir -p ${CONFIG_DIR}`);
|
|
77
|
-
apes('Set permissions on config directory', `chmod 700 ${CONFIG_DIR}`);
|
|
78
|
-
// Step 2: Copy config
|
|
79
|
-
apes('Install proxy config', `cp ${tmpConfig} ${CONFIG_PATH}`);
|
|
80
|
-
apes('Secure proxy config (root-only)', `chmod 600 ${CONFIG_PATH}`);
|
|
81
|
-
// Step 3: Create audit log directory
|
|
82
|
-
const auditDir = auditLog.substring(0, auditLog.lastIndexOf('/'));
|
|
83
|
-
if (auditDir) {
|
|
84
|
-
apes('Create audit log directory', `mkdir -p ${auditDir}`);
|
|
85
|
-
}
|
|
86
|
-
// Step 4: Clone/update proxy source
|
|
87
|
-
if (existsSync(`${INSTALL_DIR}/package.json`)) {
|
|
88
|
-
apes('Update proxy source', `git -C ${INSTALL_DIR} pull`);
|
|
89
|
-
}
|
|
90
|
-
else {
|
|
91
|
-
apes('Download proxy source', `git clone https://github.com/openape-ai/proxy.git ${INSTALL_DIR}`);
|
|
92
|
-
}
|
|
93
|
-
// Step 5: Install dependencies
|
|
94
|
-
const bunPath = execSync('which bun', { encoding: 'utf-8' }).trim();
|
|
95
|
-
apes('Install proxy dependencies', `${bunPath} install --cwd ${INSTALL_DIR}`);
|
|
96
|
-
// Step 6: Set up system service
|
|
97
|
-
if (isMacOS()) {
|
|
98
|
-
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
99
|
-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
100
|
-
<plist version="1.0">
|
|
101
|
-
<dict>
|
|
102
|
-
<key>Label</key>
|
|
103
|
-
<string>${LAUNCHD_LABEL}</string>
|
|
104
|
-
<key>ProgramArguments</key>
|
|
105
|
-
<array>
|
|
106
|
-
<string>${bunPath}</string>
|
|
107
|
-
<string>run</string>
|
|
108
|
-
<string>${INSTALL_DIR}/src/index.ts</string>
|
|
109
|
-
<string>--config</string>
|
|
110
|
-
<string>${CONFIG_PATH}</string>
|
|
111
|
-
</array>
|
|
112
|
-
<key>RunAtLoad</key>
|
|
113
|
-
<true/>
|
|
114
|
-
<key>KeepAlive</key>
|
|
115
|
-
<true/>
|
|
116
|
-
<key>StandardOutPath</key>
|
|
117
|
-
<string>/var/log/openape-proxy/stdout.log</string>
|
|
118
|
-
<key>StandardErrorPath</key>
|
|
119
|
-
<string>/var/log/openape-proxy/stderr.log</string>
|
|
120
|
-
<key>WorkingDirectory</key>
|
|
121
|
-
<string>${INSTALL_DIR}</string>
|
|
122
|
-
</dict>
|
|
123
|
-
</plist>`;
|
|
124
|
-
const tmpPlist = '/tmp/openape-proxy-launchd.plist';
|
|
125
|
-
writeFileSync(tmpPlist, plist);
|
|
126
|
-
apes('Install launchd service', `cp ${tmpPlist} ${LAUNCHD_PLIST}`);
|
|
127
|
-
apes('Load proxy service', `launchctl load ${LAUNCHD_PLIST}`);
|
|
128
|
-
}
|
|
129
|
-
console.log('\n🎉 OpenApe Proxy installed via apes!\n');
|
|
130
|
-
console.log(` Config: ${CONFIG_PATH}`);
|
|
131
|
-
console.log(` Proxy: ${INSTALL_DIR}`);
|
|
132
|
-
console.log(` Audit log: ${auditLog}`);
|
|
133
|
-
console.log(` Listening: ${listen}`);
|
|
134
|
-
console.log('\n Every step was approved by your admin. 🔐\n');
|
|
135
|
-
}
|