@coworker-jp/aidr 0.1.302 → 0.1.303
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/package.json +1 -1
- package/src/binary-fetcher.mjs +38 -2
- package/src/cli.mjs +7 -4
- package/src/red-team.mjs +91 -3
package/package.json
CHANGED
package/src/binary-fetcher.mjs
CHANGED
|
@@ -49,18 +49,54 @@ export function detectOpengrepPlatform() {
|
|
|
49
49
|
* being handled as a release problem.
|
|
50
50
|
*/
|
|
51
51
|
export class HttpStatusError extends Error {
|
|
52
|
-
constructor(url, status) {
|
|
52
|
+
constructor(url, status, payload = null) {
|
|
53
53
|
super(`GET ${url} -> HTTP ${status}`);
|
|
54
54
|
this.name = "HttpStatusError";
|
|
55
55
|
this.status = status;
|
|
56
56
|
this.url = url;
|
|
57
|
+
/**
|
|
58
|
+
* The endpoint's own JSON error body, or null when there wasn't one.
|
|
59
|
+
*
|
|
60
|
+
* Carried because a status code alone cannot say WHY. `/download/red-team`
|
|
61
|
+
* answers 403 for two unrelated reasons — a disabled access key and an
|
|
62
|
+
* account type that does not include the scanner — and until #2003 the CLI
|
|
63
|
+
* printed `GET <url> -> HTTP 403` for both, which names neither. The
|
|
64
|
+
* server writes one sentence for each (`aidr_common.red_team.
|
|
65
|
+
* red_team_refusal_message`); reading it here is what lets the caller show
|
|
66
|
+
* it instead of a bare number.
|
|
67
|
+
*
|
|
68
|
+
* Never parsed for a DECISION. It is display material, and it is
|
|
69
|
+
* sanitised at the point of display, not here — a body we could not parse
|
|
70
|
+
* must stay distinguishable from one that had no message.
|
|
71
|
+
*/
|
|
72
|
+
this.payload = payload;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Cap on the error body we will read. Big enough for any refusal we emit. */
|
|
77
|
+
const ERROR_BODY_LIMIT = 8192;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Best-effort read of a non-2xx JSON body. Returns the parsed object or null.
|
|
81
|
+
*
|
|
82
|
+
* Never throws and never rejects: a failure to read the explanation must not
|
|
83
|
+
* replace the real failure (the HTTP status) with a parse error, which would
|
|
84
|
+
* turn "your key is not entitled" into "unexpected token < in JSON".
|
|
85
|
+
*/
|
|
86
|
+
async function readErrorPayload(res) {
|
|
87
|
+
try {
|
|
88
|
+
const text = (await res.text()).slice(0, ERROR_BODY_LIMIT);
|
|
89
|
+
const parsed = JSON.parse(text);
|
|
90
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
91
|
+
} catch {
|
|
92
|
+
return null;
|
|
57
93
|
}
|
|
58
94
|
}
|
|
59
95
|
|
|
60
96
|
async function fetchBuffer(url, accessKey) {
|
|
61
97
|
const headers = accessKey ? { "x-access-key": accessKey } : {};
|
|
62
98
|
const res = await fetch(url, { redirect: "follow", headers });
|
|
63
|
-
if (!res.ok) throw new HttpStatusError(url, res.status);
|
|
99
|
+
if (!res.ok) throw new HttpStatusError(url, res.status, await readErrorPayload(res));
|
|
64
100
|
const ab = await res.arrayBuffer();
|
|
65
101
|
return Buffer.from(ab);
|
|
66
102
|
}
|
package/src/cli.mjs
CHANGED
|
@@ -784,10 +784,13 @@ async function cmdRedTeamInstall(opts) {
|
|
|
784
784
|
);
|
|
785
785
|
}
|
|
786
786
|
} catch (e) {
|
|
787
|
-
//
|
|
788
|
-
//
|
|
789
|
-
//
|
|
790
|
-
|
|
787
|
+
// サーバー側が唯一の判断者であり、**理由を知っているのもサーバーだけ**なので、
|
|
788
|
+
// 403 の本文をそのまま見せる (issue #2003)。ここが `e.message` を出していた
|
|
789
|
+
// 間、利用者が受け取っていたのは `GET <url> -> HTTP 403` だけで、応答の本文は
|
|
790
|
+
// 1 バイトも読まれていなかった —— このコメント自身が「メッセージをそのまま
|
|
791
|
+
// 見せる」「シート 10 以上が要る」と、どちらも事実でないことを書いていた。
|
|
792
|
+
const { redTeamInstallFailureText } = await import("./red-team.mjs");
|
|
793
|
+
console.error(redTeamInstallFailureText(e));
|
|
791
794
|
process.exit(1);
|
|
792
795
|
}
|
|
793
796
|
}
|
package/src/red-team.mjs
CHANGED
|
@@ -82,9 +82,10 @@ function binFileName(meta) {
|
|
|
82
82
|
* Red Team の生バイナリを取得する。
|
|
83
83
|
*
|
|
84
84
|
* verify Lambda の `GET /download/red-team/[<product>/]<platform>` はアクセス
|
|
85
|
-
* キー認証に加えて Red Team エンタイトルメント (
|
|
86
|
-
* を要求し、不適格なら 403 を返す。fetchAsset は非 2xx で throw
|
|
87
|
-
*
|
|
85
|
+
* キー認証に加えて Red Team エンタイトルメント (`account_type == "red_team"`、
|
|
86
|
+
* issue #1954) を要求し、不適格なら 403 を返す。fetchAsset は非 2xx で throw
|
|
87
|
+
* するため、ここでは特別扱いせず呼び出し側にメッセージを見せる ——
|
|
88
|
+
* **見せるものは `redTeamInstallFailureText` が組み立てる** (issue #2003)。
|
|
88
89
|
*
|
|
89
90
|
* network-scanner は従来どおり 1 セグメント (`/red-team/<platform>`) を使う
|
|
90
91
|
* (配信済みルートとバイト等価)。他 product は `/red-team/<product>/<platform>`。
|
|
@@ -107,6 +108,93 @@ export async function fetchRedTeamBinary(
|
|
|
107
108
|
return { ...result, platform, product };
|
|
108
109
|
}
|
|
109
110
|
|
|
111
|
+
/**
|
|
112
|
+
* サーバーが Red Team のエンタイトルメント拒否に使う識別子。
|
|
113
|
+
*
|
|
114
|
+
* 単一情報源は `aidr_common.red_team.RED_TEAM_ACCOUNT_REQUIRED` で、これはその
|
|
115
|
+
* 写しである。**判定の写しではない** —— ここに在るのは「サーバーが返した拒否は
|
|
116
|
+
* どの拒否か」を読むための名前だけで、可否を計算する行は 1 つも無い。
|
|
117
|
+
*/
|
|
118
|
+
export const RED_TEAM_ACCOUNT_REQUIRED = "red_team_account_required";
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* サーバー由来の文字列を端末に出す前の掃除。
|
|
122
|
+
*
|
|
123
|
+
* 制御文字 (改行とタブを除く) を落とし、長さを切る。**内容は書き換えない** ——
|
|
124
|
+
* 拒否の理由を我々が要約し直すと、サーバーが言ったことと違うものを見せる経路が
|
|
125
|
+
* できる。掃除するのは端末に対する形だけである。
|
|
126
|
+
*/
|
|
127
|
+
function sanitizeServerText(raw, limit = 1000) {
|
|
128
|
+
if (typeof raw !== "string") return "";
|
|
129
|
+
// eslint-disable-next-line no-control-regex
|
|
130
|
+
return raw.replace(/[\u0000-\u0008\u000b-\u001f\u007f]/g, "").trim().slice(0, limit);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* `red-team install` が失敗したときに端末へ出す文面を組み立てる (issue #2003)。
|
|
135
|
+
*
|
|
136
|
+
* ## 何が壊れていたか
|
|
137
|
+
*
|
|
138
|
+
* 失敗経路は `console.error(\`red-team install failed: \${e.message}\`)` の 1 行
|
|
139
|
+
* だけで、`e.message` は `fetchBuffer` が投げる `GET <url> -> HTTP 403` だった。
|
|
140
|
+
* **応答の本文は 1 バイトも読んでいなかった。** つまり利用者が受け取っていたのは
|
|
141
|
+
* **理由の書かれていない番号**であり、その横のコメントは
|
|
142
|
+
* 「403 red_team_requires_10_seats は『Pro/Trial かつ契約シート 10 以上』の
|
|
143
|
+
* エンタイトルメント不足。…メッセージをそのまま見せる」と書いていた ——
|
|
144
|
+
* **条件は #1954 で消えており、「そのまま見せる」も事実ではなかった。**
|
|
145
|
+
*
|
|
146
|
+
* ## なぜ `status` ではなく `error` で分岐するか
|
|
147
|
+
*
|
|
148
|
+
* このルートの 403 は 2 つある: **鍵が無効 / 停止中** と
|
|
149
|
+
* **Red Team アカウントではない**。番号だけで分岐すると、停止された鍵を持つ人に
|
|
150
|
+
* 「Red Team アカウントを発行してもらってください」と言うことになる ——
|
|
151
|
+
* 発行されても、その鍵は動かない。
|
|
152
|
+
*
|
|
153
|
+
* ## なぜサーバーの文面を優先するか
|
|
154
|
+
*
|
|
155
|
+
* 可否を決めるのはサーバーだけで、**理由を知っているのもサーバーだけ**である
|
|
156
|
+
* (解決済みの `account_type` を見て、通常アカウントと「解釈できない種別」を
|
|
157
|
+
* 書き分けている)。ここで書き直すと 2 つ目の説明ができて必ず食い違う。
|
|
158
|
+
* 手元の文面は**本文が無かったとき**にだけ使う保険で、そちらも
|
|
159
|
+
* プランやシート数を解決策として名指ししない (docs/product/UI_COPY.md)。
|
|
160
|
+
*/
|
|
161
|
+
export function redTeamInstallFailureText(err) {
|
|
162
|
+
const status = err?.status;
|
|
163
|
+
const payload = err?.payload && typeof err.payload === "object" ? err.payload : null;
|
|
164
|
+
const code = typeof payload?.error === "string" ? payload.error : "";
|
|
165
|
+
const serverMessage = sanitizeServerText(payload?.message);
|
|
166
|
+
|
|
167
|
+
if (code === RED_TEAM_ACCOUNT_REQUIRED) {
|
|
168
|
+
return [
|
|
169
|
+
"red-team install failed: nothing was installed — the download endpoint " +
|
|
170
|
+
"refused this access key.",
|
|
171
|
+
serverMessage ||
|
|
172
|
+
// 本文の無い古い配備向け。**シートもプランも解決策として出さない。**
|
|
173
|
+
"The scanner is only available on a Red Team account — its own account " +
|
|
174
|
+
"type, issued from the CoWorker admin console. Ask your CoWorker " +
|
|
175
|
+
"administrator to issue one, then re-run this command with that " +
|
|
176
|
+
"account's access key.",
|
|
177
|
+
].join("\n");
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (status === 401 || status === 403) {
|
|
181
|
+
// 同じ番号の別の拒否 (無効・停止中の鍵など)。**Red Team の説明を貼らない。**
|
|
182
|
+
return [
|
|
183
|
+
`red-team install failed: the download endpoint refused this access key ` +
|
|
184
|
+
`(HTTP ${status}), so nothing was installed.`,
|
|
185
|
+
serverMessage ||
|
|
186
|
+
(typeof payload?.error === "string" && payload.error) ||
|
|
187
|
+
"Check that the key is the one issued for this environment and is still " +
|
|
188
|
+
"active; ask your CoWorker administrator if it is not.",
|
|
189
|
+
].join("\n");
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// それ以外は従来どおり。**握りつぶさない** —— 分類できなかったことと、
|
|
193
|
+
// 分類した結果として何も言うことが無いことは別である。
|
|
194
|
+
const detail = sanitizeServerText(err?.message) || String(err);
|
|
195
|
+
return `red-team install failed: ${detail}`;
|
|
196
|
+
}
|
|
197
|
+
|
|
110
198
|
/**
|
|
111
199
|
* aidr の scope を `claude mcp add` の scope にマップする。
|
|
112
200
|
*
|