@openape/cli 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -29
- package/dist/commands/install-proxy-apes.js +162 -0
- package/dist/commands/install-proxy.js +42 -71
- package/dist/commands/install-sudo.js +28 -41
- package/dist/index.js +27 -8
- package/dist/utils.js +23 -24
- package/package.json +1 -1
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
|
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { execSync } from 'node:child_process';
|
|
3
|
+
import { writeFileSync } from 'node:fs';
|
|
4
|
+
import { parseArgs } from 'node:util';
|
|
5
|
+
import { ask, closePrompt, isMacOS } from '../utils.js';
|
|
6
|
+
const CONFIG_DIR = '/etc/openape-proxy';
|
|
7
|
+
const CONFIG_PATH = `${CONFIG_DIR}/config.toml`;
|
|
8
|
+
const INSTALL_DIR = '/opt/openape-proxy';
|
|
9
|
+
const LAUNCHD_LABEL = 'ai.openape.proxy';
|
|
10
|
+
const LAUNCHD_PLIST = `/Library/LaunchDaemons/${LAUNCHD_LABEL}.plist`;
|
|
11
|
+
function apes(reason, cmd) {
|
|
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
|
+
// Parse flags for non-interactive mode
|
|
35
|
+
const { values: flags } = parseArgs({
|
|
36
|
+
args: process.argv.slice(3),
|
|
37
|
+
options: {
|
|
38
|
+
'idp-url': { type: 'string' },
|
|
39
|
+
'agent-email': { type: 'string' },
|
|
40
|
+
'listen': { type: 'string' },
|
|
41
|
+
'default-action': { type: 'string' },
|
|
42
|
+
'audit-log': { type: 'string' },
|
|
43
|
+
'via-apes': { type: 'boolean' },
|
|
44
|
+
},
|
|
45
|
+
strict: false,
|
|
46
|
+
});
|
|
47
|
+
const interactive = !flags['agent-email'];
|
|
48
|
+
console.log('\n🐾 OpenApe Proxy Installer (via apes)\n');
|
|
49
|
+
console.log('Each privileged step requires approval from your admin.\n');
|
|
50
|
+
let idpUrl, agentEmail, listen, defaultAction, auditLog;
|
|
51
|
+
if (interactive) {
|
|
52
|
+
idpUrl = await ask('IdP URL', 'https://id.test.openape.at');
|
|
53
|
+
agentEmail = await ask('Agent email');
|
|
54
|
+
listen = await ask('Listen address', '127.0.0.1:9090');
|
|
55
|
+
defaultAction = await ask('Default action (block/request/request-async)', 'block');
|
|
56
|
+
auditLog = await ask('Audit log path', '/var/log/openape-proxy/audit.log');
|
|
57
|
+
closePrompt();
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
idpUrl = flags['idp-url'] || 'https://id.test.openape.at';
|
|
61
|
+
agentEmail = flags['agent-email'];
|
|
62
|
+
listen = flags['listen'] || '127.0.0.1:9090';
|
|
63
|
+
defaultAction = flags['default-action'] || 'block';
|
|
64
|
+
auditLog = flags['audit-log'] || '/var/log/openape-proxy/audit.log';
|
|
65
|
+
console.log(` IdP: ${idpUrl}`);
|
|
66
|
+
console.log(` Agent: ${agentEmail}`);
|
|
67
|
+
console.log(` Listen: ${listen}`);
|
|
68
|
+
console.log(` Default: ${defaultAction}`);
|
|
69
|
+
console.log(` Audit: ${auditLog}`);
|
|
70
|
+
}
|
|
71
|
+
// Generate config locally in /tmp
|
|
72
|
+
const config = `# OpenApe Proxy Configuration
|
|
73
|
+
# Generated by: openape install-proxy --via-apes
|
|
74
|
+
|
|
75
|
+
[proxy]
|
|
76
|
+
listen = "${listen}"
|
|
77
|
+
idp_url = "${idpUrl}"
|
|
78
|
+
agent_email = "${agentEmail}"
|
|
79
|
+
default_action = "${defaultAction}"
|
|
80
|
+
audit_log = "${auditLog}"
|
|
81
|
+
|
|
82
|
+
# Add your rules below:
|
|
83
|
+
|
|
84
|
+
# [[allow]]
|
|
85
|
+
# domain = "api.github.com"
|
|
86
|
+
# methods = ["GET"]
|
|
87
|
+
# note = "GitHub read-only"
|
|
88
|
+
|
|
89
|
+
# [[deny]]
|
|
90
|
+
# domain = "*.malware.example.com"
|
|
91
|
+
# note = "Blocked"
|
|
92
|
+
|
|
93
|
+
# [[grant_required]]
|
|
94
|
+
# domain = "api.github.com"
|
|
95
|
+
# methods = ["POST", "PUT", "DELETE"]
|
|
96
|
+
# grant_type = "allow_once"
|
|
97
|
+
# note = "Needs approval"
|
|
98
|
+
`;
|
|
99
|
+
const tmpConfig = '/tmp/openape-proxy-config.toml';
|
|
100
|
+
writeFileSync(tmpConfig, config);
|
|
101
|
+
console.log(`\n📝 Config prepared at ${tmpConfig}`);
|
|
102
|
+
// Step 1: Create config directory
|
|
103
|
+
apes('Create OpenApe Proxy config directory', `mkdir -p ${CONFIG_DIR}`);
|
|
104
|
+
apes('Set permissions on config directory', `chmod 700 ${CONFIG_DIR}`);
|
|
105
|
+
// Step 2: Copy config
|
|
106
|
+
apes('Install proxy config', `cp ${tmpConfig} ${CONFIG_PATH}`);
|
|
107
|
+
apes('Secure proxy config (root-only)', `chmod 600 ${CONFIG_PATH}`);
|
|
108
|
+
// Step 3: Create audit log directory
|
|
109
|
+
const auditDir = auditLog.substring(0, auditLog.lastIndexOf('/'));
|
|
110
|
+
if (auditDir) {
|
|
111
|
+
apes('Create audit log directory', `mkdir -p ${auditDir}`);
|
|
112
|
+
}
|
|
113
|
+
// Step 4: Clone/update proxy source
|
|
114
|
+
if (existsSync(`${INSTALL_DIR}/package.json`)) {
|
|
115
|
+
apes('Update proxy source', `git -C ${INSTALL_DIR} pull`);
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
apes('Download proxy source', `git clone https://github.com/openape-ai/proxy.git ${INSTALL_DIR}`);
|
|
119
|
+
}
|
|
120
|
+
// Step 5: Install dependencies
|
|
121
|
+
const bunPath = execSync('which bun', { encoding: 'utf-8' }).trim();
|
|
122
|
+
apes('Install proxy dependencies', `${bunPath} install --cwd ${INSTALL_DIR}`);
|
|
123
|
+
// Step 6: Set up system service
|
|
124
|
+
if (isMacOS()) {
|
|
125
|
+
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
126
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
127
|
+
<plist version="1.0">
|
|
128
|
+
<dict>
|
|
129
|
+
<key>Label</key>
|
|
130
|
+
<string>${LAUNCHD_LABEL}</string>
|
|
131
|
+
<key>ProgramArguments</key>
|
|
132
|
+
<array>
|
|
133
|
+
<string>${bunPath}</string>
|
|
134
|
+
<string>run</string>
|
|
135
|
+
<string>${INSTALL_DIR}/src/index.ts</string>
|
|
136
|
+
<string>--config</string>
|
|
137
|
+
<string>${CONFIG_PATH}</string>
|
|
138
|
+
</array>
|
|
139
|
+
<key>RunAtLoad</key>
|
|
140
|
+
<true/>
|
|
141
|
+
<key>KeepAlive</key>
|
|
142
|
+
<true/>
|
|
143
|
+
<key>StandardOutPath</key>
|
|
144
|
+
<string>/var/log/openape-proxy/stdout.log</string>
|
|
145
|
+
<key>StandardErrorPath</key>
|
|
146
|
+
<string>/var/log/openape-proxy/stderr.log</string>
|
|
147
|
+
<key>WorkingDirectory</key>
|
|
148
|
+
<string>${INSTALL_DIR}</string>
|
|
149
|
+
</dict>
|
|
150
|
+
</plist>`;
|
|
151
|
+
const tmpPlist = '/tmp/openape-proxy-launchd.plist';
|
|
152
|
+
writeFileSync(tmpPlist, plist);
|
|
153
|
+
apes('Install launchd service', `cp ${tmpPlist} ${LAUNCHD_PLIST}`);
|
|
154
|
+
apes('Load proxy service', `launchctl load ${LAUNCHD_PLIST}`);
|
|
155
|
+
}
|
|
156
|
+
console.log('\n🎉 OpenApe Proxy installed via apes!\n');
|
|
157
|
+
console.log(` Config: ${CONFIG_PATH}`);
|
|
158
|
+
console.log(` Proxy: ${INSTALL_DIR}`);
|
|
159
|
+
console.log(` Audit log: ${auditLog}`);
|
|
160
|
+
console.log(` Listening: ${listen}`);
|
|
161
|
+
console.log('\n Every step was approved by your admin. 🔐\n');
|
|
162
|
+
}
|
|
@@ -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,48 @@ 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
|
-
//
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
// Clone or update proxy source
|
|
79
|
-
if (existsSync(`${installDir}/package.json`)) {
|
|
69
|
+
// Clone/update proxy source
|
|
70
|
+
ensureDir(INSTALL_DIR, 0o755);
|
|
71
|
+
if (existsSync(`${INSTALL_DIR}/package.json`)) {
|
|
80
72
|
console.log('Updating proxy source...');
|
|
81
|
-
execSync(`cd ${
|
|
73
|
+
execSync(`cd ${INSTALL_DIR} && git pull`, { stdio: 'inherit' });
|
|
82
74
|
}
|
|
83
75
|
else {
|
|
84
76
|
console.log('Downloading proxy source...');
|
|
85
|
-
execSync(`git clone https://github.com/openape-ai/proxy.git ${
|
|
77
|
+
execSync(`git clone https://github.com/openape-ai/proxy.git ${INSTALL_DIR}`, { stdio: 'inherit' });
|
|
86
78
|
}
|
|
87
|
-
// Install dependencies
|
|
88
79
|
console.log('Installing dependencies...');
|
|
89
|
-
execSync(`cd ${
|
|
90
|
-
//
|
|
80
|
+
execSync(`cd ${INSTALL_DIR} && bun install`, { stdio: 'inherit' });
|
|
81
|
+
// System service
|
|
91
82
|
if (isMacOS()) {
|
|
92
|
-
|
|
83
|
+
setupLaunchd();
|
|
93
84
|
}
|
|
94
85
|
else if (isLinux()) {
|
|
95
|
-
|
|
86
|
+
setupSystemd();
|
|
96
87
|
}
|
|
97
88
|
else {
|
|
98
|
-
console.log(
|
|
99
|
-
console.log(` cd ${installDir} && bun run src/index.ts --config ${CONFIG_PATH}`);
|
|
89
|
+
console.log(`\n⚠️ Manual start: cd ${INSTALL_DIR} && bun run src/index.ts --config ${CONFIG_PATH}`);
|
|
100
90
|
}
|
|
101
|
-
closePrompt();
|
|
102
91
|
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
|
-
}
|
|
92
|
+
console.log(` Config: ${CONFIG_PATH}`);
|
|
93
|
+
console.log(` Source: ${INSTALL_DIR}`);
|
|
94
|
+
console.log(` Listen: ${listen}`);
|
|
95
|
+
console.log(` Audit: ${auditLog}`);
|
|
96
|
+
console.log(`\n Edit ${CONFIG_PATH} to configure rules, then restart the service.\n`);
|
|
119
97
|
}
|
|
120
|
-
|
|
98
|
+
function setupLaunchd() {
|
|
121
99
|
const bunPath = run('which bun');
|
|
122
100
|
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
123
101
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
@@ -129,7 +107,7 @@ async function setupLaunchd(installDir) {
|
|
|
129
107
|
<array>
|
|
130
108
|
<string>${bunPath}</string>
|
|
131
109
|
<string>run</string>
|
|
132
|
-
<string>${
|
|
110
|
+
<string>${INSTALL_DIR}/src/index.ts</string>
|
|
133
111
|
<string>--config</string>
|
|
134
112
|
<string>${CONFIG_PATH}</string>
|
|
135
113
|
</array>
|
|
@@ -142,20 +120,18 @@ async function setupLaunchd(installDir) {
|
|
|
142
120
|
<key>StandardErrorPath</key>
|
|
143
121
|
<string>/var/log/openape-proxy/stderr.log</string>
|
|
144
122
|
<key>WorkingDirectory</key>
|
|
145
|
-
<string>${
|
|
123
|
+
<string>${INSTALL_DIR}</string>
|
|
146
124
|
</dict>
|
|
147
125
|
</plist>`;
|
|
148
126
|
writeSecureFile(LAUNCHD_PLIST, plist, 0o644);
|
|
149
|
-
console.log(`✅ launchd service created: ${LAUNCHD_LABEL}`);
|
|
150
|
-
// Stop if already running
|
|
151
127
|
try {
|
|
152
128
|
execSync(`launchctl unload ${LAUNCHD_PLIST}`, { stdio: 'ignore' });
|
|
153
129
|
}
|
|
154
130
|
catch { }
|
|
155
131
|
execSync(`launchctl load ${LAUNCHD_PLIST}`);
|
|
156
|
-
console.log(
|
|
132
|
+
console.log(`✅ launchd service: ${LAUNCHD_LABEL}`);
|
|
157
133
|
}
|
|
158
|
-
|
|
134
|
+
function setupSystemd() {
|
|
159
135
|
const bunPath = run('which bun');
|
|
160
136
|
const unit = `[Unit]
|
|
161
137
|
Description=OpenApe HTTP Proxy
|
|
@@ -163,18 +139,13 @@ After=network.target
|
|
|
163
139
|
|
|
164
140
|
[Service]
|
|
165
141
|
Type=simple
|
|
166
|
-
ExecStart=${bunPath} run ${
|
|
167
|
-
WorkingDirectory=${
|
|
142
|
+
ExecStart=${bunPath} run ${INSTALL_DIR}/src/index.ts --config ${CONFIG_PATH}
|
|
143
|
+
WorkingDirectory=${INSTALL_DIR}
|
|
168
144
|
Restart=on-failure
|
|
169
145
|
RestartSec=5
|
|
170
|
-
StandardOutput=journal
|
|
171
|
-
StandardError=journal
|
|
172
|
-
|
|
173
|
-
# Security hardening
|
|
174
146
|
NoNewPrivileges=true
|
|
175
147
|
ProtectSystem=strict
|
|
176
148
|
ProtectHome=true
|
|
177
|
-
ReadOnlyPaths=/
|
|
178
149
|
ReadWritePaths=/var/log/openape-proxy
|
|
179
150
|
|
|
180
151
|
[Install]
|
|
@@ -184,5 +155,5 @@ WantedBy=multi-user.target
|
|
|
184
155
|
execSync('systemctl daemon-reload');
|
|
185
156
|
execSync('systemctl enable openape-proxy');
|
|
186
157
|
execSync('systemctl start openape-proxy');
|
|
187
|
-
console.log('✅ systemd service
|
|
158
|
+
console.log('✅ systemd service: openape-proxy');
|
|
188
159
|
}
|
|
@@ -1,31 +1,26 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
2
|
import { execSync } from 'node:child_process';
|
|
3
|
-
import {
|
|
3
|
+
import { requireRoot, ensureDir, writeSecureFile, run, parseFlags, requireFlag } from '../utils.js';
|
|
4
4
|
const CONFIG_DIR = '/etc/apes';
|
|
5
5
|
const CONFIG_PATH = `${CONFIG_DIR}/config.toml`;
|
|
6
6
|
const BIN_PATH = '/usr/local/bin/apes';
|
|
7
|
-
export async function installSudo() {
|
|
7
|
+
export async function installSudo(args) {
|
|
8
8
|
requireRoot();
|
|
9
|
+
const flags = parseFlags(args);
|
|
10
|
+
const idpUrl = requireFlag(flags, 'idp-url');
|
|
11
|
+
const pollTimeout = flags['poll-timeout'] || '300';
|
|
12
|
+
const pollInterval = flags['poll-interval'] || '2';
|
|
13
|
+
const force = flags['force'] === true;
|
|
9
14
|
console.log('\n🐾 OpenApe Sudo (apes) Installer\n');
|
|
10
|
-
// Check
|
|
11
|
-
if (existsSync(BIN_PATH)) {
|
|
12
|
-
|
|
13
|
-
if (overwrite.toLowerCase() !== 'y') {
|
|
14
|
-
console.log('Aborted.');
|
|
15
|
-
closePrompt();
|
|
16
|
-
return;
|
|
17
|
-
}
|
|
15
|
+
// Check existing binary
|
|
16
|
+
if (existsSync(BIN_PATH) && !force) {
|
|
17
|
+
throw new Error(`apes already installed at ${BIN_PATH}. Use --force to reinstall.`);
|
|
18
18
|
}
|
|
19
|
-
// Check
|
|
20
|
-
let hasCargo = false;
|
|
19
|
+
// Check Rust toolchain
|
|
21
20
|
try {
|
|
22
21
|
run('cargo --version');
|
|
23
|
-
hasCargo = true;
|
|
24
22
|
}
|
|
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
|
|
23
|
+
catch {
|
|
29
24
|
throw new Error('Rust toolchain required to build apes. Install it: https://rustup.rs\n' +
|
|
30
25
|
'Pre-built binaries will be available in a future release.');
|
|
31
26
|
}
|
|
@@ -46,41 +41,33 @@ export async function installSudo() {
|
|
|
46
41
|
execSync(`cp ${buildDir}/target/release/apes ${BIN_PATH}`);
|
|
47
42
|
execSync(`chown root:wheel ${BIN_PATH}`);
|
|
48
43
|
execSync(`chmod u+s ${BIN_PATH}`);
|
|
49
|
-
console.log(`✅
|
|
50
|
-
// Config
|
|
51
|
-
if (existsSync(CONFIG_PATH)) {
|
|
52
|
-
|
|
53
|
-
if (overwriteConfig.toLowerCase() !== 'y') {
|
|
54
|
-
closePrompt();
|
|
55
|
-
console.log('\n🎉 apes binary updated! Existing config preserved.\n');
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
44
|
+
console.log(`✅ Binary: ${BIN_PATH} (setuid root)`);
|
|
45
|
+
// Config
|
|
46
|
+
if (existsSync(CONFIG_PATH) && !force) {
|
|
47
|
+
console.log(`ℹ️ Config preserved: ${CONFIG_PATH}`);
|
|
58
48
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const intervalSecs = await ask('Poll interval (seconds)', '2');
|
|
62
|
-
const config = `# OpenApe Sudo (apes) Configuration
|
|
49
|
+
else {
|
|
50
|
+
const config = `# OpenApe Sudo (apes) Configuration
|
|
63
51
|
# Generated by: openape install-sudo
|
|
64
52
|
|
|
65
|
-
server_url = "${
|
|
53
|
+
server_url = "${idpUrl}"
|
|
66
54
|
|
|
67
55
|
# Agent identity is determined by the key passed via --key flag.
|
|
68
56
|
# Each user provides their own key: apes --key ~/.ssh/id_ed25519 -- <command>
|
|
69
57
|
|
|
70
58
|
[poll]
|
|
71
|
-
interval_secs = ${
|
|
72
|
-
timeout_secs = ${
|
|
59
|
+
interval_secs = ${pollInterval}
|
|
60
|
+
timeout_secs = ${pollTimeout}
|
|
73
61
|
`;
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
62
|
+
ensureDir(CONFIG_DIR, 0o700);
|
|
63
|
+
writeSecureFile(CONFIG_PATH, config);
|
|
64
|
+
console.log(`✅ Config: ${CONFIG_PATH}`);
|
|
65
|
+
}
|
|
78
66
|
console.log('\n🎉 apes installed!\n');
|
|
79
67
|
console.log(` Binary: ${BIN_PATH} (setuid root)`);
|
|
80
68
|
console.log(` Config: ${CONFIG_PATH}`);
|
|
81
69
|
console.log('\n Next steps:');
|
|
82
|
-
console.log(
|
|
83
|
-
console.log(
|
|
84
|
-
console.log('
|
|
85
|
-
console.log(' 3. Use it: apes --key ~/.ssh/id_ed25519 --reason "why" -- <command>\n');
|
|
70
|
+
console.log(` 1. Enroll: apes enroll --server ${idpUrl} --agent-email <email> --agent-name <name> --key <path>`);
|
|
71
|
+
console.log(' 2. Admin approves enrollment');
|
|
72
|
+
console.log(' 3. Use: apes --key ~/.ssh/id_ed25519 --reason "why" -- <command>\n');
|
|
86
73
|
}
|
package/dist/index.js
CHANGED
|
@@ -2,27 +2,46 @@
|
|
|
2
2
|
import { installProxy } from './commands/install-proxy.js';
|
|
3
3
|
import { installSudo } from './commands/install-sudo.js';
|
|
4
4
|
const command = process.argv[2];
|
|
5
|
+
const args = process.argv.slice(3);
|
|
5
6
|
const HELP = `
|
|
6
7
|
OpenApe CLI — install and manage OpenApe components
|
|
7
8
|
|
|
8
|
-
Usage: openape <command> [
|
|
9
|
+
Usage: openape <command> [flags]
|
|
9
10
|
|
|
10
11
|
Commands:
|
|
11
|
-
install-proxy
|
|
12
|
-
install-sudo
|
|
13
|
-
|
|
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
|
|
14
32
|
|
|
15
33
|
Examples:
|
|
16
|
-
sudo openape install-proxy
|
|
17
|
-
|
|
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
|
|
18
37
|
`;
|
|
19
38
|
async function main() {
|
|
20
39
|
switch (command) {
|
|
21
40
|
case 'install-proxy':
|
|
22
|
-
await installProxy();
|
|
41
|
+
await installProxy(args);
|
|
23
42
|
break;
|
|
24
43
|
case 'install-sudo':
|
|
25
|
-
await installSudo();
|
|
44
|
+
await installSudo(args);
|
|
26
45
|
break;
|
|
27
46
|
case 'help':
|
|
28
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
|
}
|