@openape/cli 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 openape-ai
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,53 @@
1
+ # @openape/cli
2
+
3
+ Install and manage OpenApe components.
4
+
5
+ ## Usage
6
+
7
+ ```bash
8
+ # Install the HTTP proxy (grant-controlled outbound traffic)
9
+ sudo npx @openape/cli install-proxy
10
+
11
+ # Install apes (privilege elevation via grants)
12
+ sudo npx @openape/cli install-sudo
13
+ ```
14
+
15
+ ## Commands
16
+
17
+ ### `install-proxy`
18
+
19
+ Installs the OpenApe HTTP proxy as a system service:
20
+
21
+ - Creates `/etc/openape-proxy/config.toml` (root-only, `600`)
22
+ - Downloads proxy source to `/opt/openape-proxy`
23
+ - Sets up launchd (macOS) or systemd (Linux) service
24
+ - Starts the proxy
25
+
26
+ The proxy runs as a system service. Agents send HTTP requests through it.
27
+ The config is **not accessible to agents** — only root can modify it.
28
+
29
+ ### `install-sudo`
30
+
31
+ Builds and installs `apes` (OpenApe Sudo):
32
+
33
+ - Builds from source via Cargo (Rust toolchain required)
34
+ - Installs to `/usr/local/bin/apes` with setuid bit
35
+ - Creates `/etc/apes/config.toml` (root-only, `600`)
36
+
37
+ After installation, register the agent on the IdP with `apes enroll`.
38
+
39
+ ## Security
40
+
41
+ Both installers create root-owned config files that agents cannot modify.
42
+ This is by design — the security boundary between agent and config must be
43
+ enforced by the OS file permission system.
44
+
45
+ | Path | Owner | Mode | Purpose |
46
+ |------|-------|------|---------|
47
+ | `/etc/openape-proxy/config.toml` | root | `600` | Proxy rules |
48
+ | `/etc/apes/config.toml` | root | `600` | apes config |
49
+ | `/usr/local/bin/apes` | root | `4755` (setuid) | Privilege elevation |
50
+
51
+ ## License
52
+
53
+ MIT
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@openape/cli",
3
+ "version": "0.1.0",
4
+ "description": "OpenApe CLI — install and manage OpenApe components (proxy, sudo, auth)",
5
+ "type": "module",
6
+ "bin": {
7
+ "openape": "./src/index.ts"
8
+ },
9
+ "scripts": {
10
+ "start": "bun run src/index.ts",
11
+ "typecheck": "tsc --noEmit"
12
+ },
13
+ "keywords": [
14
+ "openape",
15
+ "security",
16
+ "proxy",
17
+ "sudo",
18
+ "grants",
19
+ "agent",
20
+ "ai",
21
+ "auth",
22
+ "passkey",
23
+ "webauthn"
24
+ ],
25
+ "author": "Patrick Hofmann <patrick@hofmann.eco>",
26
+ "license": "MIT",
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "https://github.com/openape-ai/cli.git"
30
+ },
31
+ "homepage": "https://openape.at",
32
+ "bugs": {
33
+ "url": "https://github.com/openape-ai/cli/issues"
34
+ },
35
+ "dependencies": {
36
+ "smol-toml": "^1.3.0"
37
+ },
38
+ "devDependencies": {
39
+ "@types/node": "^22.0.0",
40
+ "typescript": "^5.7.0"
41
+ },
42
+ "engines": {
43
+ "node": ">=18"
44
+ },
45
+ "files": [
46
+ "src/",
47
+ "README.md",
48
+ "LICENSE"
49
+ ]
50
+ }
@@ -0,0 +1,205 @@
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
+ }
@@ -0,0 +1,102 @@
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 ADDED
@@ -0,0 +1,47 @@
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 ADDED
@@ -0,0 +1,63 @@
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
+ }