@akira-tl/forgerelay 1.0.1 → 1.1.1
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/CHANGELOG.md +36 -0
- package/README.md +4 -0
- package/capabilities/host-integration/external-mcp/GUIDE.md +29 -0
- package/capabilities/lifecycle-hooks/GUIDE.md +13 -3
- package/dist/activity/runtime/mcp-query-tools.js +16 -10
- package/dist/cli/mcp/external-mcp.js +481 -0
- package/dist/cli/mcp/status.js +295 -0
- package/dist/cli.js +14 -1
- package/dist/lsp/test-support/server-fixture.js +1 -2
- package/dist/mcp/filesystem/filesystem-tools.js +7 -3
- package/dist/mcp/hooks/command-runner.js +12 -7
- package/dist/mcp/hooks/external-mcp-transform.js +224 -0
- package/dist/mcp/hooks/hook-cli.js +3 -0
- package/dist/mcp/hooks/hooks.js +49 -3
- package/dist/mcp/oauth/auth-protocol.js +375 -0
- package/dist/mcp/oauth/oauth-provider.js +11 -12
- package/dist/mcp/oauth/oauth-store.js +2 -2
- package/dist/mcp/oauth/router.js +2 -6
- package/dist/mcp/operations/batch/executor.js +1 -1
- package/dist/mcp/operations/bulk-read.js +7 -6
- package/dist/mcp/operations/external-mcp/external-mcp-oauth.js +387 -0
- package/dist/mcp/operations/external-mcp/external-mcp-runtime.js +59 -0
- package/dist/mcp/operations/external-mcp/external-mcp.js +382 -0
- package/dist/mcp/operations/media-content.js +28 -0
- package/dist/mcp/operations/native-bulk-mutations.js +1 -1
- package/dist/mcp/process/tools.js +19 -15
- package/dist/mcp/request-context.js +18 -0
- package/dist/mcp/request-meta.js +28 -3
- package/dist/mcp/server/core/activity-support.js +2 -2
- package/dist/mcp/server/core/capabilities/external-mcp.js +31 -0
- package/dist/mcp/server/core/capabilities.js +7 -0
- package/dist/mcp/server/core/capability-registry.js +2 -0
- package/dist/mcp/server/core/tool-support.js +29 -0
- package/dist/mcp/server/operations/runtime/filesystem-tools.js +91 -48
- package/dist/mcp/server/operations/runtime/operation-runtime.js +18 -11
- package/dist/mcp/server/transport/http-server.js +42 -21
- package/dist/mcp/server/workspace/runtime/workspace-open.js +13 -9
- package/dist/mcp/server/workspace/runtime/workspace-tools.js +29 -25
- package/dist/runtime/config/config.js +4 -0
- package/dist/runtime/config/external-mcp-auth-store.js +225 -0
- package/dist/runtime/config/external-mcp-config.js +146 -0
- package/dist/runtime/config/external-mcp-registry.js +165 -0
- package/dist/runtime/testing/server-fixture.js +10 -5
- package/dist/server.js +19 -10
- package/dist/subagents/sessions/mcp/audit.js +81 -0
- package/dist/ui/.vite/manifest.json +297 -298
- package/dist/ui/activity-panel-app.html +1 -1
- package/dist/ui/assets/activity-panel-app-Bw7ZFX2g.js +114 -0
- package/dist/ui/assets/angular-html-DeUNP12X.js +1 -0
- package/dist/ui/assets/{angular-ts-BearSZDb.js → angular-ts-B3v8FKi_.js} +1 -1
- package/dist/ui/assets/{apl-CYQtr-y6.js → apl-Bs3iQm4P.js} +1 -1
- package/dist/ui/assets/{astro-DDzqKIjK.js → astro-D6GHecBY.js} +1 -1
- package/dist/ui/assets/{blade-DkwEyHvQ.js → blade-BMNbVOHv.js} +1 -1
- package/dist/ui/assets/c-BquuA6bv.js +1 -0
- package/dist/ui/assets/{cobol-xBn5JxHE.js → cobol-BuZ6kfps.js} +1 -1
- package/dist/ui/assets/{coffee-BaXXvoJw.js → coffee-DgC5uBzL.js} +1 -1
- package/dist/ui/assets/cpp-BdvWqCaB.js +1 -0
- package/dist/ui/assets/{crystal-DjOpXVU1.js → crystal-0Q5qBvNI.js} +1 -1
- package/dist/ui/assets/css-Be_T8idh.js +1 -0
- package/dist/ui/assets/{edge-DNaRizoR.js → edge-DaGLdgh_.js} +1 -1
- package/dist/ui/assets/{elixir-CwBMwZmy.js → elixir-CMgiFIzB.js} +1 -1
- package/dist/ui/assets/{elm-gOUD2CW3.js → elm-B-ufMlMC.js} +1 -1
- package/dist/ui/assets/{erb-BcR-fjUp.js → erb-CaGiOZM-.js} +1 -1
- package/dist/ui/assets/{git-rebase-BAQPKHF6.js → git-rebase-CpwUWCzK.js} +1 -1
- package/dist/ui/assets/{glimmer-js-P_wOfSz_.js → glimmer-js-BRxccvnE.js} +1 -1
- package/dist/ui/assets/{glimmer-ts-B44aY1fZ.js → glimmer-ts-CgQbPzlT.js} +1 -1
- package/dist/ui/assets/glsl-D7JmHFr0.js +1 -0
- package/dist/ui/assets/graphql-C8n0eUBK.js +1 -0
- package/dist/ui/assets/{hack-ddU2k0nt.js → hack-lVN7KVQv.js} +1 -1
- package/dist/ui/assets/haml-h18lG0pS.js +1 -0
- package/dist/ui/assets/{handlebars-BpzaUpmz.js → handlebars-lRFjRPfj.js} +1 -1
- package/dist/ui/assets/heavy-payload-Cqx9MOkH.js +290 -0
- package/dist/ui/assets/html-DtILqTE1.js +1 -0
- package/dist/ui/assets/{html-derivative-B2beVdmA.js → html-derivative-CSTSmPup.js} +1 -1
- package/dist/ui/assets/{http-iEskZy8U.js → http-DNI8fsbY.js} +1 -1
- package/dist/ui/assets/{hurl-BuUsLSKv.js → hurl-DgRi60JG.js} +1 -1
- package/dist/ui/assets/java-CW9pZ5bh.js +1 -0
- package/dist/ui/assets/javascript-Cvg2UrWL.js +1 -0
- package/dist/ui/assets/{jinja-CRpkWwrH.js → jinja-CZw_S1FB.js} +1 -1
- package/dist/ui/assets/{jison-BYfSnuHj.js → jison-C56u_FZv.js} +1 -1
- package/dist/ui/assets/json-DW2UTv1q.js +1 -0
- package/dist/ui/assets/jsx-Zj5gobdD.js +1 -0
- package/dist/ui/assets/{julia-B8Rxy7vD.js → julia-Cvcrre-L.js} +1 -1
- package/dist/ui/assets/{just-DzhIWpFP.js → just-8CZ2u6Tj.js} +1 -1
- package/dist/ui/assets/{latex-CUNjUUL-.js → latex-D289AXWG.js} +1 -1
- package/dist/ui/assets/{liquid-hUg7nC61.js → liquid-CFEX3SkZ.js} +1 -1
- package/dist/ui/assets/lua-BKpJ0ybO.js +1 -0
- package/dist/ui/assets/{marko-CQmIkwMF.js → marko-D9maZ0a6.js} +1 -1
- package/dist/ui/assets/{mdc-CB-6dHYa.js → mdc-rM9l0qWR.js} +1 -1
- package/dist/ui/assets/{nginx-CQORo_Bt.js → nginx-C9w6p2C9.js} +1 -1
- package/dist/ui/assets/{nim-BNwBNzdn.js → nim-BRePHk0J.js} +1 -1
- package/dist/ui/assets/{perl-Cnz14fAJ.js → perl-CskTmiPQ.js} +1 -1
- package/dist/ui/assets/{php-YywDo63b.js → php-DCU_FvNI.js} +1 -1
- package/dist/ui/assets/{pug-BwbJp4cM.js → pug-XLaVxTvv.js} +1 -1
- package/dist/ui/assets/{qml-D2DY6dMH.js → qml-CaONKSl-.js} +1 -1
- package/dist/ui/assets/r-BCVR0ZE3.js +1 -0
- package/dist/ui/assets/{razor-pMTG7k41.js → razor-DDnGXxnU.js} +1 -1
- package/dist/ui/assets/regexp-DWGTYYVx.js +1 -0
- package/dist/ui/assets/{rst-CO_ndDm4.js → rst-Dz3GWvQm.js} +1 -1
- package/dist/ui/assets/{ruby-xzRn9gTh.js → ruby-DC3jGtMY.js} +1 -1
- package/dist/ui/assets/{sas-CBdtFKLA.js → sas-7mNpvsjZ.js} +1 -1
- package/dist/ui/assets/scss-uBsoCJVy.js +1 -0
- package/dist/ui/assets/shellscript-BCN9N_Jf.js +1 -0
- package/dist/ui/assets/{shellsession-BaZnm5rZ.js → shellsession-Hsw46VXV.js} +1 -1
- package/dist/ui/assets/{soy-IouG3xdB.js → soy-D6KV69HU.js} +1 -1
- package/dist/ui/assets/sql-CVzVQkJh.js +1 -0
- package/dist/ui/assets/{stata-dgbkIZHP.js → stata-BwUjXNhI.js} +1 -1
- package/dist/ui/assets/{surrealql-CBROnw_p.js → surrealql-CgloWqRa.js} +1 -1
- package/dist/ui/assets/{svelte-6r6BpX4-.js → svelte-DcC3n6IC.js} +1 -1
- package/dist/ui/assets/{templ-BKD_PsbL.js → templ-DOFfnWTJ.js} +1 -1
- package/dist/ui/assets/{tex-CKY1bwHb.js → tex-ln89VPaX.js} +1 -1
- package/dist/ui/assets/{ts-tags-BiYP5N_C.js → ts-tags-CWMzCstH.js} +1 -1
- package/dist/ui/assets/tsx-DgzfiGeK.js +1 -0
- package/dist/ui/assets/{twig-BTe1oPFW.js → twig-Bi2itZL9.js} +1 -1
- package/dist/ui/assets/typescript-CQ24f4zW.js +1 -0
- package/dist/ui/assets/{vue-CT0IiySh.js → vue-BS8jE5u6.js} +1 -1
- package/dist/ui/assets/{vue-html-B-tXcbat.js → vue-html-o38xqhyB.js} +1 -1
- package/dist/ui/assets/{vue-vine-C5ZyDCrs.js → vue-vine-D2NvKv50.js} +1 -1
- package/dist/ui/assets/xml-_44rx9_2.js +1 -0
- package/dist/ui/assets/{xsl-DvDSndNI.js → xsl-B5QWQPoi.js} +1 -1
- package/dist/ui/assets/yaml-CKL2pEWe.js +1 -0
- package/dist/workspaces/relay/auth/remote-auth.js +3 -5
- package/dist/workspaces/relay/result-support.js +19 -0
- package/dist/workspaces/relay/tests/test-support.js +10 -4
- package/dist/workspaces/relay/workspace-relay.js +20 -12
- package/docs/configuration.md +200 -2
- package/docs/debugging.md +8 -5
- package/docs/roadmap.md +64 -3
- package/docs/security.md +18 -0
- package/package.json +12 -4
- package/scripts/debug/accept/bootstrap.mjs +5 -0
- package/scripts/debug/accept/harness.mjs +10 -3
- package/scripts/debug/accept/media.mjs +335 -0
- package/scripts/debug/accept/modern-http.mjs +169 -0
- package/scripts/debug/accept/support.mjs +10 -6
- package/scripts/debug/accept.mjs +12 -1
- package/scripts/debug/relay-accept/support.mjs +40 -0
- package/scripts/debug/relay-accept.mjs +8 -9
- package/scripts/debug/runtime.mjs +4 -1
- package/scripts/debug/runtime.test.mjs +9 -0
- package/scripts/release/parity-sandbox.mjs +35 -0
- package/scripts/release/release-gate.test.mjs +42 -1
- package/scripts/release-parity.mjs +3 -22
- package/scripts/release-proof.mjs +24 -4
- package/scripts/release-proof.test.mjs +12 -4
- package/dist/ui/assets/activity-panel-app-XlqSanXT.js +0 -115
- package/dist/ui/assets/angular-html-tB9EchbM.js +0 -1
- package/dist/ui/assets/c-Dv-N5hjn.js +0 -1
- package/dist/ui/assets/cpp-C3unXX-3.js +0 -1
- package/dist/ui/assets/css-8Xr4kNNH.js +0 -1
- package/dist/ui/assets/glsl-BvCQg9MI.js +0 -1
- package/dist/ui/assets/graphql-C9S90gn_.js +0 -1
- package/dist/ui/assets/haml-Bb1w9rMG.js +0 -1
- package/dist/ui/assets/heavy-payload-CJKqi8Ac.js +0 -290
- package/dist/ui/assets/html-dUOs5pA9.js +0 -1
- package/dist/ui/assets/java-BFZCrORY.js +0 -1
- package/dist/ui/assets/javascript-DaIQ0Zqj.js +0 -1
- package/dist/ui/assets/json-BQC_Yp48.js +0 -1
- package/dist/ui/assets/jsx-Dd-CJRf9.js +0 -1
- package/dist/ui/assets/lua-BoZTiL2I.js +0 -1
- package/dist/ui/assets/r-B5nYsy-M.js +0 -1
- package/dist/ui/assets/regexp-oP0lbks_.js +0 -1
- package/dist/ui/assets/scss-CEqgKQNf.js +0 -1
- package/dist/ui/assets/shellscript-DzJJiWQC.js +0 -1
- package/dist/ui/assets/sql-DC2rdTzV.js +0 -1
- package/dist/ui/assets/tsx-D2OMiPNF.js +0 -1
- package/dist/ui/assets/typescript-BGIABksB.js +0 -1
- package/dist/ui/assets/xml-CfeBNlMP.js +0 -1
- package/dist/ui/assets/yaml-umbj2r1g.js +0 -1
|
@@ -198,20 +198,20 @@ export function curlRequest({ method = "GET", url, headers = {}, body }) {
|
|
|
198
198
|
return { status, headers: responseHeaders, body: responseBody };
|
|
199
199
|
}
|
|
200
200
|
|
|
201
|
-
export async function waitForHealth(child) {
|
|
201
|
+
export async function waitForHealth(child, baseUrl = debugBaseUrl) {
|
|
202
202
|
for (let attempt = 0; attempt < 60; attempt += 1) {
|
|
203
203
|
if (child.exitCode !== null) {
|
|
204
204
|
throw new Error(`debug server exited before health check: ${child.exitCode}`);
|
|
205
205
|
}
|
|
206
206
|
try {
|
|
207
|
-
const response = jsonRequest(`${
|
|
207
|
+
const response = jsonRequest(`${baseUrl}/healthz`);
|
|
208
208
|
if (response.status === 200) return response.json;
|
|
209
209
|
} catch {
|
|
210
210
|
// Server is still starting.
|
|
211
211
|
}
|
|
212
212
|
await delay(100);
|
|
213
213
|
}
|
|
214
|
-
throw new Error(
|
|
214
|
+
throw new Error(`debug server did not become healthy at ${baseUrl}`);
|
|
215
215
|
}
|
|
216
216
|
|
|
217
217
|
export async function stopServer(child) {
|
|
@@ -397,13 +397,13 @@ export function readHookEntries(path) {
|
|
|
397
397
|
.map((line) => JSON.parse(line));
|
|
398
398
|
}
|
|
399
399
|
|
|
400
|
-
export async function
|
|
400
|
+
export async function assertPortFree(port) {
|
|
401
401
|
await new Promise((resolvePromise, rejectPromise) => {
|
|
402
|
-
const socket = connect({ host: "127.0.0.1", port
|
|
402
|
+
const socket = connect({ host: "127.0.0.1", port });
|
|
403
403
|
socket.setTimeout(500);
|
|
404
404
|
socket.once("connect", () => {
|
|
405
405
|
socket.destroy();
|
|
406
|
-
rejectPromise(new Error(
|
|
406
|
+
rejectPromise(new Error(`debug port ${port} is already in use; stop the existing debug server first`));
|
|
407
407
|
});
|
|
408
408
|
socket.once("error", (error) => {
|
|
409
409
|
socket.destroy();
|
|
@@ -420,6 +420,10 @@ export async function assertDebugPortFree() {
|
|
|
420
420
|
});
|
|
421
421
|
}
|
|
422
422
|
|
|
423
|
+
export async function assertDebugPortFree() {
|
|
424
|
+
await assertPortFree(7677);
|
|
425
|
+
}
|
|
426
|
+
|
|
423
427
|
export function assertCurlAvailable() {
|
|
424
428
|
const result = spawnSync("curl", ["--version"], { encoding: "utf8" });
|
|
425
429
|
if (result.status !== 0) {
|
package/scripts/debug/accept.mjs
CHANGED
|
@@ -263,7 +263,16 @@ try {
|
|
|
263
263
|
assert.ok(Array.isArray(codeIntelligenceSchema.oneOf));
|
|
264
264
|
assert.deepEqual(
|
|
265
265
|
codeIntelligenceSchema.oneOf.map((variant) => variant.properties.operation.const),
|
|
266
|
-
[
|
|
266
|
+
[
|
|
267
|
+
"definition",
|
|
268
|
+
"hover",
|
|
269
|
+
"references",
|
|
270
|
+
"documentSymbols",
|
|
271
|
+
"workspaceSymbols",
|
|
272
|
+
"diagnostics",
|
|
273
|
+
"managed.status",
|
|
274
|
+
"managed.install",
|
|
275
|
+
],
|
|
267
276
|
);
|
|
268
277
|
for (const operation of ["references", "documentSymbols", "workspaceSymbols", "diagnostics"]) {
|
|
269
278
|
const boundedSchema = codeIntelligenceSchema.oneOf.find(
|
|
@@ -311,7 +320,9 @@ try {
|
|
|
311
320
|
"host-integration",
|
|
312
321
|
"shell-processes",
|
|
313
322
|
"code-intelligence",
|
|
323
|
+
"external-mcp",
|
|
314
324
|
"workspace-tasks",
|
|
325
|
+
"workspace-checkpoints",
|
|
315
326
|
"batch-execution",
|
|
316
327
|
]);
|
|
317
328
|
const hooksGuide = callTool(oauth.accessToken, sessionId, 78, "read", {
|
|
@@ -119,6 +119,46 @@ export function assertSafeRelayInspection(result, { executionWorkspaceId, execut
|
|
|
119
119
|
assert.doesNotMatch(json, /"ws_[0-9a-f]{10}"/);
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
+
const RELAY_IMAGE_BASE64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=";
|
|
123
|
+
|
|
124
|
+
export function installRelayImageFixture(executionCheckout) {
|
|
125
|
+
writeFileSync(join(executionCheckout, "relay-image.png"), Buffer.from(RELAY_IMAGE_BASE64, "base64"));
|
|
126
|
+
return RELAY_IMAGE_BASE64;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function assertRelayedImageResult(result, expectedBase64, gatewayStateDir) {
|
|
130
|
+
assertToolOk(result, "read image through Workspace Relay");
|
|
131
|
+
const image = result.content.find((entry) => entry.type === "image");
|
|
132
|
+
assert.ok(image, "Workspace Relay did not preserve MCP ImageContent");
|
|
133
|
+
assert.equal(image.mimeType, "image/png");
|
|
134
|
+
assert.equal(image.data, expectedBase64);
|
|
135
|
+
assert.deepEqual(result.structuredContent.media, {
|
|
136
|
+
type: "image",
|
|
137
|
+
mimeType: "image/png",
|
|
138
|
+
bytes: Buffer.byteLength(expectedBase64, "base64"),
|
|
139
|
+
});
|
|
140
|
+
assert.equal(JSON.stringify(result.structuredContent).includes(expectedBase64), false);
|
|
141
|
+
assert.equal(JSON.stringify(result._meta ?? {}).includes(expectedBase64), false);
|
|
142
|
+
assertDirectoryDoesNotContain(gatewayStateDir, expectedBase64);
|
|
143
|
+
pass("Relay ImageContent passthrough", "Execution 7678 image bytes and MIME reached Gateway 7677 unchanged without Gateway persistence");
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function assertDirectoryDoesNotContain(root, needle) {
|
|
147
|
+
if (!existsSync(root)) return;
|
|
148
|
+
const target = Buffer.from(needle, "utf8");
|
|
149
|
+
const pending = [root];
|
|
150
|
+
while (pending.length > 0) {
|
|
151
|
+
const directory = pending.pop();
|
|
152
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
153
|
+
const path = join(directory, entry.name);
|
|
154
|
+
if (entry.isDirectory()) pending.push(path);
|
|
155
|
+
else if (entry.isFile()) {
|
|
156
|
+
assert.equal(readFileSync(path).includes(target), false, `Gateway persisted relayed image base64 in ${path}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
122
162
|
export function assertToolOk(result, label) {
|
|
123
163
|
assert.equal(result.isError, undefined, `${label}: ${toolText(result)}`);
|
|
124
164
|
}
|
|
@@ -3,18 +3,11 @@ import { spawn, spawnSync } from "node:child_process";
|
|
|
3
3
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
4
4
|
import { once } from "node:events";
|
|
5
5
|
import { connect } from "node:net";
|
|
6
|
-
import {
|
|
7
|
-
existsSync,
|
|
8
|
-
mkdirSync,
|
|
9
|
-
readFileSync,
|
|
10
|
-
readdirSync,
|
|
11
|
-
rmSync,
|
|
12
|
-
writeFileSync,
|
|
13
|
-
} from "node:fs";
|
|
6
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
14
7
|
import { join, resolve } from "node:path";
|
|
15
8
|
import { setTimeout as delay } from "node:timers/promises";
|
|
16
9
|
import { debugRoot, repoRoot } from "./runtime.mjs";
|
|
17
|
-
import { relayAcceptanceTopology, assertPortsFree, setupGitProject, runGit, taskStateFiles, findTaskStateFile, assertTaskBodyAbsentFromGateway, assertSafeRelayInspection, assertToolOk, toolText, pass } from "./relay-accept/support.mjs";
|
|
10
|
+
import { relayAcceptanceTopology, assertPortsFree, setupGitProject, runGit, taskStateFiles, findTaskStateFile, assertTaskBodyAbsentFromGateway, assertSafeRelayInspection, installRelayImageFixture, assertRelayedImageResult, assertToolOk, toolText, pass } from "./relay-accept/support.mjs";
|
|
18
11
|
|
|
19
12
|
const {
|
|
20
13
|
gatewayPort, executionPort, gatewayBaseUrl, gatewayMcpUrl, executionBaseUrl,
|
|
@@ -30,6 +23,7 @@ await assertPortsFree([gatewayPort, executionPort]);
|
|
|
30
23
|
rmSync(acceptanceRoot, { recursive: true, force: true });
|
|
31
24
|
mkdirSync(gatewayLocalProject, { recursive: true });
|
|
32
25
|
mkdirSync(executionCheckout, { recursive: true });
|
|
26
|
+
const relayImageBase64 = installRelayImageFixture(executionCheckout);
|
|
33
27
|
mkdirSync(gatewayConfigDir, { recursive: true });
|
|
34
28
|
mkdirSync(executionConfigDir, { recursive: true });
|
|
35
29
|
writeFileSync(join(gatewayLocalProject, "sentinel.txt"), "gateway-local-content\n");
|
|
@@ -112,6 +106,10 @@ try {
|
|
|
112
106
|
assert.equal(openedB.structuredContent.workspaceId, checkoutRelayId);
|
|
113
107
|
pass("relay persistent identity", `two Gateway conversations reused ${checkoutRelayId}`);
|
|
114
108
|
|
|
109
|
+
assertRelayedImageResult(callTool(gatewayMcpUrl, oauth.accessToken, sessionB, nextId(), "read", {
|
|
110
|
+
workspaceId: checkoutRelayId, path: "relay-image.png",
|
|
111
|
+
}, conversationB), relayImageBase64, gatewayStateDir);
|
|
112
|
+
|
|
115
113
|
const checkoutList = callTool(gatewayMcpUrl, oauth.accessToken, sessionA, nextId(), "capability", {
|
|
116
114
|
workspaceId: checkoutRelayId,
|
|
117
115
|
name: "workspace.tasks",
|
|
@@ -508,6 +506,7 @@ try {
|
|
|
508
506
|
ports: [gatewayPort, executionPort],
|
|
509
507
|
verified: [
|
|
510
508
|
"persistent relay checkout identity across Gateway conversations",
|
|
509
|
+
"MCP ImageContent passthrough from Execution 7678 through Gateway 7677",
|
|
511
510
|
"Execution-owned Task state and body-free reminders",
|
|
512
511
|
"safe relayed inspection projection",
|
|
513
512
|
"close/reopen/delete parity",
|
|
@@ -90,21 +90,24 @@ export function createInteractiveDebugEnvironment({
|
|
|
90
90
|
|
|
91
91
|
export function createDebugEnvironment({
|
|
92
92
|
ownerToken,
|
|
93
|
+
configDir = debugConfigDir,
|
|
93
94
|
stateDir = resolve(debugRoot, "state"),
|
|
94
95
|
worktreeRoot = resolve(debugRoot, "worktrees"),
|
|
95
96
|
hookLog = debugHookLog,
|
|
96
97
|
widgets = process.env.FORGERELAY_DEBUG_WIDGETS ?? "off",
|
|
97
98
|
} = {}) {
|
|
98
99
|
const token = ownerToken ?? process.env.FORGERELAY_DEBUG_OWNER_TOKEN ?? createDebugOwnerToken();
|
|
100
|
+
const resolvedConfigDir = resolve(configDir);
|
|
99
101
|
mkdirSync(debugRoot, { recursive: true });
|
|
100
102
|
|
|
101
103
|
return {
|
|
102
104
|
ownerToken: token,
|
|
105
|
+
configDir: resolvedConfigDir,
|
|
103
106
|
env: {
|
|
104
107
|
...process.env,
|
|
105
108
|
HOST: "127.0.0.1",
|
|
106
109
|
PORT: "7677",
|
|
107
|
-
FORGERELAY_CONFIG_DIR:
|
|
110
|
+
FORGERELAY_CONFIG_DIR: resolvedConfigDir,
|
|
108
111
|
FORGERELAY_PUBLIC_BASE_URL: debugBaseUrl,
|
|
109
112
|
FORGERELAY_ALLOWED_ROOTS: repoRoot,
|
|
110
113
|
FORGERELAY_STATE_DIR: stateDir,
|
|
@@ -5,6 +5,7 @@ import { join } from "node:path";
|
|
|
5
5
|
import test from "node:test";
|
|
6
6
|
|
|
7
7
|
import {
|
|
8
|
+
createDebugEnvironment,
|
|
8
9
|
createInteractiveDebugEnvironment,
|
|
9
10
|
interactiveDebugConfigDir,
|
|
10
11
|
interactiveDebugUrls,
|
|
@@ -64,6 +65,14 @@ test("interactive debug uses one dedicated persisted config under ~/.forgerelay/
|
|
|
64
65
|
assert.equal(result.env.FORGERELAY_LOG_LEVEL, "debug");
|
|
65
66
|
});
|
|
66
67
|
|
|
68
|
+
test("acceptance debug environment may isolate its writable config directory", () => {
|
|
69
|
+
const isolatedConfigDir = join(tmpdir(), "forgerelay-acceptance-config");
|
|
70
|
+
const result = createDebugEnvironment({ configDir: isolatedConfigDir });
|
|
71
|
+
|
|
72
|
+
assert.equal(result.configDir, isolatedConfigDir);
|
|
73
|
+
assert.equal(result.env.FORGERELAY_CONFIG_DIR, isolatedConfigDir);
|
|
74
|
+
});
|
|
75
|
+
|
|
67
76
|
test("interactive debug config directory may be explicitly relocated without falling back to product config", async (t) => {
|
|
68
77
|
const { home } = await withDebugConfig(t);
|
|
69
78
|
const customDir = join(home, "custom-debug-config");
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { cpSync, mkdirSync, mkdtempSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
|
|
6
|
+
export function createParitySandbox(repoRoot) {
|
|
7
|
+
const sandbox = mkdtempSync(join(tmpdir(), "forgerelay-release-parity-node22-"));
|
|
8
|
+
copyTrackedTree(repoRoot, sandbox);
|
|
9
|
+
initializeGitIndex(sandbox);
|
|
10
|
+
return sandbox;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function initializeGitIndex(sandbox) {
|
|
14
|
+
execFileSync("git", ["init", "--quiet"], {
|
|
15
|
+
cwd: sandbox,
|
|
16
|
+
stdio: "ignore",
|
|
17
|
+
});
|
|
18
|
+
execFileSync("git", ["add", "--force", "--all", "--", "."], {
|
|
19
|
+
cwd: sandbox,
|
|
20
|
+
stdio: "ignore",
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function copyTrackedTree(repoRoot, destinationRoot) {
|
|
25
|
+
const files = execFileSync("git", ["ls-files", "-z"], {
|
|
26
|
+
cwd: repoRoot,
|
|
27
|
+
encoding: "utf8",
|
|
28
|
+
}).split("\0").filter(Boolean);
|
|
29
|
+
for (const relativePath of files) {
|
|
30
|
+
const source = join(repoRoot, relativePath);
|
|
31
|
+
const destination = join(destinationRoot, relativePath);
|
|
32
|
+
mkdirSync(dirname(destination), { recursive: true });
|
|
33
|
+
cpSync(source, destination, { recursive: true, force: true });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
|
-
import {
|
|
2
|
+
import { execFileSync, spawnSync } from "node:child_process";
|
|
3
|
+
import { readFile, realpath, rm } from "node:fs/promises";
|
|
3
4
|
import { dirname, resolve } from "node:path";
|
|
4
5
|
import test from "node:test";
|
|
5
6
|
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { createParitySandbox } from "./parity-sandbox.mjs";
|
|
6
8
|
|
|
7
9
|
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
|
|
8
10
|
|
|
@@ -67,6 +69,44 @@ test("architecture gate treats the append-only release-note archive as an explic
|
|
|
67
69
|
assert.match(architecture, /"docs\/releases"/);
|
|
68
70
|
});
|
|
69
71
|
|
|
72
|
+
test("release parity sandbox gives architecture an isolated non-empty Git index", async () => {
|
|
73
|
+
const sandbox = createParitySandbox(repoRoot);
|
|
74
|
+
try {
|
|
75
|
+
const architecture = spawnSync(process.execPath, ["scripts/ci/architecture.mjs"], {
|
|
76
|
+
cwd: sandbox,
|
|
77
|
+
encoding: "utf8",
|
|
78
|
+
windowsHide: true,
|
|
79
|
+
shell: false,
|
|
80
|
+
});
|
|
81
|
+
assert.equal(
|
|
82
|
+
architecture.status,
|
|
83
|
+
0,
|
|
84
|
+
architecture.stderr || architecture.stdout || "architecture check did not exit cleanly",
|
|
85
|
+
);
|
|
86
|
+
assert.doesNotMatch(architecture.stdout, /Architecture check passed: 0 tracked files;/);
|
|
87
|
+
|
|
88
|
+
const gitRoot = execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
|
89
|
+
cwd: sandbox,
|
|
90
|
+
encoding: "utf8",
|
|
91
|
+
}).trim();
|
|
92
|
+
const [canonicalGitRoot, canonicalSandbox, canonicalRepoRoot] = await Promise.all([
|
|
93
|
+
realpath(gitRoot),
|
|
94
|
+
realpath(sandbox),
|
|
95
|
+
realpath(repoRoot),
|
|
96
|
+
]);
|
|
97
|
+
assert.equal(canonicalGitRoot, canonicalSandbox);
|
|
98
|
+
assert.notEqual(canonicalGitRoot, canonicalRepoRoot);
|
|
99
|
+
|
|
100
|
+
const tracked = execFileSync("git", ["ls-files", "-z"], {
|
|
101
|
+
cwd: sandbox,
|
|
102
|
+
encoding: "utf8",
|
|
103
|
+
}).split("\0").filter(Boolean);
|
|
104
|
+
assert.ok(tracked.length > 0, "parity sandbox must expose copied tracked files through its own Git index");
|
|
105
|
+
} finally {
|
|
106
|
+
await rm(sandbox, { recursive: true, force: true });
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
70
110
|
test("release runtime and local parity share the checked-in Node contract", async () => {
|
|
71
111
|
const nodeVersion = (await readFile(resolve(repoRoot, ".nvmrc"), "utf8")).trim();
|
|
72
112
|
assert.equal(nodeVersion, "22.19.0");
|
|
@@ -77,6 +117,7 @@ test("release runtime and local parity share the checked-in Node contract", asyn
|
|
|
77
117
|
const source = await readFile(resolve(repoRoot, "scripts/release-parity.mjs"), "utf8");
|
|
78
118
|
assert.match(source, /readFileSync\(join\(repoRoot, "\.nvmrc"\), "utf8"\)/);
|
|
79
119
|
assert.match(source, /const NPM_VERSION = "11\.19\.1"/);
|
|
120
|
+
assert.match(source, /const sandbox = createParitySandbox\(repoRoot\)/);
|
|
80
121
|
const ciWorkflow = await readFile(resolve(repoRoot, ".github/workflows/ci.yml"), "utf8");
|
|
81
122
|
assert.match(ciWorkflow, /npm install --global npm@11\.19\.1/);
|
|
82
123
|
assert.ok(source.includes('["npm", "ci", "--no-audit", "--no-fund"]'));
|
|
@@ -1,21 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { execFileSync, spawnSync } from "node:child_process";
|
|
4
|
-
import {
|
|
4
|
+
import { readFileSync, rmSync } from "node:fs";
|
|
5
5
|
import { dirname, join, resolve } from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { createParitySandbox } from "./release/parity-sandbox.mjs";
|
|
7
8
|
|
|
8
9
|
const NPM_VERSION = "11.19.1";
|
|
9
10
|
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
10
11
|
const NODE_VERSION = readFileSync(join(repoRoot, ".nvmrc"), "utf8").trim();
|
|
11
|
-
const debugRoot = join(repoRoot, ".forgerelay-debug");
|
|
12
|
-
const sandbox = mkdtempSync(join(ensureDirectory(debugRoot), "release-parity-node22-"));
|
|
13
12
|
const npx = process.platform === "win32" ? "npx.cmd" : "npx";
|
|
14
13
|
|
|
15
14
|
assertCleanReleaseHead();
|
|
15
|
+
const sandbox = createParitySandbox(repoRoot);
|
|
16
16
|
|
|
17
17
|
try {
|
|
18
|
-
copyTrackedTree(sandbox);
|
|
19
18
|
const env = {
|
|
20
19
|
...process.env,
|
|
21
20
|
FORGERELAY_ALLOWED_ROOTS: sandbox,
|
|
@@ -61,24 +60,6 @@ function assertCleanReleaseHead() {
|
|
|
61
60
|
}
|
|
62
61
|
}
|
|
63
62
|
|
|
64
|
-
function ensureDirectory(path) {
|
|
65
|
-
mkdirSync(path, { recursive: true });
|
|
66
|
-
return `${path}/`;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function copyTrackedTree(destinationRoot) {
|
|
70
|
-
const files = execFileSync("git", ["ls-files", "-z"], {
|
|
71
|
-
cwd: repoRoot,
|
|
72
|
-
encoding: "utf8",
|
|
73
|
-
}).split("\0").filter(Boolean);
|
|
74
|
-
for (const relativePath of files) {
|
|
75
|
-
const source = join(repoRoot, relativePath);
|
|
76
|
-
const destination = join(destinationRoot, relativePath);
|
|
77
|
-
mkdirSync(dirname(destination), { recursive: true });
|
|
78
|
-
cpSync(source, destination, { recursive: true, force: true });
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
63
|
function runNodeNpm(cwd, env, command, label) {
|
|
83
64
|
run(
|
|
84
65
|
cwd,
|
|
@@ -164,6 +164,29 @@ function testEndpoint(name, fallback) {
|
|
|
164
164
|
return fallback;
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
+
function githubAuthHeaders() {
|
|
168
|
+
const token = process.env.GH_TOKEN?.trim()
|
|
169
|
+
|| process.env.GITHUB_TOKEN?.trim()
|
|
170
|
+
|| localGhToken();
|
|
171
|
+
return {
|
|
172
|
+
Accept: "application/vnd.github+json",
|
|
173
|
+
"User-Agent": "ForgeRelay-release-tag-gate",
|
|
174
|
+
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function localGhToken() {
|
|
179
|
+
if (process.env.NODE_ENV === "test") return "";
|
|
180
|
+
try {
|
|
181
|
+
return execFileSync("gh", ["auth", "token", "--hostname", "github.com"], {
|
|
182
|
+
encoding: "utf8",
|
|
183
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
184
|
+
}).trim();
|
|
185
|
+
} catch {
|
|
186
|
+
return "";
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
167
190
|
async function artifactExists(label, url, headers = {}) {
|
|
168
191
|
let response;
|
|
169
192
|
try {
|
|
@@ -195,10 +218,7 @@ async function assertReleaseTagMutable(tag, version) {
|
|
|
195
218
|
).toString();
|
|
196
219
|
const [npmPublished, githubReleased] = await Promise.all([
|
|
197
220
|
artifactExists(`npm package ${pkg.name}@${version}`, npmUrl),
|
|
198
|
-
artifactExists(`GitHub Release ${tag}`, githubUrl,
|
|
199
|
-
Accept: "application/vnd.github+json",
|
|
200
|
-
"User-Agent": "ForgeRelay-release-tag-gate",
|
|
201
|
-
}),
|
|
221
|
+
artifactExists(`GitHub Release ${tag}`, githubUrl, githubAuthHeaders()),
|
|
202
222
|
]);
|
|
203
223
|
if (npmPublished) {
|
|
204
224
|
throw new Error(`npm package ${pkg.name}@${version} already exists; release tag ${tag} is immutable`);
|
|
@@ -22,11 +22,18 @@ async function runProof(cwd, action, env = {}) {
|
|
|
22
22
|
});
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
async function artifactApiEnv(
|
|
25
|
+
async function artifactApiEnv(
|
|
26
|
+
t,
|
|
27
|
+
{ npmStatus = 404, githubStatus = 404, githubRequiredToken } = {},
|
|
28
|
+
) {
|
|
26
29
|
const server = createServer((request, response) => {
|
|
27
30
|
if (request.url?.startsWith("/npm/")) response.statusCode = npmStatus;
|
|
28
|
-
else if (request.url?.startsWith("/github/"))
|
|
29
|
-
|
|
31
|
+
else if (request.url?.startsWith("/github/")) {
|
|
32
|
+
response.statusCode = githubRequiredToken
|
|
33
|
+
&& request.headers.authorization !== `Bearer ${githubRequiredToken}`
|
|
34
|
+
? 403
|
|
35
|
+
: githubStatus;
|
|
36
|
+
} else response.statusCode = 500;
|
|
30
37
|
response.end();
|
|
31
38
|
});
|
|
32
39
|
await new Promise((resolve, reject) => {
|
|
@@ -41,6 +48,7 @@ async function artifactApiEnv(t, { npmStatus = 404, githubStatus = 404 } = {}) {
|
|
|
41
48
|
NODE_ENV: "test",
|
|
42
49
|
FORGERELAY_TEST_NPM_REGISTRY_URL: `${base}/npm/`,
|
|
43
50
|
FORGERELAY_TEST_GITHUB_API_URL: `${base}/github/`,
|
|
51
|
+
...(githubRequiredToken ? { GH_TOKEN: githubRequiredToken } : {}),
|
|
44
52
|
};
|
|
45
53
|
}
|
|
46
54
|
|
|
@@ -121,7 +129,7 @@ test("release tag hook gate uses repository facts instead of requiring a local p
|
|
|
121
129
|
await git(root, ["add", "tracked.txt"]);
|
|
122
130
|
await git(root, ["commit", "--allow-empty", "-m", "new head"]);
|
|
123
131
|
await runProof(root, "write");
|
|
124
|
-
const mutableEnv = await artifactApiEnv(t);
|
|
132
|
+
const mutableEnv = await artifactApiEnv(t, { githubRequiredToken: "release-test-token" });
|
|
125
133
|
const deletion = await runProof(root, "check-hook", {
|
|
126
134
|
...mutableEnv,
|
|
127
135
|
FORGERELAY_HOOK_PAYLOAD: JSON.stringify({ command: "git push --delete origin v1.2.3" }),
|