@autodevjapan/godd-mcp-alpha 2.9.2 → 2.9.4
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/godd.cjs +158 -152
- package/godd-tk-assets.json +26 -0
- package/package.json +3 -1
- package/scripts/godd-tk-assets.js +154 -0
- package/scripts/postinstall-godd-tk.js +228 -75
- package/scripts/postinstall.js +30 -1
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"releaseTag": "godd-tk-v0.1.0",
|
|
4
|
+
"targets": {
|
|
5
|
+
"darwin-arm64": {
|
|
6
|
+
"artifact": "godd-tk-aarch64-apple-darwin",
|
|
7
|
+
"bytes": 1450928,
|
|
8
|
+
"sha256": "7560ed51aacfcf278e02557339355b5a16a6c45965e3f10b1e47f15d2e14582e"
|
|
9
|
+
},
|
|
10
|
+
"darwin-x64": {
|
|
11
|
+
"artifact": "godd-tk-x86_64-apple-darwin",
|
|
12
|
+
"bytes": 1686888,
|
|
13
|
+
"sha256": "8771dcb838271ef32b1077cd104c582386c38403bb8ccf28986c3f957a762fee"
|
|
14
|
+
},
|
|
15
|
+
"linux-x64": {
|
|
16
|
+
"artifact": "godd-tk-x86_64-unknown-linux-musl",
|
|
17
|
+
"bytes": 2147096,
|
|
18
|
+
"sha256": "6c81e2fef94338540371bcf144d376f7cfc7a68b170f08749ecf7f08a550d5be"
|
|
19
|
+
},
|
|
20
|
+
"win32-x64": {
|
|
21
|
+
"artifact": "godd-tk-x86_64-pc-windows-msvc.exe",
|
|
22
|
+
"bytes": 1647104,
|
|
23
|
+
"sha256": "875918e53546d8730a3d28149a333498443704b45ef1cc41a3b7d098865e5f18"
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@autodevjapan/godd-mcp-alpha",
|
|
3
|
-
"version": "2.9.
|
|
3
|
+
"version": "2.9.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Senior-engineering workflows for AI coding clients — plan, build, test, review, and ship with one command",
|
|
6
6
|
"license": "UNLICENSED",
|
|
@@ -18,10 +18,12 @@
|
|
|
18
18
|
"assets/godd-mcp-alpha-icon.png",
|
|
19
19
|
"assets/native",
|
|
20
20
|
"native-assets.json",
|
|
21
|
+
"godd-tk-assets.json",
|
|
21
22
|
"dist/godd.cjs",
|
|
22
23
|
"scripts/postinstall.js",
|
|
23
24
|
"scripts/postinstall-godd-native.js",
|
|
24
25
|
"scripts/native-assets.js",
|
|
26
|
+
"scripts/godd-tk-assets.js",
|
|
25
27
|
"scripts/postinstall-godd-tk.js",
|
|
26
28
|
"scripts/write-sha256.js",
|
|
27
29
|
"README.md"
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readFileSync, statSync } from "node:fs";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
const packageRoot = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
7
|
+
|
|
8
|
+
const MANIFEST_KEYS = ["releaseTag", "schemaVersion", "targets"];
|
|
9
|
+
const TARGET_KEYS = ["artifact", "bytes", "sha256"];
|
|
10
|
+
const EXPECTED_TARGET_ARTIFACTS = Object.freeze({
|
|
11
|
+
"darwin-arm64": "godd-tk-aarch64-apple-darwin",
|
|
12
|
+
"darwin-x64": "godd-tk-x86_64-apple-darwin",
|
|
13
|
+
"linux-x64": "godd-tk-x86_64-unknown-linux-musl",
|
|
14
|
+
"win32-x64": "godd-tk-x86_64-pc-windows-msvc.exe",
|
|
15
|
+
});
|
|
16
|
+
const SHA256_PATTERN = /^[0-9a-f]{64}$/;
|
|
17
|
+
export const GODD_TK_RELEASE_TAG_PATTERN = /^godd-tk-v\d+\.\d+\.\d+$/;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* godd-tk の取得物が pinned release と一致しなかったときの終了コード。
|
|
21
|
+
*
|
|
22
|
+
* `postinstall.js` は godd-tk を `required:false`(opt-in)として扱うため、
|
|
23
|
+
* 通常の失敗(プラットフォーム未対応 / `gh` 不在 / ダウンロード失敗)は
|
|
24
|
+
* exit 0 = 「取得しない」として install を続行させる。
|
|
25
|
+
* それと **区別** して「取得したが digest が一致しない」= 供給経路の改竄疑い
|
|
26
|
+
* だけをこの専用コードで表現し、`postinstall.js` 側で install 全体を
|
|
27
|
+
* fail-closed に倒す。sysexits.h の EX_DATAERR に合わせて 65 を使う。
|
|
28
|
+
*/
|
|
29
|
+
export const GODD_TK_INTEGRITY_EXIT_CODE = 65;
|
|
30
|
+
|
|
31
|
+
function isRecord(value) {
|
|
32
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function requireExactKeys(value, expected, label) {
|
|
36
|
+
if (!isRecord(value)) throw new Error(`${label} must be an object`);
|
|
37
|
+
const actual = Object.keys(value).sort();
|
|
38
|
+
const sortedExpected = [...expected].sort();
|
|
39
|
+
if (JSON.stringify(actual) !== JSON.stringify(sortedExpected)) {
|
|
40
|
+
throw new Error(
|
|
41
|
+
`${label} keys mismatch: expected=${sortedExpected.join(",")} actual=${actual.join(",")}`,
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function validateGoddTkManifest(value) {
|
|
47
|
+
requireExactKeys(value, MANIFEST_KEYS, "godd-tk manifest");
|
|
48
|
+
if (value.schemaVersion !== 1) {
|
|
49
|
+
throw new Error("godd-tk manifest schemaVersion must be 1");
|
|
50
|
+
}
|
|
51
|
+
if (
|
|
52
|
+
typeof value.releaseTag !== "string" ||
|
|
53
|
+
!GODD_TK_RELEASE_TAG_PATTERN.test(value.releaseTag)
|
|
54
|
+
) {
|
|
55
|
+
throw new Error("godd-tk manifest releaseTag is invalid");
|
|
56
|
+
}
|
|
57
|
+
requireExactKeys(
|
|
58
|
+
value.targets,
|
|
59
|
+
Object.keys(EXPECTED_TARGET_ARTIFACTS),
|
|
60
|
+
"godd-tk manifest targets",
|
|
61
|
+
);
|
|
62
|
+
const artifacts = new Set();
|
|
63
|
+
for (const [target, expectedArtifact] of Object.entries(
|
|
64
|
+
EXPECTED_TARGET_ARTIFACTS,
|
|
65
|
+
)) {
|
|
66
|
+
const entry = value.targets[target];
|
|
67
|
+
requireExactKeys(entry, TARGET_KEYS, `godd-tk manifest target ${target}`);
|
|
68
|
+
if (entry.artifact !== expectedArtifact) {
|
|
69
|
+
throw new Error(`godd-tk manifest artifact mismatch for ${target}`);
|
|
70
|
+
}
|
|
71
|
+
if (artifacts.has(entry.artifact)) {
|
|
72
|
+
throw new Error(`godd-tk manifest artifact is duplicated: ${entry.artifact}`);
|
|
73
|
+
}
|
|
74
|
+
artifacts.add(entry.artifact);
|
|
75
|
+
if (!Number.isSafeInteger(entry.bytes) || entry.bytes <= 0) {
|
|
76
|
+
throw new Error(`godd-tk manifest bytes are invalid for ${target}`);
|
|
77
|
+
}
|
|
78
|
+
if (typeof entry.sha256 !== "string" || !SHA256_PATTERN.test(entry.sha256)) {
|
|
79
|
+
throw new Error(`godd-tk manifest SHA-256 is invalid for ${target}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return value;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function deepFreeze(value) {
|
|
86
|
+
if (!isRecord(value)) return value;
|
|
87
|
+
for (const nested of Object.values(value)) deepFreeze(nested);
|
|
88
|
+
return Object.freeze(value);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* drift gate も postinstall も、この manifest を「期待値」として読む。
|
|
93
|
+
* 浅い freeze だと `manifest.targets[t].sha256` を実行時に書き換えられ、
|
|
94
|
+
* 期待値そのものが可変になってしまうため深く凍結する。
|
|
95
|
+
*/
|
|
96
|
+
export const GODD_TK_MANIFEST = deepFreeze(
|
|
97
|
+
validateGoddTkManifest(
|
|
98
|
+
JSON.parse(readFileSync(join(packageRoot, "godd-tk-assets.json"), "utf8")),
|
|
99
|
+
),
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* postinstall が取得するリリースタグ。
|
|
104
|
+
*
|
|
105
|
+
* 期待 digest と同じファイルに置くことが陳腐化対策の中核。
|
|
106
|
+
* タグだけを別の定数として持つと「タグは新しいのに digest は古い」状態が
|
|
107
|
+
* 作れてしまうため、**タグの単一の出どころはこの manifest だけ**にする。
|
|
108
|
+
* 新しい godd-tk release を配布対象にするには manifest 全体を差し替える必要が
|
|
109
|
+
* あり、tag と digest が構造的に分岐できない。
|
|
110
|
+
*/
|
|
111
|
+
export const GODD_TK_RELEASE_TAG = GODD_TK_MANIFEST.releaseTag;
|
|
112
|
+
|
|
113
|
+
export const GODD_TK_TARGETS = Object.freeze(
|
|
114
|
+
Object.fromEntries(
|
|
115
|
+
Object.entries(GODD_TK_MANIFEST.targets).map(([target, value]) => [
|
|
116
|
+
target,
|
|
117
|
+
Object.freeze({ ...value }),
|
|
118
|
+
]),
|
|
119
|
+
),
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
export function getGoddTkTarget(platform, arch, targets = GODD_TK_TARGETS) {
|
|
123
|
+
const key = `${platform}-${arch}`;
|
|
124
|
+
return Object.hasOwn(targets, key) ? targets[key] : null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function checksumBuffer(buffer) {
|
|
128
|
+
return createHash("sha256").update(buffer).digest("hex");
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* 取得済みファイルを manifest の bytes / sha256 と照合する。
|
|
133
|
+
* 不一致は必ず throw する(戻り値の真偽値では表現しない)。
|
|
134
|
+
*/
|
|
135
|
+
export function verifyGoddTkArtifact(
|
|
136
|
+
path,
|
|
137
|
+
target,
|
|
138
|
+
read = readFileSync,
|
|
139
|
+
stat = statSync,
|
|
140
|
+
) {
|
|
141
|
+
const size = stat(path).size;
|
|
142
|
+
if (size !== target.bytes) {
|
|
143
|
+
throw new Error(
|
|
144
|
+
`godd-tk size mismatch for ${target.artifact}: expected=${target.bytes} actual=${size}`,
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
const sha256 = checksumBuffer(read(path));
|
|
148
|
+
if (sha256 !== target.sha256) {
|
|
149
|
+
throw new Error(
|
|
150
|
+
`godd-tk SHA-256 mismatch for ${target.artifact}: expected=${target.sha256} actual=${sha256}`,
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
return { bytes: size, sha256 };
|
|
154
|
+
}
|
|
@@ -3,46 +3,44 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* GoDD Token Killer — postinstall script
|
|
5
5
|
*
|
|
6
|
-
* Downloads the pre-built godd-tk binary
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* Downloads the pinned pre-built godd-tk binary from GitHub Releases via the
|
|
7
|
+
* `gh` CLI (supports private repos), verifies it against the committed
|
|
8
|
+
* `godd-tk-assets.json` manifest (size + SHA-256), and only then places it in
|
|
9
|
+
* ~/.local/bin/ (Unix) or %LOCALAPPDATA%/godd-tk/ (Windows).
|
|
10
|
+
*
|
|
11
|
+
* 真正性の根拠は `gh` の TLS/認証**ではなく**、リポジトリにコミットされ
|
|
12
|
+
* レビューを通った manifest の digest である(Issue #1474)。
|
|
9
13
|
*/
|
|
10
14
|
|
|
11
|
-
import {
|
|
12
|
-
import { join } from "node:path";
|
|
15
|
+
import { chmodSync, existsSync, mkdirSync, mkdtempSync, renameSync, rmSync, unlinkSync } from "node:fs";
|
|
16
|
+
import { join, resolve } from "node:path";
|
|
13
17
|
import { homedir, platform, arch } from "node:os";
|
|
14
|
-
import { execFileSync
|
|
18
|
+
import { execFileSync } from "node:child_process";
|
|
19
|
+
import { fileURLToPath } from "node:url";
|
|
20
|
+
import {
|
|
21
|
+
GODD_TK_INTEGRITY_EXIT_CODE,
|
|
22
|
+
GODD_TK_RELEASE_TAG,
|
|
23
|
+
getGoddTkTarget,
|
|
24
|
+
verifyGoddTkArtifact,
|
|
25
|
+
} from "./godd-tk-assets.js";
|
|
15
26
|
|
|
16
27
|
const REPO = "AutoDevJapan/GoDD-MCP-Alpha";
|
|
17
28
|
const BINARY_NAME = "godd-tk";
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
"darwin-arm64": "godd-tk-aarch64-apple-darwin",
|
|
23
|
-
"linux-x64": "godd-tk-x86_64-unknown-linux-musl",
|
|
24
|
-
"win32-x64": "godd-tk-x86_64-pc-windows-msvc.exe",
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
function getInstallDir() {
|
|
28
|
-
if (platform() === "win32") {
|
|
29
|
-
const localAppData = process.env.LOCALAPPDATA || join(homedir(), "AppData", "Local");
|
|
29
|
+
|
|
30
|
+
export function getInstallDir(currentPlatform = platform(), environment = process.env) {
|
|
31
|
+
if (currentPlatform === "win32") {
|
|
32
|
+
const localAppData = environment.LOCALAPPDATA || join(homedir(), "AppData", "Local");
|
|
30
33
|
return join(localAppData, "godd-tk");
|
|
31
34
|
}
|
|
32
35
|
return join(homedir(), ".local", "bin");
|
|
33
36
|
}
|
|
34
37
|
|
|
35
|
-
function
|
|
36
|
-
const
|
|
37
|
-
return TARGETS[key];
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function getBinaryPath(installDir) {
|
|
41
|
-
const ext = platform() === "win32" ? ".exe" : "";
|
|
38
|
+
export function getBinaryPath(installDir, currentPlatform = platform()) {
|
|
39
|
+
const ext = currentPlatform === "win32" ? ".exe" : "";
|
|
42
40
|
return join(installDir, `${BINARY_NAME}${ext}`);
|
|
43
41
|
}
|
|
44
42
|
|
|
45
|
-
function
|
|
43
|
+
function defaultIsGhAvailable() {
|
|
46
44
|
try {
|
|
47
45
|
execFileSync("gh", ["--version"], { stdio: "ignore", timeout: 5000 });
|
|
48
46
|
return true;
|
|
@@ -51,76 +49,231 @@ function isGhAvailable() {
|
|
|
51
49
|
}
|
|
52
50
|
}
|
|
53
51
|
|
|
54
|
-
|
|
55
|
-
|
|
52
|
+
function defaultDownload(tag, artifact, directory) {
|
|
53
|
+
execFileSync(
|
|
54
|
+
"gh",
|
|
55
|
+
[
|
|
56
|
+
"release",
|
|
57
|
+
"download",
|
|
58
|
+
tag,
|
|
59
|
+
"--repo",
|
|
60
|
+
REPO,
|
|
61
|
+
"--pattern",
|
|
62
|
+
artifact,
|
|
63
|
+
"--dir",
|
|
64
|
+
directory,
|
|
65
|
+
"--clobber",
|
|
66
|
+
],
|
|
67
|
+
{ stdio: "pipe", timeout: 60000 },
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* 返す `status` は 3 系統を厳密に区別する:
|
|
73
|
+
* - "installed" / "already-installed" … 検証済みバイナリが所定の場所にある
|
|
74
|
+
* - "skipped" … そもそも取得しない(exit 0、install 継続)。
|
|
75
|
+
* 既存の未検証バイナリが居た場合は必ず隔離済み
|
|
76
|
+
* - "integrity-failure" … 検証を通らないバイナリを実行経路から
|
|
77
|
+
* 外せなかった(exit 65 で fail-closed)
|
|
78
|
+
*/
|
|
79
|
+
export function installGoddTk({
|
|
80
|
+
currentPlatform = platform(),
|
|
81
|
+
currentArch = arch(),
|
|
82
|
+
installDir = getInstallDir(currentPlatform),
|
|
83
|
+
isGhAvailable = defaultIsGhAvailable,
|
|
84
|
+
download = defaultDownload,
|
|
85
|
+
verify = verifyGoddTkArtifact,
|
|
86
|
+
exists = existsSync,
|
|
87
|
+
makeDir = (path) => mkdirSync(path, { recursive: true }),
|
|
88
|
+
// staging は必ず install 先と同じファイルシステム上に作る。
|
|
89
|
+
// os.tmpdir() を使うと /tmp が tmpfs / $HOME が別デバイスの環境で
|
|
90
|
+
// rename が常に EXDEV になり、検証機能そのものが無言で死ぬ。
|
|
91
|
+
makeTempDir = (parent) => mkdtempSync(join(parent, ".godd-tk-")),
|
|
92
|
+
removeTree = (path) => rmSync(path, { recursive: true, force: true }),
|
|
93
|
+
move = renameSync,
|
|
94
|
+
removeFile = unlinkSync,
|
|
95
|
+
chmod = chmodSync,
|
|
96
|
+
now = () => Date.now(),
|
|
97
|
+
log = console.log,
|
|
98
|
+
logError = console.error,
|
|
99
|
+
} = {}) {
|
|
100
|
+
const target = getGoddTkTarget(currentPlatform, currentArch);
|
|
56
101
|
if (!target) {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
return;
|
|
102
|
+
log(`[godd-tk] No pre-built binary for ${currentPlatform}-${currentArch}, skipping.`);
|
|
103
|
+
log(`[godd-tk] Build from source: cd godd-mcp-alpha/godd-tk && cargo build --release`);
|
|
104
|
+
return { status: "skipped", reason: "unsupported-platform" };
|
|
60
105
|
}
|
|
61
106
|
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
if (existsSync(binaryPath)) {
|
|
107
|
+
const binaryPath = getBinaryPath(installDir, currentPlatform);
|
|
108
|
+
let unverifiedExisting = false;
|
|
109
|
+
if (exists(binaryPath)) {
|
|
66
110
|
try {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
return;
|
|
70
|
-
} catch {
|
|
71
|
-
//
|
|
111
|
+
verify(binaryPath, target);
|
|
112
|
+
log(`[godd-tk] Already installed and verified: ${binaryPath} (${GODD_TK_RELEASE_TAG})`);
|
|
113
|
+
return { status: "already-installed", binaryPath };
|
|
114
|
+
} catch (error) {
|
|
115
|
+
// 既存バイナリが pinned release と違う(自前ビルド・旧バージョン・改竄)。
|
|
116
|
+
// 「検証済み」と偽らず、pinned release での置き換えを試みる。
|
|
117
|
+
unverifiedExisting = true;
|
|
118
|
+
logError(
|
|
119
|
+
`[godd-tk] Existing binary does not match the pinned release ${GODD_TK_RELEASE_TAG}: ` +
|
|
120
|
+
`${error instanceof Error ? error.message : error}`,
|
|
121
|
+
);
|
|
122
|
+
log(`[godd-tk] Reinstalling the pinned, digest-verified release.`);
|
|
72
123
|
}
|
|
73
124
|
}
|
|
74
125
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
126
|
+
/**
|
|
127
|
+
* 「取得できなかった」経路の共通終端。
|
|
128
|
+
*
|
|
129
|
+
* 既存の未検証バイナリを検知していた場合、ここで黙って exit 0 に戻ると
|
|
130
|
+
* 未検証バイナリが実行経路に残り続ける(= 無言スキップ)。必ず隔離し、
|
|
131
|
+
* 隔離できないときだけ fail-closed に倒す。
|
|
132
|
+
*/
|
|
133
|
+
function finishWithoutInstall(reason) {
|
|
134
|
+
if (!unverifiedExisting || !exists(binaryPath)) {
|
|
135
|
+
return { status: "skipped", reason };
|
|
136
|
+
}
|
|
137
|
+
// rename は宛先を黙って上書きする。クロックが巻き戻る環境(NTP バックステップ /
|
|
138
|
+
// スナップショット復元 / 固定クロックのコンテナ)で先行隔離物を消すと、
|
|
139
|
+
// 隔離の目的である証跡保全そのものが失われるため必ず空きパスを探す。
|
|
140
|
+
const stamp = now();
|
|
141
|
+
let quarantinePath = `${binaryPath}.unverified-${stamp}`;
|
|
142
|
+
for (let suffix = 1; exists(quarantinePath); suffix += 1) {
|
|
143
|
+
if (suffix > 1000) {
|
|
144
|
+
logError(
|
|
145
|
+
`[godd-tk] SECURITY: could not find a free quarantine path for ${binaryPath}.`,
|
|
146
|
+
);
|
|
147
|
+
return { status: "integrity-failure", reason: "unverified-existing-binary" };
|
|
148
|
+
}
|
|
149
|
+
quarantinePath = `${binaryPath}.unverified-${stamp}-${suffix}`;
|
|
150
|
+
}
|
|
151
|
+
try {
|
|
152
|
+
move(binaryPath, quarantinePath);
|
|
153
|
+
} catch (error) {
|
|
154
|
+
logError(
|
|
155
|
+
`[godd-tk] SECURITY: could not quarantine the unverified binary at ${binaryPath} — ` +
|
|
156
|
+
`${error instanceof Error ? error.message : error}`,
|
|
157
|
+
);
|
|
158
|
+
return { status: "integrity-failure", reason: "unverified-existing-binary" };
|
|
159
|
+
}
|
|
160
|
+
logError(
|
|
161
|
+
`[godd-tk] Quarantined the unverified binary to ${quarantinePath}; ` +
|
|
162
|
+
`it is no longer on the install path. Delete it once you have confirmed its origin.`,
|
|
163
|
+
);
|
|
164
|
+
return { status: "skipped", reason };
|
|
80
165
|
}
|
|
81
166
|
|
|
82
|
-
|
|
83
|
-
|
|
167
|
+
// 想定外の例外が `unverifiedExisting === true` のまま escape すると、
|
|
168
|
+
// 未検証バイナリを隔離しないまま install が続いてしまう(M2 と同じクラスの穴)。
|
|
169
|
+
// 取得〜配置の全体を包み、必ず finishWithoutInstall を通す。
|
|
170
|
+
// `integrity-failure` は throw ではなく return なので、この catch で降格することはない。
|
|
84
171
|
try {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
execSync(
|
|
91
|
-
`gh release download ${VERSION_TAG} --repo ${REPO} --pattern "${target}" --dir "${tmpDir}" --clobber`,
|
|
92
|
-
{ stdio: "pipe", timeout: 60000 }
|
|
172
|
+
return runDownloadAndInstall();
|
|
173
|
+
} catch (error) {
|
|
174
|
+
logError(
|
|
175
|
+
`[godd-tk] Unexpected installer error: ${error instanceof Error ? error.message : error}`,
|
|
93
176
|
);
|
|
177
|
+
return finishWithoutInstall("unexpected-error");
|
|
178
|
+
}
|
|
94
179
|
|
|
95
|
-
|
|
96
|
-
if (!
|
|
97
|
-
|
|
180
|
+
function runDownloadAndInstall() {
|
|
181
|
+
if (!isGhAvailable()) {
|
|
182
|
+
log(`[godd-tk] GitHub CLI (gh) not found — required for private repo download.`);
|
|
183
|
+
log(`[godd-tk] Install gh: https://cli.github.com/ then run this script again.`);
|
|
184
|
+
log(`[godd-tk] Or build from source: cd godd-mcp-alpha/godd-tk && cargo build --release`);
|
|
185
|
+
return finishWithoutInstall("gh-unavailable");
|
|
98
186
|
}
|
|
99
187
|
|
|
100
|
-
|
|
101
|
-
renameSync(downloadedPath, binaryPath);
|
|
188
|
+
log(`[godd-tk] Downloading ${target.artifact} (${GODD_TK_RELEASE_TAG}) via gh CLI...`);
|
|
102
189
|
|
|
103
|
-
|
|
104
|
-
|
|
190
|
+
let tmpDir;
|
|
191
|
+
try {
|
|
192
|
+
makeDir(installDir);
|
|
193
|
+
tmpDir = makeTempDir(installDir);
|
|
194
|
+
} catch (error) {
|
|
195
|
+
log(
|
|
196
|
+
`[godd-tk] Could not prepare a staging directory under ${installDir}: ` +
|
|
197
|
+
`${error instanceof Error ? error.message : error}`,
|
|
198
|
+
);
|
|
199
|
+
return finishWithoutInstall("staging-unavailable");
|
|
105
200
|
}
|
|
106
201
|
|
|
107
|
-
|
|
108
|
-
try {
|
|
202
|
+
const downloadedPath = join(tmpDir, target.artifact);
|
|
203
|
+
try {
|
|
204
|
+
try {
|
|
205
|
+
download(GODD_TK_RELEASE_TAG, target.artifact, tmpDir);
|
|
206
|
+
} catch (error) {
|
|
207
|
+
log(`[godd-tk] Download failed: ${error instanceof Error ? error.message : error}`);
|
|
208
|
+
log(`[godd-tk] Build from source: cd godd-mcp-alpha/godd-tk && cargo build --release`);
|
|
209
|
+
return finishWithoutInstall("download-failed");
|
|
210
|
+
}
|
|
211
|
+
if (!exists(downloadedPath)) {
|
|
212
|
+
log(`[godd-tk] Downloaded file not found at ${downloadedPath}`);
|
|
213
|
+
log(`[godd-tk] Build from source: cd godd-mcp-alpha/godd-tk && cargo build --release`);
|
|
214
|
+
return finishWithoutInstall("download-failed");
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ここから先は「取得できた」ケース。digest 不一致は skip ではなく失敗。
|
|
218
|
+
try {
|
|
219
|
+
verify(downloadedPath, target);
|
|
220
|
+
} catch (error) {
|
|
221
|
+
logError(
|
|
222
|
+
`[godd-tk] SECURITY: downloaded artifact failed integrity verification — ` +
|
|
223
|
+
`${error instanceof Error ? error.message : error}`,
|
|
224
|
+
);
|
|
225
|
+
logError(
|
|
226
|
+
`[godd-tk] The artifact published at ${GODD_TK_RELEASE_TAG} does not match the digest ` +
|
|
227
|
+
`committed in godd-mcp-alpha/godd-tk-assets.json. Installation is aborted and nothing was installed.`,
|
|
228
|
+
);
|
|
229
|
+
return { status: "integrity-failure", reason: "digest-mismatch" };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
try {
|
|
233
|
+
if (exists(binaryPath)) removeFile(binaryPath);
|
|
234
|
+
move(downloadedPath, binaryPath);
|
|
235
|
+
} catch (error) {
|
|
236
|
+
log(`[godd-tk] Installation failed: ${error instanceof Error ? error.message : error}`);
|
|
237
|
+
log(`[godd-tk] Build from source: cd godd-mcp-alpha/godd-tk && cargo build --release`);
|
|
238
|
+
return finishWithoutInstall("install-failed");
|
|
239
|
+
}
|
|
240
|
+
// ここまで来た時点で、置かれているのは検証済み bytes だけ。
|
|
241
|
+
unverifiedExisting = false;
|
|
109
242
|
|
|
110
|
-
|
|
111
|
-
|
|
243
|
+
if (currentPlatform !== "win32") {
|
|
244
|
+
try {
|
|
245
|
+
chmod(binaryPath, 0o755);
|
|
246
|
+
} catch (error) {
|
|
247
|
+
logError(
|
|
248
|
+
`[godd-tk] Installed verified bytes but could not set the executable bit on ${binaryPath}: ` +
|
|
249
|
+
`${error instanceof Error ? error.message : error}`,
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
112
253
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
254
|
+
log(`[godd-tk] Installed and verified (sha256 ${target.sha256}): ${binaryPath}`);
|
|
255
|
+
log(`[godd-tk] Run 'godd-tk init --agent cursor' to set up hooks`);
|
|
256
|
+
if (currentPlatform === "win32") {
|
|
257
|
+
const pathDirs = (process.env.PATH || "").toLowerCase().split(";");
|
|
258
|
+
if (!pathDirs.includes(installDir.toLowerCase())) {
|
|
259
|
+
log(`[godd-tk] Add ${installDir} to your PATH for global access`);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return { status: "installed", binaryPath };
|
|
263
|
+
} finally {
|
|
264
|
+
try {
|
|
265
|
+
removeTree(tmpDir);
|
|
266
|
+
} catch {
|
|
267
|
+
// best effort
|
|
118
268
|
}
|
|
119
269
|
}
|
|
120
|
-
} catch (err) {
|
|
121
|
-
console.log(`[godd-tk] Download failed: ${err.message}`);
|
|
122
|
-
console.log(`[godd-tk] Build from source: cd godd-mcp-alpha/godd-tk && cargo build --release`);
|
|
123
270
|
}
|
|
124
271
|
}
|
|
125
272
|
|
|
126
|
-
|
|
273
|
+
export function toExitCode(result) {
|
|
274
|
+
return result.status === "integrity-failure" ? GODD_TK_INTEGRITY_EXIT_CODE : 0;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
278
|
+
process.exitCode = toExitCode(installGoddTk());
|
|
279
|
+
}
|
package/scripts/postinstall.js
CHANGED
|
@@ -5,6 +5,11 @@ import { dirname, join, resolve } from "node:path";
|
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
|
|
7
7
|
const scriptsDirectory = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
|
|
9
|
+
// `godd-tk-assets.js` は import 時に manifest を読んで検証する I/O モジュールなので、
|
|
10
|
+
// `required:true` の godd-native 経路まで巻き込まないようここでは値だけを持つ。
|
|
11
|
+
// `GODD_TK_INTEGRITY_EXIT_CODE` との同値は tests/godd-tk-asset-integrity.test.ts が固定する。
|
|
12
|
+
const GODD_TK_INTEGRITY_EXIT_CODE = 65;
|
|
8
13
|
export const INSTALLERS = [
|
|
9
14
|
{ name: "godd-native", script: "postinstall-godd-native.js", required: true },
|
|
10
15
|
{
|
|
@@ -12,12 +17,21 @@ export const INSTALLERS = [
|
|
|
12
17
|
script: "postinstall-godd-tk.js",
|
|
13
18
|
required: false,
|
|
14
19
|
optInEnvironment: "GODD_INSTALL_GODD_TK",
|
|
20
|
+
// opt-in の任意コンポーネントでも、供給経路の完全性検証に失敗した場合だけは
|
|
21
|
+
// install 全体を失敗させる(無言スキップ禁止 / Issue #1474)。
|
|
22
|
+
integrityExitCode: GODD_TK_INTEGRITY_EXIT_CODE,
|
|
15
23
|
},
|
|
16
24
|
];
|
|
17
25
|
|
|
18
26
|
export function runInstallers(execute = spawnSync) {
|
|
19
27
|
let failed = false;
|
|
20
|
-
for (const {
|
|
28
|
+
for (const {
|
|
29
|
+
name,
|
|
30
|
+
script,
|
|
31
|
+
required,
|
|
32
|
+
optInEnvironment,
|
|
33
|
+
integrityExitCode,
|
|
34
|
+
} of INSTALLERS) {
|
|
21
35
|
if (
|
|
22
36
|
optInEnvironment &&
|
|
23
37
|
process.env[optInEnvironment] !== "1"
|
|
@@ -34,10 +48,25 @@ export function runInstallers(execute = spawnSync) {
|
|
|
34
48
|
if (result.error) {
|
|
35
49
|
console.error(`[${name}] Installer could not be executed: ${result.error.message}`);
|
|
36
50
|
if (required) failed = true;
|
|
51
|
+
} else if (result.status === null) {
|
|
52
|
+
// spawnSync は timeout / シグナル kill で status:null, signal:"SIGTERM" を返す。
|
|
53
|
+
// 「非ゼロ終了」と一緒くたにせず、何も検証できていないことを明示する。
|
|
54
|
+
console.error(
|
|
55
|
+
`[${name}] Installer was terminated by signal ${result.signal ?? "unknown"}; nothing was verified.`,
|
|
56
|
+
);
|
|
57
|
+
if (required) failed = true;
|
|
37
58
|
} else if (result.status !== 0) {
|
|
38
59
|
if (required) {
|
|
39
60
|
console.error(`[${name}] Required installer exited with status ${result.status}; installation failed.`);
|
|
40
61
|
failed = true;
|
|
62
|
+
} else if (
|
|
63
|
+
integrityExitCode !== undefined &&
|
|
64
|
+
result.status === integrityExitCode
|
|
65
|
+
) {
|
|
66
|
+
console.error(
|
|
67
|
+
`[${name}] Artifact integrity verification failed (status ${result.status}); installation failed.`,
|
|
68
|
+
);
|
|
69
|
+
failed = true;
|
|
41
70
|
} else {
|
|
42
71
|
console.error(`[${name}] Optional installer exited with status ${result.status}; MCP installation continues without this optional component.`);
|
|
43
72
|
}
|