agent-mcp-hub 0.5.2
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/LICENSE +373 -0
- package/README.md +246 -0
- package/dist/adapters/claude.d.ts +2 -0
- package/dist/adapters/claude.js +15 -0
- package/dist/adapters/claude.js.map +1 -0
- package/dist/adapters/codex.d.ts +2 -0
- package/dist/adapters/codex.js +16 -0
- package/dist/adapters/codex.js.map +1 -0
- package/dist/adapters/cursor.d.ts +2 -0
- package/dist/adapters/cursor.js +33 -0
- package/dist/adapters/cursor.js.map +1 -0
- package/dist/adapters/opencode.d.ts +2 -0
- package/dist/adapters/opencode.js +23 -0
- package/dist/adapters/opencode.js.map +1 -0
- package/dist/ansi.d.ts +5 -0
- package/dist/ansi.js +15 -0
- package/dist/ansi.js.map +1 -0
- package/dist/confirm.d.ts +36 -0
- package/dist/confirm.js +65 -0
- package/dist/confirm.js.map +1 -0
- package/dist/exec.d.ts +146 -0
- package/dist/exec.js +494 -0
- package/dist/exec.js.map +1 -0
- package/dist/failure.d.ts +27 -0
- package/dist/failure.js +210 -0
- package/dist/failure.js.map +1 -0
- package/dist/git.d.ts +59 -0
- package/dist/git.js +127 -0
- package/dist/git.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/registry.d.ts +70 -0
- package/dist/registry.js +159 -0
- package/dist/registry.js.map +1 -0
- package/dist/server.d.ts +15 -0
- package/dist/server.js +509 -0
- package/dist/server.js.map +1 -0
- package/dist/types.d.ts +43 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +75 -0
package/dist/failure.js
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { AgentStalledError, InvalidCwdError, OutputLimitError, ServerBusyError, SpawnError, TimeoutError, } from "./exec.js";
|
|
2
|
+
import { stripAnsi } from "./ansi.js";
|
|
3
|
+
export { stripAnsi } from "./ansi.js";
|
|
4
|
+
/** Lowercased, ANSI-free view of `s` used for phrase matching (never shown). */
|
|
5
|
+
export function normalize(s) {
|
|
6
|
+
return stripAnsi(s).toLowerCase();
|
|
7
|
+
}
|
|
8
|
+
/** Upstream-overload markers — checked FIRST so they never become auth. */
|
|
9
|
+
const BUSY_PHRASES = ["rate limit", "429", "overloaded", "try again later", "503", "server busy"];
|
|
10
|
+
/**
|
|
11
|
+
* High-confidence, adapter-specific auth phrases (R10). Only these trigger
|
|
12
|
+
* `not_authenticated`; broad HTTP/token markers (`401`, `unauthorized`, …) are
|
|
13
|
+
* deliberately absent so a bare log line like `HTTP 401 GET /x` stays a
|
|
14
|
+
* `tool_failure`.
|
|
15
|
+
*/
|
|
16
|
+
const PRIMARY_AUTH_PHRASES = [
|
|
17
|
+
"not logged in",
|
|
18
|
+
"not authenticated",
|
|
19
|
+
"sign in",
|
|
20
|
+
"sign-in",
|
|
21
|
+
"please log in",
|
|
22
|
+
"please run /login",
|
|
23
|
+
"login required",
|
|
24
|
+
"authentication required",
|
|
25
|
+
"no credentials",
|
|
26
|
+
"no api key",
|
|
27
|
+
"set openai_api_key",
|
|
28
|
+
"set anthropic_api_key",
|
|
29
|
+
"invalid api key",
|
|
30
|
+
"api key not found",
|
|
31
|
+
"press any key to sign in",
|
|
32
|
+
// NB: only the specific "please run /login" (above) — bare "please run" is too
|
|
33
|
+
// broad (e.g. "please run `npm install`" is NOT an auth failure).
|
|
34
|
+
"auth login",
|
|
35
|
+
];
|
|
36
|
+
/** Quota/billing is a spend problem, never an auth problem — suppresses auth. */
|
|
37
|
+
const QUOTA_PHRASES = ["insufficient_quota", "billing"];
|
|
38
|
+
const CONFIG_PHRASES = [
|
|
39
|
+
"not configured",
|
|
40
|
+
"no default model",
|
|
41
|
+
"no model",
|
|
42
|
+
"no provider",
|
|
43
|
+
"missing config",
|
|
44
|
+
"model not found",
|
|
45
|
+
];
|
|
46
|
+
const CAUSE_MAX = 160;
|
|
47
|
+
const TAIL_MAX = 500;
|
|
48
|
+
/** Clip to the first `max` chars, appending an ellipsis marker when truncated. */
|
|
49
|
+
function clipHead(s, max) {
|
|
50
|
+
return s.length <= max ? s : s.slice(0, max) + "…";
|
|
51
|
+
}
|
|
52
|
+
/** Clip to the last `max` chars, prefixing an ellipsis marker when truncated. */
|
|
53
|
+
function clipTail(s, max) {
|
|
54
|
+
return s.length <= max ? s : "…" + s.slice(s.length - max);
|
|
55
|
+
}
|
|
56
|
+
/** First non-empty ANSI-stripped line of stderr (falling back to stdout), clipped. */
|
|
57
|
+
function causeSnippet(result) {
|
|
58
|
+
const source = result.stderr.trim().length > 0 ? result.stderr : result.stdout;
|
|
59
|
+
const line = stripAnsi(source)
|
|
60
|
+
.split("\n")
|
|
61
|
+
.map((l) => l.trim())
|
|
62
|
+
.find((l) => l.length > 0);
|
|
63
|
+
return clipHead(line ?? "", CAUSE_MAX);
|
|
64
|
+
}
|
|
65
|
+
/** Trimmed, ANSI-stripped stderr/stdout tail for tool_failure bodies. */
|
|
66
|
+
function outputTail(result) {
|
|
67
|
+
const source = result.stderr.trim().length > 0 ? result.stderr : result.stdout;
|
|
68
|
+
const cleaned = stripAnsi(source).trim();
|
|
69
|
+
return cleaned.length === 0 ? "(no output)" : clipTail(cleaned, TAIL_MAX);
|
|
70
|
+
}
|
|
71
|
+
/** `Fix: run \`<loginCommand>\`` plus an optional `(or set <ENV>)` clause. */
|
|
72
|
+
function remediation(adapter) {
|
|
73
|
+
const env = adapter.apiKeyEnv ? ` (or set ${adapter.apiKeyEnv})` : "";
|
|
74
|
+
return `Fix: run \`${adapter.loginCommand}\`${env}.`;
|
|
75
|
+
}
|
|
76
|
+
function classifyError(adapter, error) {
|
|
77
|
+
// Before SpawnError: a missing cwd also surfaces as ENOENT, and reporting it as
|
|
78
|
+
// "not installed" sends the caller after the wrong fault entirely.
|
|
79
|
+
if (error instanceof InvalidCwdError) {
|
|
80
|
+
return {
|
|
81
|
+
code: "invalid_cwd",
|
|
82
|
+
message: `${adapter.name} failed: cwd is not a directory this server can see.\n` +
|
|
83
|
+
`${error.cwd}\n` +
|
|
84
|
+
`Fix: pass an absolute path that exists on this machine — the path must be ` +
|
|
85
|
+
`resolvable by the server process itself.`,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
if (error instanceof SpawnError) {
|
|
89
|
+
return {
|
|
90
|
+
code: "not_installed",
|
|
91
|
+
message: `${adapter.name} failed: not installed.\n` +
|
|
92
|
+
`${adapter.name} was not found on PATH (spawn failed).\n` +
|
|
93
|
+
`Fix: install the CLI and ensure it is on PATH.`,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
if (error instanceof ServerBusyError) {
|
|
97
|
+
return {
|
|
98
|
+
code: "server_busy",
|
|
99
|
+
message: `${adapter.name} failed: server busy.\n` +
|
|
100
|
+
`The agent pool/queue is full.\n` +
|
|
101
|
+
`Fix: retry shortly.`,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
if (error instanceof AgentStalledError) {
|
|
105
|
+
// Matched BEFORE TimeoutError: a stalled agent is a network-path problem, not
|
|
106
|
+
// a "wait longer" problem. The message names the agent, quotes the matched
|
|
107
|
+
// signature, points at the common cause (TLS-intercepting proxy), and tells
|
|
108
|
+
// the caller to treat the agent as unavailable. Never suggests raising the
|
|
109
|
+
// timeout — that is what the timed_out message wrongly says.
|
|
110
|
+
return {
|
|
111
|
+
code: "stream_stalled",
|
|
112
|
+
message: `${adapter.name} failed: stream stalled.\n` +
|
|
113
|
+
`Detected diagnostic reconnect pattern: "${error.signature}" (strike ${error.strikes}).\n` +
|
|
114
|
+
`The agent cannot complete a run in this environment — common cause is a TLS-intercepting ` +
|
|
115
|
+
`proxy or a network path that drops the agent's backend connection.\n` +
|
|
116
|
+
`Treat ${adapter.name} as unavailable until the network path is fixed.`,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
if (error instanceof TimeoutError) {
|
|
120
|
+
// Split by WHICH cap fired: an idle timeout means the agent went silent
|
|
121
|
+
// (likely hung or its backend is unreachable) — raising the cap won't help,
|
|
122
|
+
// so we point at the CLI/model/connection instead. A total timeout means a
|
|
123
|
+
// genuinely long run outgrew the runtime cap, so raising it is the fix.
|
|
124
|
+
if (error.kind === "idle") {
|
|
125
|
+
return {
|
|
126
|
+
code: "timed_out",
|
|
127
|
+
message: `${adapter.name} failed: no output (idle timeout).\n` +
|
|
128
|
+
`The agent produced no output for ${error.timeoutMs}ms — it may be hung, ` +
|
|
129
|
+
`or its model/backend is unreachable.\n` +
|
|
130
|
+
`Fix: check the ${adapter.name} CLI, its model, and the network connection.`,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
code: "timed_out",
|
|
135
|
+
message: `${adapter.name} failed: timed out after ${error.timeoutMs}ms.\n` +
|
|
136
|
+
`The total runtime cap was exceeded before the agent finished.\n` +
|
|
137
|
+
`Fix: raise timeoutMs (or MCP_AGENT_TIMEOUT_MS), or check the agent/model is responsive.`,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
if (error instanceof OutputLimitError) {
|
|
141
|
+
return {
|
|
142
|
+
code: "output_limit",
|
|
143
|
+
message: `${adapter.name} failed: output limit exceeded.\n` +
|
|
144
|
+
`The agent produced more than ${error.maxOutputBytes} bytes.\n` +
|
|
145
|
+
`Fix: narrow the prompt, or raise maxOutputBytes.`,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
// Default class — every thrown error (incl. the opencode dash-guard) lands here.
|
|
149
|
+
const raw = error instanceof Error ? error.message : String(error);
|
|
150
|
+
return {
|
|
151
|
+
code: "tool_failure",
|
|
152
|
+
message: `${adapter.name} failed:\n${clipHead(stripAnsi(raw).trim(), TAIL_MAX)}`,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
function classifyResult(adapter, result) {
|
|
156
|
+
const text = normalize(`${result.stderr}\n${result.stdout}`);
|
|
157
|
+
// 1. Overload first: these must never be read as auth.
|
|
158
|
+
if (BUSY_PHRASES.some((p) => text.includes(p))) {
|
|
159
|
+
return {
|
|
160
|
+
code: "server_busy",
|
|
161
|
+
message: `${adapter.name} failed: server busy.\n` +
|
|
162
|
+
`${causeSnippet(result)}\n` +
|
|
163
|
+
`Fix: retry shortly.`,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
// 2. Auth — precision over recall: a primary phrase is required, and quota
|
|
167
|
+
// always wins (it is a spend problem, not an auth problem).
|
|
168
|
+
const hasPrimaryAuth = PRIMARY_AUTH_PHRASES.some((p) => text.includes(p));
|
|
169
|
+
const hasQuota = QUOTA_PHRASES.some((p) => text.includes(p));
|
|
170
|
+
if (hasPrimaryAuth && !hasQuota) {
|
|
171
|
+
return {
|
|
172
|
+
code: "not_authenticated",
|
|
173
|
+
message: `${adapter.name} failed: not authenticated.\n` +
|
|
174
|
+
`${causeSnippet(result)}\n` +
|
|
175
|
+
remediation(adapter),
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
// 3. Configuration.
|
|
179
|
+
if (CONFIG_PHRASES.some((p) => text.includes(p))) {
|
|
180
|
+
return {
|
|
181
|
+
code: "not_configured",
|
|
182
|
+
message: `${adapter.name} failed: not configured.\n` +
|
|
183
|
+
`${causeSnippet(result)}\n` +
|
|
184
|
+
`Fix: set a model/provider (run \`${adapter.loginCommand}\` or configure the CLI)` +
|
|
185
|
+
`${adapter.apiKeyEnv ? ` — or set ${adapter.apiKeyEnv}` : ""}.`,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
// 4. Default.
|
|
189
|
+
return {
|
|
190
|
+
code: "tool_failure",
|
|
191
|
+
message: `${adapter.name} failed (exit ${result.exitCode ?? "unknown"}).\n${outputTail(result)}`,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Map any agent failure to a stable `{ code, message }` with clean, ANSI-free,
|
|
196
|
+
* actionable text. Pure — no I/O. Precedence (R3): the error branch (classified
|
|
197
|
+
* by TYPE) takes priority over the result branch; within the result branch,
|
|
198
|
+
* overload → auth → config → generic tool_failure.
|
|
199
|
+
*/
|
|
200
|
+
export function classifyFailure(adapter, outcome) {
|
|
201
|
+
if (outcome.error !== undefined && outcome.error !== null) {
|
|
202
|
+
return classifyError(adapter, outcome.error);
|
|
203
|
+
}
|
|
204
|
+
if (outcome.result) {
|
|
205
|
+
return classifyResult(adapter, outcome.result);
|
|
206
|
+
}
|
|
207
|
+
// Neither supplied — defensive fallback, should not happen in practice.
|
|
208
|
+
return { code: "tool_failure", message: `${adapter.name} failed.\n(no output)` };
|
|
209
|
+
}
|
|
210
|
+
//# sourceMappingURL=failure.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"failure.js","sourceRoot":"","sources":["../src/failure.ts"],"names":[],"mappings":"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"}
|
package/dist/git.d.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { Exec } from "./exec.js";
|
|
2
|
+
/**
|
|
3
|
+
* Hardening for running git against an UNTRUSTED working tree. git config is a
|
|
4
|
+
* code-execution surface: a hostile `.git/config` / `.gitattributes` in the repo
|
|
5
|
+
* being inspected can make a plain `git diff`/`git status` run an arbitrary
|
|
6
|
+
* command (CVE-2025-27613/27614 family). Every git call below goes through
|
|
7
|
+
* `git()`, which neutralises the known execution vectors:
|
|
8
|
+
*
|
|
9
|
+
* - `-c core.fsmonitor=` / `-c core.hooksPath=/dev/null` — a command-line `-c`
|
|
10
|
+
* is the HIGHEST-precedence config source (above the repo's own `.git/config`),
|
|
11
|
+
* so this disables a hostile local fsmonitor/hook even though the local config
|
|
12
|
+
* is still read.
|
|
13
|
+
* - `GIT_CONFIG_NOSYSTEM=1`, `GIT_CONFIG_GLOBAL`/`GIT_CONFIG_SYSTEM=/dev/null` —
|
|
14
|
+
* ignore system/global config entirely.
|
|
15
|
+
* - `--no-pager` / `GIT_PAGER=cat`, `GIT_TERMINAL_PROMPT=0` — no pager, no prompt.
|
|
16
|
+
* - diff subcommands additionally pass `--no-ext-diff --no-textconv` (see
|
|
17
|
+
* `diffArgs`), disabling external-diff and textconv drivers from ANY config.
|
|
18
|
+
*
|
|
19
|
+
* Documented RESIDUAL: an attacker-named `filter.<driver>.clean` assigned via the
|
|
20
|
+
* repo's own `.gitattributes` cannot be disabled by a flag for a fully
|
|
21
|
+
* attacker-controlled `.git/`. Closing that requires operating on a clean clone
|
|
22
|
+
* (git's own guidance); it is out of scope for this in-place hardening and is
|
|
23
|
+
* tracked as a follow-up.
|
|
24
|
+
*/
|
|
25
|
+
export declare const GIT_HARDENING_ENV: Readonly<Record<string, string>>;
|
|
26
|
+
export declare const GIT_HARDENING_TOPLEVEL: string[];
|
|
27
|
+
/** The full argv a hardened git call uses for `subcommand` (exported for tests). */
|
|
28
|
+
export declare function hardenedGitArgs(subcommand: string[]): string[];
|
|
29
|
+
/** A newly-created file surfaced to the reviewer with its bounded contents. */
|
|
30
|
+
export interface UntrackedFile {
|
|
31
|
+
path: string;
|
|
32
|
+
/**
|
|
33
|
+
* The file as a git add-diff (leading `+`, `diff --git`/`Binary files …`
|
|
34
|
+
* headers), decoded as UTF-8 and capped at MAX_UNTRACKED_FILE_BYTES. A
|
|
35
|
+
* placeholder when the file could not be read.
|
|
36
|
+
*/
|
|
37
|
+
content: string;
|
|
38
|
+
/** True when `content` was cut at the byte cap OR the file was unreadable. */
|
|
39
|
+
truncated: boolean;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Per-file byte cap. A malicious or generated file must not blow up the review
|
|
43
|
+
* prompt, so contents past this point are dropped and `truncated` is set.
|
|
44
|
+
*/
|
|
45
|
+
export declare const MAX_UNTRACKED_FILE_BYTES: number;
|
|
46
|
+
/**
|
|
47
|
+
* Cap on how many untracked files we read contents for. Beyond this the review
|
|
48
|
+
* would be unusably large; the overflow is signalled via `untrackedTruncated`.
|
|
49
|
+
*/
|
|
50
|
+
export declare const MAX_UNTRACKED_FILES = 50;
|
|
51
|
+
export declare function isGitRepo(exec: Exec, cwd: string): Promise<boolean>;
|
|
52
|
+
export declare function worktreeDirty(exec: Exec, cwd: string): Promise<boolean>;
|
|
53
|
+
export declare function captureChange(exec: Exec, cwd: string): Promise<{
|
|
54
|
+
stat: string;
|
|
55
|
+
diff: string;
|
|
56
|
+
untracked: UntrackedFile[];
|
|
57
|
+
/** True when more than MAX_UNTRACKED_FILES exist; the overflow is not read. */
|
|
58
|
+
untrackedTruncated: boolean;
|
|
59
|
+
}>;
|
package/dist/git.js
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// Routes git AND file reads through the injected Exec — no direct process
|
|
2
|
+
// spawning and no node:fs, keeping this module free of I/O side effects (C1).
|
|
3
|
+
/**
|
|
4
|
+
* Hardening for running git against an UNTRUSTED working tree. git config is a
|
|
5
|
+
* code-execution surface: a hostile `.git/config` / `.gitattributes` in the repo
|
|
6
|
+
* being inspected can make a plain `git diff`/`git status` run an arbitrary
|
|
7
|
+
* command (CVE-2025-27613/27614 family). Every git call below goes through
|
|
8
|
+
* `git()`, which neutralises the known execution vectors:
|
|
9
|
+
*
|
|
10
|
+
* - `-c core.fsmonitor=` / `-c core.hooksPath=/dev/null` — a command-line `-c`
|
|
11
|
+
* is the HIGHEST-precedence config source (above the repo's own `.git/config`),
|
|
12
|
+
* so this disables a hostile local fsmonitor/hook even though the local config
|
|
13
|
+
* is still read.
|
|
14
|
+
* - `GIT_CONFIG_NOSYSTEM=1`, `GIT_CONFIG_GLOBAL`/`GIT_CONFIG_SYSTEM=/dev/null` —
|
|
15
|
+
* ignore system/global config entirely.
|
|
16
|
+
* - `--no-pager` / `GIT_PAGER=cat`, `GIT_TERMINAL_PROMPT=0` — no pager, no prompt.
|
|
17
|
+
* - diff subcommands additionally pass `--no-ext-diff --no-textconv` (see
|
|
18
|
+
* `diffArgs`), disabling external-diff and textconv drivers from ANY config.
|
|
19
|
+
*
|
|
20
|
+
* Documented RESIDUAL: an attacker-named `filter.<driver>.clean` assigned via the
|
|
21
|
+
* repo's own `.gitattributes` cannot be disabled by a flag for a fully
|
|
22
|
+
* attacker-controlled `.git/`. Closing that requires operating on a clean clone
|
|
23
|
+
* (git's own guidance); it is out of scope for this in-place hardening and is
|
|
24
|
+
* tracked as a follow-up.
|
|
25
|
+
*/
|
|
26
|
+
export const GIT_HARDENING_ENV = {
|
|
27
|
+
GIT_CONFIG_NOSYSTEM: "1",
|
|
28
|
+
GIT_CONFIG_GLOBAL: "/dev/null",
|
|
29
|
+
GIT_CONFIG_SYSTEM: "/dev/null",
|
|
30
|
+
GIT_PAGER: "cat",
|
|
31
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
32
|
+
};
|
|
33
|
+
export const GIT_HARDENING_TOPLEVEL = [
|
|
34
|
+
"-c",
|
|
35
|
+
"core.fsmonitor=",
|
|
36
|
+
"-c",
|
|
37
|
+
"core.hooksPath=/dev/null",
|
|
38
|
+
"--no-pager",
|
|
39
|
+
];
|
|
40
|
+
/** The full argv a hardened git call uses for `subcommand` (exported for tests). */
|
|
41
|
+
export function hardenedGitArgs(subcommand) {
|
|
42
|
+
return [...GIT_HARDENING_TOPLEVEL, ...subcommand];
|
|
43
|
+
}
|
|
44
|
+
/** Run a read-only git subcommand hardened against the worktree's own config. */
|
|
45
|
+
function git(exec, cwd, subcommand, extra) {
|
|
46
|
+
return exec("git", hardenedGitArgs(subcommand), {
|
|
47
|
+
cwd,
|
|
48
|
+
env: GIT_HARDENING_ENV,
|
|
49
|
+
...extra,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
/** Diff args with the external-diff and textconv execution vectors disabled. */
|
|
53
|
+
function diffArgs(...rest) {
|
|
54
|
+
return ["diff", "--no-ext-diff", "--no-textconv", ...rest];
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Per-file byte cap. A malicious or generated file must not blow up the review
|
|
58
|
+
* prompt, so contents past this point are dropped and `truncated` is set.
|
|
59
|
+
*/
|
|
60
|
+
export const MAX_UNTRACKED_FILE_BYTES = 64 * 1024;
|
|
61
|
+
/**
|
|
62
|
+
* Cap on how many untracked files we read contents for. Beyond this the review
|
|
63
|
+
* would be unusably large; the overflow is signalled via `untrackedTruncated`.
|
|
64
|
+
*/
|
|
65
|
+
export const MAX_UNTRACKED_FILES = 50;
|
|
66
|
+
export async function isGitRepo(exec, cwd) {
|
|
67
|
+
const result = await git(exec, cwd, ["rev-parse", "--is-inside-work-tree"]);
|
|
68
|
+
return result.exitCode === 0 && result.stdout.trim() === "true";
|
|
69
|
+
}
|
|
70
|
+
export async function worktreeDirty(exec, cwd) {
|
|
71
|
+
const result = await git(exec, cwd, ["status", "--porcelain"]);
|
|
72
|
+
return result.stdout.length > 0;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Read one untracked file's contents through the injected Exec, bounded to
|
|
76
|
+
* MAX_UNTRACKED_FILE_BYTES. `git diff --no-index -- /dev/null <path>` prints the
|
|
77
|
+
* file as an addition diff and, crucially, collapses BINARY files to a short
|
|
78
|
+
* "Binary files … differ" line — so a binary blob can never flood the prompt.
|
|
79
|
+
* git exits 1 when a diff exists (the normal case) and 0 for an empty file; any
|
|
80
|
+
* other code means it could not read the file. Surfacing that as an explicit
|
|
81
|
+
* placeholder (truncated) is safer than a silent empty body, which a reviewer
|
|
82
|
+
* would read as "nothing to see here".
|
|
83
|
+
*/
|
|
84
|
+
async function readUntracked(exec, cwd, path) {
|
|
85
|
+
// `--` fences `path` as a positional, so a path starting with `-` is safe; the
|
|
86
|
+
// hardened `git()` disables ext-diff/textconv so reading the file can't execute.
|
|
87
|
+
const result = await git(exec, cwd, diffArgs("--no-index", "--", "/dev/null", path));
|
|
88
|
+
if (result.exitCode !== 0 && result.exitCode !== 1) {
|
|
89
|
+
return { path, content: "(unreadable — git could not read this file)", truncated: true };
|
|
90
|
+
}
|
|
91
|
+
const raw = result.stdout;
|
|
92
|
+
const bytes = Buffer.from(raw, "utf8");
|
|
93
|
+
if (bytes.length > MAX_UNTRACKED_FILE_BYTES) {
|
|
94
|
+
return {
|
|
95
|
+
path,
|
|
96
|
+
content: bytes.subarray(0, MAX_UNTRACKED_FILE_BYTES).toString("utf8"),
|
|
97
|
+
truncated: true,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
return { path, content: raw, truncated: false };
|
|
101
|
+
}
|
|
102
|
+
export async function captureChange(exec, cwd) {
|
|
103
|
+
const statResult = await git(exec, cwd, diffArgs("--stat", "HEAD"));
|
|
104
|
+
const diffResult = await git(exec, cwd, diffArgs("HEAD"));
|
|
105
|
+
// `-z`: NUL-delimited, verbatim paths. Splitting on "\n" + trim would drop or
|
|
106
|
+
// corrupt filenames containing newlines/leading/trailing whitespace, letting a
|
|
107
|
+
// runner hide a payload file from the review.
|
|
108
|
+
const untrackedResult = await git(exec, cwd, [
|
|
109
|
+
"ls-files",
|
|
110
|
+
"--others",
|
|
111
|
+
"--exclude-standard",
|
|
112
|
+
"-z",
|
|
113
|
+
]);
|
|
114
|
+
const paths = untrackedResult.stdout.split("\0").filter((p) => p.length > 0);
|
|
115
|
+
const untrackedTruncated = paths.length > MAX_UNTRACKED_FILES;
|
|
116
|
+
const untracked = [];
|
|
117
|
+
for (const path of paths.slice(0, MAX_UNTRACKED_FILES)) {
|
|
118
|
+
untracked.push(await readUntracked(exec, cwd, path));
|
|
119
|
+
}
|
|
120
|
+
return {
|
|
121
|
+
stat: statResult.stdout,
|
|
122
|
+
diff: diffResult.stdout,
|
|
123
|
+
untracked,
|
|
124
|
+
untrackedTruncated,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
//# sourceMappingURL=git.js.map
|
package/dist/git.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"git.js","sourceRoot":"","sources":["../src/git.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { enabledAdapters } from "./registry.js";
|
|
4
|
+
import { buildServer } from "./server.js";
|
|
5
|
+
async function main() {
|
|
6
|
+
const server = buildServer(enabledAdapters());
|
|
7
|
+
await server.connect(new StdioServerTransport());
|
|
8
|
+
// stdio server stays alive until the client closes the pipe
|
|
9
|
+
}
|
|
10
|
+
main().catch((err) => {
|
|
11
|
+
console.error("agent-mcp-hub fatal:", err);
|
|
12
|
+
process.exit(1);
|
|
13
|
+
});
|
|
14
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE1C,KAAK,UAAU,IAAI;IACjB,MAAM,MAAM,GAAG,WAAW,CAAC,eAAe,EAAE,CAAC,CAAC;IAC9C,MAAM,MAAM,CAAC,OAAO,CAAC,IAAI,oBAAoB,EAAE,CAAC,CAAC;IACjD,4DAA4D;AAC9D,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,GAAY,EAAE,EAAE;IAC5B,OAAO,CAAC,KAAK,CAAC,sBAAsB,EAAE,GAAG,CAAC,CAAC;IAC3C,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { Exec } from "./exec.js";
|
|
2
|
+
import type { AgentAdapter } from "./types.js";
|
|
3
|
+
export declare function allAdapters(): AgentAdapter[];
|
|
4
|
+
/**
|
|
5
|
+
* Union of every adapter's credential env var, DERIVED from the adapter list so a
|
|
6
|
+
* new agent's key is covered automatically — there is no hand-maintained
|
|
7
|
+
* duplicate to forget (forgetting one silently leaked the new key to every other
|
|
8
|
+
* agent). This is the set of secrets that must be isolated between agents.
|
|
9
|
+
*/
|
|
10
|
+
export declare function allCredentialEnvVars(): readonly string[];
|
|
11
|
+
/**
|
|
12
|
+
* The credential keys to STRIP from `adapter`'s child environment: every OTHER
|
|
13
|
+
* agent's key. An adapter with its own `apiKeyEnv` keeps exactly that one. A
|
|
14
|
+
* provider-agnostic adapter (no `apiKeyEnv`, e.g. opencode) owns none, so ALL
|
|
15
|
+
* tracked keys are stripped — it must never inherit a sibling's secret. Applied
|
|
16
|
+
* to real runs AND to availability probes, so no spawn path leaks a key.
|
|
17
|
+
*/
|
|
18
|
+
export declare function credentialStripKeys(adapter: AgentAdapter): readonly string[];
|
|
19
|
+
/**
|
|
20
|
+
* Selects which adapters to expose based on MCP_AGENTS (comma-separated,
|
|
21
|
+
* whitespace-tolerant, case-sensitive). Unset or empty-after-parse yields all
|
|
22
|
+
* adapters (never an empty server). Unknown names fail fast with an actionable
|
|
23
|
+
* message so typos are surfaced at wiring time rather than silently dropped.
|
|
24
|
+
*/
|
|
25
|
+
export declare function enabledAdapters(agentsSpec?: string | undefined): AgentAdapter[];
|
|
26
|
+
/**
|
|
27
|
+
* Availability has THREE independent axes, and collapsing them is how a broken
|
|
28
|
+
* agent passes for a healthy one:
|
|
29
|
+
*
|
|
30
|
+
* - `installed` — the binary resolves on PATH with the exec bit. A filesystem
|
|
31
|
+
* fact; never inferred from running the binary.
|
|
32
|
+
* - `usable` — a probe actually succeeded. `available` mirrors this and is kept
|
|
33
|
+
* for callers written against the old boolean shape.
|
|
34
|
+
* - `reason` — why not, when not.
|
|
35
|
+
*
|
|
36
|
+
* The old implementation asked `--version` the availability question and trusted
|
|
37
|
+
* its exit code. `codex --version` exits 0 while printing
|
|
38
|
+
* "WARNING: … could not create PATH aliases: Read-only file system", so a codex
|
|
39
|
+
* that could not run a single task reported `available: true`. Exit codes are the
|
|
40
|
+
* weakest possible evidence; the output is where the truth is.
|
|
41
|
+
*/
|
|
42
|
+
export interface AgentAvailability {
|
|
43
|
+
name: string;
|
|
44
|
+
installed: boolean;
|
|
45
|
+
usable: boolean;
|
|
46
|
+
/** Mirrors `usable`. Retained so existing callers keep working. */
|
|
47
|
+
available: boolean;
|
|
48
|
+
reason?: string;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Resolves a binary the way spawn will: absolute paths as-is, otherwise the first
|
|
52
|
+
* executable match walking PATH. Returns undefined when nothing matches, which is
|
|
53
|
+
* the ONLY thing that may set `installed: false`.
|
|
54
|
+
*/
|
|
55
|
+
export declare function resolveOnPath(binary: string, pathEnv?: string | undefined): string | undefined;
|
|
56
|
+
/**
|
|
57
|
+
* Identifier-shaped lines only. A CLI can exit 0 while emitting a banner, an
|
|
58
|
+
* update notice, or "you are not logged in" prose — none of which are ids. Lines
|
|
59
|
+
* containing whitespace are prose; lines without it are candidate ids.
|
|
60
|
+
*/
|
|
61
|
+
export declare function parseProbeIds(stdout: string): string[];
|
|
62
|
+
/** Injected so availability stays testable without depending on the host's PATH. */
|
|
63
|
+
export type ResolveBinary = (binary: string) => string | undefined;
|
|
64
|
+
/**
|
|
65
|
+
* Probes one adapter. Never throws: a probe failure is data, not an exception.
|
|
66
|
+
* The probe command is per-adapter because the CLIs disagree — `opencode models`
|
|
67
|
+
* lists models, `codex models` is not a subcommand at all — so there is no single
|
|
68
|
+
* command that proves usability across all four.
|
|
69
|
+
*/
|
|
70
|
+
export declare function checkAvailability(adapter: AgentAdapter, exec: Exec, resolve?: ResolveBinary): Promise<AgentAvailability>;
|