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
|
|
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
|
-
|
|
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
|
-
-
|
|
68
|
-
-
|
|
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
|
-
-
|
|
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.
|
|
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": [
|
package/src/bridge/command.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
//
|
|
604
|
-
//
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
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" —
|
|
8
|
-
*
|
|
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
|
|
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: '
|
|
142
|
-
| { kind: '
|
|
145
|
+
| { kind: 'invalid-helper'; name: string }
|
|
146
|
+
| { kind: 'auth'; mode: CredentialHelperMode }
|
|
147
|
+
| { kind: 'helper'; mode: CredentialHelperMode };
|
|
143
148
|
|
|
144
|
-
|
|
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',
|
|
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',
|
|
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
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
-
//
|
|
203
|
-
const why =
|
|
204
|
-
|
|
205
|
-
|
|
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
|
-
|
|
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 || '',
|
|
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
|
|
272
|
-
return resolveHead(locator, opts.executor ?? defaultGitExecutor(), env, configArgs,
|
|
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,
|
|
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,
|
|
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}`}`);
|