@autodevjapan/godd-mcp-alpha 2.9.4 → 2.10.1

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.
@@ -12,8 +12,8 @@
12
12
  * レビューを通った manifest の digest である(Issue #1474)。
13
13
  */
14
14
 
15
- import { chmodSync, existsSync, mkdirSync, mkdtempSync, renameSync, rmSync, unlinkSync } from "node:fs";
16
- import { join, resolve } from "node:path";
15
+ import { chmodSync, existsSync, mkdirSync, renameSync } from "node:fs";
16
+ import { basename, join, resolve } from "node:path";
17
17
  import { homedir, platform, arch } from "node:os";
18
18
  import { execFileSync } from "node:child_process";
19
19
  import { fileURLToPath } from "node:url";
@@ -23,6 +23,16 @@ import {
23
23
  getGoddTkTarget,
24
24
  verifyGoddTkArtifact,
25
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";
26
36
 
27
37
  const REPO = "AutoDevJapan/GoDD-MCP-Alpha";
28
38
  const BINARY_NAME = "godd-tk";
@@ -73,8 +83,8 @@ function defaultDownload(tag, artifact, directory) {
73
83
  * - "installed" / "already-installed" … 検証済みバイナリが所定の場所にある
74
84
  * - "skipped" … そもそも取得しない(exit 0、install 継続)。
75
85
  * 既存の未検証バイナリが居た場合は必ず隔離済み
76
- * - "integrity-failure" … 検証を通らないバイナリを実行経路から
77
- * 外せなかった(exit 65 で fail-closed)
86
+ * - "integrity-failure" … integrity / atomic recovery 境界の失敗。
87
+ * rollback成功時もexit 65でfail-closed
78
88
  */
79
89
  export function installGoddTk({
80
90
  currentPlatform = platform(),
@@ -85,13 +95,13 @@ export function installGoddTk({
85
95
  verify = verifyGoddTkArtifact,
86
96
  exists = existsSync,
87
97
  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 }),
98
+ makeTempDir,
99
+ removeTree,
100
+ publishOperations,
101
+ isProcessAlive,
102
+ recoverInterruptedFinish =
103
+ process.env.GODD_RECOVER_GODD_TK_INSTALL === "1",
93
104
  move = renameSync,
94
- removeFile = unlinkSync,
95
105
  chmod = chmodSync,
96
106
  now = () => Date.now(),
97
107
  log = console.log,
@@ -105,19 +115,86 @@ export function installGoddTk({
105
115
  }
106
116
 
107
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
+ }
156
+
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) {
166
+ try {
167
+ result = runLockedInstall();
168
+ } catch {
169
+ logError(`[godd-tk] Unexpected installer error.`);
170
+ result = { status: "integrity-failure", reason: "unexpected-error" };
171
+ }
172
+ }
173
+
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 };
182
+ }
183
+ return result;
184
+
185
+ function runLockedInstall() {
108
186
  let unverifiedExisting = false;
109
187
  if (exists(binaryPath)) {
110
188
  try {
111
189
  verify(binaryPath, target);
112
- log(`[godd-tk] Already installed and verified: ${binaryPath} (${GODD_TK_RELEASE_TAG})`);
190
+ log(`[godd-tk] Already installed and verified (${GODD_TK_RELEASE_TAG}).`);
113
191
  return { status: "already-installed", binaryPath };
114
- } catch (error) {
192
+ } catch {
115
193
  // 既存バイナリが pinned release と違う(自前ビルド・旧バージョン・改竄)。
116
194
  // 「検証済み」と偽らず、pinned release での置き換えを試みる。
117
195
  unverifiedExisting = true;
118
196
  logError(
119
- `[godd-tk] Existing binary does not match the pinned release ${GODD_TK_RELEASE_TAG}: ` +
120
- `${error instanceof Error ? error.message : error}`,
197
+ `[godd-tk] Existing binary does not match the pinned release ${GODD_TK_RELEASE_TAG}.`,
121
198
  );
122
199
  log(`[godd-tk] Reinstalling the pinned, digest-verified release.`);
123
200
  }
@@ -142,7 +219,7 @@ export function installGoddTk({
142
219
  for (let suffix = 1; exists(quarantinePath); suffix += 1) {
143
220
  if (suffix > 1000) {
144
221
  logError(
145
- `[godd-tk] SECURITY: could not find a free quarantine path for ${binaryPath}.`,
222
+ `[godd-tk] SECURITY: quarantine namespace is exhausted.`,
146
223
  );
147
224
  return { status: "integrity-failure", reason: "unverified-existing-binary" };
148
225
  }
@@ -150,35 +227,26 @@ export function installGoddTk({
150
227
  }
151
228
  try {
152
229
  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
- );
230
+ } catch {
231
+ logError(`[godd-tk] SECURITY: could not quarantine the unverified binary.`);
158
232
  return { status: "integrity-failure", reason: "unverified-existing-binary" };
159
233
  }
160
234
  logError(
161
- `[godd-tk] Quarantined the unverified binary to ${quarantinePath}; ` +
235
+ `[godd-tk] Quarantined the unverified binary as ${basename(quarantinePath)}; ` +
162
236
  `it is no longer on the install path. Delete it once you have confirmed its origin.`,
163
237
  );
164
238
  return { status: "skipped", reason };
165
239
  }
166
-
167
- // 想定外の例外が `unverifiedExisting === true` のまま escape すると、
168
- // 未検証バイナリを隔離しないまま install が続いてしまう(M2 と同じクラスの穴)。
169
- // 取得〜配置の全体を包み、必ず finishWithoutInstall を通す。
170
- // `integrity-failure` は throw ではなく return なので、この catch で降格することはない。
171
- try {
172
- return runDownloadAndInstall();
173
- } catch (error) {
174
- logError(
175
- `[godd-tk] Unexpected installer error: ${error instanceof Error ? error.message : error}`,
176
- );
177
- return finishWithoutInstall("unexpected-error");
178
- }
179
-
180
- function runDownloadAndInstall() {
181
- if (!isGhAvailable()) {
240
+ let ghAvailable;
241
+ try {
242
+ ghAvailable = isGhAvailable();
243
+ } catch {
244
+ logError(
245
+ `[godd-tk] Unexpected installer error.`,
246
+ );
247
+ return finishWithoutInstall("unexpected-error");
248
+ }
249
+ if (!ghAvailable) {
182
250
  log(`[godd-tk] GitHub CLI (gh) not found — required for private repo download.`);
183
251
  log(`[godd-tk] Install gh: https://cli.github.com/ then run this script again.`);
184
252
  log(`[godd-tk] Or build from source: cd godd-mcp-alpha/godd-tk && cargo build --release`);
@@ -186,30 +254,16 @@ export function installGoddTk({
186
254
  }
187
255
 
188
256
  log(`[godd-tk] Downloading ${target.artifact} (${GODD_TK_RELEASE_TAG}) via gh CLI...`);
189
-
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");
200
- }
201
-
202
- const downloadedPath = join(tmpDir, target.artifact);
203
- try {
257
+ const downloadedPath = join(transaction.tempDir, target.artifact);
204
258
  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}`);
259
+ download(GODD_TK_RELEASE_TAG, target.artifact, transaction.tempDir);
260
+ } catch {
261
+ log(`[godd-tk] Download failed.`);
208
262
  log(`[godd-tk] Build from source: cd godd-mcp-alpha/godd-tk && cargo build --release`);
209
263
  return finishWithoutInstall("download-failed");
210
264
  }
211
265
  if (!exists(downloadedPath)) {
212
- log(`[godd-tk] Downloaded file not found at ${downloadedPath}`);
266
+ log(`[godd-tk] Downloaded artifact is missing (${target.artifact}).`);
213
267
  log(`[godd-tk] Build from source: cd godd-mcp-alpha/godd-tk && cargo build --release`);
214
268
  return finishWithoutInstall("download-failed");
215
269
  }
@@ -217,56 +271,69 @@ export function installGoddTk({
217
271
  // ここから先は「取得できた」ケース。digest 不一致は skip ではなく失敗。
218
272
  try {
219
273
  verify(downloadedPath, target);
220
- } catch (error) {
274
+ } catch {
221
275
  logError(
222
- `[godd-tk] SECURITY: downloaded artifact failed integrity verification — ` +
223
- `${error instanceof Error ? error.message : error}`,
276
+ `[godd-tk] SECURITY: downloaded artifact failed integrity verification.`,
224
277
  );
225
278
  logError(
226
279
  `[godd-tk] The artifact published at ${GODD_TK_RELEASE_TAG} does not match the digest ` +
227
280
  `committed in godd-mcp-alpha/godd-tk-assets.json. Installation is aborted and nothing was installed.`,
228
281
  );
282
+ if (unverifiedExisting && exists(binaryPath)) {
283
+ const quarantine = finishWithoutInstall("digest-mismatch");
284
+ if (quarantine.status === "integrity-failure") return quarantine;
285
+ }
229
286
  return { status: "integrity-failure", reason: "digest-mismatch" };
230
287
  }
231
288
 
232
289
  try {
233
- if (exists(binaryPath)) removeFile(binaryPath);
234
- move(downloadedPath, binaryPath);
290
+ if (currentPlatform !== "win32") chmod(downloadedPath, 0o755);
291
+ publishAtomicBinary(
292
+ transaction,
293
+ downloadedPath,
294
+ (path) => verify(path, target),
295
+ undefined,
296
+ publishOperations,
297
+ );
235
298
  } catch (error) {
236
- log(`[godd-tk] Installation failed: ${error instanceof Error ? error.message : 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.`);
237
322
  log(`[godd-tk] Build from source: cd godd-mcp-alpha/godd-tk && cargo build --release`);
238
323
  return finishWithoutInstall("install-failed");
239
324
  }
240
325
  // ここまで来た時点で、置かれているのは検証済み bytes だけ。
241
326
  unverifiedExisting = false;
242
327
 
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
- }
253
-
254
- log(`[godd-tk] Installed and verified (sha256 ${target.sha256}): ${binaryPath}`);
328
+ log(`[godd-tk] Installed and verified (${GODD_TK_RELEASE_TAG}).`);
255
329
  log(`[godd-tk] Run 'godd-tk init --agent cursor' to set up hooks`);
256
330
  if (currentPlatform === "win32") {
257
331
  const pathDirs = (process.env.PATH || "").toLowerCase().split(";");
258
332
  if (!pathDirs.includes(installDir.toLowerCase())) {
259
- log(`[godd-tk] Add ${installDir} to your PATH for global access`);
333
+ log(`[godd-tk] Add the godd-tk install directory to PATH for global access`);
260
334
  }
261
335
  }
262
336
  return { status: "installed", binaryPath };
263
- } finally {
264
- try {
265
- removeTree(tmpDir);
266
- } catch {
267
- // best effort
268
- }
269
- }
270
337
  }
271
338
  }
272
339