@bridge_gpt/mcp-server 0.2.68 → 0.2.70
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 +226 -320
- package/build/agent-launchers/claude-executor-adapter.js +17 -4
- package/build/agents.generated.js +4 -4
- package/build/artifact-diff.js +110 -0
- package/build/bin-entrypoint.js +36 -0
- package/build/bridge-permissions.js +650 -98
- package/build/check-update-cli.js +74 -0
- package/build/claude-review-workflow-drift.js +3 -3
- package/build/claude-settings.js +84 -2
- package/build/cli-help.js +7 -1
- package/build/codex-bridge-profile.js +1 -1
- package/build/codex-home.js +9 -1
- package/build/codex-launcher-pin.js +598 -0
- package/build/commands.generated.js +8 -8
- package/build/conductor/bridge-api-client.js +598 -19
- package/build/conductor/bring-up-facts.js +9 -9
- package/build/conductor/doctor.js +466 -18
- package/build/conductor/epic-child-continuation.js +54 -3
- package/build/conductor/epic-implementer-cli.js +132 -34
- package/build/conductor/epic-runtime.js +2 -2
- package/build/conductor/epic-state.js +1 -1
- package/build/conductor/hook-runtime-qualification.js +1 -1
- package/build/conductor/install-doctor.js +4 -2
- package/build/conductor/launch-need-probes.js +337 -0
- package/build/conductor/local-merge.js +241 -56
- package/build/conductor/merge-credential.js +172 -0
- package/build/conductor/merge-execution-eligibility.js +1 -1
- package/build/conductor/merged-child-check.js +95 -0
- package/build/conductor/plan-trim.js +30 -0
- package/build/conductor/pr-ci-producer.js +1 -1
- package/build/conductor/readiness-cli.js +11 -6
- package/build/conductor/readiness-prerequisites.js +41 -0
- package/build/conductor/readiness-sections.js +345 -55
- package/build/conductor/readiness.js +99 -87
- package/build/conductor/recovery-cli.js +606 -21
- package/build/conductor/recovery-operations.js +306 -7
- package/build/conductor/repair-contract.js +47 -2
- package/build/conductor/repository-workflow-remedy.js +8 -6
- package/build/conductor/stored-merge-credential.js +76 -0
- package/build/conductor/stored-merge-token-probe.js +28 -0
- package/build/conductor/temporary-plan.js +50 -0
- package/build/conductor/wait-run-cli.js +759 -0
- package/build/conductor/worker-database-doctor-inspector.js +49 -0
- package/build/conductor-bin.js +2 -2
- package/build/conductor-claude-hook-bin.js +2 -2
- package/build/conductor-claude-hook-removed-stub-bin.js +2 -5
- package/build/conductor-removed-stub-bin.js +2 -5
- package/build/connect-github-api.js +33 -14
- package/build/credential-store.js +356 -13
- package/build/docs.generated.js +27 -14
- package/build/doctor.js +203 -38
- package/build/drive-epic-help.js +212 -0
- package/build/drive-epic.js +1840 -373
- package/build/epic-ci-workflow-drift.js +77 -0
- package/build/epic-ci-workflow-install.js +183 -0
- package/build/epic-ci-workflow.js +157 -0
- package/build/epic-contract-context.js +326 -0
- package/build/epic-implementer-bin.js +131 -120
- package/build/epic-implementer-claude-hook-bin.js +2 -2
- package/build/escalation-reachability.js +134 -0
- package/build/executor/cleanup-report.js +99 -0
- package/build/executor/cli.js +12 -1
- package/build/executor/completion-contract.js +68 -11
- package/build/executor/completion-supervision.js +99 -0
- package/build/executor/delivery-outcome.js +179 -0
- package/build/executor/deps.js +3 -0
- package/build/executor/env.js +71 -0
- package/build/executor/failure-reason-codes.js +126 -0
- package/build/executor/heartbeat.js +38 -0
- package/build/executor/http-client.js +115 -4
- package/build/executor/integration-job.js +298 -5
- package/build/executor/job-errors.js +36 -6
- package/build/executor/job-runner.js +720 -36
- package/build/executor/likely-tests.js +36 -2
- package/build/executor/merge-job.js +145 -2
- package/build/executor/observation.js +73 -4
- package/build/executor/permissions.js +193 -97
- package/build/executor/preflight.js +14 -3
- package/build/executor/process-containment.js +28 -2
- package/build/executor/prompt-spec.js +50 -5
- package/build/executor/rebase-finalization.js +38 -3
- package/build/executor/reconcile-overlap-job.js +35 -7
- package/build/executor/required-work-ledger.js +236 -81
- package/build/executor/resume-pre-spawn.js +38 -1
- package/build/executor/runner.js +91 -3
- package/build/executor/test-clock.js +8 -0
- package/build/executor/types.js +37 -2
- package/build/executor/worker-cli-launcher.js +259 -0
- package/build/executor/worker-command.js +41 -4
- package/build/executor/worker-database-target.js +91 -0
- package/build/executor/worker-database.js +908 -0
- package/build/executor/worker-finalization.js +106 -0
- package/build/executor/worker-guard-hook.js +203 -0
- package/build/executor/worker-resources.js +47 -0
- package/build/executor/worker-verification-guidance.js +15 -2
- package/build/executor/worktree.js +303 -15
- package/build/executor-launch.js +11 -1
- package/build/generated-assets-doctor.js +9 -1
- package/build/generated-assets-health.js +13 -4
- package/build/generated-assets-legacy.js +39 -0
- package/build/generated-assets-paths.js +12 -1
- package/build/generated-assets-repair.js +4 -1
- package/build/generated-assets-test-support.js +12 -0
- package/build/github-remote.js +33 -0
- package/build/github-sign-in.js +62 -0
- package/build/index.js +1214 -465
- package/build/init.js +39 -1
- package/build/install-bridge-conductor-gate.js +78 -0
- package/build/install-bridge-conductor-setup.js +413 -0
- package/build/install-bridge-conductor.js +1226 -267
- package/build/install-bridge.js +629 -350
- package/build/launcher-config-inspection.js +184 -19
- package/build/mcp-host-targets.js +4 -0
- package/build/mcp-identity.js +7 -0
- package/build/mcp-install-state.js +8 -2
- package/build/merge-pull-request.js +6 -0
- package/build/merge-workflow-install.js +763 -345
- package/build/phase-result-artifacts.js +75 -20
- package/build/pipeline-orchestrator.js +247 -4
- package/build/pipelines.generated.js +35 -35
- package/build/plan-discovery.js +15 -2
- package/build/plan-execution-ledger.js +306 -17
- package/build/plan-phase-routing.js +48 -13
- package/build/plane/cli.js +105 -51
- package/build/plane/defaults.js +3 -0
- package/build/plane/plane-freshness.js +294 -0
- package/build/plane/schema-compatibility.js +137 -0
- package/build/plane/shutdown-deps.js +38 -0
- package/build/plane/shutdown.js +83 -8
- package/build/plane/terminal-run-cleanup.js +303 -0
- package/build/printed-remedy-commands.js +26 -4
- package/build/readiness-check.js +6 -2
- package/build/readiness-prerequisites.generated.js +532 -0
- package/build/readme.generated.js +1 -1
- package/build/session-upgrade-notice.js +86 -0
- package/build/setup-epic.js +400 -30
- package/build/shared-resources.generated.js +5 -0
- package/build/start-tickets.js +43 -15
- package/build/test-command-detection.js +247 -0
- package/build/test-command-validation.js +73 -0
- package/build/testing-config-client.js +150 -0
- package/build/ticket-delivery-artifacts.js +258 -0
- package/build/ticket-delivery-enrollment.js +306 -0
- package/build/ticket-delivery.js +205 -0
- package/build/ticket-review-summary.js +14 -0
- package/build/tool-call-advisory.js +162 -0
- package/build/tool-surface-gating.js +28 -89
- package/build/update-check.js +97 -16
- package/build/update-status.js +61 -71
- package/build/upgrade-advice.js +15 -11
- package/build/upgrade-cli.js +360 -20
- package/build/v2-readiness-requirements.js +552 -0
- package/build/version.generated.js +3 -3
- package/build/worker-guard-hook-bin.js +2 -2
- package/build/worktree-core.js +22 -7
- package/docs/BRIDGE-DIRECTORY-README.md +48 -0
- package/docs/EPIC-IMPLEMENTER-INSTALL.md +12 -867
- package/docs/EPIC-IMPLEMENTER.md +104 -63
- package/docs/epic-implementer/getting-started.md +131 -0
- package/docs/epic-implementer/self-hosting.md +133 -0
- package/package.json +3 -3
- package/pipelines/idea-to-ticket.json +1 -1
- package/pipelines/implement-ticket.json +8 -8
- package/pipelines/review-ticket.json +2 -2
- package/design-assets/favicon/favicon-32x32.png +0 -0
- package/design-assets/just-logo-rough-draft.png +0 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BAPI-1426 — the thin client for the server's ticket delivery authority.
|
|
3
|
+
*
|
|
4
|
+
* The Bridge API decides whether a managed ticket is ready to publish, merge, or
|
|
5
|
+
* complete. This module only carries requests to `/ticket-delivery/...` and hands
|
|
6
|
+
* the server's answer back. It holds no readiness policy: it never counts ledger
|
|
7
|
+
* records, never decides an outcome, and never turns a failed call into a green
|
|
8
|
+
* result.
|
|
9
|
+
*
|
|
10
|
+
* Every function returns a result object instead of throwing on an HTTP or
|
|
11
|
+
* network failure, so a caller can keep local work and retry later. Messages are
|
|
12
|
+
* sanitized and never carry the API key or any request header.
|
|
13
|
+
*/
|
|
14
|
+
import { redactSecrets } from "./secret-redaction.js";
|
|
15
|
+
export const READINESS_BOUNDARIES = ["publish", "merge", "complete"];
|
|
16
|
+
const STATUS_CODES = {
|
|
17
|
+
400: "BAD_REQUEST",
|
|
18
|
+
401: "UNAUTHORIZED",
|
|
19
|
+
403: "FORBIDDEN",
|
|
20
|
+
404: "NOT_FOUND",
|
|
21
|
+
409: "CONFLICT",
|
|
22
|
+
422: "VALIDATION_ERROR",
|
|
23
|
+
429: "RATE_LIMITED",
|
|
24
|
+
500: "INTERNAL_ERROR",
|
|
25
|
+
502: "BAD_GATEWAY",
|
|
26
|
+
503: "SERVICE_UNAVAILABLE",
|
|
27
|
+
504: "GATEWAY_TIMEOUT",
|
|
28
|
+
};
|
|
29
|
+
const MAX_MESSAGE_LENGTH = 500;
|
|
30
|
+
/** Strip credentials and bound the length of any text that leaves this module. */
|
|
31
|
+
export function sanitizeDeliveryMessage(raw, apiKey) {
|
|
32
|
+
let text = typeof raw === "string" ? raw : raw instanceof Error ? raw.message : String(raw);
|
|
33
|
+
if (apiKey && apiKey.length >= 4)
|
|
34
|
+
text = text.split(apiKey).join("[REDACTED]");
|
|
35
|
+
text = redactSecrets(text);
|
|
36
|
+
return text.length > MAX_MESSAGE_LENGTH ? `${text.slice(0, MAX_MESSAGE_LENGTH)}…` : text;
|
|
37
|
+
}
|
|
38
|
+
function deliveryUrl(baseUrl, path, query) {
|
|
39
|
+
const url = new URL(`${baseUrl.replace(/\/+$/, "")}/ticket-delivery${path}`);
|
|
40
|
+
for (const [key, value] of Object.entries(query ?? {}))
|
|
41
|
+
url.searchParams.set(key, value);
|
|
42
|
+
return url.toString();
|
|
43
|
+
}
|
|
44
|
+
async function request(deps, method, path, options = {}) {
|
|
45
|
+
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
46
|
+
let headers = {};
|
|
47
|
+
try {
|
|
48
|
+
headers = await deps.headers();
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
return {
|
|
52
|
+
ok: false,
|
|
53
|
+
status: 0,
|
|
54
|
+
error: "CREDENTIALS_UNAVAILABLE",
|
|
55
|
+
message: sanitizeDeliveryMessage(err),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
const apiKey = headers["X-API-Key"];
|
|
59
|
+
let resp;
|
|
60
|
+
try {
|
|
61
|
+
resp = await fetchImpl(deliveryUrl(deps.baseUrl, path, options.query), {
|
|
62
|
+
method,
|
|
63
|
+
headers: options.body === undefined ? headers : { ...headers, "Content-Type": "application/json" },
|
|
64
|
+
...(options.body === undefined ? {} : { body: JSON.stringify(options.body) }),
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
return {
|
|
69
|
+
ok: false,
|
|
70
|
+
status: 0,
|
|
71
|
+
error: "NETWORK_ERROR",
|
|
72
|
+
message: sanitizeDeliveryMessage(err, apiKey),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
let raw = "";
|
|
76
|
+
try {
|
|
77
|
+
raw = await resp.text();
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
return {
|
|
81
|
+
ok: false,
|
|
82
|
+
status: resp.status,
|
|
83
|
+
error: "INVALID_RESPONSE",
|
|
84
|
+
message: sanitizeDeliveryMessage(err, apiKey),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
if (resp.ok) {
|
|
88
|
+
try {
|
|
89
|
+
const body = JSON.parse(raw);
|
|
90
|
+
if (body === null || typeof body !== "object" || Array.isArray(body)) {
|
|
91
|
+
throw new Error("response body is not a JSON object");
|
|
92
|
+
}
|
|
93
|
+
return { ok: true, status: resp.status, body: body };
|
|
94
|
+
}
|
|
95
|
+
catch (err) {
|
|
96
|
+
return {
|
|
97
|
+
ok: false,
|
|
98
|
+
status: resp.status,
|
|
99
|
+
error: "INVALID_RESPONSE",
|
|
100
|
+
message: sanitizeDeliveryMessage(err, apiKey),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
let error = STATUS_CODES[resp.status] ?? "UNKNOWN_ERROR";
|
|
105
|
+
let message = raw ? raw : `HTTP ${resp.status}`;
|
|
106
|
+
let detail;
|
|
107
|
+
try {
|
|
108
|
+
const parsed = JSON.parse(raw);
|
|
109
|
+
detail = parsed?.detail;
|
|
110
|
+
if (detail !== null && typeof detail === "object" && !Array.isArray(detail)) {
|
|
111
|
+
const d = detail;
|
|
112
|
+
if (typeof d.error === "string")
|
|
113
|
+
error = d.error;
|
|
114
|
+
message = typeof d.message === "string" ? d.message : JSON.stringify(d);
|
|
115
|
+
}
|
|
116
|
+
else if (typeof detail === "string") {
|
|
117
|
+
message = detail;
|
|
118
|
+
}
|
|
119
|
+
else if (detail !== undefined) {
|
|
120
|
+
message = JSON.stringify(detail);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
// Not JSON: keep the raw text as the message.
|
|
125
|
+
}
|
|
126
|
+
return {
|
|
127
|
+
ok: false,
|
|
128
|
+
status: resp.status,
|
|
129
|
+
error,
|
|
130
|
+
message: sanitizeDeliveryMessage(message, apiKey),
|
|
131
|
+
...(detail !== undefined ? { detail: JSON.parse(sanitizeDeliveryDetail(detail, apiKey)) } : {}),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
function sanitizeDeliveryDetail(detail, apiKey) {
|
|
135
|
+
let text = JSON.stringify(detail);
|
|
136
|
+
if (apiKey && apiKey.length >= 4)
|
|
137
|
+
text = text.split(apiKey).join("[REDACTED]");
|
|
138
|
+
return redactSecrets(text);
|
|
139
|
+
}
|
|
140
|
+
// ---------------------------------------------------------------------------
|
|
141
|
+
// Operations
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
/** Bind a managed execution. Replaying the same registration key resumes it. */
|
|
144
|
+
export function registerExecution(deps, body) {
|
|
145
|
+
return request(deps, "POST", "/executions", { body });
|
|
146
|
+
}
|
|
147
|
+
/** Find the active attempt for a ticket and branch. A 404 means there is none. */
|
|
148
|
+
export function recoverExecution(deps, query) {
|
|
149
|
+
return request(deps, "GET", "/executions/recover", { query });
|
|
150
|
+
}
|
|
151
|
+
const RUN_RECOVERY_CODES = {
|
|
152
|
+
TICKET_DELIVERY_RUN_UNMANAGED: "unmanaged",
|
|
153
|
+
TICKET_DELIVERY_RUN_NO_RECOVERABLE_EXECUTION: "no_recoverable_execution",
|
|
154
|
+
TICKET_DELIVERY_RUN_AMBIGUOUS: "ambiguous",
|
|
155
|
+
TICKET_DELIVERY_RUN_LOOKUP_FAILED: "lookup_failed",
|
|
156
|
+
};
|
|
157
|
+
const RUN_RECOVERY_MESSAGES = {
|
|
158
|
+
unmanaged: "The server confirmed no managed execution exists for this run and ticket.",
|
|
159
|
+
no_recoverable_execution: "The ticket belongs to the run but has no active or completed execution to read.",
|
|
160
|
+
ambiguous: "More than one active execution exists for this run and ticket.",
|
|
161
|
+
lookup_failed: "The run-scoped lookup failed. This is not proof the ticket is unmanaged.",
|
|
162
|
+
};
|
|
163
|
+
/**
|
|
164
|
+
* Find a run child's execution by run id and ticket, with no branch. Only an
|
|
165
|
+
* allowlisted server code maps to `unmanaged`, `no_recoverable_execution` or
|
|
166
|
+
* `ambiguous`. Every other failure, including a bare 404 from an older server,
|
|
167
|
+
* a 5xx, an unreadable body and a transport error, is `lookup_failed`.
|
|
168
|
+
*/
|
|
169
|
+
export async function recoverExecutionForRun(deps, query) {
|
|
170
|
+
const result = await request(deps, "GET", "/executions/recover", {
|
|
171
|
+
query,
|
|
172
|
+
});
|
|
173
|
+
if (result.ok)
|
|
174
|
+
return { ok: true, body: result.body };
|
|
175
|
+
const detail = result.detail;
|
|
176
|
+
const code = typeof detail?.error === "string" ? detail.error : undefined;
|
|
177
|
+
const mapped = code !== undefined ? RUN_RECOVERY_CODES[code] : undefined;
|
|
178
|
+
const outcome = mapped ?? "lookup_failed";
|
|
179
|
+
return { ok: false, outcome, status: result.status, message: RUN_RECOVERY_MESSAGES[outcome] };
|
|
180
|
+
}
|
|
181
|
+
/** Whether a branch is held by a managed ticket. Grants no submission authority. */
|
|
182
|
+
export function getBranchHold(deps, query) {
|
|
183
|
+
return request(deps, "GET", "/holds", { query });
|
|
184
|
+
}
|
|
185
|
+
/** Upload one durable evidence batch. `retryable: true` means nothing was stored. */
|
|
186
|
+
export function submitEvidence(deps, attemptId, body) {
|
|
187
|
+
return request(deps, "POST", `/executions/${encodeURIComponent(attemptId)}/evidence`, { body });
|
|
188
|
+
}
|
|
189
|
+
/** The server-accepted ledger, which is the only source of post-PR ownership. */
|
|
190
|
+
export function getAcceptedLedger(deps, attemptId, query) {
|
|
191
|
+
return request(deps, "GET", `/executions/${encodeURIComponent(attemptId)}/ledger`, { query });
|
|
192
|
+
}
|
|
193
|
+
/** The server's readiness decision at one boundary. */
|
|
194
|
+
export function getReadiness(deps, attemptId, query) {
|
|
195
|
+
const params = { repo_name: query.repo_name };
|
|
196
|
+
if (query.boundary)
|
|
197
|
+
params.boundary = query.boundary;
|
|
198
|
+
return request(deps, "GET", `/executions/${encodeURIComponent(attemptId)}/readiness`, {
|
|
199
|
+
query: params,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
/** Ask the server to confirm delivery after merge. Only it can say `delivery_complete`. */
|
|
203
|
+
export function completeDelivery(deps, attemptId, body) {
|
|
204
|
+
return request(deps, "POST", `/executions/${encodeURIComponent(attemptId)}/complete`, { body });
|
|
205
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function ticketReviewSummary(projection, proposed) {
|
|
2
|
+
if (projection?.enabled === true && projection.provenance === 'enabled')
|
|
3
|
+
return 'on';
|
|
4
|
+
if (projection?.enabled === false && projection.provenance === 'recorded' &&
|
|
5
|
+
typeof projection.actor === 'string' && /^actor_[a-z0-9]{4,32}$/.test(projection.actor) &&
|
|
6
|
+
typeof projection.recorded_at === 'string' && /^\d{4}-\d{2}-\d{2}T.+(?:Z|\+00:00)$/.test(projection.recorded_at) && Number.isFinite(Date.parse(projection.recorded_at))) {
|
|
7
|
+
return `off (set by ${projection.actor} on ${projection.recorded_at.slice(0, 10)})`;
|
|
8
|
+
}
|
|
9
|
+
if (projection?.enabled === false)
|
|
10
|
+
return 'off (legacy choice not recorded)';
|
|
11
|
+
if (proposed?.spec_review_gate === false)
|
|
12
|
+
return 'off requested (admin key required; choice recorded at create)';
|
|
13
|
+
return 'on';
|
|
14
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* First-completed-tool-result upgrade notice (BAPI-1446).
|
|
3
|
+
*
|
|
4
|
+
* Wraps the SDK's `tools/call` handler so the first eligible completed result in
|
|
5
|
+
* a stdio session carries one extra text content item naming both versions and
|
|
6
|
+
* `/upgrade-bridge`. Tool execution is untouched: the original handler runs
|
|
7
|
+
* outside the decoration error boundary, so a thrown tool error propagates
|
|
8
|
+
* exactly as it would without this wrapper, and any failure while decorating
|
|
9
|
+
* returns the original result.
|
|
10
|
+
*
|
|
11
|
+
* Eligibility is decided synchronously, after the original handler resolves, by
|
|
12
|
+
* `getNoticeDecision`. It never waits for a background lookup. The once-per-
|
|
13
|
+
* session flag is set only after a successful decoration, so an unknown latest
|
|
14
|
+
* version, a silent decision, or a result that cannot be decorated safely leaves
|
|
15
|
+
* the opportunity for a later call.
|
|
16
|
+
*
|
|
17
|
+
* Like `installToolSurfaceListOverride`, this reads the SDK's private
|
|
18
|
+
* `_requestHandlers` map through one localized compatibility adapter.
|
|
19
|
+
*/
|
|
20
|
+
import { getMethodLiteral } from "@modelcontextprotocol/sdk/server/zod-json-schema-compat.js";
|
|
21
|
+
import { getNoticeDecision } from "./session-upgrade-notice.js";
|
|
22
|
+
import { formatSessionUpgradeNotice } from "./upgrade-advice.js";
|
|
23
|
+
/** Fixed diagnostic. It never carries exception text or private map contents. */
|
|
24
|
+
export const TOOL_CALL_ADVISORY_INSTALL_FAILURE = "Bridge MCP upgrade notice: tools/call advisory could not be installed (incompatible MCP SDK).";
|
|
25
|
+
/**
|
|
26
|
+
* Private provenance mark for the genuine, server-sourced `ping` advice item. A
|
|
27
|
+
* symbol-keyed property survives a shallow copy and is ignored by
|
|
28
|
+
* `JSON.stringify`, so it never reaches the wire. The symbol is not exported, so
|
|
29
|
+
* only this module can create or recognize the mark.
|
|
30
|
+
*/
|
|
31
|
+
const SERVER_ADVICE_MARK = Symbol("bridge.serverUpgradeAdvice");
|
|
32
|
+
/** Mark a content item as the genuine server-sourced upgrade advice. Returns it. */
|
|
33
|
+
export function markServerUpgradeAdvice(item) {
|
|
34
|
+
Object.defineProperty(item, SERVER_ADVICE_MARK, {
|
|
35
|
+
value: true,
|
|
36
|
+
enumerable: true,
|
|
37
|
+
configurable: true,
|
|
38
|
+
writable: false,
|
|
39
|
+
});
|
|
40
|
+
return item;
|
|
41
|
+
}
|
|
42
|
+
function isServerUpgradeAdvice(item) {
|
|
43
|
+
return (item !== null &&
|
|
44
|
+
typeof item === "object" &&
|
|
45
|
+
item[SERVER_ADVICE_MARK] === true);
|
|
46
|
+
}
|
|
47
|
+
/** Protocol servers that already carry the wrapper, so reinstall is a no-op. */
|
|
48
|
+
const installedServers = new WeakSet();
|
|
49
|
+
function authoritativeFrom(getStatus) {
|
|
50
|
+
const status = getStatus?.();
|
|
51
|
+
if (status && status.known === true && typeof status.latestVersion === "string") {
|
|
52
|
+
return { known: true, latest: status.latestVersion };
|
|
53
|
+
}
|
|
54
|
+
return { known: false };
|
|
55
|
+
}
|
|
56
|
+
function emit(stderr) {
|
|
57
|
+
try {
|
|
58
|
+
stderr?.(TOOL_CALL_ADVISORY_INSTALL_FAILURE);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
// A diagnostic sink failure must never affect startup.
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Install the wrapper. Returns true when it is in place (including an earlier
|
|
66
|
+
* install on the same protocol server) and false after emitting the one fixed
|
|
67
|
+
* diagnostic. Never throws, and leaves the original handler in place on failure.
|
|
68
|
+
*/
|
|
69
|
+
export function installToolCallAdvisory(options) {
|
|
70
|
+
const { protocolServer, callSchema, env, currentVersion, getStatus, stderr } = options;
|
|
71
|
+
try {
|
|
72
|
+
if (protocolServer && installedServers.has(protocolServer))
|
|
73
|
+
return true;
|
|
74
|
+
let method;
|
|
75
|
+
try {
|
|
76
|
+
method = getMethodLiteral(callSchema);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
emit(stderr);
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
if (method !== "tools/call") {
|
|
83
|
+
emit(stderr);
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
const handlers = protocolServer?._requestHandlers;
|
|
87
|
+
if (!handlers || typeof handlers.get !== "function") {
|
|
88
|
+
emit(stderr);
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
const original = handlers.get(method);
|
|
92
|
+
if (typeof original !== "function") {
|
|
93
|
+
emit(stderr);
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
// Delivery state belongs to this installation, so it is isolated to one
|
|
97
|
+
// stdio session.
|
|
98
|
+
let delivered = false;
|
|
99
|
+
const decorate = (result) => {
|
|
100
|
+
if (delivered)
|
|
101
|
+
return result;
|
|
102
|
+
if (result === null || typeof result !== "object")
|
|
103
|
+
return result;
|
|
104
|
+
const content = result.content;
|
|
105
|
+
if (!Array.isArray(content))
|
|
106
|
+
return result;
|
|
107
|
+
const decision = getNoticeDecision(env, currentVersion, authoritativeFrom(getStatus));
|
|
108
|
+
if (!decision.show)
|
|
109
|
+
return result;
|
|
110
|
+
if (content.some(isServerUpgradeAdvice)) {
|
|
111
|
+
// The genuine ping advice already told the session.
|
|
112
|
+
delivered = true;
|
|
113
|
+
return result;
|
|
114
|
+
}
|
|
115
|
+
const notice = formatSessionUpgradeNotice(decision.current, decision.latest);
|
|
116
|
+
const decorated = {
|
|
117
|
+
...result,
|
|
118
|
+
content: [...content, { type: "text", text: notice }],
|
|
119
|
+
};
|
|
120
|
+
delivered = true;
|
|
121
|
+
return decorated;
|
|
122
|
+
};
|
|
123
|
+
const wrapped = async (request, extra) => {
|
|
124
|
+
// Outside the boundary below: a tool-call error propagates unchanged.
|
|
125
|
+
const result = await original(request, extra);
|
|
126
|
+
try {
|
|
127
|
+
return decorate(result);
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
return result;
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
try {
|
|
134
|
+
// Replace in place; never remove the original first.
|
|
135
|
+
protocolServer.setRequestHandler(callSchema, wrapped);
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
// The replacement may have begun before the failure. Put the original back.
|
|
139
|
+
try {
|
|
140
|
+
if (handlers.get(method) !== original) {
|
|
141
|
+
try {
|
|
142
|
+
protocolServer.setRequestHandler(callSchema, original);
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
handlers.set(method, original);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
// Nothing more can be done safely.
|
|
151
|
+
}
|
|
152
|
+
emit(stderr);
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
installedServers.add(protocolServer);
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
emit(stderr);
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
@@ -372,116 +372,55 @@ function setsEqual(a, b) {
|
|
|
372
372
|
return true;
|
|
373
373
|
}
|
|
374
374
|
// ---------------------------------------------------------------------------
|
|
375
|
-
//
|
|
375
|
+
// Asset-recovery advisory composition
|
|
376
376
|
// ---------------------------------------------------------------------------
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
377
|
+
//
|
|
378
|
+
// BAPI-1446 removed the update-staleness decoration (BAPI-728) from `tools/list`:
|
|
379
|
+
// the `_meta` key and the text prepended to the `ping` description were an
|
|
380
|
+
// invisible channel, and the upgrade notice now reaches a session through the
|
|
381
|
+
// startup instructions, the first completed tool result, and the SessionStart
|
|
382
|
+
// hook. Only the asset-recovery advisory decorates the list now.
|
|
383
|
+
/** The tool whose description carries the model-visible recovery advisory. */
|
|
380
384
|
const ADVISORY_HOST_TOOL = "ping";
|
|
381
|
-
/**
|
|
382
|
-
* Wrap a `tools/list` handler so a KNOWN-stale server tells the model so.
|
|
383
|
-
*
|
|
384
|
-
* Why decorate the list at all: the staleness value was already computed on every
|
|
385
|
-
* server start and emitted to an MCP log sink nothing reads, so a session talking
|
|
386
|
-
* to a twenty-release-old build looked completely normal — the visible symptom was
|
|
387
|
-
* an agent that could not find `council`, with no signal explaining why. Tool
|
|
388
|
-
* discovery is the one surface every session reads without being asked to.
|
|
389
|
-
*
|
|
390
|
-
* What is preserved, exactly: tool NAMES, their ORDER, their input/output SCHEMAS,
|
|
391
|
-
* their enabled state, and every other response property. The advisory is added in
|
|
392
|
-
* two places, both additive:
|
|
393
|
-
*
|
|
394
|
-
* - `_meta` — the protocol's sanctioned extension point, for clients that
|
|
395
|
-
* surface structured metadata;
|
|
396
|
-
* - the `ping` tool's DESCRIPTION — the surface a model actually reads. `ping`
|
|
397
|
-
* is the topically correct host: its description already documents the
|
|
398
|
-
* version/upgrade metadata it returns. Note this does not require anyone to
|
|
399
|
-
* CALL `ping` — reading its description during ordinary discovery is enough,
|
|
400
|
-
* which is the precise gap the ticket describes.
|
|
401
|
-
*
|
|
402
|
-
* The description text is appended at RUNTIME only, over the `ping` description
|
|
403
|
-
* from the static tool catalog. That static catalog is untouched, so the
|
|
404
|
-
* description prose pins that guard `mcp_server/src/index.ts` are unaffected —
|
|
405
|
-
* but a LIVE `tools/list` capture DOES include this decoration whenever the
|
|
406
|
-
* update status is stale, and so is NOT automatically safe for tools-budget
|
|
407
|
-
* measurement. Budget measurements stay repository-owned only because their
|
|
408
|
-
* child process explicitly disables the update check via
|
|
409
|
-
* `BAPI_MCP_UPDATE_CHECK_ENABLED=false` (BAPI-917) — see
|
|
410
|
-
* `mcp_server/src/integration/measure-tools.ts`.
|
|
411
|
-
*
|
|
412
|
-
* Fail-open: if the advisory cannot be produced, the original result is returned
|
|
413
|
-
* byte-for-byte.
|
|
414
|
-
*/
|
|
415
|
-
export function decorateListResultWithUpdateAdvisory(original, advisory) {
|
|
416
|
-
if (!advisory)
|
|
417
|
-
return original;
|
|
418
|
-
try {
|
|
419
|
-
const tools = original.tools.map((tool) => tool.name === ADVISORY_HOST_TOOL
|
|
420
|
-
? { ...tool, description: `${advisory}\n\n${tool.description ?? ""}`.trimEnd() }
|
|
421
|
-
: tool);
|
|
422
|
-
return {
|
|
423
|
-
...original,
|
|
424
|
-
tools,
|
|
425
|
-
_meta: { ...(original._meta ?? {}), [UPDATE_ADVISORY_META_KEY]: advisory },
|
|
426
|
-
};
|
|
427
|
-
}
|
|
428
|
-
catch {
|
|
429
|
-
return original;
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
/**
|
|
433
|
-
* Compose an advisory-decorating handler over `inner`.
|
|
434
|
-
*
|
|
435
|
-
* `inner` is the capability gate's handler when dynamic gating is enabled, and
|
|
436
|
-
* the SDK's own captured handler when it is disabled — so turning gating off
|
|
437
|
-
* never turns update visibility off with it.
|
|
438
|
-
*/
|
|
439
|
-
export function createUpdateAdvisoryListHandler(inner, getAdvisory, onListServed) {
|
|
440
|
-
return async (request, extra) => {
|
|
441
|
-
const result = await inner(request, extra);
|
|
442
|
-
try {
|
|
443
|
-
onListServed?.();
|
|
444
|
-
return decorateListResultWithUpdateAdvisory(result, getAdvisory());
|
|
445
|
-
}
|
|
446
|
-
catch {
|
|
447
|
-
// Never let advisory decoration break tool discovery.
|
|
448
|
-
return result;
|
|
449
|
-
}
|
|
450
|
-
};
|
|
451
|
-
}
|
|
452
|
-
/** Compose once over undecorated SDK results, preserving both metadata keys.
|
|
385
|
+
/** Compose once over undecorated SDK results, preserving the recovery metadata.
|
|
453
386
|
* Weak keys prevent retained results from accumulating session-wide state. */
|
|
454
387
|
const undecoratedResults = new WeakMap();
|
|
455
|
-
export function composeListAdvisories(original,
|
|
388
|
+
export function composeListAdvisories(original, recovery) {
|
|
456
389
|
const base = undecoratedResults.get(original) ?? original;
|
|
457
|
-
const updated = decorateListResultWithUpdateAdvisory(base, update);
|
|
458
390
|
if (!recovery)
|
|
459
|
-
return
|
|
391
|
+
return base;
|
|
460
392
|
try {
|
|
461
|
-
const anchor = base.tools.find(t => t.name ===
|
|
462
|
-
const result = { ...
|
|
463
|
-
tools:
|
|
464
|
-
? { ...tool, description: `${
|
|
465
|
-
_meta: { ...(
|
|
393
|
+
const anchor = base.tools.find(t => t.name === ADVISORY_HOST_TOOL) ?? base.tools[0];
|
|
394
|
+
const result = { ...base,
|
|
395
|
+
tools: base.tools.map(tool => tool.name === anchor?.name
|
|
396
|
+
? { ...tool, description: `${recovery.text}\n\n${anchor?.description ?? ""}`.trimEnd() } : tool),
|
|
397
|
+
_meta: { ...(base._meta ?? {}), "bridge-api/asset-recovery": { host: recovery.host,
|
|
466
398
|
missingCount: recovery.missingCount, paths: recovery.paths, capabilities: recovery.capabilities, command: recovery.command } },
|
|
467
399
|
};
|
|
468
400
|
undecoratedResults.set(result, base);
|
|
469
401
|
return result;
|
|
470
402
|
}
|
|
471
403
|
catch {
|
|
472
|
-
return
|
|
404
|
+
return base;
|
|
473
405
|
}
|
|
474
406
|
}
|
|
475
|
-
|
|
407
|
+
/**
|
|
408
|
+
* Compose the recovery advisory over `inner`.
|
|
409
|
+
*
|
|
410
|
+
* `inner` is the capability gate's handler when dynamic gating is enabled, and
|
|
411
|
+
* the SDK's own captured handler when it is disabled — so turning gating off
|
|
412
|
+
* never turns recovery visibility off with it. A failure to retrieve or apply the
|
|
413
|
+
* advisory returns the undecorated original result.
|
|
414
|
+
*/
|
|
415
|
+
export function createComposedAdvisoryListHandler(inner, getRecovery) {
|
|
476
416
|
return async (request, extra) => {
|
|
477
417
|
const original = await inner(request, extra);
|
|
478
418
|
try {
|
|
479
419
|
const recovery = await getRecovery();
|
|
480
|
-
|
|
481
|
-
return composeListAdvisories(original, getUpdate(), recovery);
|
|
420
|
+
return composeListAdvisories(original, recovery);
|
|
482
421
|
}
|
|
483
422
|
catch {
|
|
484
|
-
return
|
|
423
|
+
return original;
|
|
485
424
|
}
|
|
486
425
|
};
|
|
487
426
|
}
|