pi-codex-marketplace 0.5.0 → 0.6.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 CHANGED
@@ -37,7 +37,7 @@ Nine subcommands, no arguments = 總覽:
37
37
 
38
38
  | 子命令 | 行為 |
39
39
  |--------|------|
40
- | `add <路徑\|網址>` | 註冊 marketplace(本機資料夾、GitHub 完整網址、`owner/repo` 簡寫皆收),自動偵測 codex/claude 格式並告知 `偵測:<format> marketplace · N plugins`。重複註冊同來源被拒絕並提示下一步。Git 來源以安全線取得(`core.hooksPath=/dev/null`、`GIT_LFS_SKIP_SMUDGE=1`、`GIT_TERMINAL_PROMPT=0`),catalog 解析失敗明示錯誤、不註冊。私有 HTTPS repo 預設 credential-free(有權限也可能 401)——先以 `PI_CODEX_MARKETPLACE_CREDENTIAL_HELPERS` 核准 credential helper 再 add(逐次生效、永不持久化),或改用 SSH 定位器;用法與範例見下方〈私有 Git repo:Credentialed Acquisition〉。 |
40
+ | `add <路徑\|網址>` | 註冊 marketplace(本機資料夾、GitHub 完整網址、`owner/repo` 簡寫皆收),自動偵測 codex/claude 格式並告知 `偵測:<format> marketplace · N plugins`。重複註冊同來源被拒絕並提示下一步。Git 來源以安全線取得(`core.hooksPath=/dev/null`、`GIT_LFS_SKIP_SMUDGE=1`、`GIT_TERMINAL_PROMPT=0`),catalog 解析失敗明示錯誤、不註冊。私有 HTTPS repo **開箱即用**:自動偵測本機憑證來源(`gh` 登入/macOS 鑰匙圈/`credential-store`)並逐次核准;也可用 `PI_CODEX_MARKETPLACE_CREDENTIAL_HELPERS` 顯式覆蓋核准清單(逐次生效、永不持久化),或改用 SSH 定位器;用法與範例見下方〈私有 Git repo:Credentialed Acquisition〉。 |
41
41
  | `list [名稱]` | 列出 plugins(編號/所屬 marketplace/狀態:可安裝・已裝啟用・已裝停用・unavailable+原因),可帶 marketplace 名稱過濾。 |
42
42
  | `install <編號\|名稱>` | 裝到**當下最新**並自動啟用+reload。成功話術如 `安裝 "name"(N skills:a, b, c)· 已重新載入生效`;同名衝突列出 `⚠ skill "b" 與既有同名,未投影(名稱衝突)`。**重複安裝=重抓最新覆寫**(重裝=更新,不報錯)。 |
43
43
  | `update` | 對全部已註冊 marketplace 重抓最新:有變化的 plugin 升到最新、無變化各自顯示「無變化」;整體以「已重新載入生效」收尾(有變時)。 |
@@ -55,7 +55,15 @@ Nine subcommands, no arguments = 總覽:
55
55
 
56
56
  ### 私有 Git repo:Credentialed Acquisition(核准式取得)
57
57
 
58
- 預設 `add`/`update` Git 來源完全 credential-free:不執行任何 credential helper,因此「有權限卻失敗」時,私有 HTTPS repo 會以 401 失敗(並提示核准或改用 SSH)。若要核准 credential helper,以逗號分隔設定環境變數(**逐次生效、永不持久化**,`add` 與 `update` 共用同一核准來源):
58
+ 對私有 HTTPS repo,`add`/`update` **開箱即用**:預設會自動偵測本機已存在的憑證來源並逐次核准(**固定白名單**,不讀本機 gitconfig 的任意 helper;偵測結果只限該次呼叫、永不持久化):
59
+
60
+ | 偵測來源 | 核准的 credential helper |
61
+ |---|---|
62
+ | `gh` CLI 已登入(`gh auth status` 成功) | `!gh auth git-credential` |
63
+ | macOS 原生鑰匙圈 helper 存在 | `osxkeychain` |
64
+ | git `credential-store` 存在 | `store`(明文憑證檔,請知悉風險) |
65
+
66
+ 已註冊或欲手動控制時,以逗號分隔設定環境變數——**顯式設定完全覆蓋自動偵測**(逐次生效、永不持久化,`add` 與 `update` 共用同一核准來源):
59
67
 
60
68
  ```
61
69
  PI_CODEX_MARKETPLACE_CREDENTIAL_HELPERS='store, !f() { echo "username=${GITHUB_USER}"; echo "password=${GITHUB_TOKEN}"; }; f'
@@ -64,10 +72,11 @@ PI_CODEX_MARKETPLACE_CREDENTIAL_HELPERS='store, !f() { echo "username=${GITHUB_U
64
72
 
65
73
  Credentialed Acquisition 語意(安全線):
66
74
 
67
- - 核准僅限該次呼叫(per-invocation):環境變數逐指令讀取,**不寫入** Bridge State、設定檔或任何持久化位置;重開 session 後需重新設定。
68
- - 值為 git `credential.helper` 字串,逗號分隔、各項 trim、空項目忽略;未設定或空白=未核准(行為與 credential-free 完全相同,安全線其餘禁制不變)。
75
+ - 預設(未設 env)=自動偵測固定白名單(gh/osxkeychain/store),僅限該次呼叫:**不寫入** Bridge State、設定檔或任何持久化位置;偵測到的 helper 以命令列 `credential.helper=` 傳入,gitconfig 的其他 helper(含 URL-scoped)仍被排除。
76
+ - 設定 env 後完全覆蓋:值為 git `credential.helper` 字串,逗號分隔、各項 trim、空項目忽略;未設定且偵測無結果=無任何 helper(行為與 credential-free 完全相同,安全線其餘禁制不變)。
69
77
  - 憑證與核准清單**永不**進入指令輸出、Bridge State、Canonical Git Locator(定位器)、Validation Snapshot(快照)或 cache identity(快取身份)——取得流程的 identity 判定與憑證完全無關。
70
- - 已核准 helper 仍被遠端拒絕(401)時,錯誤訊息提示檢查憑證;未核准時提示核准或改用 SSH。
78
+ - 自動偵測或已核准的 helper 仍被遠端拒絕(401)時,錯誤訊息提示檢查登入(`gh auth status`/keychain)或設環境變數核准其他 helper;本機偵測不到任何憑證來源時提示設環境變數或改用 SSH。
79
+ - 設定的 helper 名稱無效時(例如直接寫 `gh`——它不是 git 原生的 credential helper 執行檔),錯誤訊息指出正確寫法:原生 helper 名稱(`osxkeychain`/`store`)或 shell form(`!gh auth git-credential`)。
71
80
 
72
81
  #### SSH 定位器:私有 repo 的替代路徑
73
82
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-codex-marketplace",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Bridge Package for Codex and Claude Marketplace compatibility in Pi — 極簡 /codex-marketplace 純文字指令(add/list/install/update/disable/enable/remove/forget)、單一 Global Bridge State、當下最新安裝與即時投影",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -31,7 +31,11 @@ import { findEntryByManifestName, GIT_FAMILY_UNAVAILABLE_REASON, type Catalog, t
31
31
  import type { ValidationFinding } from '../registration/findings.js';
32
32
  import { normalizeGitLocator } from '../registration/git-locator.js';
33
33
  import { acquireGitSource, cleanupAcquisition, type GitExecutor } from '../registration/git-acquisition.js';
34
- import { CREDENTIAL_HELPERS_ENV, parseCredentialHelpers } from '../registration/credential-helpers.js';
34
+ import {
35
+ CREDENTIAL_HELPERS_ENV,
36
+ resolveApprovedHelpers,
37
+ type CredentialHelperDetector,
38
+ } from '../registration/credential-helpers.js';
35
39
  import { gitSourceKey } from '../registration/source-key.js';
36
40
  import { buildGitSnapshot } from '../registration/snapshot.js';
37
41
  import { SourceCache } from '../cache/source-cache.js';
@@ -43,6 +47,8 @@ export interface CommandOptions {
43
47
  cwd?: string;
44
48
  /** Git executor seam for tests — mocks `git` invocations (ls-remote/clone/checkout). */
45
49
  gitExecutor?: GitExecutor;
50
+ /** 自動偵測 seam for tests — mocks gh/keychain/store detection (#117). */
51
+ credentialHelperDetector?: CredentialHelperDetector;
46
52
  }
47
53
 
48
54
  export interface CommandResult {
@@ -599,12 +605,14 @@ export async function runCommand(
599
605
  ): Promise<CommandResult> {
600
606
  const rawArgs = typeof argv === 'string' ? argv.trim().split(/\s+/).filter(Boolean) : [...argv];
601
607
 
602
- // Credentialed Acquisition (#109):逐次核准的 credential helper allowlist。
603
- // 解析結果只經既有 AcquisitionTrustOptions 傳給 Git 取得;底層不讀環境變數,
604
- // 未核准(空字串/未設定)時 trust undefined,行為與 credential-free 完全一致。
605
- const approvedHelpers = parseCredentialHelpers(process.env[CREDENTIAL_HELPERS_ENV]);
606
- const acquireTrust: { allowedCredentialHelpers: string[] } | undefined =
607
- approvedHelpers.length > 0 ? { allowedCredentialHelpers: approvedHelpers } : undefined;
608
+ // Credentialed Acquisition (#109,#117):逐次核准的 credential helper allowlist。
609
+ // env 顯式設定 完全覆蓋;未設定/空白 → 自動偵測固定白名單(gh/keychain/store),
610
+ // 開箱即用(私有 repo 不再要求先設 env)。解析結果只經既有 AcquisitionTrustOptions
611
+ // 傳給 Git 取得;底層不讀環境變數,未核准(偵測也無結果)時 trust 為 undefined,
612
+ // 行為與 credential-free 完全一致。
613
+ const resolved = resolveApprovedHelpers(process.env[CREDENTIAL_HELPERS_ENV], opts.credentialHelperDetector);
614
+ const acquireTrust: { allowedCredentialHelpers: string[]; helperMode: 'detected' | 'approved' } | undefined =
615
+ resolved.helpers.length > 0 ? { allowedCredentialHelpers: resolved.helpers, helperMode: resolved.mode as 'detected' | 'approved' } : undefined;
608
616
 
609
617
  // Strip leading command token if passed
610
618
  if (rawArgs.length > 0 && (rawArgs[0] === '/codex-marketplace' || rawArgs[0] === 'codex-marketplace')) {
@@ -4,17 +4,71 @@
4
4
  *
5
5
  * Per-invocation approval of git credential helpers via
6
6
  * `PI_CODEX_MARKETPLACE_CREDENTIAL_HELPERS` (comma-separated `credential.helper`
7
- * strings). Empty/unset means "not approved" — behavior identical to today.
8
- * This module only parses; the resulting allowlist travels through
7
+ * strings). Empty/unset means "not approved" — but since #117 the approval may also
8
+ * come from an auto-detected fixed allowlist, so empty/unset falls back to detection.
9
+ * This module only parses/detects; the resulting allowlist travels through
9
10
  * AcquisitionTrustOptions and never touches Bridge State, snapshots, or cache identity.
10
11
  */
11
12
 
13
+ import { spawnSync } from 'node:child_process';
14
+ import { existsSync } from 'node:fs';
15
+ import { join } from 'node:path';
16
+
12
17
  export const CREDENTIAL_HELPERS_ENV = 'PI_CODEX_MARKETPLACE_CREDENTIAL_HELPERS';
13
18
 
19
+ /**
20
+ * 允許清單來源:#117 開箱即用後,credential helper 可能有兩種來源。
21
+ * - approved:使用者經 `PI_CODEX_MARKETPLACE_CREDENTIAL_HELPERS` 顯式核准(可完全覆蓋偵測)。
22
+ * - detected:預設自動偵測的固定白名單(gh CLI/macOS keychain/git credential-store)。
23
+ * - none:無任何 helper 可用(錯誤訊息給「未核准」指引變體)。
24
+ */
25
+ export type CredentialHelperMode = 'none' | 'detected' | 'approved';
26
+
27
+ /**
28
+ * 自動偵測介面——可注入(測試)或使用預設真機偵測。偵測目標是**固定白名單**,
29
+ * 不是讀本機 gitconfig:gitconfig 的任意 helper(含 URL-scoped)仍被命令列
30
+ * `credential.helper=` 清空排除,僅白名單上的 helper 參與(#117,與 ADR 0006
31
+ * 被拒的「隱式信任本機 git config」不同)。
32
+ */
33
+ export interface CredentialHelperDetector {
34
+ /** gh CLI 已登入(`gh auth status` exit 0) */
35
+ ghLoggedIn(): boolean;
36
+ /** git 原生 credential helper 執行檔存在(`git --exec-path` 或 PATH) */
37
+ hasGitHelper(name: string): boolean;
38
+ }
39
+
40
+ const defaultDetector: CredentialHelperDetector = {
41
+ ghLoggedIn() {
42
+ try {
43
+ const res = spawnSync('gh', ['auth', 'status'], { stdio: 'ignore', timeout: 5000 });
44
+ return res.status === 0;
45
+ } catch {
46
+ return false;
47
+ }
48
+ },
49
+ hasGitHelper(name) {
50
+ try {
51
+ const execPath = spawnSync('git', ['--exec-path'], { stdio: ['ignore', 'pipe', 'ignore'], timeout: 5000 });
52
+ if (execPath.status === 0) {
53
+ const dir = String(execPath.stdout ?? '').trim();
54
+ if (dir && existsSync(join(dir, `git-${name}`))) return true;
55
+ }
56
+ } catch {
57
+ // fall through to PATH check
58
+ }
59
+ try {
60
+ const which = spawnSync('sh', ['-c', `command -v git-${name}`], { stdio: ['ignore', 'pipe', 'ignore'], timeout: 5000 });
61
+ return which.status === 0 && String(which.stdout ?? '').trim().length > 0;
62
+ } catch {
63
+ return false;
64
+ }
65
+ },
66
+ };
67
+
14
68
  /**
15
69
  * Parse the approval env var into a helper allowlist.
16
70
  * Comma-separated, each entry trimmed, empty entries ignored.
17
- * Empty/unset → no approval ([]).
71
+ * Empty/unset → no explicit approval ([]).
18
72
  */
19
73
  export function parseCredentialHelpers(raw: string | undefined): string[] {
20
74
  if (!raw) return [];
@@ -22,4 +76,27 @@ export function parseCredentialHelpers(raw: string | undefined): string[] {
22
76
  .split(',')
23
77
  .map((entry) => entry.trim())
24
78
  .filter((entry) => entry.length > 0);
79
+ }
80
+
81
+ export interface ResolvedCredentialAllowlist {
82
+ helpers: string[];
83
+ mode: CredentialHelperMode;
84
+ }
85
+
86
+ /**
87
+ * 解析最終 allowlist(#117 開箱即用):
88
+ * - env 有非空內容 → 顯式核准,完全覆蓋自動偵測(可預測性不變)。
89
+ * - env 未設定/空白 → 自動偵測固定白名單。
90
+ */
91
+ export function resolveApprovedHelpers(
92
+ raw: string | undefined,
93
+ detector: CredentialHelperDetector = defaultDetector,
94
+ ): ResolvedCredentialAllowlist {
95
+ const parsed = parseCredentialHelpers(raw);
96
+ if (parsed.length > 0) return { helpers: parsed, mode: 'approved' };
97
+ const detected: string[] = [];
98
+ if (detector.ghLoggedIn()) detected.push('!gh auth git-credential');
99
+ if (detector.hasGitHelper('credential-osxkeychain')) detected.push('osxkeychain');
100
+ if (detector.hasGitHelper('credential-store')) detected.push('store');
101
+ return { helpers: detected, mode: detected.length > 0 ? 'detected' : 'none' };
25
102
  }
@@ -53,6 +53,7 @@ export const RULE = {
53
53
  GIT_TRUST_REDIRECT: 'GIT-32',
54
54
  GIT_TRUST_CREDENTIAL_HELPER: 'GIT-33',
55
55
  GIT_TRUST_AUTH_REQUIRED: 'GIT-34',
56
+ GIT_TRUST_CREDENTIAL_HELPER_INVALID: 'GIT-35',
56
57
  } as const;
57
58
 
58
59
  export const CODE = {
@@ -77,6 +78,7 @@ export const CODE = {
77
78
  GIT_TRUST_REDIRECT: 'GIT_TRUST_REDIRECT',
78
79
  GIT_TRUST_CREDENTIAL_HELPER: 'GIT_TRUST_CREDENTIAL_HELPER',
79
80
  GIT_TRUST_AUTH_REQUIRED: 'GIT_TRUST_AUTH_REQUIRED',
81
+ GIT_TRUST_CREDENTIAL_HELPER_INVALID: 'GIT_TRUST_CREDENTIAL_HELPER_INVALID',
80
82
  GIT_REPO_NOT_FOUND: 'GIT_REPO_NOT_FOUND',
81
83
  } as const;
82
84
 
@@ -15,7 +15,7 @@ import { join } from 'node:path';
15
15
 
16
16
  import { CODE, RULE, blocking, type ValidationFinding } from './findings.js';
17
17
  import type { CanonicalGitLocator } from './git-locator.js';
18
- import { CREDENTIAL_HELPERS_ENV } from './credential-helpers.js';
18
+ import { CREDENTIAL_HELPERS_ENV, type CredentialHelperMode } from './credential-helpers.js';
19
19
 
20
20
  export interface GitExecutor {
21
21
  (args: string[], opts?: { cwd?: string; env?: Record<string, string> }): Promise<{
@@ -47,6 +47,8 @@ export function defaultGitExecutor(): GitExecutor {
47
47
  export interface AcquisitionTrustOptions {
48
48
  knownHostsFile?: string;
49
49
  allowedCredentialHelpers?: string[];
50
+ /** 允許清單來源(#117):approved=env 顯式核准;detected=自動偵測白名單;缺省視為 approved(相容既有 caller)。 */
51
+ helperMode?: CredentialHelperMode;
50
52
  gitPath?: string;
51
53
  sshCommand?: string;
52
54
  allowRedirects?: boolean;
@@ -129,19 +131,33 @@ function isFullHex(s: string): boolean {
129
131
 
130
132
  /**
131
133
  * 共用失敗分類(#110):ls-remote 與 clone 兩條取得路徑採用同一分類與訊息。
132
- * - auth:伺服器 401(authentication failed)→ GIT-34,訊息依核准狀態分兩變體;
133
- * - helper:credential source 拒絕(原字串匹配的 helper 拒絕情境)→ GIT-33 保留;
134
+ * - auth:伺服器 401(authentication failed)→ GIT-34,訊息依 helper 模式分三變體(none/detected/approved)。
135
+ * - invalid-helper:核准的 helper 名稱無效(git 找不到對應執行檔,如 `gh`)→ GIT-35,
136
+ * 訊息指出正確寫法(原生 helper 名稱或 `!命令` shell form)。放在 auth 之前:stderr 常同時含兩者。
134
137
  * - not-found:「repository not found」類字串 → 標明 repo 不存在(保留非 GitHub 情境;
135
138
  * GitHub smart-HTTP 對不存在 repo 實測回 401,落入 auth 分支)。
139
+ * - helper:credential source 拒絕(原字串匹配的 helper 拒絕情境)→ GIT-33 保留。
136
140
  */
137
141
  type FailureKind =
138
142
  | { kind: 'host-key'; isChanged: boolean }
139
143
  | { kind: 'redirect' }
140
144
  | { kind: 'not-found' }
141
- | { kind: 'auth'; approved: boolean }
142
- | { kind: 'helper'; approved: boolean };
145
+ | { kind: 'invalid-helper'; name: string }
146
+ | { kind: 'auth'; mode: CredentialHelperMode }
147
+ | { kind: 'helper'; mode: CredentialHelperMode };
143
148
 
144
- function classifyFailure(stderr: string, approvedHelpers: boolean): FailureKind | null {
149
+ const INVALID_HELPER_RE = /git: 'credential-([^']+)' is not a git command/i;
150
+
151
+ /**
152
+ * 從 allowlist 推導模式:未提供時視為 approved(既有 caller 語意);
153
+ * 空 allowlist → none。
154
+ */
155
+ function modeOf(trust: AcquisitionTrustOptions | undefined): CredentialHelperMode {
156
+ if (trust?.helperMode) return trust.helperMode;
157
+ return (trust?.allowedCredentialHelpers?.length ?? 0) > 0 ? 'approved' : 'none';
158
+ }
159
+
160
+ function classifyFailure(stderr: string, mode: CredentialHelperMode): FailureKind | null {
145
161
  const lower = stderr.toLowerCase();
146
162
  if (lower.includes('host key verification failed') || lower.includes('unknown host key') || lower.includes('offending')) {
147
163
  return { kind: 'host-key', isChanged: lower.includes('changed') || lower.includes('offending') || lower.includes('key changed') };
@@ -152,8 +168,12 @@ function classifyFailure(stderr: string, approvedHelpers: boolean): FailureKind
152
168
  if (lower.includes('repository not found') || lower.includes('repo not found') || lower.includes('does not appear to be a git repository') || lower.includes("' not found")) {
153
169
  return { kind: 'not-found' };
154
170
  }
171
+ const invalidMatch = stderr.match(INVALID_HELPER_RE);
172
+ if (invalidMatch && (lower.includes('credentials') || lower.includes('credential-'))) {
173
+ return { kind: 'invalid-helper', name: invalidMatch[1] };
174
+ }
155
175
  if (lower.includes('authentication failed')) {
156
- return { kind: 'auth', approved: approvedHelpers };
176
+ return { kind: 'auth', mode };
157
177
  }
158
178
  if (
159
179
  lower.includes('could not read username') ||
@@ -161,7 +181,7 @@ function classifyFailure(stderr: string, approvedHelpers: boolean): FailureKind
161
181
  lower.includes('terminal prompts disabled') ||
162
182
  lower.includes('credential')
163
183
  ) {
164
- return { kind: 'helper', approved: approvedHelpers };
184
+ return { kind: 'helper', mode };
165
185
  }
166
186
  return null;
167
187
  }
@@ -186,11 +206,27 @@ function failureFinding(kind: FailureKind, locator: CanonicalGitLocator, stderr:
186
206
  RULE.GIT_TRUST_AUTH_REQUIRED,
187
207
  `Acquisition Trust Base violation: repository not found — '${locator.canonicalUrl}' does not exist (check the URL or owner/repo name) — ${stderr.trim()}`,
188
208
  );
209
+ case 'invalid-helper': {
210
+ // GIT-35:核准的 helper 名稱無效(git 找不到 `git-credential-<name>` 執行檔)。
211
+ // 典型:使用者直覺設 `PI_CODEX_MARKETPLACE_CREDENTIAL_HELPERS=gh`,git 抱怨
212
+ // `credential-gh` is not a git command;正確寫法是 `!gh auth git-credential`。
213
+ return trustFinding(
214
+ CODE.GIT_TRUST_CREDENTIAL_HELPER_INVALID,
215
+ RULE.GIT_TRUST_CREDENTIAL_HELPER_INVALID,
216
+ `Acquisition Trust Base violation: '${kind.name}' is not a valid git credential helper — use a native helper name (osxkeychain / store) or a shell form like '!gh auth git-credential' — ${stderr.trim()}`,
217
+ );
218
+ }
189
219
  case 'auth': {
190
- // 伺服器 401:訊息依核准狀態分兩變體(未核准 指出 credential-free 且需認證,指引核准或 SSH;已核准仍 401 → 指引檢查登入)。
191
- const why = kind.approved
192
- ? `approved credential helper did not provide valid credentials — check your login with 'gh auth status' or your keychain`
193
- : `repository requires authentication (private or nonexistent) and this acquisition is credential-free approve a credential helper via ${CREDENTIAL_HELPERS_ENV}, or switch to an SSH locator`;
220
+ // 伺服器 401:訊息依 helper 模式分三變體(#117):
221
+ // - none:本機也偵測不到任何憑證來源 → 指引設 env 或改 SSH。
222
+ // - detected:自動偵測白名單被伺服器拒絕 指引檢查登入,或手動核准其他 helper。
223
+ // - approved:顯式核准仍 401 指引檢查登入。
224
+ const why =
225
+ kind.mode === 'none'
226
+ ? `repository requires authentication (private or nonexistent); no credential source was detected on this machine (gh CLI / macOS keychain / git credential-store) and this acquisition is credential-free — approve a credential helper via ${CREDENTIAL_HELPERS_ENV}, or switch to an SSH locator`
227
+ : kind.mode === 'detected'
228
+ ? `repository requires authentication; the credential sources auto-detected on this machine (gh / macOS keychain / credential-store) were rejected by the server — check your login with 'gh auth status' or your keychain, or set ${CREDENTIAL_HELPERS_ENV} to approve a different helper`
229
+ : `approved credential helper did not provide valid credentials — check your login with 'gh auth status' or your keychain`;
194
230
  return trustFinding(
195
231
  CODE.GIT_TRUST_AUTH_REQUIRED,
196
232
  RULE.GIT_TRUST_AUTH_REQUIRED,
@@ -199,10 +235,11 @@ function failureFinding(kind: FailureKind, locator: CanonicalGitLocator, stderr:
199
235
  }
200
236
  case 'helper': {
201
237
  // GIT-33 保留:credential source 拒絕(could not read Username/Password 等原字串匹配);
202
- // 訊息依核准狀態分兩變體,未核准時維持原「not approved」措辭。
203
- const why = kind.approved
204
- ? `approved credential helper failed to supply credentials — check your login with 'gh auth status' or your keychain`
205
- : `credential helper/agent not approved — set ${CREDENTIAL_HELPERS_ENV} to approve one, or use SSH`;
238
+ // 訊息依模式分變體:none 維持原「not approved」措辭。
239
+ const why =
240
+ kind.mode === 'none'
241
+ ? `credential helper/agent not approved — set ${CREDENTIAL_HELPERS_ENV} to approve one, or use SSH`
242
+ : `approved or auto-detected credential helper failed to supply credentials — check your login with 'gh auth status' or your keychain`;
206
243
  return trustFinding(
207
244
  CODE.GIT_TRUST_CREDENTIAL_HELPER,
208
245
  RULE.GIT_TRUST_CREDENTIAL_HELPER,
@@ -217,12 +254,12 @@ async function resolveHead(
217
254
  executor: GitExecutor,
218
255
  env: Record<string, string>,
219
256
  configArgs: string[],
220
- approvedHelpers: boolean,
257
+ mode: CredentialHelperMode,
221
258
  ): Promise<{ ok: true; sha: string } | { ok: false; findings: ValidationFinding[]; stderr?: string }> {
222
259
  const lsArgs = [...configArgs, 'ls-remote', locator.canonicalUrl, 'HEAD'];
223
260
  const res = await executor(lsArgs, { env });
224
261
  if (res.exitCode !== 0) {
225
- const kind = classifyFailure(res.stderr || '', approvedHelpers);
262
+ const kind = classifyFailure(res.stderr || '', mode);
226
263
  if (kind) {
227
264
  return {
228
265
  ok: false,
@@ -268,8 +305,8 @@ export async function resolveGitRevision(
268
305
  ): Promise<{ ok: true; sha: string } | { ok: false; findings: ValidationFinding[]; stderr?: string }> {
269
306
  const env = hardenedEnv(opts.trust, locator);
270
307
  const configArgs = hardenedConfigArgs(opts.trust);
271
- const approvedHelpers = (opts.trust?.allowedCredentialHelpers?.length ?? 0) > 0;
272
- return resolveHead(locator, opts.executor ?? defaultGitExecutor(), env, configArgs, approvedHelpers);
308
+ const mode = modeOf(opts.trust);
309
+ return resolveHead(locator, opts.executor ?? defaultGitExecutor(), env, configArgs, mode);
273
310
  }
274
311
 
275
312
  /**
@@ -281,8 +318,9 @@ export async function acquireGitSource(opts: AcquireOptions): Promise<AcquireRes
281
318
  const trust = opts.trust;
282
319
  const env = hardenedEnv(trust, locator);
283
320
  const configArgs = hardenedConfigArgs(trust);
321
+ const mode = modeOf(trust);
284
322
 
285
- const resolved = await resolveHead(locator, executor, env, configArgs, (trust?.allowedCredentialHelpers?.length ?? 0) > 0);
323
+ const resolved = await resolveHead(locator, executor, env, configArgs, mode);
286
324
  if (!resolved.ok) {
287
325
  return { ok: false, findings: (resolved as { findings: ValidationFinding[] }).findings, stderr: (resolved as { stderr?: string }).stderr };
288
326
  }
@@ -301,7 +339,7 @@ export async function acquireGitSource(opts: AcquireOptions): Promise<AcquireRes
301
339
  const cloneRes = await executor(cloneArgs, { env });
302
340
  if (cloneRes.exitCode !== 0) {
303
341
  const stderr = cloneRes.stderr || '';
304
- const kind = classifyFailure(stderr, (trust?.allowedCredentialHelpers?.length ?? 0) > 0);
342
+ const kind = classifyFailure(stderr, mode);
305
343
  const finding = kind
306
344
  ? failureFinding(kind, locator, stderr)
307
345
  : acquireFinding(`git clone failed: ${stderr.trim() || `exit ${cloneRes.exitCode}`}`);