@autodevjapan/godd-mcp-alpha 2.9.3 → 2.10.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 +49 -3
- package/assets/native/godd-native-aarch64-apple-darwin +0 -0
- package/assets/native/godd-native-aarch64-apple-darwin.sha256 +1 -1
- package/assets/native/godd-native-x86_64-apple-darwin +0 -0
- package/assets/native/godd-native-x86_64-apple-darwin.sha256 +1 -1
- package/assets/native/godd-native-x86_64-pc-windows-msvc.exe +0 -0
- package/assets/native/godd-native-x86_64-pc-windows-msvc.exe.sha256 +1 -1
- package/assets/native/godd-native-x86_64-unknown-linux-musl +0 -0
- package/assets/native/godd-native-x86_64-unknown-linux-musl.sha256 +1 -1
- package/dist/godd.cjs +212 -206
- package/godd-tk-assets.json +26 -0
- package/native-assets.json +12 -12
- package/package.json +4 -1
- package/scripts/atomic-binary-install.js +679 -0
- package/scripts/godd-tk-assets.js +154 -0
- package/scripts/native-assets.js +3 -2
- package/scripts/postinstall-godd-tk.js +297 -77
- package/scripts/postinstall.js +30 -1
|
@@ -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
|
+
}
|
package/scripts/native-assets.js
CHANGED
|
@@ -173,12 +173,13 @@ export const EXPECTED_NATIVE_ASSET_FILES = Object.freeze(
|
|
|
173
173
|
// 気づけない)。到達前に検知するため、警告用の閾値と ratchet 上限を定義する。
|
|
174
174
|
export const NATIVE_ASSET_HEADROOM_WARNING_BYTES = 2_000_000;
|
|
175
175
|
|
|
176
|
-
// `godd-native-v0.2.
|
|
176
|
+
// `godd-native-v0.2.18` で観測した baseline(残 headroom 16,616,644 bytes)。
|
|
177
|
+
// v0.2.18 releaseの4 binaryと4 checksum合計を固定し、absolute上限の33.23%を残す。
|
|
177
178
|
// これを超える増加は「サイズ削減」か「上限の再設計」の判断を必ず挟ませるため、
|
|
178
179
|
// 単調増加を止める ratchet として固定する(緩めるときは spec の根拠更新とセット)。
|
|
179
180
|
// native release ごとに新しい `baselineBytes` まで引き下げること(spec の
|
|
180
181
|
// 「native release 後のチェックリスト」参照)。据え置くと ratchet が機能しなくなる。
|
|
181
|
-
export const NATIVE_ASSET_BASELINE_CEILING_BYTES =
|
|
182
|
+
export const NATIVE_ASSET_BASELINE_CEILING_BYTES = 33_383_356;
|
|
182
183
|
|
|
183
184
|
/**
|
|
184
185
|
* exact 8 files の合計 bytes と `maximumAssetBytes` の余裕を判定する。
|
|
@@ -3,46 +3,54 @@
|
|
|
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, renameSync } from "node:fs";
|
|
16
|
+
import { basename, 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";
|
|
26
|
+
import {
|
|
27
|
+
AtomicInstallCleanupError,
|
|
28
|
+
AtomicInstallIntegrityError,
|
|
29
|
+
AtomicInstallRollbackError,
|
|
30
|
+
beginAtomicBinaryInstall,
|
|
31
|
+
finishAtomicBinaryInstall,
|
|
32
|
+
publishAtomicBinary,
|
|
33
|
+
recoverAtomicBinaryInstall,
|
|
34
|
+
recoverInterruptedAtomicFinish,
|
|
35
|
+
} from "./atomic-binary-install.js";
|
|
15
36
|
|
|
16
37
|
const REPO = "AutoDevJapan/GoDD-MCP-Alpha";
|
|
17
38
|
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");
|
|
39
|
+
|
|
40
|
+
export function getInstallDir(currentPlatform = platform(), environment = process.env) {
|
|
41
|
+
if (currentPlatform === "win32") {
|
|
42
|
+
const localAppData = environment.LOCALAPPDATA || join(homedir(), "AppData", "Local");
|
|
30
43
|
return join(localAppData, "godd-tk");
|
|
31
44
|
}
|
|
32
45
|
return join(homedir(), ".local", "bin");
|
|
33
46
|
}
|
|
34
47
|
|
|
35
|
-
function
|
|
36
|
-
const
|
|
37
|
-
return TARGETS[key];
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function getBinaryPath(installDir) {
|
|
41
|
-
const ext = platform() === "win32" ? ".exe" : "";
|
|
48
|
+
export function getBinaryPath(installDir, currentPlatform = platform()) {
|
|
49
|
+
const ext = currentPlatform === "win32" ? ".exe" : "";
|
|
42
50
|
return join(installDir, `${BINARY_NAME}${ext}`);
|
|
43
51
|
}
|
|
44
52
|
|
|
45
|
-
function
|
|
53
|
+
function defaultIsGhAvailable() {
|
|
46
54
|
try {
|
|
47
55
|
execFileSync("gh", ["--version"], { stdio: "ignore", timeout: 5000 });
|
|
48
56
|
return true;
|
|
@@ -51,76 +59,288 @@ function isGhAvailable() {
|
|
|
51
59
|
}
|
|
52
60
|
}
|
|
53
61
|
|
|
54
|
-
|
|
55
|
-
|
|
62
|
+
function defaultDownload(tag, artifact, directory) {
|
|
63
|
+
execFileSync(
|
|
64
|
+
"gh",
|
|
65
|
+
[
|
|
66
|
+
"release",
|
|
67
|
+
"download",
|
|
68
|
+
tag,
|
|
69
|
+
"--repo",
|
|
70
|
+
REPO,
|
|
71
|
+
"--pattern",
|
|
72
|
+
artifact,
|
|
73
|
+
"--dir",
|
|
74
|
+
directory,
|
|
75
|
+
"--clobber",
|
|
76
|
+
],
|
|
77
|
+
{ stdio: "pipe", timeout: 60000 },
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* 返す `status` は 3 系統を厳密に区別する:
|
|
83
|
+
* - "installed" / "already-installed" … 検証済みバイナリが所定の場所にある
|
|
84
|
+
* - "skipped" … そもそも取得しない(exit 0、install 継続)。
|
|
85
|
+
* 既存の未検証バイナリが居た場合は必ず隔離済み
|
|
86
|
+
* - "integrity-failure" … integrity / atomic recovery 境界の失敗。
|
|
87
|
+
* rollback成功時もexit 65でfail-closed
|
|
88
|
+
*/
|
|
89
|
+
export function installGoddTk({
|
|
90
|
+
currentPlatform = platform(),
|
|
91
|
+
currentArch = arch(),
|
|
92
|
+
installDir = getInstallDir(currentPlatform),
|
|
93
|
+
isGhAvailable = defaultIsGhAvailable,
|
|
94
|
+
download = defaultDownload,
|
|
95
|
+
verify = verifyGoddTkArtifact,
|
|
96
|
+
exists = existsSync,
|
|
97
|
+
makeDir = (path) => mkdirSync(path, { recursive: true }),
|
|
98
|
+
makeTempDir,
|
|
99
|
+
removeTree,
|
|
100
|
+
publishOperations,
|
|
101
|
+
isProcessAlive,
|
|
102
|
+
recoverInterruptedFinish =
|
|
103
|
+
process.env.GODD_RECOVER_GODD_TK_INSTALL === "1",
|
|
104
|
+
move = renameSync,
|
|
105
|
+
chmod = chmodSync,
|
|
106
|
+
now = () => Date.now(),
|
|
107
|
+
log = console.log,
|
|
108
|
+
logError = console.error,
|
|
109
|
+
} = {}) {
|
|
110
|
+
const target = getGoddTkTarget(currentPlatform, currentArch);
|
|
56
111
|
if (!target) {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
return;
|
|
112
|
+
log(`[godd-tk] No pre-built binary for ${currentPlatform}-${currentArch}, skipping.`);
|
|
113
|
+
log(`[godd-tk] Build from source: cd godd-mcp-alpha/godd-tk && cargo build --release`);
|
|
114
|
+
return { status: "skipped", reason: "unsupported-platform" };
|
|
60
115
|
}
|
|
61
116
|
|
|
62
|
-
const
|
|
63
|
-
|
|
117
|
+
const binaryPath = getBinaryPath(installDir, currentPlatform);
|
|
118
|
+
let transaction;
|
|
119
|
+
try {
|
|
120
|
+
makeDir(installDir);
|
|
121
|
+
if (recoverInterruptedFinish) {
|
|
122
|
+
const recovery = recoverInterruptedAtomicFinish({
|
|
123
|
+
binaryPath,
|
|
124
|
+
...(isProcessAlive ? { isProcessAlive } : {}),
|
|
125
|
+
});
|
|
126
|
+
if (recovery.status === "blocked") {
|
|
127
|
+
logError(
|
|
128
|
+
`[godd-tk] SECURITY: explicit install recovery was rejected (${recovery.reason}).`,
|
|
129
|
+
);
|
|
130
|
+
return { status: "integrity-failure", reason: recovery.reason };
|
|
131
|
+
}
|
|
132
|
+
if (recovery.status === "recovered") {
|
|
133
|
+
log(`[godd-tk] Recovered an interrupted install owner release.`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
transaction = beginAtomicBinaryInstall({
|
|
137
|
+
installDir,
|
|
138
|
+
binaryPath,
|
|
139
|
+
...(makeTempDir ? { makeTempDir: (prefix) => makeTempDir(prefix) } : {}),
|
|
140
|
+
...(isProcessAlive ? { isProcessAlive } : {}),
|
|
141
|
+
});
|
|
142
|
+
} catch {
|
|
143
|
+
logError(
|
|
144
|
+
`[godd-tk] SECURITY: could not create the atomic install transaction.`,
|
|
145
|
+
);
|
|
146
|
+
return { status: "integrity-failure", reason: "transaction-unavailable" };
|
|
147
|
+
}
|
|
148
|
+
if (transaction.status === "busy") {
|
|
149
|
+
log(`[godd-tk] Another verified install transaction is active; skipping this attempt.`);
|
|
150
|
+
return { status: "skipped", reason: "install-busy" };
|
|
151
|
+
}
|
|
152
|
+
if (transaction.status === "blocked") {
|
|
153
|
+
logError(`[godd-tk] SECURITY: install lock could not be validated (${transaction.reason}).`);
|
|
154
|
+
return { status: "integrity-failure", reason: transaction.reason };
|
|
155
|
+
}
|
|
64
156
|
|
|
65
|
-
|
|
157
|
+
let result;
|
|
158
|
+
let retainTransaction = false;
|
|
159
|
+
try {
|
|
160
|
+
recoverAtomicBinaryInstall(transaction, (path) => verify(path, target));
|
|
161
|
+
} catch {
|
|
162
|
+
logError(`[godd-tk] SECURITY: interrupted install recovery failed.`);
|
|
163
|
+
result = { status: "integrity-failure", reason: "recovery-failed" };
|
|
164
|
+
}
|
|
165
|
+
if (!result) {
|
|
66
166
|
try {
|
|
67
|
-
|
|
68
|
-
console.log(`[godd-tk] Already installed: ${version}`);
|
|
69
|
-
return;
|
|
167
|
+
result = runLockedInstall();
|
|
70
168
|
} catch {
|
|
71
|
-
|
|
169
|
+
logError(`[godd-tk] Unexpected installer error.`);
|
|
170
|
+
result = { status: "integrity-failure", reason: "unexpected-error" };
|
|
72
171
|
}
|
|
73
172
|
}
|
|
74
173
|
|
|
75
|
-
if (
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
174
|
+
if (retainTransaction) return result;
|
|
175
|
+
const cleanup = finishAtomicBinaryInstall(
|
|
176
|
+
transaction,
|
|
177
|
+
removeTree ? { removeTree } : undefined,
|
|
178
|
+
);
|
|
179
|
+
if (!cleanup.ok) {
|
|
180
|
+
logError(`[godd-tk] SECURITY: install transaction cleanup failed.`);
|
|
181
|
+
return { status: "integrity-failure", reason: cleanup.reason };
|
|
80
182
|
}
|
|
183
|
+
return result;
|
|
81
184
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
185
|
+
function runLockedInstall() {
|
|
186
|
+
let unverifiedExisting = false;
|
|
187
|
+
if (exists(binaryPath)) {
|
|
188
|
+
try {
|
|
189
|
+
verify(binaryPath, target);
|
|
190
|
+
log(`[godd-tk] Already installed and verified (${GODD_TK_RELEASE_TAG}).`);
|
|
191
|
+
return { status: "already-installed", binaryPath };
|
|
192
|
+
} catch {
|
|
193
|
+
// 既存バイナリが pinned release と違う(自前ビルド・旧バージョン・改竄)。
|
|
194
|
+
// 「検証済み」と偽らず、pinned release での置き換えを試みる。
|
|
195
|
+
unverifiedExisting = true;
|
|
196
|
+
logError(
|
|
197
|
+
`[godd-tk] Existing binary does not match the pinned release ${GODD_TK_RELEASE_TAG}.`,
|
|
198
|
+
);
|
|
199
|
+
log(`[godd-tk] Reinstalling the pinned, digest-verified release.`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
89
202
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
203
|
+
/**
|
|
204
|
+
* 「取得できなかった」経路の共通終端。
|
|
205
|
+
*
|
|
206
|
+
* 既存の未検証バイナリを検知していた場合、ここで黙って exit 0 に戻ると
|
|
207
|
+
* 未検証バイナリが実行経路に残り続ける(= 無言スキップ)。必ず隔離し、
|
|
208
|
+
* 隔離できないときだけ fail-closed に倒す。
|
|
209
|
+
*/
|
|
210
|
+
function finishWithoutInstall(reason) {
|
|
211
|
+
if (!unverifiedExisting || !exists(binaryPath)) {
|
|
212
|
+
return { status: "skipped", reason };
|
|
213
|
+
}
|
|
214
|
+
// rename は宛先を黙って上書きする。クロックが巻き戻る環境(NTP バックステップ /
|
|
215
|
+
// スナップショット復元 / 固定クロックのコンテナ)で先行隔離物を消すと、
|
|
216
|
+
// 隔離の目的である証跡保全そのものが失われるため必ず空きパスを探す。
|
|
217
|
+
const stamp = now();
|
|
218
|
+
let quarantinePath = `${binaryPath}.unverified-${stamp}`;
|
|
219
|
+
for (let suffix = 1; exists(quarantinePath); suffix += 1) {
|
|
220
|
+
if (suffix > 1000) {
|
|
221
|
+
logError(
|
|
222
|
+
`[godd-tk] SECURITY: quarantine namespace is exhausted.`,
|
|
223
|
+
);
|
|
224
|
+
return { status: "integrity-failure", reason: "unverified-existing-binary" };
|
|
225
|
+
}
|
|
226
|
+
quarantinePath = `${binaryPath}.unverified-${stamp}-${suffix}`;
|
|
227
|
+
}
|
|
228
|
+
try {
|
|
229
|
+
move(binaryPath, quarantinePath);
|
|
230
|
+
} catch {
|
|
231
|
+
logError(`[godd-tk] SECURITY: could not quarantine the unverified binary.`);
|
|
232
|
+
return { status: "integrity-failure", reason: "unverified-existing-binary" };
|
|
233
|
+
}
|
|
234
|
+
logError(
|
|
235
|
+
`[godd-tk] Quarantined the unverified binary as ${basename(quarantinePath)}; ` +
|
|
236
|
+
`it is no longer on the install path. Delete it once you have confirmed its origin.`,
|
|
93
237
|
);
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
238
|
+
return { status: "skipped", reason };
|
|
239
|
+
}
|
|
240
|
+
let ghAvailable;
|
|
241
|
+
try {
|
|
242
|
+
ghAvailable = isGhAvailable();
|
|
243
|
+
} catch {
|
|
244
|
+
logError(
|
|
245
|
+
`[godd-tk] Unexpected installer error.`,
|
|
246
|
+
);
|
|
247
|
+
return finishWithoutInstall("unexpected-error");
|
|
98
248
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
chmodSync(binaryPath, 0o755);
|
|
249
|
+
if (!ghAvailable) {
|
|
250
|
+
log(`[godd-tk] GitHub CLI (gh) not found — required for private repo download.`);
|
|
251
|
+
log(`[godd-tk] Install gh: https://cli.github.com/ then run this script again.`);
|
|
252
|
+
log(`[godd-tk] Or build from source: cd godd-mcp-alpha/godd-tk && cargo build --release`);
|
|
253
|
+
return finishWithoutInstall("gh-unavailable");
|
|
105
254
|
}
|
|
106
255
|
|
|
107
|
-
|
|
108
|
-
|
|
256
|
+
log(`[godd-tk] Downloading ${target.artifact} (${GODD_TK_RELEASE_TAG}) via gh CLI...`);
|
|
257
|
+
const downloadedPath = join(transaction.tempDir, target.artifact);
|
|
258
|
+
try {
|
|
259
|
+
download(GODD_TK_RELEASE_TAG, target.artifact, transaction.tempDir);
|
|
260
|
+
} catch {
|
|
261
|
+
log(`[godd-tk] Download failed.`);
|
|
262
|
+
log(`[godd-tk] Build from source: cd godd-mcp-alpha/godd-tk && cargo build --release`);
|
|
263
|
+
return finishWithoutInstall("download-failed");
|
|
264
|
+
}
|
|
265
|
+
if (!exists(downloadedPath)) {
|
|
266
|
+
log(`[godd-tk] Downloaded artifact is missing (${target.artifact}).`);
|
|
267
|
+
log(`[godd-tk] Build from source: cd godd-mcp-alpha/godd-tk && cargo build --release`);
|
|
268
|
+
return finishWithoutInstall("download-failed");
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// ここから先は「取得できた」ケース。digest 不一致は skip ではなく失敗。
|
|
272
|
+
try {
|
|
273
|
+
verify(downloadedPath, target);
|
|
274
|
+
} catch {
|
|
275
|
+
logError(
|
|
276
|
+
`[godd-tk] SECURITY: downloaded artifact failed integrity verification.`,
|
|
277
|
+
);
|
|
278
|
+
logError(
|
|
279
|
+
`[godd-tk] The artifact published at ${GODD_TK_RELEASE_TAG} does not match the digest ` +
|
|
280
|
+
`committed in godd-mcp-alpha/godd-tk-assets.json. Installation is aborted and nothing was installed.`,
|
|
281
|
+
);
|
|
282
|
+
if (unverifiedExisting && exists(binaryPath)) {
|
|
283
|
+
const quarantine = finishWithoutInstall("digest-mismatch");
|
|
284
|
+
if (quarantine.status === "integrity-failure") return quarantine;
|
|
285
|
+
}
|
|
286
|
+
return { status: "integrity-failure", reason: "digest-mismatch" };
|
|
287
|
+
}
|
|
109
288
|
|
|
110
|
-
|
|
111
|
-
|
|
289
|
+
try {
|
|
290
|
+
if (currentPlatform !== "win32") chmod(downloadedPath, 0o755);
|
|
291
|
+
publishAtomicBinary(
|
|
292
|
+
transaction,
|
|
293
|
+
downloadedPath,
|
|
294
|
+
(path) => verify(path, target),
|
|
295
|
+
undefined,
|
|
296
|
+
publishOperations,
|
|
297
|
+
);
|
|
298
|
+
} catch (error) {
|
|
299
|
+
if (error instanceof AtomicInstallCleanupError) {
|
|
300
|
+
retainTransaction = true;
|
|
301
|
+
logError(
|
|
302
|
+
`[godd-tk] SECURITY: verified publish cleanup is pending; transaction retained.`,
|
|
303
|
+
);
|
|
304
|
+
return {
|
|
305
|
+
status: "integrity-failure",
|
|
306
|
+
reason: "publish-cleanup-pending",
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
if (error instanceof AtomicInstallIntegrityError) {
|
|
310
|
+
logError(`[godd-tk] SECURITY: published artifact failed integrity verification.`);
|
|
311
|
+
return {
|
|
312
|
+
status: "integrity-failure",
|
|
313
|
+
reason: "post-publish-integrity-failed",
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
if (error instanceof AtomicInstallRollbackError) {
|
|
317
|
+
retainTransaction = true;
|
|
318
|
+
logError(`[godd-tk] SECURITY: publish rollback failed; transaction retained.`);
|
|
319
|
+
return { status: "integrity-failure", reason: "rollback-failed" };
|
|
320
|
+
}
|
|
321
|
+
log(`[godd-tk] Installation failed.`);
|
|
322
|
+
log(`[godd-tk] Build from source: cd godd-mcp-alpha/godd-tk && cargo build --release`);
|
|
323
|
+
return finishWithoutInstall("install-failed");
|
|
324
|
+
}
|
|
325
|
+
// ここまで来た時点で、置かれているのは検証済み bytes だけ。
|
|
326
|
+
unverifiedExisting = false;
|
|
112
327
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
328
|
+
log(`[godd-tk] Installed and verified (${GODD_TK_RELEASE_TAG}).`);
|
|
329
|
+
log(`[godd-tk] Run 'godd-tk init --agent cursor' to set up hooks`);
|
|
330
|
+
if (currentPlatform === "win32") {
|
|
331
|
+
const pathDirs = (process.env.PATH || "").toLowerCase().split(";");
|
|
332
|
+
if (!pathDirs.includes(installDir.toLowerCase())) {
|
|
333
|
+
log(`[godd-tk] Add the godd-tk install directory to PATH for global access`);
|
|
334
|
+
}
|
|
118
335
|
}
|
|
119
|
-
|
|
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`);
|
|
336
|
+
return { status: "installed", binaryPath };
|
|
123
337
|
}
|
|
124
338
|
}
|
|
125
339
|
|
|
126
|
-
|
|
340
|
+
export function toExitCode(result) {
|
|
341
|
+
return result.status === "integrity-failure" ? GODD_TK_INTEGRITY_EXIT_CODE : 0;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
345
|
+
process.exitCode = toExitCode(installGoddTk());
|
|
346
|
+
}
|
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
|
}
|