@aegisagentics/agent 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/README.md +239 -0
- package/npm/install.js +229 -0
- package/npm/lib/cli.js +80 -0
- package/npm/lib/codex-hooks.js +165 -0
- package/npm/lib/codex-plugin.js +186 -0
- package/npm/lib/codex-runtime.js +81 -0
- package/npm/lib/codex.js +110 -0
- package/npm/lib/config.js +106 -0
- package/npm/lib/release-download.js +275 -0
- package/npm/release-manifest.json +23 -0
- package/package.json +34 -0
package/README.md
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
# Aegis Agent
|
|
2
|
+
|
|
3
|
+
Local installer, hook runtime, and public npm bootstrapper for Aegis ControlTower coding-agent telemetry.
|
|
4
|
+
|
|
5
|
+
`@aegisagentics/agent` installs a native `aegis-agent` runtime, registers supported local agent hooks, and sends safe, governed telemetry to Aegis ControlTower. The npm package is intentionally thin: it does not ship platform binaries. Release binaries are built by CI, checksummed, uploaded to the Aegis website release path, then downloaded and verified during install.
|
|
6
|
+
|
|
7
|
+
## Quick Start
|
|
8
|
+
|
|
9
|
+
Create an agent installation in ControlTower, then run the generated command:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
AEGIS_API_KEY="agk_or_bootstrap_token" npx -y @aegisagentics/agent install \
|
|
13
|
+
--api-base-url "https://stage.0.aegisagentics.com" \
|
|
14
|
+
--otlp-http-base-url "https://otel.stage.0.aegisagentics.com" \
|
|
15
|
+
--installation-id "agent_install_..." \
|
|
16
|
+
--vendors codex
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Useful follow-up commands:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
aegis-agent status
|
|
23
|
+
aegis-agent doctor
|
|
24
|
+
aegis-agent flush
|
|
25
|
+
npx -y @aegisagentics/agent codex-plugin uninstall
|
|
26
|
+
npx -y @aegisagentics/agent uninstall --vendors codex
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
For local development without downloading a release binary:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
cargo build --release
|
|
33
|
+
AEGIS_AGENT_BINARY_PATH="$PWD/target/release/aegis-agent" npm exec -- aegis-agent --help
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## What This Repo Contains
|
|
37
|
+
|
|
38
|
+
```text
|
|
39
|
+
package.json Public npm bootstrapper metadata
|
|
40
|
+
npm/install.js User-facing npx entrypoint
|
|
41
|
+
npm/lib/ Bootstrapper, Codex hook, and release download logic
|
|
42
|
+
npm/test/ Node test suite
|
|
43
|
+
npm/release-manifest.json Publish-time manifest embedded in the npm package
|
|
44
|
+
crates/aegis-agent/ Native Rust runtime
|
|
45
|
+
.github/workflows/release.yml CI release workflow for binaries + npm
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Install Lifecycle
|
|
49
|
+
|
|
50
|
+
1. A ControlTower admin creates an agent installation.
|
|
51
|
+
2. ControlTower returns an install command with API base URL, OTLP collector URL, installation ID, and scoped agent key.
|
|
52
|
+
3. The npm bootstrapper detects the user's platform.
|
|
53
|
+
4. If the native runtime is missing, the bootstrapper downloads the matching release artifact from `https://aegisagentics.com/releases/aegis-agent`.
|
|
54
|
+
5. The bootstrapper verifies byte size and SHA-256 before extracting.
|
|
55
|
+
6. The native runtime writes local config and owner-only credentials.
|
|
56
|
+
7. Vendor-specific hook files are installed. Codex currently uses both plugin registration and Aegis-owned direct `hooks.json` entries for reliable local execution.
|
|
57
|
+
8. Agent hooks send safe metadata, OTLP facts, queue-backed human-readable logs, and governed capture candidates according to local capture policy.
|
|
58
|
+
|
|
59
|
+
## Local Files
|
|
60
|
+
|
|
61
|
+
```text
|
|
62
|
+
~/.config/aegis-agent/config.json
|
|
63
|
+
~/.config/aegis-agent/credentials.json
|
|
64
|
+
~/.local/share/aegis-agent/bin/aegis-agent
|
|
65
|
+
~/.local/share/aegis-agent/queue/events.jsonl
|
|
66
|
+
~/.local/share/aegis-agent/codex-marketplace
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
`credentials.json` is written with owner-only permissions on Unix systems. Production installers should move to OS keychain storage where available, but file permissions are enforced today.
|
|
70
|
+
|
|
71
|
+
## Codex Integration
|
|
72
|
+
|
|
73
|
+
The Codex marketplace generated by this package creates:
|
|
74
|
+
|
|
75
|
+
```text
|
|
76
|
+
codex-marketplace/.agents/plugins/marketplace.json
|
|
77
|
+
codex-marketplace/plugins/aegis/.codex-plugin/plugin.json
|
|
78
|
+
codex-marketplace/plugins/aegis/hooks/hooks.json
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Hook commands point to the installed native runtime:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
~/.local/share/aegis-agent/bin/aegis-agent hook --vendor=codex --event=PostToolUse
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Codex passes hook payloads on stdin. Command-line arguments only identify the vendor and lifecycle event.
|
|
88
|
+
|
|
89
|
+
## Telemetry Paths
|
|
90
|
+
|
|
91
|
+
The plugin intentionally separates facts, logs, and artifacts:
|
|
92
|
+
|
|
93
|
+
| Data type | Path | Notes |
|
|
94
|
+
| --- | --- | --- |
|
|
95
|
+
| Run/session/tool facts | Plugin -> OTLP collector -> ControlTower API | Metadata and span facts. Collector forwards agent auth. |
|
|
96
|
+
| Human-readable logs | Plugin -> API -> queue -> worker -> Postgres | Safe operator log lines only. No raw prompts or tool payloads. |
|
|
97
|
+
| Capture artifacts | Plugin/gateway -> Capture API -> queue -> worker -> Presidio -> database | Redacted content only after server-side sanitization. |
|
|
98
|
+
|
|
99
|
+
The local safe queue is removed only after required downstream delivery succeeds. If the collector or API is unavailable, retry with:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
aegis-agent flush
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Capture Policy
|
|
106
|
+
|
|
107
|
+
The default capture policy is `metadata`.
|
|
108
|
+
|
|
109
|
+
Stored metadata may include:
|
|
110
|
+
|
|
111
|
+
- vendor and lifecycle event
|
|
112
|
+
- vendor event name
|
|
113
|
+
- session, turn, trace, and span identifiers
|
|
114
|
+
- model and source
|
|
115
|
+
- tool name and tool call id
|
|
116
|
+
- working directory and transcript path when the agent provides them
|
|
117
|
+
- vendor timestamp, status, duration, and raw payload size
|
|
118
|
+
|
|
119
|
+
Raw prompts, agent messages, tool inputs, and tool outputs are not sent in facts, logs, local queues, or ClickHouse telemetry. If `redacted` capture is enabled, content candidates are sent to the capture API for server-side Presidio redaction before persistence.
|
|
120
|
+
|
|
121
|
+
## Release Architecture
|
|
122
|
+
|
|
123
|
+
The public npm package does not contain Rust binaries. Release files live under the Aegis website bucket and are served through CloudFront:
|
|
124
|
+
|
|
125
|
+
```text
|
|
126
|
+
https://aegisagentics.com/releases/aegis-agent/channels/stable.json
|
|
127
|
+
https://aegisagentics.com/releases/aegis-agent/versions/<version>/manifest.json
|
|
128
|
+
https://aegisagentics.com/releases/aegis-agent/versions/<version>/<platform>/aegis-agent.tar.gz
|
|
129
|
+
https://aegisagentics.com/releases/aegis-agent/versions/<version>/<platform>/aegis-agent.tar.gz.sha256
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Supported platform keys:
|
|
133
|
+
|
|
134
|
+
- `darwin-arm64`
|
|
135
|
+
- `darwin-x64`
|
|
136
|
+
- `linux-arm64`
|
|
137
|
+
- `linux-x64`
|
|
138
|
+
|
|
139
|
+
The release workflow:
|
|
140
|
+
|
|
141
|
+
1. Validates the release version against `package.json`.
|
|
142
|
+
2. Runs npm tests.
|
|
143
|
+
3. Runs Cargo tests.
|
|
144
|
+
4. Builds native release binaries for supported targets.
|
|
145
|
+
5. Packages `aegis-agent.tar.gz`.
|
|
146
|
+
6. Computes SHA-256 and byte size.
|
|
147
|
+
7. Generates version and channel manifests.
|
|
148
|
+
8. Uploads release files under `s3://aegis-agentics-website/releases/aegis-agent/*`.
|
|
149
|
+
9. Invalidates the CloudFront channel and manifest paths.
|
|
150
|
+
10. Runs `npm pack --dry-run`.
|
|
151
|
+
11. Publishes `@aegisagentics/agent` to npm.
|
|
152
|
+
|
|
153
|
+
## GitHub Release Configuration
|
|
154
|
+
|
|
155
|
+
Required repository secrets:
|
|
156
|
+
|
|
157
|
+
```text
|
|
158
|
+
AWS_ROLE_ARN=<release publisher IAM role ARN from website infrastructure output>
|
|
159
|
+
NPM_TOKEN=<npm automation token with publish rights for @aegisagentics/agent>
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Optional repository variables:
|
|
163
|
+
|
|
164
|
+
```text
|
|
165
|
+
AWS_REGION=us-east-1
|
|
166
|
+
SSM_ROOT_PREFIX=/aegis-agentics
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
The AWS role is scoped to:
|
|
170
|
+
|
|
171
|
+
- read the website bucket and CloudFront distribution ID from SSM
|
|
172
|
+
- list only the `releases/aegis-agent` prefix
|
|
173
|
+
- put/get objects only under `releases/aegis-agent/*`
|
|
174
|
+
- create CloudFront invalidations
|
|
175
|
+
|
|
176
|
+
It does not grant general website deploy permissions or S3 delete access.
|
|
177
|
+
|
|
178
|
+
## Publishing a Release
|
|
179
|
+
|
|
180
|
+
The release workflow runs when a tag matching `v*.*.*` is pushed. Update
|
|
181
|
+
`package.json`, `Cargo.toml`, and `Cargo.lock` to the same semver version,
|
|
182
|
+
commit the change, then push `main` and the version tag:
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
git status
|
|
186
|
+
git add package.json Cargo.toml Cargo.lock
|
|
187
|
+
git commit -m "Release 0.1.0"
|
|
188
|
+
git push origin main
|
|
189
|
+
git tag -a v0.1.0 -m "Release v0.1.0"
|
|
190
|
+
git push origin v0.1.0
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
If `main` already contains the exact release version, only the tag push is
|
|
194
|
+
needed:
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
git tag -a v0.1.0 -m "Release v0.1.0"
|
|
198
|
+
git push origin v0.1.0
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Manual release runs are also supported from GitHub Actions with
|
|
202
|
+
`workflow_dispatch`; the requested version must still match `package.json`.
|
|
203
|
+
|
|
204
|
+
## Development
|
|
205
|
+
|
|
206
|
+
Run the npm tests:
|
|
207
|
+
|
|
208
|
+
```bash
|
|
209
|
+
npm test
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Run Rust tests:
|
|
213
|
+
|
|
214
|
+
```bash
|
|
215
|
+
cargo test --locked
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Validate publish contents:
|
|
219
|
+
|
|
220
|
+
```bash
|
|
221
|
+
npm pack --dry-run
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Useful release-download overrides:
|
|
225
|
+
|
|
226
|
+
```bash
|
|
227
|
+
AEGIS_AGENT_RELEASE_BASE_URL="https://localhost/releases/aegis-agent"
|
|
228
|
+
AEGIS_AGENT_RELEASE_VERSION="0.1.0"
|
|
229
|
+
AEGIS_AGENT_FORCE_BINARY_DOWNLOAD=1
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
## Security Notes
|
|
233
|
+
|
|
234
|
+
- Do not commit `.env`, private keys, generated packages, or local credentials.
|
|
235
|
+
- The npm `files` allowlist keeps the package limited to the bootstrapper, README, and generated release manifest.
|
|
236
|
+
- Archive extraction rejects absolute paths and `..` path traversal.
|
|
237
|
+
- Downloaded binaries are size-checked and SHA-256 checked before install.
|
|
238
|
+
- Activity logs are intentionally human-readable and metadata-only.
|
|
239
|
+
- Tool input/output belongs to the governed capture artifact pipeline, not activity logs.
|
package/npm/install.js
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
import { aegisPaths } from "./lib/config.js";
|
|
8
|
+
import { parseBootstrapCommand } from "./lib/cli.js";
|
|
9
|
+
import {
|
|
10
|
+
hookFilesForCodexUninstall,
|
|
11
|
+
installAegisHooksFiles,
|
|
12
|
+
scrubAegisHooksFiles
|
|
13
|
+
} from "./lib/codex-hooks.js";
|
|
14
|
+
import { installCodexPlugin, uninstallCodexPlugin } from "./lib/codex-plugin.js";
|
|
15
|
+
import {
|
|
16
|
+
detectCodexHome,
|
|
17
|
+
prepareCodexPluginInstallTarget
|
|
18
|
+
} from "./lib/codex-runtime.js";
|
|
19
|
+
import { ensureNativeBinary } from "./lib/release-download.js";
|
|
20
|
+
|
|
21
|
+
async function main() {
|
|
22
|
+
const binaryPath = resolveBinaryPath();
|
|
23
|
+
const args = process.argv.slice(2);
|
|
24
|
+
const paths = aegisPaths(process.env.HOME);
|
|
25
|
+
|
|
26
|
+
if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
|
|
27
|
+
printHelp(binaryPath);
|
|
28
|
+
return 0;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let command;
|
|
32
|
+
try {
|
|
33
|
+
command = parseBootstrapCommand(args);
|
|
34
|
+
} catch (error) {
|
|
35
|
+
console.error(`aegis-agent: ${error.message}`);
|
|
36
|
+
return 2;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (command.kind === "codex-plugin") {
|
|
40
|
+
if (command.codexPluginAction === "install") {
|
|
41
|
+
const status = await ensureBootstrapBinary(binaryPath);
|
|
42
|
+
if (status !== 0) {
|
|
43
|
+
return status;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return await runCodexPluginAction(command.codexPluginAction, paths, {
|
|
47
|
+
softFail: false
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
let codexPluginFailed = false;
|
|
52
|
+
if (command.codexPluginAction === "uninstall") {
|
|
53
|
+
const status = await runCodexPluginAction("uninstall", paths, {
|
|
54
|
+
softFail: true
|
|
55
|
+
});
|
|
56
|
+
codexPluginFailed = status !== 0;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (!fs.existsSync(binaryPath) && command.nativeArgs[0] !== "uninstall") {
|
|
60
|
+
const status = await ensureBootstrapBinary(binaryPath);
|
|
61
|
+
if (status !== 0) {
|
|
62
|
+
return status;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (!fs.existsSync(binaryPath)) {
|
|
67
|
+
if (args[0] === "uninstall") {
|
|
68
|
+
console.log(`Aegis Agent runtime already missing at ${binaryPath}`);
|
|
69
|
+
return codexPluginFailed ? 1 : 0;
|
|
70
|
+
}
|
|
71
|
+
printMissingBinary(binaryPath);
|
|
72
|
+
return 1;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const result = spawnSync(binaryPath, command.nativeArgs, {
|
|
76
|
+
env: process.env,
|
|
77
|
+
stdio: "inherit"
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
if (result.error) {
|
|
81
|
+
console.error(`aegis-agent: failed to execute ${binaryPath}: ${result.error.message}`);
|
|
82
|
+
return 1;
|
|
83
|
+
}
|
|
84
|
+
const nativeStatus = result.status ?? 1;
|
|
85
|
+
if (nativeStatus !== 0) {
|
|
86
|
+
return nativeStatus;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (command.codexPluginAction === "install") {
|
|
90
|
+
return await runCodexPluginAction("install", paths, {
|
|
91
|
+
softFail: false
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return codexPluginFailed ? 1 : 0;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function ensureBootstrapBinary(binaryPath) {
|
|
99
|
+
if (process.env.AEGIS_AGENT_BINARY_PATH) {
|
|
100
|
+
return 0;
|
|
101
|
+
}
|
|
102
|
+
if (fs.existsSync(binaryPath) && process.env.AEGIS_AGENT_FORCE_BINARY_DOWNLOAD !== "1") {
|
|
103
|
+
return 0;
|
|
104
|
+
}
|
|
105
|
+
try {
|
|
106
|
+
const result = await ensureNativeBinary({ binaryPath });
|
|
107
|
+
if (result.downloaded) {
|
|
108
|
+
console.log(`Downloaded Aegis Agent runtime: ${binaryPath}`);
|
|
109
|
+
}
|
|
110
|
+
return 0;
|
|
111
|
+
} catch (error) {
|
|
112
|
+
console.error(`aegis-agent: failed to install native runtime: ${error.message}`);
|
|
113
|
+
return 1;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function resolveBinaryPath() {
|
|
118
|
+
if (process.env.AEGIS_AGENT_BINARY_PATH) {
|
|
119
|
+
return process.env.AEGIS_AGENT_BINARY_PATH;
|
|
120
|
+
}
|
|
121
|
+
const paths = aegisPaths(process.env.HOME);
|
|
122
|
+
if (fs.existsSync(paths.binaryFile)) {
|
|
123
|
+
return paths.binaryFile;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
|
127
|
+
const devBinary = path.resolve(moduleDir, "..", "target", "release", "aegis-agent");
|
|
128
|
+
if (fs.existsSync(devBinary)) {
|
|
129
|
+
return devBinary;
|
|
130
|
+
}
|
|
131
|
+
return paths.binaryFile;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function printHelp(binaryPath) {
|
|
135
|
+
console.log(`Aegis Agent installer bootstrap
|
|
136
|
+
|
|
137
|
+
Usage:
|
|
138
|
+
AEGIS_API_KEY="<one-time-key>" npx -y @aegisagentics/agent install --api-base-url "https://controltower.example.com" --otlp-http-base-url "https://otel.controltower.example.com" --installation-id "agent_install_..." --vendors codex,claude,cursor
|
|
139
|
+
npx -y @aegisagentics/agent codex-plugin install
|
|
140
|
+
npx -y @aegisagentics/agent codex-plugin uninstall
|
|
141
|
+
npx -y @aegisagentics/agent uninstall --vendors codex
|
|
142
|
+
aegis-agent-install install --api-key "$AEGIS_API_KEY" --api-base-url "https://controltower.example.com" --otlp-http-base-url "https://otel.controltower.example.com" --installation-id "agent_install_..." --vendors codex
|
|
143
|
+
|
|
144
|
+
The npm package is a bootstrapper. The hook runtime is the native binary:
|
|
145
|
+
${binaryPath}
|
|
146
|
+
|
|
147
|
+
For source builds, set AEGIS_AGENT_BINARY_PATH to a locally built aegis-agent binary.
|
|
148
|
+
`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function runCodexPluginAction(action, paths, { softFail }) {
|
|
152
|
+
try {
|
|
153
|
+
if (action === "install") {
|
|
154
|
+
const target = await prepareCodexPluginInstallTarget({
|
|
155
|
+
userPaths: paths
|
|
156
|
+
});
|
|
157
|
+
if (!fs.existsSync(target.marketplaceFile)) {
|
|
158
|
+
throw new Error(
|
|
159
|
+
`Codex marketplace file not found at ${target.marketplaceFile}. Run the Aegis install command first.`
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
const hookInstallResults = await installAegisHooksFiles({
|
|
163
|
+
binaryPath: target.binaryFile,
|
|
164
|
+
files: hookFilesForCodexUninstall({
|
|
165
|
+
codexHome: target.codexHome,
|
|
166
|
+
userPaths: paths
|
|
167
|
+
})
|
|
168
|
+
});
|
|
169
|
+
const addedHooks = hookInstallResults.reduce((sum, result) => sum + result.added, 0);
|
|
170
|
+
const replacedHooks = hookInstallResults.reduce((sum, result) => sum + result.replaced, 0);
|
|
171
|
+
await installCodexPlugin({ marketplaceFile: target.marketplaceFile });
|
|
172
|
+
if (target.mirrored) {
|
|
173
|
+
console.log(`Prepared Codex-isolated Aegis runtime: ${target.codexHome}`);
|
|
174
|
+
}
|
|
175
|
+
console.log(
|
|
176
|
+
`Installed Aegis Codex hooks.json entries: ${addedHooks} added, ${replacedHooks} replaced`
|
|
177
|
+
);
|
|
178
|
+
console.log("Codex plugin installed: aegis@aegis");
|
|
179
|
+
return 0;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (action === "uninstall") {
|
|
183
|
+
let pluginError = null;
|
|
184
|
+
try {
|
|
185
|
+
await uninstallCodexPlugin();
|
|
186
|
+
} catch (error) {
|
|
187
|
+
pluginError = error;
|
|
188
|
+
}
|
|
189
|
+
const scrubResults = await scrubAegisHooksFiles(
|
|
190
|
+
hookFilesForCodexUninstall({
|
|
191
|
+
codexHome: detectCodexHome(),
|
|
192
|
+
userPaths: paths
|
|
193
|
+
})
|
|
194
|
+
);
|
|
195
|
+
const removedHooks = scrubResults.reduce((sum, result) => sum + result.removed, 0);
|
|
196
|
+
if (removedHooks > 0) {
|
|
197
|
+
console.log(`Removed ${removedHooks} Aegis hook entr${removedHooks === 1 ? "y" : "ies"} from Codex hooks.json`);
|
|
198
|
+
}
|
|
199
|
+
if (pluginError) {
|
|
200
|
+
throw pluginError;
|
|
201
|
+
}
|
|
202
|
+
console.log("Codex plugin uninstalled: aegis@aegis");
|
|
203
|
+
return 0;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
throw new Error(`Unsupported Codex plugin action: ${action}`);
|
|
207
|
+
} catch (error) {
|
|
208
|
+
const message = `Codex plugin ${action} failed: ${error.message}`;
|
|
209
|
+
if (softFail) {
|
|
210
|
+
console.warn(`aegis-agent: ${message}`);
|
|
211
|
+
return 1;
|
|
212
|
+
}
|
|
213
|
+
console.error(`aegis-agent: ${message}`);
|
|
214
|
+
return 1;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function printMissingBinary(binaryPath) {
|
|
219
|
+
console.error(`aegis-agent: native binary not found at ${binaryPath}
|
|
220
|
+
|
|
221
|
+
The npm bootstrapper downloads and verifies the release runtime when it is missing.
|
|
222
|
+
For local development:
|
|
223
|
+
cd crates/aegis-agent
|
|
224
|
+
cargo build --release
|
|
225
|
+
AEGIS_AGENT_BINARY_PATH="$PWD/target/release/aegis-agent" node ../../npm/install.js --help
|
|
226
|
+
`);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
process.exitCode = await main();
|
package/npm/lib/cli.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { normalizeVendorList } from "./config.js";
|
|
2
|
+
|
|
3
|
+
export function parseBootstrapCommand(args) {
|
|
4
|
+
const command = args[0] ?? "";
|
|
5
|
+
|
|
6
|
+
if (command === "codex-plugin") {
|
|
7
|
+
const action = args[1] ?? "";
|
|
8
|
+
if (action !== "install" && action !== "uninstall") {
|
|
9
|
+
throw new Error(
|
|
10
|
+
"Usage: aegis-agent codex-plugin <install|uninstall>"
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
return {
|
|
14
|
+
codexPluginAction: action,
|
|
15
|
+
kind: "codex-plugin",
|
|
16
|
+
nativeArgs: []
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (command === "install") {
|
|
21
|
+
const vendors = extractVendorList(args, "codex");
|
|
22
|
+
return {
|
|
23
|
+
codexPluginAction: vendors.includes("codex") ? "install" : null,
|
|
24
|
+
kind: "native",
|
|
25
|
+
nativeArgs: args
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (command === "uninstall") {
|
|
30
|
+
const vendors = extractVendorList(args, "codex");
|
|
31
|
+
return {
|
|
32
|
+
codexPluginAction: vendors.includes("codex") ? "uninstall" : null,
|
|
33
|
+
kind: "native",
|
|
34
|
+
nativeArgs: stripOption(args, "--vendors")
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
codexPluginAction: null,
|
|
40
|
+
kind: "native",
|
|
41
|
+
nativeArgs: args
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function extractVendorList(args, fallback) {
|
|
46
|
+
const values = [];
|
|
47
|
+
|
|
48
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
49
|
+
const arg = args[index];
|
|
50
|
+
if (arg === "--vendors") {
|
|
51
|
+
if (index + 1 >= args.length) {
|
|
52
|
+
throw new Error("--vendors requires a value");
|
|
53
|
+
}
|
|
54
|
+
values.push(args[index + 1]);
|
|
55
|
+
index += 1;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (arg.startsWith("--vendors=")) {
|
|
59
|
+
values.push(arg.slice("--vendors=".length));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return normalizeVendorList(values.length > 0 ? values.join(",") : fallback);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function stripOption(args, optionName) {
|
|
67
|
+
const stripped = [];
|
|
68
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
69
|
+
const arg = args[index];
|
|
70
|
+
if (arg === optionName) {
|
|
71
|
+
index += 1;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (arg.startsWith(`${optionName}=`)) {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
stripped.push(arg);
|
|
78
|
+
}
|
|
79
|
+
return stripped;
|
|
80
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import { buildCodexHooks } from "./codex.js";
|
|
5
|
+
|
|
6
|
+
const AEGIS_COMMAND_MARKERS = [
|
|
7
|
+
"/aegis-agent/bin/aegis-agent hook --vendor=codex",
|
|
8
|
+
"/aegis-agent hook --vendor=codex",
|
|
9
|
+
"aegis-agent hook --vendor=codex"
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
export function hookFilesForCodexUninstall({ codexHome, userPaths }) {
|
|
13
|
+
const files = [path.join(userPaths.homeDir, ".codex", "hooks.json")];
|
|
14
|
+
if (codexHome && path.resolve(codexHome) !== path.resolve(userPaths.homeDir)) {
|
|
15
|
+
files.push(path.join(codexHome, "hooks.json"));
|
|
16
|
+
}
|
|
17
|
+
return [...new Set(files.map((file) => path.resolve(file)))];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function scrubAegisHooksFiles(files) {
|
|
21
|
+
const results = [];
|
|
22
|
+
for (const file of files) {
|
|
23
|
+
results.push({
|
|
24
|
+
file,
|
|
25
|
+
...(await scrubAegisHooksFile(file))
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
return results;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function installAegisHooksFiles({ binaryPath, files }) {
|
|
32
|
+
const results = [];
|
|
33
|
+
for (const hooksFile of files) {
|
|
34
|
+
results.push({
|
|
35
|
+
file: hooksFile,
|
|
36
|
+
...(await installAegisHooksFile({ binaryPath, hooksFile }))
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
return results;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function installAegisHooksFile({ binaryPath, hooksFile }) {
|
|
43
|
+
let document = { hooks: {} };
|
|
44
|
+
try {
|
|
45
|
+
document = JSON.parse(await fs.readFile(hooksFile, "utf8"));
|
|
46
|
+
} catch (error) {
|
|
47
|
+
if (error.code !== "ENOENT") {
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (!document || typeof document !== "object" || Array.isArray(document)) {
|
|
53
|
+
document = { hooks: {} };
|
|
54
|
+
}
|
|
55
|
+
if (!isObject(document.hooks)) {
|
|
56
|
+
document.hooks = {};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const scrubbed = removeAegisHandlers(document);
|
|
60
|
+
const aegisHooks = buildCodexHooks({ binaryPath }).hooks;
|
|
61
|
+
|
|
62
|
+
for (const [eventName, groups] of Object.entries(aegisHooks)) {
|
|
63
|
+
const currentGroups = Array.isArray(scrubbed.document.hooks[eventName])
|
|
64
|
+
? scrubbed.document.hooks[eventName]
|
|
65
|
+
: [];
|
|
66
|
+
scrubbed.document.hooks[eventName] = [...currentGroups, ...groups];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
await fs.mkdir(path.dirname(hooksFile), { recursive: true });
|
|
70
|
+
await fs.writeFile(hooksFile, `${JSON.stringify(scrubbed.document, null, 2)}\n`, "utf8");
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
added: Object.keys(aegisHooks).length,
|
|
74
|
+
replaced: scrubbed.removed
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function scrubAegisHooksFile(file) {
|
|
79
|
+
let raw;
|
|
80
|
+
try {
|
|
81
|
+
raw = await fs.readFile(file, "utf8");
|
|
82
|
+
} catch (error) {
|
|
83
|
+
if (error.code === "ENOENT") {
|
|
84
|
+
return { missing: true, removed: 0, removedFile: false };
|
|
85
|
+
}
|
|
86
|
+
throw error;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const document = JSON.parse(raw);
|
|
90
|
+
if (!document || typeof document !== "object" || !isObject(document.hooks)) {
|
|
91
|
+
return { missing: false, removed: 0, removedFile: false };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const { document: nextDocument, removed } = removeAegisHandlers(document);
|
|
95
|
+
|
|
96
|
+
if (removed === 0) {
|
|
97
|
+
return { missing: false, removed: 0, removedFile: false };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (Object.keys(nextDocument.hooks).length === 0 && Object.keys(nextDocument).length === 1) {
|
|
101
|
+
await fs.unlink(file);
|
|
102
|
+
return { missing: false, removed, removedFile: true };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
await fs.writeFile(file, `${JSON.stringify(nextDocument, null, 2)}\n`, "utf8");
|
|
106
|
+
return { missing: false, removed, removedFile: false };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function removeAegisHandlers(document) {
|
|
110
|
+
let removed = 0;
|
|
111
|
+
const nextHooks = {};
|
|
112
|
+
|
|
113
|
+
for (const [eventName, groups] of Object.entries(document.hooks ?? {})) {
|
|
114
|
+
if (!Array.isArray(groups)) {
|
|
115
|
+
nextHooks[eventName] = groups;
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const nextGroups = [];
|
|
120
|
+
for (const group of groups) {
|
|
121
|
+
if (!isObject(group) || !Array.isArray(group.hooks)) {
|
|
122
|
+
nextGroups.push(group);
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const nextHandlers = group.hooks.filter((handler) => {
|
|
127
|
+
if (isAegisHookHandler(handler)) {
|
|
128
|
+
removed += 1;
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
return true;
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
if (nextHandlers.length > 0) {
|
|
135
|
+
nextGroups.push({
|
|
136
|
+
...group,
|
|
137
|
+
hooks: nextHandlers
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (nextGroups.length > 0) {
|
|
143
|
+
nextHooks[eventName] = nextGroups;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
document: {
|
|
149
|
+
...document,
|
|
150
|
+
hooks: nextHooks
|
|
151
|
+
},
|
|
152
|
+
removed
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function isAegisHookHandler(handler) {
|
|
157
|
+
if (!isObject(handler) || typeof handler.command !== "string") {
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
return AEGIS_COMMAND_MARKERS.some((marker) => handler.command.includes(marker));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function isObject(value) {
|
|
164
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
165
|
+
}
|