iterate-skill-installer 3.5.1 → 3.5.2
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 +9 -2
- package/README.zh-CN.md +7 -2
- package/lib/installer.js +120 -43
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -39,8 +39,15 @@ npx iterate-skill-installer --version
|
|
|
39
39
|
|
|
40
40
|
1. Checks for Python 3 on your system.
|
|
41
41
|
2. Fetches the latest iterate-skill release from GitHub.
|
|
42
|
-
3. Downloads the release tarball
|
|
43
|
-
|
|
42
|
+
3. Downloads the release tarball, `SHA256SUMS.txt` and its detached GPG
|
|
43
|
+
signature (`iterate-skill.tar.gz.asc`).
|
|
44
|
+
4. Verifies BOTH the tarball checksum (integrity) and the signature against the
|
|
45
|
+
pinned iterate signing key (authenticity). **Signing is mandatory**: a
|
|
46
|
+
release without a valid signature is refused, because `SHA256SUMS.txt`
|
|
47
|
+
travels in the same release as the tarball and therefore cannot be the only
|
|
48
|
+
anchor. Install GnuPG (`gpg`) if you do not have it; hosts that cannot run
|
|
49
|
+
gpg may opt out explicitly with `ITERATE_ALLOW_UNSIGNED=1` (a bad signature
|
|
50
|
+
is never bypassed).
|
|
44
51
|
5. Extracts the release into a temporary directory.
|
|
45
52
|
6. Runs the bundled Python install script (`scripts/install.py`) which:
|
|
46
53
|
- Detects installed AI coding assistants on your machine.
|
package/README.zh-CN.md
CHANGED
|
@@ -39,8 +39,13 @@ npx iterate-skill-installer --version
|
|
|
39
39
|
|
|
40
40
|
1. 检查你系统上的 Python 3。
|
|
41
41
|
2. 从 GitHub 获取最新的 iterate-skill 发布版本。
|
|
42
|
-
3. 下载发布 tarball
|
|
43
|
-
|
|
42
|
+
3. 下载发布 tarball、`SHA256SUMS.txt` 及其分离式 GPG 签名
|
|
43
|
+
(`iterate-skill.tar.gz.asc`)。
|
|
44
|
+
4. 同时校验 tarball 校验和(完整性)与签名(真实性,对照内嵌的 iterate 签名公钥)。
|
|
45
|
+
**签名为强制**:没有有效签名的发布会直接拒绝安装——因为 `SHA256SUMS.txt` 与
|
|
46
|
+
tarball 出自同一个 release,不能作为唯一信任锚点。没有 `gpg` 请先安装 GnuPG;
|
|
47
|
+
确实无法运行 gpg 的环境可显式设置 `ITERATE_ALLOW_UNSIGNED=1` 放行(但**坏签名
|
|
48
|
+
永不放行**)。
|
|
44
49
|
5. 解压发布内容到临时目录。
|
|
45
50
|
6. 运行自带的 Python 安装脚本(`scripts/install.py`),它会:
|
|
46
51
|
- 检测你机器上已安装的 AI 编程助手。
|
package/lib/installer.js
CHANGED
|
@@ -424,6 +424,71 @@ function enforceDownloadedSize(destPath, maxBytes) {
|
|
|
424
424
|
}
|
|
425
425
|
}
|
|
426
426
|
|
|
427
|
+
// True when the operator explicitly opted out of signature enforcement. Kept
|
|
428
|
+
// as an ENVIRONMENT variable (not a flag) so it cannot be enabled from a
|
|
429
|
+
// project config and shows up in the invoking environment for audit.
|
|
430
|
+
const ALLOW_UNSIGNED_ENV = 'ITERATE_ALLOW_UNSIGNED';
|
|
431
|
+
|
|
432
|
+
function allowUnsigned() {
|
|
433
|
+
return String(process.env[ALLOW_UNSIGNED_ENV] || '').trim() !== '';
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Apply the MANDATORY release-signature policy (mirrors
|
|
438
|
+
* ``iterate_cli/signing.py`` and ``scripts/install.py``).
|
|
439
|
+
*
|
|
440
|
+
* Returns an error MESSAGE when the install must abort, or null when it may
|
|
441
|
+
* proceed. The ``ITERATE_ALLOW_UNSIGNED`` override downgrades "cannot check"
|
|
442
|
+
* (no ``.asc`` asset / no ``gpg`` binary) to a warning, but it NEVER bypasses a
|
|
443
|
+
* BAD signature — that is exactly the tampering/key-substitution case.
|
|
444
|
+
*
|
|
445
|
+
* @param {object} state
|
|
446
|
+
* @param {boolean} state.hasSignatureAsset release published an ``.asc``
|
|
447
|
+
* @param {string} [state.status] 'verified' | 'invalid' | 'unavailable'
|
|
448
|
+
* @param {string} [state.detail] verifier detail for messages
|
|
449
|
+
* @param {string} [state.downloadError] failure to FETCH the ``.asc``
|
|
450
|
+
* @param {boolean} [state.allowUnsigned] override (defaults to the env var)
|
|
451
|
+
*/
|
|
452
|
+
function signaturePolicyError({
|
|
453
|
+
hasSignatureAsset,
|
|
454
|
+
status = null,
|
|
455
|
+
detail = '',
|
|
456
|
+
downloadError = null,
|
|
457
|
+
allowUnsigned: override = allowUnsigned(),
|
|
458
|
+
} = {}) {
|
|
459
|
+
if (downloadError) {
|
|
460
|
+
// The release DID publish a signature but we could not fetch it: a
|
|
461
|
+
// transport problem, not a licence to skip authenticity.
|
|
462
|
+
return (
|
|
463
|
+
`Could not download the release signature (${downloadError}). Refusing to install ` +
|
|
464
|
+
`without authenticity verification; retry once the network recovers. Set ` +
|
|
465
|
+
`${ALLOW_UNSIGNED_ENV}=1 only if you accept an unauthenticated install.`
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
if (!hasSignatureAsset) {
|
|
469
|
+
if (override) return null;
|
|
470
|
+
return (
|
|
471
|
+
'Release is UNSIGNED: it publishes no .asc signature, so authenticity cannot be ' +
|
|
472
|
+
`verified (SHA256SUMS travels in the same release and is not an independent ` +
|
|
473
|
+
`anchor). Set ${ALLOW_UNSIGNED_ENV}=1 only if you accept an unauthenticated install.`
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
if (status === 'verified') return null;
|
|
477
|
+
if (status === 'invalid') {
|
|
478
|
+
return (
|
|
479
|
+
`GPG signature verification FAILED (${detail}). Refusing to install: the tarball ` +
|
|
480
|
+
`is not signed by the pinned iterate signing key. This is either tampering or a ` +
|
|
481
|
+
`key rotation this installer version does not know about; ${ALLOW_UNSIGNED_ENV} ` +
|
|
482
|
+
'does NOT bypass a bad signature.'
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
if (override) return null;
|
|
486
|
+
return (
|
|
487
|
+
`Cannot verify the release signature (${detail || 'gpg unavailable'}). Install GnuPG ` +
|
|
488
|
+
`(gpg) and retry, or set ${ALLOW_UNSIGNED_ENV}=1 to accept an unauthenticated install.`
|
|
489
|
+
);
|
|
490
|
+
}
|
|
491
|
+
|
|
427
492
|
function sha256File(filePath) {
|
|
428
493
|
return new Promise((resolve, reject) => {
|
|
429
494
|
const hash = crypto.createHash('sha256');
|
|
@@ -447,7 +512,11 @@ function sha256File(filePath) {
|
|
|
447
512
|
// Runs inside an isolated temporary GNUPGHOME so the end user's own keyring and
|
|
448
513
|
// gpg-agent are never touched or modified.
|
|
449
514
|
function verifyGpgDetachedSignature(artifactPath, signaturePath, publicKeyArmor, tmpDir) {
|
|
450
|
-
|
|
515
|
+
// NOTE: gpg derives its agent socket from the homedir
|
|
516
|
+
// (<homedir>/S.gpg-agent) and unix sockets cap out around 104 characters, so
|
|
517
|
+
// keep this path SHORT — a deep tmpdir made gpg fail with "File name too
|
|
518
|
+
// long: can't connect to the gpg-agent".
|
|
519
|
+
const gpgHome = path.join(tmpDir, 'g');
|
|
451
520
|
fs.mkdirSync(gpgHome, { recursive: true, mode: 0o700 });
|
|
452
521
|
const keyPath = path.join(tmpDir, 'iterate-skill-pubkey.asc');
|
|
453
522
|
fs.writeFileSync(keyPath, publicKeyArmor, 'utf8');
|
|
@@ -1252,20 +1321,27 @@ async function main(options = {}) {
|
|
|
1252
1321
|
hint('Retry, or set GITHUB_TOKEN if this is a rate-limit artefact.');
|
|
1253
1322
|
return 1;
|
|
1254
1323
|
}
|
|
1255
|
-
// Announce releases that carry the tarball but not the checksum
|
|
1256
|
-
// are skipped, which silently installs an OLDER release when the
|
|
1257
|
-
// is mid-upload. Saying so out loud keeps "installed an old
|
|
1324
|
+
// Announce releases that carry the tarball but not the checksum/signature
|
|
1325
|
+
// pair: they are skipped, which silently installs an OLDER release when the
|
|
1326
|
+
// newest one is mid-upload. Saying so out loud keeps "installed an old
|
|
1327
|
+
// version" visible.
|
|
1258
1328
|
for (const candidate of releases) {
|
|
1259
1329
|
if (!candidate || candidate.draft || candidate.prerelease) continue;
|
|
1260
1330
|
const assets = Array.isArray(candidate.assets) ? candidate.assets : [];
|
|
1261
1331
|
const hasTarball = assets.some((a) => a && a.name === TARBALL_ASSET_NAME);
|
|
1262
|
-
const
|
|
1263
|
-
if (
|
|
1332
|
+
const missing = [];
|
|
1333
|
+
if (!assets.some((a) => a && a.name === CHECKSUMS_ASSET_NAME)) missing.push(CHECKSUMS_ASSET_NAME);
|
|
1334
|
+
if (!assets.some((a) => a && a.name === TARBALL_SIGNATURE_ASSET_NAME)) missing.push(TARBALL_SIGNATURE_ASSET_NAME);
|
|
1335
|
+
if (hasTarball && missing.length > 0) {
|
|
1264
1336
|
warning(
|
|
1265
|
-
`Skipping release ${candidate.tag_name}: has ${TARBALL_ASSET_NAME} but no ${
|
|
1337
|
+
`Skipping release ${candidate.tag_name}: has ${TARBALL_ASSET_NAME} but no ${missing.join(' / ')}.`,
|
|
1266
1338
|
);
|
|
1267
1339
|
}
|
|
1268
1340
|
}
|
|
1341
|
+
// A usable release must carry the tarball, its checksums AND its detached
|
|
1342
|
+
// signature: the signature is mandatory at install time (see
|
|
1343
|
+
// signaturePolicyError), so selecting a release without one would only turn
|
|
1344
|
+
// "found the latest" into "failed to install".
|
|
1269
1345
|
const release = releases.find(
|
|
1270
1346
|
(r) =>
|
|
1271
1347
|
r &&
|
|
@@ -1273,12 +1349,14 @@ async function main(options = {}) {
|
|
|
1273
1349
|
!r.prerelease &&
|
|
1274
1350
|
Array.isArray(r.assets) &&
|
|
1275
1351
|
r.assets.some((a) => a && a.name === TARBALL_ASSET_NAME) &&
|
|
1276
|
-
r.assets.some((a) => a && a.name === CHECKSUMS_ASSET_NAME)
|
|
1352
|
+
r.assets.some((a) => a && a.name === CHECKSUMS_ASSET_NAME) &&
|
|
1353
|
+
r.assets.some((a) => a && a.name === TARBALL_SIGNATURE_ASSET_NAME),
|
|
1277
1354
|
);
|
|
1278
1355
|
|
|
1279
1356
|
if (!release) {
|
|
1280
1357
|
error(
|
|
1281
|
-
`No published release in the last 100 carries a ${TARBALL_ASSET_NAME} +
|
|
1358
|
+
`No published release in the last 100 carries a ${TARBALL_ASSET_NAME} + ` +
|
|
1359
|
+
`${CHECKSUMS_ASSET_NAME} + ${TARBALL_SIGNATURE_ASSET_NAME} asset triple.`
|
|
1282
1360
|
);
|
|
1283
1361
|
hint('This is usually a GitHub API rate limit; retry with GITHUB_TOKEN set.');
|
|
1284
1362
|
return 1;
|
|
@@ -1306,7 +1384,7 @@ async function main(options = {}) {
|
|
|
1306
1384
|
return 1;
|
|
1307
1385
|
}
|
|
1308
1386
|
|
|
1309
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), '
|
|
1387
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ig-install-'));
|
|
1310
1388
|
const tarballPath = path.join(tmpDir, TARBALL_ASSET_NAME);
|
|
1311
1389
|
const checksumPath = path.join(tmpDir, CHECKSUMS_ASSET_NAME);
|
|
1312
1390
|
|
|
@@ -1358,19 +1436,17 @@ async function main(options = {}) {
|
|
|
1358
1436
|
`\x1b[32m✓\x1b[0m SHA256 checksum verified`,
|
|
1359
1437
|
]);
|
|
1360
1438
|
|
|
1361
|
-
// GPG authenticity check
|
|
1362
|
-
// release also publishes a detached ASCII-armored signature over the
|
|
1363
|
-
// tarball. When a `gpg` binary is present we verify it against the embedded
|
|
1364
|
-
// signing key: a *valid* signature upgrades the release from
|
|
1365
|
-
// "integrity-guaranteed" to "authenticity-guaranteed". If gpg is
|
|
1366
|
-
// unavailable we degrade to a warning (SHA256 already guards integrity —
|
|
1367
|
-
// that check still stands on its own).
|
|
1439
|
+
// GPG authenticity check — MANDATORY (see signaturePolicyError).
|
|
1368
1440
|
//
|
|
1369
|
-
//
|
|
1370
|
-
//
|
|
1371
|
-
//
|
|
1372
|
-
//
|
|
1373
|
-
//
|
|
1441
|
+
// SHA256SUMS.txt travels in the SAME release as the tarball, so an attacker
|
|
1442
|
+
// who can replace one can replace both; the pinned public key below is the
|
|
1443
|
+
// only anchor they cannot forge. A missing `.asc`, a host without `gpg`, an
|
|
1444
|
+
// unfetchable signature, or a signature that does not verify all ABORT the
|
|
1445
|
+
// install. The single documented escape hatch is ``ITERATE_ALLOW_UNSIGNED``
|
|
1446
|
+
// (hosts without GnuPG) and it never bypasses a BAD signature.
|
|
1447
|
+
let signatureStatus = null;
|
|
1448
|
+
let signatureDetail = '';
|
|
1449
|
+
let signatureDownloadError = null;
|
|
1374
1450
|
if (signatureAsset) {
|
|
1375
1451
|
const signaturePath = path.join(tmpDir, TARBALL_SIGNATURE_ASSET_NAME);
|
|
1376
1452
|
try {
|
|
@@ -1378,30 +1454,28 @@ async function main(options = {}) {
|
|
|
1378
1454
|
maxBytes: CHECKSUMS_MAX_BYTES,
|
|
1379
1455
|
});
|
|
1380
1456
|
const sig = verifyGpgDetachedSignature(tarballPath, signaturePath, ITERATE_SIGNING_PUBLIC_KEY, tmpDir);
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
} else if (sig.status === 'invalid') {
|
|
1384
|
-
error(
|
|
1385
|
-
`GPG signature verification FAILED (${sig.detail}) — refusing to install.\n` +
|
|
1386
|
-
' The tarball was signed with a key that does not match the pinned ' +
|
|
1387
|
-
'iterate-skill signing key: this is either tampering or a key rotation ' +
|
|
1388
|
-
'that this installer version does not know about. Report it before retrying.',
|
|
1389
|
-
);
|
|
1390
|
-
return 1;
|
|
1391
|
-
} else {
|
|
1392
|
-
warning(
|
|
1393
|
-
`gpg not available (${sig.detail}); skipping signature check — SHA256 integrity still enforced.`
|
|
1394
|
-
);
|
|
1395
|
-
}
|
|
1457
|
+
signatureStatus = sig.status;
|
|
1458
|
+
signatureDetail = sig.detail;
|
|
1396
1459
|
} catch (err) {
|
|
1397
|
-
|
|
1460
|
+
signatureDownloadError = err.message;
|
|
1398
1461
|
}
|
|
1399
|
-
}
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1462
|
+
}
|
|
1463
|
+
const signatureError = signaturePolicyError({
|
|
1464
|
+
hasSignatureAsset: Boolean(signatureAsset),
|
|
1465
|
+
status: signatureStatus,
|
|
1466
|
+
detail: signatureDetail,
|
|
1467
|
+
downloadError: signatureDownloadError,
|
|
1468
|
+
});
|
|
1469
|
+
if (signatureError) {
|
|
1470
|
+
error(signatureError);
|
|
1471
|
+
return 1;
|
|
1472
|
+
}
|
|
1473
|
+
if (signatureStatus === 'verified') {
|
|
1474
|
+
success('GPG signature verified.');
|
|
1475
|
+
} else if (allowUnsigned()) {
|
|
1403
1476
|
warning(
|
|
1404
|
-
|
|
1477
|
+
`Signature NOT verified (${signatureDetail || 'no signature'}) but accepted because ` +
|
|
1478
|
+
`${ALLOW_UNSIGNED_ENV} is set — SHA256 integrity only.`,
|
|
1405
1479
|
);
|
|
1406
1480
|
}
|
|
1407
1481
|
|
|
@@ -1490,6 +1564,9 @@ module.exports = {
|
|
|
1490
1564
|
isGithubApiUrl,
|
|
1491
1565
|
normalizeToken,
|
|
1492
1566
|
validateTokenShape,
|
|
1567
|
+
allowUnsigned,
|
|
1568
|
+
signaturePolicyError,
|
|
1569
|
+
ALLOW_UNSIGNED_ENV,
|
|
1493
1570
|
buildAuthFlags,
|
|
1494
1571
|
supportsCurlFailWithBody,
|
|
1495
1572
|
// Executed under test for the timeout/null-exit-code hardening; retaining
|