@ashulab/agent-forge 0.1.0 → 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 +32 -46
- package/dist/launcher.cjs +143 -52
- package/package.json +1 -1
- package/schema/agents.example.json +0 -1
- package/schema/agents.schema.json +7 -3
package/README.md
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# Agent Forge
|
|
2
2
|
|
|
3
|
-
A small CLI for launching coding agents (`claude`, `codex`, `antigravity`) with a
|
|
4
|
-
**GitHub App identity** and a provider CLI chosen at runtime.
|
|
3
|
+
A small CLI for launching coding agents (`claude`, `codex`, `antigravity`) with a **GitHub App identity** and a provider CLI chosen at runtime.
|
|
5
4
|
|
|
6
5
|
On each run the launcher:
|
|
7
6
|
|
|
@@ -9,18 +8,15 @@ On each run the launcher:
|
|
|
9
8
|
- generates a GitHub App installation token
|
|
10
9
|
- detects installed provider CLIs on your PATH
|
|
11
10
|
- asks which provider to use (or takes it from a flag)
|
|
12
|
-
- starts the provider with the GitHub token and a per-agent git identity injected
|
|
13
|
-
|
|
14
|
-
- writes the agent's `systemPrompt` into the provider's startup memory file
|
|
15
|
-
(`CLAUDE.md` / `AGENTS.md`) so subagents inherit the identity
|
|
11
|
+
- starts the provider with the GitHub token and a per-agent git identity injected into the environment
|
|
12
|
+
- writes the agent's `systemPrompt` into the provider's startup memory file (`CLAUDE.md` / `AGENTS.md`) so subagents inherit the identity
|
|
16
13
|
|
|
17
14
|
Provider auth stays in the provider CLI — the launcher never handles provider API keys.
|
|
18
15
|
|
|
19
16
|
## Requirements
|
|
20
17
|
|
|
21
18
|
- Node.js 24+
|
|
22
|
-
- a GitHub App with an installation on the target account/org —
|
|
23
|
-
[how to create one](docs/github-app.md)
|
|
19
|
+
- a GitHub App with an installation on the target account/org — [how to create one](docs/github-app.md)
|
|
24
20
|
- the App private key stored locally (e.g. in `~/.ssh`)
|
|
25
21
|
- one of these provider CLIs on your PATH: `claude`, `codex`, `agy` (antigravity)
|
|
26
22
|
|
|
@@ -38,22 +34,15 @@ pnpm dlx @ashulab/agent-forge --help
|
|
|
38
34
|
|
|
39
35
|
## Configuration
|
|
40
36
|
|
|
41
|
-
Your agents live in a single machine-global registry — you launch the tool from
|
|
42
|
-
inside each target repo, but the identities are not per-repo. The launcher reads
|
|
43
|
-
`agents.json` from the first of these that exists:
|
|
37
|
+
Your agents live in a single machine-global registry — you launch the tool from inside each target repo, but the identities are not per-repo. The launcher reads `agents.json` from the first of these that exists:
|
|
44
38
|
|
|
45
39
|
1. `$AGENT_FORGE_CONFIG`
|
|
46
40
|
2. `~/.config/agent-forge/agents.json` — the global registry (`agent-forge add` writes here)
|
|
47
41
|
3. `./agents.json` — only if it already exists
|
|
48
42
|
|
|
49
|
-
The easiest path is `agent-forge add` (see below), which creates the global
|
|
50
|
-
file for you. Do **not** drop `agents.json` into a project you run the agent in:
|
|
51
|
-
it holds real App / installation IDs and private-key paths and must stay out of
|
|
52
|
-
version control.
|
|
43
|
+
The easiest path is `agent-forge add` (see below), which creates the global file for you. Do **not** drop `agents.json` into a project you run the agent in: it holds real App / installation IDs and private-key paths and must stay out of version control.
|
|
53
44
|
|
|
54
|
-
The shape, for reference — the `"$schema"` line points at the published schema so
|
|
55
|
-
any editor that understands JSON Schema gives you field descriptions,
|
|
56
|
-
required-field checks and typo detection:
|
|
45
|
+
The shape, for reference — the `"$schema"` line points at the published schema so any editor that understands JSON Schema gives you field descriptions, required-field checks and typo detection:
|
|
57
46
|
|
|
58
47
|
```json
|
|
59
48
|
{
|
|
@@ -63,7 +52,6 @@ required-field checks and typo detection:
|
|
|
63
52
|
"name": "ops-agent",
|
|
64
53
|
"label": "Ops Agent",
|
|
65
54
|
"appId": "123456",
|
|
66
|
-
"installationId": "987654",
|
|
67
55
|
"botName": "ops-agent",
|
|
68
56
|
"privateKeyPath": "~/.ssh/ops-agent.private-key.pem",
|
|
69
57
|
"systemPrompt": "You are Ops Agent, a senior DevOps engineer. Prefer safe, minimal changes and explain operational risks clearly."
|
|
@@ -72,28 +60,38 @@ required-field checks and typo detection:
|
|
|
72
60
|
}
|
|
73
61
|
```
|
|
74
62
|
|
|
75
|
-
-
|
|
76
|
-
|
|
77
|
-
|
|
63
|
+
- The **installation id is not stored** — it is resolved on each run, in order: the owner of the repo you launch in, then `account`, then the App's sole installation. So the same agent works across every org the App is installed on, and it survives an uninstall/reinstall (which rotates the id).
|
|
64
|
+
- Add `"account": "<org-or-user login>"` only when you run outside a matching repo and the App has more than one installation.
|
|
65
|
+
- `installationId` is still accepted as an explicit override — set it to pin a specific id or skip the lookup (offline, or in a git credential helper).
|
|
66
|
+
- `botId` is optional. When omitted it is resolved from the GitHub API using `botName` so the git author email links commits to the bot. Set it explicitly only to skip that lookup (e.g. offline).
|
|
78
67
|
- Do not put provider-specific config here — providers are auto-detected from PATH.
|
|
79
68
|
|
|
80
69
|
### Least-privilege tokens (optional)
|
|
81
70
|
|
|
82
|
-
By default the installation token carries the full installation scope. Narrow it
|
|
83
|
-
per agent with either or both of:
|
|
71
|
+
By default the installation token carries the full installation scope. Narrow it per agent with either or both of:
|
|
84
72
|
|
|
85
73
|
```json
|
|
86
74
|
{
|
|
87
75
|
"repositories": ["some-repo", "another-repo"],
|
|
88
|
-
"permissions": { "contents": "
|
|
76
|
+
"permissions": { "contents": "read", "pull_requests": "write" }
|
|
89
77
|
}
|
|
90
78
|
```
|
|
91
79
|
|
|
80
|
+
**`permissions` is a complete allowlist, not a patch.** GitHub grants *only* the keys you list and drops everything else — even permissions the GitHub App itself has. If you set `permissions` at all, you must list every scope the agent needs. Common cases:
|
|
81
|
+
|
|
82
|
+
| The agent needs to… | Minimum `permissions` |
|
|
83
|
+
| --- | --- |
|
|
84
|
+
| Push commits / open PRs | `{ "contents": "write", "pull_requests": "write" }` |
|
|
85
|
+
| Review PRs (incl. inline comments) | `{ "contents": "read", "pull_requests": "write" }` |
|
|
86
|
+
| Read-only (clone, read issues) | `{ "contents": "read" }` |
|
|
87
|
+
|
|
88
|
+
Omitting the `permissions` key entirely is the safe default — the token then carries whatever the App grants. `repositories` works the same way: list it and the token can only touch those repos.
|
|
89
|
+
|
|
90
|
+
On launch, `agent-forge` prints the scope GitHub actually granted (`token` row in the summary); `agent-forge token --agent <name>` prints it to stderr. If a call fails with `403 Resource not accessible by integration`, check that row first.
|
|
91
|
+
|
|
92
92
|
## Add an agent
|
|
93
93
|
|
|
94
|
-
You need a GitHub App first — its **App ID**, a **private key**, and an
|
|
95
|
-
**installation**. If you don't have one yet, follow
|
|
96
|
-
[docs/github-app.md](docs/github-app.md).
|
|
94
|
+
You need a GitHub App first — its **App ID**, a **private key**, and an **installation**. If you don't have one yet, follow [docs/github-app.md](docs/github-app.md).
|
|
97
95
|
|
|
98
96
|
### Guided (recommended)
|
|
99
97
|
|
|
@@ -101,19 +99,11 @@ You need a GitHub App first — its **App ID**, a **private key**, and an
|
|
|
101
99
|
agent-forge add
|
|
102
100
|
```
|
|
103
101
|
|
|
104
|
-
You provide the **GitHub App ID** and the **path to its private key**. The wizard
|
|
105
|
-
derives the rest from the GitHub API — slug (`botName`), bot user id, and the
|
|
106
|
-
installation (auto-selected when there is only one) — asks for a label and an
|
|
107
|
-
optional system prompt, mints a test token to confirm it works, and writes the
|
|
108
|
-
entry. It writes to the resolved config path — the global registry
|
|
109
|
-
(`~/.config/agent-forge/agents.json`) unless `$AGENT_FORGE_CONFIG` or an
|
|
110
|
-
existing `./agents.json` redirects it.
|
|
102
|
+
You provide the **GitHub App ID** and the **path to its private key**. The wizard derives the rest from the GitHub API — slug (`botName`), bot user id, and the account (asked only when the App is installed on more than one) — asks for a label and an optional system prompt, mints a test token to confirm it works, and writes the entry. It writes to the resolved config path — the global registry (`~/.config/agent-forge/agents.json`) unless `$AGENT_FORGE_CONFIG` or an existing `./agents.json` redirects it.
|
|
111
103
|
|
|
112
104
|
### Manual
|
|
113
105
|
|
|
114
|
-
Copy one agent object inside the `agents` array and set unique values for `name`,
|
|
115
|
-
`label`, `appId`, `installationId`, `botName`, `privateKeyPath`. Make sure the
|
|
116
|
-
private key file exists and is readable.
|
|
106
|
+
Copy one agent object inside the `agents` array and set unique values for `name`, `label`, `appId`, `botName`, `privateKeyPath` — plus `account` if the App has more than one installation. Make sure the private key file exists and is readable.
|
|
117
107
|
|
|
118
108
|
## Launch
|
|
119
109
|
|
|
@@ -140,15 +130,9 @@ agent-forge --help
|
|
|
140
130
|
### Provider-specific prompt behavior
|
|
141
131
|
|
|
142
132
|
- `claude`: passed via `--append-system-prompt` (lands in the real system prompt)
|
|
143
|
-
- `codex` / `antigravity` (`agy`): no CLI flag — identity comes from `AGENTS.md`
|
|
144
|
-
only (`agy --prompt` is headless `--print`; a codex positional arg is a fake
|
|
145
|
-
first user turn)
|
|
133
|
+
- `codex` / `antigravity` (`agy`): no CLI flag — identity comes from `AGENTS.md` only (`agy --prompt` is headless `--print`; a codex positional arg is a fake first user turn)
|
|
146
134
|
|
|
147
|
-
The launcher also writes `systemPrompt` into the provider's startup memory file
|
|
148
|
-
in the working directory (`CLAUDE.md` for claude, `AGENTS.md` for codex and
|
|
149
|
-
antigravity), inside a managed `agent-forge:identity` block. This keeps the
|
|
150
|
-
identity in place for subagents the provider spawns, not just its main thread.
|
|
151
|
-
The block is rewritten on each run and safe to keep in version control.
|
|
135
|
+
The launcher also writes `systemPrompt` into the provider's startup memory file in the working directory (`CLAUDE.md` for claude, `AGENTS.md` for codex and antigravity), inside a managed `agent-forge:identity` block. This keeps the identity in place for subagents the provider spawns, not just its main thread. The block is rewritten on each run and safe to keep in version control.
|
|
152
136
|
|
|
153
137
|
## Token refresh
|
|
154
138
|
|
|
@@ -165,6 +149,8 @@ git config credential.https://github.com.helper \
|
|
|
165
149
|
'!f() { test "$1" = get && echo username=x-access-token && echo "password=$(agent-forge token --agent ops-agent)"; }; f'
|
|
166
150
|
```
|
|
167
151
|
|
|
152
|
+
Resolution still works here — the repo you push from is the one used. Setting `installationId` explicitly just skips the lookup on every push.
|
|
153
|
+
|
|
168
154
|
## Contributing
|
|
169
155
|
|
|
170
156
|
See [CONTRIBUTING.md](.github/CONTRIBUTING.md) and [AGENTS.md](./AGENTS.md).
|
package/dist/launcher.cjs
CHANGED
|
@@ -4015,7 +4015,7 @@ var require_jsonwebtoken = __commonJS({
|
|
|
4015
4015
|
});
|
|
4016
4016
|
|
|
4017
4017
|
// src/launcher.js
|
|
4018
|
-
var
|
|
4018
|
+
var import_node_child_process3 = require("node:child_process");
|
|
4019
4019
|
var import_node_path4 = require("node:path");
|
|
4020
4020
|
var import_node_util3 = require("node:util");
|
|
4021
4021
|
|
|
@@ -5219,7 +5219,7 @@ ${r2}
|
|
|
5219
5219
|
var import_node_fs = require("node:fs");
|
|
5220
5220
|
var import_node_os = require("node:os");
|
|
5221
5221
|
var import_node_path = require("node:path");
|
|
5222
|
-
var REQUIRED_FIELDS = ["name", "appId", "
|
|
5222
|
+
var REQUIRED_FIELDS = ["name", "appId", "privateKeyPath"];
|
|
5223
5223
|
var SCHEMA_URL = "https://raw.githubusercontent.com/AshuLab/agent-forge/main/schema/agents.schema.json";
|
|
5224
5224
|
var CONFIG_HOME = process.env.XDG_CONFIG_HOME || (0, import_node_path.join)((0, import_node_os.homedir)(), ".config");
|
|
5225
5225
|
var GLOBAL_CONFIG = (0, import_node_path.join)(CONFIG_HOME, "agent-forge", "agents.json");
|
|
@@ -5270,25 +5270,31 @@ function validateAgents(agents) {
|
|
|
5270
5270
|
seen.add(agent.name);
|
|
5271
5271
|
});
|
|
5272
5272
|
}
|
|
5273
|
-
function
|
|
5273
|
+
function readAgentsOrEmpty() {
|
|
5274
5274
|
const path = resolveConfigPath();
|
|
5275
|
-
if (!(0, import_node_fs.existsSync)(path))
|
|
5276
|
-
throw new Error(
|
|
5277
|
-
`No agents.json found. Run "agent-forge add" to create one at ${GLOBAL_CONFIG}, or point AGENT_FORGE_CONFIG at an existing file.`
|
|
5278
|
-
);
|
|
5279
|
-
}
|
|
5275
|
+
if (!(0, import_node_fs.existsSync)(path)) return [];
|
|
5280
5276
|
let parsed;
|
|
5281
5277
|
try {
|
|
5282
5278
|
parsed = JSON.parse((0, import_node_fs.readFileSync)(path, "utf8"));
|
|
5283
5279
|
} catch (error) {
|
|
5284
5280
|
throw new Error(`agents.json is not valid JSON: ${error.message}`);
|
|
5285
5281
|
}
|
|
5286
|
-
if (!Array.isArray(parsed.agents) || parsed.agents.length === 0)
|
|
5287
|
-
throw new Error("agents.json: no agents defined");
|
|
5288
|
-
}
|
|
5282
|
+
if (!Array.isArray(parsed.agents) || parsed.agents.length === 0) return [];
|
|
5289
5283
|
validateAgents(parsed.agents);
|
|
5290
5284
|
return parsed.agents;
|
|
5291
5285
|
}
|
|
5286
|
+
function readAgents() {
|
|
5287
|
+
const agents = readAgentsOrEmpty();
|
|
5288
|
+
if (agents.length === 0) {
|
|
5289
|
+
if (!(0, import_node_fs.existsSync)(resolveConfigPath())) {
|
|
5290
|
+
throw new Error(
|
|
5291
|
+
`No agents.json found. Run "agent-forge add" to create one at ${GLOBAL_CONFIG}, or point AGENT_FORGE_CONFIG at an existing file.`
|
|
5292
|
+
);
|
|
5293
|
+
}
|
|
5294
|
+
throw new Error("agents.json: no agents defined");
|
|
5295
|
+
}
|
|
5296
|
+
return agents;
|
|
5297
|
+
}
|
|
5292
5298
|
function listAgents() {
|
|
5293
5299
|
const agents = readAgents();
|
|
5294
5300
|
const rows = agents.map(
|
|
@@ -5298,6 +5304,7 @@ function listAgents() {
|
|
|
5298
5304
|
}
|
|
5299
5305
|
|
|
5300
5306
|
// src/github.js
|
|
5307
|
+
var import_node_child_process = require("node:child_process");
|
|
5301
5308
|
var import_node_fs2 = require("node:fs");
|
|
5302
5309
|
var import_node_os2 = require("node:os");
|
|
5303
5310
|
var import_node_path2 = require("node:path");
|
|
@@ -5349,42 +5356,104 @@ async function resolveBotId(agent) {
|
|
|
5349
5356
|
}
|
|
5350
5357
|
return String(data.id);
|
|
5351
5358
|
}
|
|
5359
|
+
async function listInstallations(jwtToken) {
|
|
5360
|
+
const items = await githubApi("/app/installations", { jwtToken });
|
|
5361
|
+
return items.map((item) => ({
|
|
5362
|
+
id: String(item.id),
|
|
5363
|
+
account: item.account?.login || item.account?.slug || "unknown"
|
|
5364
|
+
}));
|
|
5365
|
+
}
|
|
5352
5366
|
async function fetchAppMetadata(appId, privateKeyPath) {
|
|
5353
5367
|
const privateKey = readPrivateKey(privateKeyPath, `app ${appId}`);
|
|
5354
5368
|
const jwtToken = signAppJwt(appId, privateKey);
|
|
5355
5369
|
const app = await githubApi("/app", { jwtToken });
|
|
5356
|
-
const installations = await githubApi("/app/installations", { jwtToken });
|
|
5357
5370
|
return {
|
|
5358
5371
|
slug: app.slug,
|
|
5359
5372
|
name: app.name || app.slug,
|
|
5360
|
-
installations:
|
|
5361
|
-
id: String(item.id),
|
|
5362
|
-
account: item.account?.login || item.account?.slug || "unknown"
|
|
5363
|
-
}))
|
|
5373
|
+
installations: await listInstallations(jwtToken)
|
|
5364
5374
|
};
|
|
5365
5375
|
}
|
|
5366
|
-
|
|
5367
|
-
|
|
5368
|
-
|
|
5376
|
+
function pickInstallation(installations, agent) {
|
|
5377
|
+
if (installations.length === 0) {
|
|
5378
|
+
throw new Error(`GitHub App ${agent.appId} has no installations. Install it on an account or org first.`);
|
|
5379
|
+
}
|
|
5380
|
+
if (agent.account) {
|
|
5381
|
+
const want = String(agent.account).toLowerCase();
|
|
5382
|
+
const match = installations.find((item) => item.account.toLowerCase() === want);
|
|
5383
|
+
if (!match) {
|
|
5384
|
+
throw new Error(
|
|
5385
|
+
`GitHub App ${agent.appId} is not installed on "${agent.account}". Installed on: ${installations.map((item) => item.account).join(", ")}.`
|
|
5386
|
+
);
|
|
5387
|
+
}
|
|
5388
|
+
return match.id;
|
|
5389
|
+
}
|
|
5390
|
+
if (installations.length === 1) return installations[0].id;
|
|
5391
|
+
throw new Error(
|
|
5392
|
+
`GitHub App ${agent.appId} is installed on ${installations.length} accounts (${installations.map((item) => item.account).join(", ")}). Run inside a repo under one of them, or add "account" to agent "${agent.name}".`
|
|
5393
|
+
);
|
|
5394
|
+
}
|
|
5395
|
+
function parseGithubRemote(url) {
|
|
5396
|
+
const match = String(url).trim().match(/github\.com[:/]([^/\s]+)\/([^/\s]+?)(?:\.git)?\/?$/);
|
|
5397
|
+
return match ? { owner: match[1], repo: match[2] } : null;
|
|
5398
|
+
}
|
|
5399
|
+
function currentRepoSlug() {
|
|
5400
|
+
try {
|
|
5401
|
+
const url = (0, import_node_child_process.execFileSync)("git", ["remote", "get-url", "origin"], {
|
|
5402
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
5403
|
+
}).toString();
|
|
5404
|
+
return parseGithubRemote(url);
|
|
5405
|
+
} catch {
|
|
5406
|
+
return null;
|
|
5407
|
+
}
|
|
5408
|
+
}
|
|
5409
|
+
async function installationForRepo(slug, jwtToken) {
|
|
5410
|
+
try {
|
|
5411
|
+
const data = await githubApi(`/repos/${slug.owner}/${slug.repo}/installation`, { jwtToken });
|
|
5412
|
+
return String(data.id);
|
|
5413
|
+
} catch {
|
|
5414
|
+
return null;
|
|
5415
|
+
}
|
|
5416
|
+
}
|
|
5417
|
+
async function resolveInstallationId(agent, jwtToken) {
|
|
5418
|
+
if (agent.installationId) return String(agent.installationId);
|
|
5419
|
+
const slug = currentRepoSlug();
|
|
5420
|
+
if (slug) {
|
|
5421
|
+
const id = await installationForRepo(slug, jwtToken);
|
|
5422
|
+
if (id) return id;
|
|
5423
|
+
}
|
|
5424
|
+
return pickInstallation(await listInstallations(jwtToken), agent);
|
|
5425
|
+
}
|
|
5426
|
+
function buildTokenScope(agent) {
|
|
5369
5427
|
const scope = {};
|
|
5370
5428
|
if (Array.isArray(agent.repositories)) scope.repositories = agent.repositories;
|
|
5371
5429
|
if (agent.permissions && typeof agent.permissions === "object") scope.permissions = agent.permissions;
|
|
5372
|
-
|
|
5430
|
+
return scope;
|
|
5431
|
+
}
|
|
5432
|
+
async function generateGithubAppToken(agent) {
|
|
5433
|
+
const privateKey = readPrivateKey(agent.privateKeyPath, agent.name || agent.appId);
|
|
5434
|
+
const jwtToken = signAppJwt(agent.appId, privateKey);
|
|
5435
|
+
const installationId = await resolveInstallationId(agent, jwtToken);
|
|
5436
|
+
const data = await githubApi(`/app/installations/${installationId}/access_tokens`, {
|
|
5373
5437
|
jwtToken,
|
|
5374
|
-
body:
|
|
5438
|
+
body: buildTokenScope(agent)
|
|
5375
5439
|
});
|
|
5376
|
-
return
|
|
5440
|
+
return {
|
|
5441
|
+
token: data.token,
|
|
5442
|
+
// What GitHub actually granted, after both the App's config and our scoping.
|
|
5443
|
+
permissions: data.permissions || {},
|
|
5444
|
+
repositorySelection: data.repository_selection || "all"
|
|
5445
|
+
};
|
|
5377
5446
|
}
|
|
5378
5447
|
|
|
5379
5448
|
// src/providers.js
|
|
5380
|
-
var
|
|
5449
|
+
var import_node_child_process2 = require("node:child_process");
|
|
5381
5450
|
var PROVIDER_OPTIONS = [
|
|
5382
5451
|
{ value: "claude", label: "Claude", defaultCommand: "claude" },
|
|
5383
5452
|
{ value: "codex", label: "Codex", defaultCommand: "codex" },
|
|
5384
5453
|
{ value: "antigravity", label: "Antigravity", defaultCommand: "agy", aliases: ["antigravity", "agy"] }
|
|
5385
5454
|
];
|
|
5386
5455
|
function isCommandInstalled(command) {
|
|
5387
|
-
const result = (0,
|
|
5456
|
+
const result = (0, import_node_child_process2.spawnSync)(process.platform === "win32" ? "where" : "which", [command], { stdio: "ignore" });
|
|
5388
5457
|
return result.status === 0;
|
|
5389
5458
|
}
|
|
5390
5459
|
function detectAvailableProviders() {
|
|
@@ -5477,8 +5546,8 @@ function keep(value) {
|
|
|
5477
5546
|
}
|
|
5478
5547
|
return value;
|
|
5479
5548
|
}
|
|
5480
|
-
async function addAgentWizard() {
|
|
5481
|
-
intro("Add agent");
|
|
5549
|
+
async function addAgentWizard({ embedded = false } = {}) {
|
|
5550
|
+
if (!embedded) intro("Add agent");
|
|
5482
5551
|
const appId = keep(
|
|
5483
5552
|
await text({
|
|
5484
5553
|
message: "GitHub App ID",
|
|
@@ -5500,14 +5569,14 @@ async function addAgentWizard() {
|
|
|
5500
5569
|
if (meta.installations.length === 0) {
|
|
5501
5570
|
throw new Error("This App has no installations. Install it on an account or org first.");
|
|
5502
5571
|
}
|
|
5503
|
-
let
|
|
5572
|
+
let account;
|
|
5504
5573
|
if (meta.installations.length === 1) {
|
|
5505
|
-
note(`${meta.installations[0].account}
|
|
5574
|
+
note(`${meta.installations[0].account} \u2014 resolved automatically each run`, "Installation");
|
|
5506
5575
|
} else {
|
|
5507
|
-
|
|
5576
|
+
account = keep(
|
|
5508
5577
|
await select({
|
|
5509
|
-
message: "
|
|
5510
|
-
options: meta.installations.map((i2) => ({ value: i2.
|
|
5578
|
+
message: "Which account is this agent for?",
|
|
5579
|
+
options: meta.installations.map((i2) => ({ value: i2.account, label: `${i2.account} (${i2.id})` }))
|
|
5511
5580
|
})
|
|
5512
5581
|
);
|
|
5513
5582
|
}
|
|
@@ -5518,48 +5587,63 @@ async function addAgentWizard() {
|
|
|
5518
5587
|
name,
|
|
5519
5588
|
label,
|
|
5520
5589
|
appId,
|
|
5521
|
-
installationId,
|
|
5522
5590
|
botName: meta.slug,
|
|
5523
5591
|
privateKeyPath,
|
|
5592
|
+
...account ? { account } : {},
|
|
5524
5593
|
...systemPrompt ? { systemPrompt } : {}
|
|
5525
5594
|
};
|
|
5526
5595
|
const check = spinner();
|
|
5527
5596
|
check.start("Minting a test token");
|
|
5528
|
-
await generateGithubAppToken(agent);
|
|
5529
|
-
|
|
5597
|
+
const { permissions } = await generateGithubAppToken(agent);
|
|
5598
|
+
const scope = Object.entries(permissions).map(([name2, level]) => `${name2}:${level}`).join(" ");
|
|
5599
|
+
check.stop(scope ? `Test token ok \xB7 ${scope}` : "Test token ok");
|
|
5530
5600
|
const path = addAgent(agent);
|
|
5531
|
-
|
|
5601
|
+
if (embedded) {
|
|
5602
|
+
note(`Wrote ${path}`, "Agent added");
|
|
5603
|
+
} else {
|
|
5604
|
+
outro(`Wrote ${path}`);
|
|
5605
|
+
}
|
|
5606
|
+
return agent.name;
|
|
5532
5607
|
}
|
|
5533
5608
|
async function interactiveSelection(defaults = {}) {
|
|
5534
5609
|
const availableProviders = detectAvailableProviders();
|
|
5535
5610
|
if (availableProviders.length === 0) {
|
|
5536
5611
|
throw new Error("No installed provider was detected. Install claude, codex, or antigravity before running the launcher.");
|
|
5537
5612
|
}
|
|
5538
|
-
let
|
|
5539
|
-
if (
|
|
5540
|
-
|
|
5541
|
-
|
|
5542
|
-
|
|
5543
|
-
|
|
5544
|
-
|
|
5545
|
-
|
|
5546
|
-
|
|
5547
|
-
})
|
|
5548
|
-
);
|
|
5613
|
+
let agents = readAgentsOrEmpty();
|
|
5614
|
+
if (agents.length === 0 && !defaults.agent) {
|
|
5615
|
+
note("No agents configured yet. Each agent is a GitHub App identity the launcher runs under.", "First run");
|
|
5616
|
+
if (!keep(await confirm({ message: "Add one now?" }))) {
|
|
5617
|
+
cancel(`Run "agent-forge add" when you're ready.`);
|
|
5618
|
+
process.exit(0);
|
|
5619
|
+
}
|
|
5620
|
+
await addAgentWizard({ embedded: true });
|
|
5621
|
+
agents = readAgents();
|
|
5549
5622
|
}
|
|
5550
|
-
const agents = readAgents();
|
|
5551
5623
|
let agentChoice = defaults.agent;
|
|
5552
5624
|
if (agentChoice && !agents.some((agent) => agent.name === agentChoice)) {
|
|
5553
5625
|
throw new Error(`Agent not found: ${agentChoice}`);
|
|
5554
5626
|
}
|
|
5555
5627
|
if (!agentChoice) {
|
|
5556
|
-
agentChoice = keep(
|
|
5628
|
+
agentChoice = agents.length === 1 ? agents[0].name : keep(
|
|
5557
5629
|
await select({
|
|
5558
5630
|
message: "Which agent do you want to launch?",
|
|
5559
5631
|
options: agents.map((agent) => ({ value: agent.name, label: agent.label || agent.name }))
|
|
5560
5632
|
})
|
|
5561
5633
|
);
|
|
5562
5634
|
}
|
|
5635
|
+
let providerChoice = defaults.provider;
|
|
5636
|
+
if (providerChoice && !availableProviders.some((provider) => provider.value === providerChoice)) {
|
|
5637
|
+
throw new Error(`The provider ${providerChoice} is not installed or not available in PATH.`);
|
|
5638
|
+
}
|
|
5639
|
+
if (!providerChoice) {
|
|
5640
|
+
providerChoice = availableProviders.length === 1 ? availableProviders[0].value : keep(
|
|
5641
|
+
await select({
|
|
5642
|
+
message: "Which provider do you want to use?",
|
|
5643
|
+
options: availableProviders.map((provider) => ({ value: provider.value, label: provider.label }))
|
|
5644
|
+
})
|
|
5645
|
+
);
|
|
5646
|
+
}
|
|
5563
5647
|
if (!keep(await confirm({ message: `Launch ${agentChoice} with ${providerChoice}?` }))) {
|
|
5564
5648
|
cancel("Cancelled.");
|
|
5565
5649
|
process.exit(0);
|
|
@@ -5619,7 +5703,7 @@ async function launchAgent(agentName, providerName) {
|
|
|
5619
5703
|
const providerInfo = getProviderInfo(providerName, agent);
|
|
5620
5704
|
const s = spinner();
|
|
5621
5705
|
s.start("Minting installation token");
|
|
5622
|
-
const githubToken = await generateGithubAppToken(agent);
|
|
5706
|
+
const { token: githubToken, permissions, repositorySelection } = await generateGithubAppToken(agent);
|
|
5623
5707
|
const botId = await resolveBotId(agent);
|
|
5624
5708
|
const gitIdentity = buildGitIdentity(agent, botId);
|
|
5625
5709
|
s.stop(`Token ready for ${(0, import_node_util3.styleText)("green", gitIdentity.GIT_AUTHOR_NAME)}`);
|
|
@@ -5630,17 +5714,20 @@ async function launchAgent(agentName, providerName) {
|
|
|
5630
5714
|
GH_TOKEN: githubToken,
|
|
5631
5715
|
...gitIdentity
|
|
5632
5716
|
};
|
|
5717
|
+
const scopeEntries = Object.entries(permissions);
|
|
5718
|
+
const scopeText = scopeEntries.length ? scopeEntries.map(([name, level]) => `${name}:${level}`).join(" ") : "full installation scope";
|
|
5633
5719
|
const row = (label, value) => `${(0, import_node_util3.styleText)("dim", label.padEnd(10))}${value}`;
|
|
5634
5720
|
const summary = [
|
|
5635
5721
|
row("provider", (0, import_node_util3.styleText)(["cyan", "bold"], providerName)),
|
|
5636
|
-
row("identity", (0, import_node_util3.styleText)("green", gitIdentity.GIT_AUTHOR_NAME))
|
|
5722
|
+
row("identity", (0, import_node_util3.styleText)("green", gitIdentity.GIT_AUTHOR_NAME)),
|
|
5723
|
+
row("token", (0, import_node_util3.styleText)("dim", repositorySelection === "selected" ? `${scopeText} \xB7 selected repos` : scopeText))
|
|
5637
5724
|
];
|
|
5638
5725
|
if (identityFile) {
|
|
5639
5726
|
summary.push(row("memory", (0, import_node_util3.styleText)("dim", (0, import_node_path4.relative)(process.cwd(), identityFile) || identityFile)));
|
|
5640
5727
|
}
|
|
5641
5728
|
note(summary.join("\n"), (0, import_node_util3.styleText)("bold", agent.label || agent.name));
|
|
5642
5729
|
const providerArgs = getProviderPromptArgs(providerName, agent);
|
|
5643
|
-
const child = (0,
|
|
5730
|
+
const child = (0, import_node_child_process3.spawn)(providerInfo.command, providerArgs, {
|
|
5644
5731
|
stdio: "inherit",
|
|
5645
5732
|
env: runtimeEnv,
|
|
5646
5733
|
shell: false
|
|
@@ -5672,7 +5759,11 @@ async function main() {
|
|
|
5672
5759
|
if (!flags.agent) {
|
|
5673
5760
|
throw new Error("Usage: agent-forge token --agent <name>");
|
|
5674
5761
|
}
|
|
5675
|
-
|
|
5762
|
+
const minted = await generateGithubAppToken(findAgent(flags.agent));
|
|
5763
|
+
const scope = Object.entries(minted.permissions).map(([name, level]) => `${name}:${level}`).join(" ");
|
|
5764
|
+
if (scope) process.stderr.write(`scope: ${scope}${minted.repositorySelection === "selected" ? " (selected repos)" : ""}
|
|
5765
|
+
`);
|
|
5766
|
+
process.stdout.write(minted.token);
|
|
5676
5767
|
return;
|
|
5677
5768
|
}
|
|
5678
5769
|
showIntro();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ashulab/agent-forge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Give claude/codex/antigravity their own GitHub identity: a bot account, a scoped token minted per run, git attribution to match",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Diego Ghersi <ghersidl@gmail.com>",
|
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
"name": "ops-agent",
|
|
6
6
|
"label": "Ops Agent",
|
|
7
7
|
"appId": "000000",
|
|
8
|
-
"installationId": "00000000",
|
|
9
8
|
"botName": "ops-agent",
|
|
10
9
|
"privateKeyPath": "~/.ssh/ops-agent.private-key.pem",
|
|
11
10
|
"systemPrompt": "You are Ops Agent, a senior DevOps engineer. Prefer safe, minimal changes and explain operational risks clearly."
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"items": {
|
|
12
12
|
"type": "object",
|
|
13
13
|
"additionalProperties": false,
|
|
14
|
-
"required": ["name", "label", "appId", "
|
|
14
|
+
"required": ["name", "label", "appId", "botName", "privateKeyPath"],
|
|
15
15
|
"properties": {
|
|
16
16
|
"name": {
|
|
17
17
|
"type": "string",
|
|
@@ -25,9 +25,13 @@
|
|
|
25
25
|
"type": "string",
|
|
26
26
|
"description": "GitHub App ID (numeric, stored as a string)"
|
|
27
27
|
},
|
|
28
|
+
"account": {
|
|
29
|
+
"type": "string",
|
|
30
|
+
"description": "Account/org login where the App is installed. Fallback for installation-id resolution when you run outside a matching repo. Needed only when the App has more than one installation. Resolution order: repo owner, then account, then the sole installation."
|
|
31
|
+
},
|
|
28
32
|
"installationId": {
|
|
29
33
|
"type": "string",
|
|
30
|
-
"description": "
|
|
34
|
+
"description": "Optional override. Normally the installation id is resolved (repo owner, then account, then the sole installation). Set this to pin a specific id or skip the lookup (offline, or in a git credential helper)."
|
|
31
35
|
},
|
|
32
36
|
"botName": {
|
|
33
37
|
"type": "string",
|
|
@@ -61,7 +65,7 @@
|
|
|
61
65
|
"permissions": {
|
|
62
66
|
"type": "object",
|
|
63
67
|
"additionalProperties": { "type": "string" },
|
|
64
|
-
"description": "Least-privilege:
|
|
68
|
+
"description": "Least-privilege: complete allowlist of installation token permissions. GitHub grants ONLY these keys and drops the rest, even ones the App has. Reviewing PRs needs { \"contents\": \"read\", \"pull_requests\": \"write\" }. Omit this key to inherit the App's full grant."
|
|
65
69
|
},
|
|
66
70
|
"providers": {
|
|
67
71
|
"type": "object",
|