@integrity-labs/agt-cli 0.28.835 → 0.28.836
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/dist/assets/review/obiwan-auth.mjs +69 -13
- package/dist/assets/review/post-review-findings.mjs +53 -3
- package/dist/bin/agt.js +3 -3
- package/dist/{chunk-REBHUZ7N.js → chunk-E6B43IJZ.js} +2 -2
- package/dist/lib/manager-worker.js +2 -2
- package/dist/lib/manager-worker.js.map +1 -1
- package/package.json +1 -1
- /package/dist/{chunk-REBHUZ7N.js.map → chunk-E6B43IJZ.js.map} +0 -0
|
@@ -4,10 +4,12 @@
|
|
|
4
4
|
* copy is downstream. Delivered alongside `post-review-findings.mjs` by
|
|
5
5
|
* `provisionReviewPoster()` (apps/cli/src/lib/review-poster-asset.ts).
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* `
|
|
10
|
-
*
|
|
7
|
+
* DIVERGED FROM THE ORIGIN, deliberately (ENG-10001). `resolveIdentity` now
|
|
8
|
+
* VERIFIES with GitHub that the App it minted for is the one named in
|
|
9
|
+
* `EXPECTED_APP_SLUG`, and throws when it is not; `post-review-findings.mjs`
|
|
10
|
+
* refuses to write anything at all without that verified identity. The plugin
|
|
11
|
+
* copy stays permissive because a human runs it and can see whose name ends up
|
|
12
|
+
* on the comment. This copy's only caller is an unattended agent, which cannot.
|
|
11
13
|
*/
|
|
12
14
|
/**
|
|
13
15
|
* obiwan-auth — mint a GitHub App installation token for the review bot.
|
|
@@ -42,6 +44,13 @@
|
|
|
42
44
|
import { createSign, randomUUID } from 'node:crypto';
|
|
43
45
|
import { readFileSync, statSync } from 'node:fs';
|
|
44
46
|
|
|
47
|
+
/**
|
|
48
|
+
* The App this reviewer is, as GitHub spells it. `slug` — not the display name,
|
|
49
|
+
* not the app id — is what forms the comment author `<slug>[bot]`, so it is the
|
|
50
|
+
* only value that answers "whose name will be on the finding".
|
|
51
|
+
*/
|
|
52
|
+
export const EXPECTED_APP_SLUG = 'obi-wan-shinobi-reviewer';
|
|
53
|
+
|
|
45
54
|
const b64url = (buf) =>
|
|
46
55
|
Buffer.from(buf).toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
47
56
|
|
|
@@ -134,6 +143,23 @@ export async function ghFetch(url, jwt, method = 'GET', fetchImpl = fetch, timeo
|
|
|
134
143
|
export async function mintInstallationToken({ repo, appId, privateKey, request = ghFetch }) {
|
|
135
144
|
const jwt = buildAppJwt({ appId, privateKey });
|
|
136
145
|
|
|
146
|
+
// WHO WE ACTUALLY ARE, asked of GitHub rather than inferred from config.
|
|
147
|
+
// `appId` is operator-supplied, and a wrong one does not fail — it mints a
|
|
148
|
+
// perfectly valid token for a DIFFERENT App, whose `[bot]` name then appears
|
|
149
|
+
// on the review. That is the outcome AC3 forbids, minus the tell: it does not
|
|
150
|
+
// look like a fallback, it looks like a working bot under the wrong name.
|
|
151
|
+
//
|
|
152
|
+
// This hop needs the JWT: an installation token cannot call `GET /app`.
|
|
153
|
+
const app = await request('https://api.github.com/app', jwt);
|
|
154
|
+
if (app.status !== 200 || !app.body?.slug) {
|
|
155
|
+
throw new Error(
|
|
156
|
+
`app identity lookup failed (${app.status}): ${app.body?.message ?? 'no slug in the response'}` +
|
|
157
|
+
(app.status === 401
|
|
158
|
+
? ' — a 401 here means the JWT did not verify: check OBIWAN_APP_ID matches the key you downloaded'
|
|
159
|
+
: ''),
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
137
163
|
const inst = await request(`https://api.github.com/repos/${repo}/installation`, jwt);
|
|
138
164
|
if (inst.status === 404) {
|
|
139
165
|
throw new Error(
|
|
@@ -158,18 +184,31 @@ export async function mintInstallationToken({ repo, appId, privateKey, request =
|
|
|
158
184
|
if (tok.status !== 201 || !tok.body?.token) {
|
|
159
185
|
throw new Error(`token exchange failed (${tok.status}): ${tok.body?.message ?? 'no token in response'}`);
|
|
160
186
|
}
|
|
161
|
-
return {
|
|
187
|
+
return {
|
|
188
|
+
token: tok.body.token,
|
|
189
|
+
expiresAt: tok.body.expires_at,
|
|
190
|
+
installationId: inst.body.id,
|
|
191
|
+
appSlug: app.body.slug,
|
|
192
|
+
};
|
|
162
193
|
}
|
|
163
194
|
|
|
164
195
|
/**
|
|
165
196
|
* The token to authenticate as, or null to keep whatever `gh` is already using.
|
|
166
197
|
*
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
*
|
|
172
|
-
*
|
|
198
|
+
* TWO OUTCOMES, AND THEY ARE NOT THE SAME KIND OF THING.
|
|
199
|
+
*
|
|
200
|
+
* An UNCONFIGURED App returns `kind: 'fallback'` rather than throwing, because
|
|
201
|
+
* the read-only modes are still useful without it — `--status` lists this
|
|
202
|
+
* reviewer's threads with any credentials at all. What the fallback must never
|
|
203
|
+
* do is write, and enforcing that is the CALLER's job: see `refuseIdentity` in
|
|
204
|
+
* `post-review-findings.mjs`, which is where the fail-closed decision lives so
|
|
205
|
+
* that it can distinguish a dry run from a post.
|
|
206
|
+
*
|
|
207
|
+
* A MISCONFIGURED App throws. Absence degrades; pointing at the wrong App is a
|
|
208
|
+
* mistake to be loud about, and a machine that would post under a name nobody
|
|
209
|
+
* chose should stop rather than narrow its mode. The cost is that `--status`
|
|
210
|
+
* also fails there, which is the right way round: the operator learns of the
|
|
211
|
+
* misconfiguration at the first command they run, not at the first post.
|
|
173
212
|
*/
|
|
174
213
|
/**
|
|
175
214
|
* The two halves of the fallback diagnostic, named so the test can assert them
|
|
@@ -202,8 +241,25 @@ export async function resolveIdentity({ repo, env = process.env, mint = mintInst
|
|
|
202
241
|
};
|
|
203
242
|
}
|
|
204
243
|
const privateKey = resolvePrivateKey(env.OBIWAN_PRIVATE_KEY);
|
|
205
|
-
const { token, expiresAt } = await mint({ repo, appId: env.OBIWAN_APP_ID, privateKey });
|
|
206
|
-
|
|
244
|
+
const { token, expiresAt, appSlug } = await mint({ repo, appId: env.OBIWAN_APP_ID, privateKey });
|
|
245
|
+
|
|
246
|
+
// A MISSING slug is a failure, not a pass. It means this token was minted by
|
|
247
|
+
// something that did not perform the `GET /app` hop — an older `mint`, or a
|
|
248
|
+
// future refactor that drops it — and reading "nothing to compare" as
|
|
249
|
+
// "nothing wrong" is exactly how a check that still exists stops checking.
|
|
250
|
+
if (appSlug !== EXPECTED_APP_SLUG) {
|
|
251
|
+
throw new Error(
|
|
252
|
+
`OBIWAN_APP_ID ${env.OBIWAN_APP_ID} authenticates as ` +
|
|
253
|
+
`${appSlug ? `"${appSlug}"` : 'an App whose identity was not verified'}, ` +
|
|
254
|
+
`not "${EXPECTED_APP_SLUG}" — refusing to post under an identity nobody chose. ` +
|
|
255
|
+
'Point OBIWAN_APP_ID and OBIWAN_PRIVATE_KEY at the Obi Wan Shinobi App.',
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// `why` is derived from what GitHub returned, never from the constant above.
|
|
260
|
+
// Printing the expected name would report the identity the code hoped for
|
|
261
|
+
// rather than the one it got, which is the whole failure this guards.
|
|
262
|
+
return { kind: 'app', token, expiresAt, appSlug, why: `${appSlug}[bot] via App ${env.OBIWAN_APP_ID}` };
|
|
207
263
|
}
|
|
208
264
|
|
|
209
265
|
/** Never let a token reach a log or an error message intact. */
|
|
@@ -14,8 +14,10 @@
|
|
|
14
14
|
*
|
|
15
15
|
* Origin: the Smithers Claude Code plugin (~/.claude/plugins/smithers/scripts/).
|
|
16
16
|
* That copy is now DOWNSTREAM of this one. Do NOT add an equality sync test:
|
|
17
|
-
* this copy is expected to diverge
|
|
18
|
-
*
|
|
17
|
+
* this copy is expected to diverge, and already has — `refuseIdentity()` below
|
|
18
|
+
* makes a write without the VERIFIED review-bot identity impossible here, while
|
|
19
|
+
* the plugin copy stays permissive for the human who runs it. A `--min-severity`
|
|
20
|
+
* floor is the remaining planned divergence (ENG-10001).
|
|
19
21
|
*/
|
|
20
22
|
/**
|
|
21
23
|
* post-review-findings — deliver `/s:code-review` and `/s:security-review`
|
|
@@ -416,6 +418,45 @@ export function refuseMode({ state, status, resolve, pr }) {
|
|
|
416
418
|
return null;
|
|
417
419
|
}
|
|
418
420
|
|
|
421
|
+
/**
|
|
422
|
+
* Whether this identity may WRITE. Deliberately a second function rather than
|
|
423
|
+
* another clause in refuseMode(): they gate different axes, and their read-only
|
|
424
|
+
* sets genuinely differ.
|
|
425
|
+
*
|
|
426
|
+
* refuseMode asks "may this MODE touch a PR in this STATE", and there a dry run
|
|
427
|
+
* counts as a write, because computing findings against a closed PR is pointless
|
|
428
|
+
* work. Here a dry run is a READ — it posts nothing, so who we are cannot matter
|
|
429
|
+
* — and refusing it would deny an operator the one command that shows what the
|
|
430
|
+
* misconfiguration would have done. So `willWrite` is `--post || --resolve`, and
|
|
431
|
+
* nothing else. Folding the two together would force one of those two readings
|
|
432
|
+
* onto the other.
|
|
433
|
+
*
|
|
434
|
+
* FAIL CLOSED, WITH NO OVERRIDE FLAG. There is deliberately no
|
|
435
|
+
* `--allow-fallback-identity`. This copy's only caller is an unattended agent,
|
|
436
|
+
* so any escape hatch is one the AGENT can take, and an agent writing under an
|
|
437
|
+
* identity nobody chose is the exact thing being guarded. A documented bypass of
|
|
438
|
+
* a fail-closed check is not a fail-closed check.
|
|
439
|
+
*
|
|
440
|
+
* AND THE FALLBACK IS NOT WHAT IT LOOKS LIKE ON A HOST. The obvious reading —
|
|
441
|
+
* "it would post as the operator" — is a workstation reading. ENG-7506
|
|
442
|
+
* materialises the Augmented App's installation token into the agent session as
|
|
443
|
+
* GITHUB_ACCESS_TOKEN and GITHUB_TOKEN, and `gh` reads GITHUB_TOKEN. So a
|
|
444
|
+
* fallback write from a managed agent posts as the PLATFORM's bot, on a review
|
|
445
|
+
* the platform did not make. Unattributable rather than misattributed, which is
|
|
446
|
+
* no better: nobody can mute it, and nobody can tell whose claim it is.
|
|
447
|
+
*
|
|
448
|
+
* Returns a refusal message, or null to proceed.
|
|
449
|
+
*/
|
|
450
|
+
export function refuseIdentity({ kind, willWrite }) {
|
|
451
|
+
if (!willWrite || kind === 'app') return null;
|
|
452
|
+
return (
|
|
453
|
+
'refusing to write as anything but the review bot: OBIWAN_APP_ID / OBIWAN_PRIVATE_KEY ' +
|
|
454
|
+
'are not configured, so --post and --resolve would write under whatever credentials ' +
|
|
455
|
+
'`gh` happens to hold. Configure the App (see obiwan-auth.mjs), or drop --post to see ' +
|
|
456
|
+
'what would have been posted.'
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
|
|
419
460
|
async function main() {
|
|
420
461
|
const a = parseArgs(process.argv.slice(2));
|
|
421
462
|
if (!a.pr) throw new Error('--pr is required');
|
|
@@ -431,9 +472,18 @@ async function main() {
|
|
|
431
472
|
console.log(
|
|
432
473
|
identity.kind === 'app'
|
|
433
474
|
? `identity: ${identity.why} (token expires ${identity.expiresAt})`
|
|
434
|
-
: `identity: FALLBACK —
|
|
475
|
+
: `identity: FALLBACK — whatever \`gh\` already holds, which is NOT the review bot ` +
|
|
476
|
+
`(${identity.why}); reads are fine, --post and --resolve will refuse`,
|
|
435
477
|
);
|
|
436
478
|
|
|
479
|
+
// Before the PR is even read, so a misconfigured machine learns it from the
|
|
480
|
+
// first line of output rather than after a full scan it cannot deliver.
|
|
481
|
+
const identityRefusal = refuseIdentity({
|
|
482
|
+
kind: identity.kind,
|
|
483
|
+
willWrite: Boolean(a.post) || Boolean(a.resolve),
|
|
484
|
+
});
|
|
485
|
+
if (identityRefusal) throw new Error(identityRefusal);
|
|
486
|
+
|
|
437
487
|
// Read PR state BEFORE the --resolve branch, not after it (CodeRabbit, #156).
|
|
438
488
|
// --resolve returned early, and resolveFinding posts a reply before it
|
|
439
489
|
// resolves — so a merged PR could still take a bot comment through this door
|
package/dist/bin/agt.js
CHANGED
|
@@ -40,7 +40,7 @@ import {
|
|
|
40
40
|
success,
|
|
41
41
|
table,
|
|
42
42
|
warn
|
|
43
|
-
} from "../chunk-
|
|
43
|
+
} from "../chunk-E6B43IJZ.js";
|
|
44
44
|
import {
|
|
45
45
|
getProjectDir,
|
|
46
46
|
isSessionResumeDisabled,
|
|
@@ -5467,7 +5467,7 @@ import { execFileSync, execSync } from "child_process";
|
|
|
5467
5467
|
import { existsSync as existsSync11, realpathSync as realpathSync2 } from "fs";
|
|
5468
5468
|
import chalk18 from "chalk";
|
|
5469
5469
|
import ora16 from "ora";
|
|
5470
|
-
var cliVersion = true ? "0.28.
|
|
5470
|
+
var cliVersion = true ? "0.28.836" : "dev";
|
|
5471
5471
|
async function fetchLatestVersion() {
|
|
5472
5472
|
const host2 = getHost();
|
|
5473
5473
|
if (!host2) return null;
|
|
@@ -6658,7 +6658,7 @@ function handleError(err) {
|
|
|
6658
6658
|
}
|
|
6659
6659
|
|
|
6660
6660
|
// src/bin/agt.ts
|
|
6661
|
-
var cliVersion2 = true ? "0.28.
|
|
6661
|
+
var cliVersion2 = true ? "0.28.836" : "dev";
|
|
6662
6662
|
var program = new Command();
|
|
6663
6663
|
program.name("agt").description("Augmented CLI \u2014 agent provisioning and management").version(cliVersion2).option("--json", "Emit machine-readable JSON output (suppress spinners and colors)").option("--skip-update-check", "Skip the automatic update check on startup");
|
|
6664
6664
|
program.hook("preAction", async (thisCommand, actionCommand) => {
|
|
@@ -6537,7 +6537,7 @@ function exchangeFailureKind(err) {
|
|
|
6537
6537
|
}
|
|
6538
6538
|
|
|
6539
6539
|
// src/lib/api-client.ts
|
|
6540
|
-
var agtCliVersion = true ? "0.28.
|
|
6540
|
+
var agtCliVersion = true ? "0.28.836" : "dev";
|
|
6541
6541
|
var lastConfigHash = null;
|
|
6542
6542
|
function setConfigHash(hash) {
|
|
6543
6543
|
lastConfigHash = hash && hash.length > 0 ? hash : null;
|
|
@@ -10895,4 +10895,4 @@ export {
|
|
|
10895
10895
|
managerInstallSystemUnitCommand,
|
|
10896
10896
|
managerUninstallSystemUnitCommand
|
|
10897
10897
|
};
|
|
10898
|
-
//# sourceMappingURL=chunk-
|
|
10898
|
+
//# sourceMappingURL=chunk-E6B43IJZ.js.map
|
|
@@ -60,7 +60,7 @@ import {
|
|
|
60
60
|
safeWriteJsonAtomic,
|
|
61
61
|
setConfigHash,
|
|
62
62
|
tripClass
|
|
63
|
-
} from "../chunk-
|
|
63
|
+
} from "../chunk-E6B43IJZ.js";
|
|
64
64
|
import {
|
|
65
65
|
getProjectDir as getProjectDir2,
|
|
66
66
|
getReadyTasks,
|
|
@@ -13669,7 +13669,7 @@ var agentRestartTimezoneInputs = /* @__PURE__ */ new Map();
|
|
|
13669
13669
|
var lastVersionCheckAt = 0;
|
|
13670
13670
|
var VERSION_CHECK_INTERVAL_MS = 5 * 60 * 1e3;
|
|
13671
13671
|
var lastResponsivenessProbeAt = 0;
|
|
13672
|
-
var agtCliVersion = true ? "0.28.
|
|
13672
|
+
var agtCliVersion = true ? "0.28.836" : "dev";
|
|
13673
13673
|
function resolveBrewPath(execFileSync2) {
|
|
13674
13674
|
try {
|
|
13675
13675
|
const out = execFileSync2("which", ["brew"], { timeout: 5e3 }).toString().trim();
|