Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/security.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ It never exposes raw DevSpace workspace, edit, write, or shell tools, and
`run_github_completion_loop()` caller through a JSON-lines host bridge. The
Python caller cannot merge `main` directly: its CAS merge port is routed back
into the existing `git_merge_pull_request` core. Integration generations are
same-repository, non-force PR-branch updates and are capped at 3 generations
same-repository, non-force PR-branch updates and are capped at 5 generations
and 2700 seconds. Any affected semantic/authority/test/transport dimension
fails closed for fresh Candidate acceptance.

Expand Down
2 changes: 1 addition & 1 deletion scripts/github_completion_bridge.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ def reconcile_post_merge(
initial_evidence=evidence,
request=request,
port=HostPort(),
max_generations=int(start.get("max_generations", 3)),
max_generations=int(start.get("max_generations", 5)),
max_elapsed_seconds=float(start.get("max_elapsed_seconds", 2700.0)),
git_root=nexus_root,
)
Expand Down
39 changes: 39 additions & 0 deletions src/github-completion-host.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import {
MAX_COMPLETION_INTEGRATION_GENERATIONS,
isTransientRequiredChecksDecision,
parseCompletionGeneration,
parseGitHubCompletionToolInput,
} from "./github-completion-host.js";

const missingReason = "no check run or commit status matches the required context for the expected head SHA";
Expand Down Expand Up @@ -48,3 +51,39 @@ describe("isTransientRequiredChecksDecision", () => {
);
});
});

describe("completion integration generation cap (#1384)", () => {
const baseInput = {
initialEvidence: {},
standingGrantRequest: {},
mergeMethod: "merge",
ownerConfirmation: true,
};

it("matches the Nexus-new MAX_INTEGRATION_GENERATIONS=5 loop budget", () => {
assert.equal(MAX_COMPLETION_INTEGRATION_GENERATIONS, 5);
});

it("defaults maxGenerations to 5 and accepts 4 and 5", () => {
assert.equal(parseGitHubCompletionToolInput({ ...baseInput }).maxGenerations, 5);
assert.equal(parseGitHubCompletionToolInput({ ...baseInput, maxGenerations: 4 }).maxGenerations, 4);
assert.equal(parseGitHubCompletionToolInput({ ...baseInput, maxGenerations: 5 }).maxGenerations, 5);
});

it("rejects maxGenerations above 5 or below 1", () => {
for (const value of [0, 6, 2.5, "5"]) {
assert.throws(
() => parseGitHubCompletionToolInput({ ...baseInput, maxGenerations: value }),
/maxGenerations_INVALID/,
);
}
});

it("port generation guard admits I4 and I5 and rejects I6", () => {
assert.equal(parseCompletionGeneration(4), 4);
assert.equal(parseCompletionGeneration(5), 5);
for (const value of [0, 6, null, "4"]) {
assert.throws(() => parseCompletionGeneration(value), /generation_INVALID/);
}
});
});
19 changes: 15 additions & 4 deletions src/github-completion-host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@ const FULL_SHA = /^[0-9a-f]{40}$/;
const SAFE_PATH = /^(?!\/)(?!.*(?:^|\/)\.\.(?:\/|$))(?!.*\\)(?!.*[\u0000-\u001f\u007f]).+$/;
const REPOSITORY = "James3014/Nexus-new";
const DEFAULT_BRANCH = "main";
/**
* Mirrors Nexus-new `MAX_INTEGRATION_GENERATIONS` /
* `DEFAULT_MAX_INTEGRATION_GENERATIONS` in
* `nexus/orchestrator/github_completion_loop.py` (#1384). The host must not
* advertise or enforce a tighter budget than the loop it drives.
*/
export const MAX_COMPLETION_INTEGRATION_GENERATIONS = 5;
const COMPLETION_TARGET = {
remoteName: "origin",
repository: REPOSITORY,
Expand Down Expand Up @@ -75,6 +82,10 @@ function positiveInt(value: unknown, name: string, maximum: number): number {
return value;
}

export function parseCompletionGeneration(value: unknown): number {
return positiveInt(value, "generation", MAX_COMPLETION_INTEGRATION_GENERATIONS);
}

export function isTransientRequiredChecksDecision(
decision: Awaited<ReturnType<typeof evaluateRequiredChecks>>,
): boolean {
Expand Down Expand Up @@ -111,8 +122,8 @@ export function parseGitHubCompletionToolInput(raw: Record<string, unknown>): Gi
throw new Error("ownerConfirmation must be exactly true");
}
const maxGenerations = raw.maxGenerations === undefined
? 3
: positiveInt(raw.maxGenerations, "maxGenerations", 3);
? MAX_COMPLETION_INTEGRATION_GENERATIONS
: positiveInt(raw.maxGenerations, "maxGenerations", MAX_COMPLETION_INTEGRATION_GENERATIONS);
const maxElapsedSeconds = raw.maxElapsedSeconds === undefined
? 2700
: positiveInt(raw.maxElapsedSeconds, "maxElapsedSeconds", 2700);
Expand Down Expand Up @@ -272,7 +283,7 @@ class CompletionHostPort {
const baseSha = normalizeSha(params.base_sha, "base_sha");
const baseTreeSha = normalizeSha(params.base_tree_sha, "base_tree_sha");
const expectedHead = normalizeSha(params.expected_pr_head_sha, "expected_pr_head_sha");
const generation = positiveInt(params.generation, "generation", 3);
const generation = parseCompletionGeneration(params.generation);
const target = await this.target();
const pr = await this.transport.getPullRequest(REPOSITORY, this.prNumber);
if (pr.state !== "OPEN" || pr.isDraft) {
Expand Down Expand Up @@ -368,7 +379,7 @@ class CompletionHostPort {

private async readRequiredChecks(params: Record<string, unknown>): Promise<Record<string, unknown>> {
const headSha = normalizeSha(params.head_sha, "checks_head_sha");
const generation = positiveInt(params.generation, "generation", 3);
const generation = parseCompletionGeneration(params.generation);
const timeoutSeconds = typeof params.timeout_seconds === "number"
? Math.max(0, Math.min(params.timeout_seconds, 2700))
: 2700;
Expand Down
6 changes: 6 additions & 0 deletions src/nexus-gateway-proxy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,12 @@ try {
for (const forbiddenKey of ["workspaceId", "cwd", "repo", "repository", "remote", "branch", "execution_lane", "executionLane", "internal", "completion"]) {
assert.ok(!properties.includes(forbiddenKey), `public schema must NOT contain ${forbiddenKey}`);
}
// #1384: the completion action's generation budget matches the Nexus-new loop maximum (5).
const completionTool = schemaPayload.result.tools.find((tool: { name: string }) => tool.name === "github_complete_pull_request");
assert.ok(completionTool, "github_complete_pull_request must be advertised");
assert.equal(completionTool.inputSchema.properties?.maxGenerations?.maximum, 5);
assert.equal(completionTool.inputSchema.properties?.maxGenerations?.minimum, 1);
assert.match(completionTool.description, /at most 5 non-force PR integration generations/);
await schemaBoundary.close();
}

Expand Down
6 changes: 3 additions & 3 deletions src/nexus-gateway-proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {
type MergeEffectGuard,
type TrustedIntegrationTarget,
} from "./git-pr-merge.js";
import { runGitHubCompletionTool } from "./github-completion-host.js";
import { MAX_COMPLETION_INTEGRATION_GENERATIONS, runGitHubCompletionTool } from "./github-completion-host.js";

type JsonSchema = {
type?: string | string[];
Expand Down Expand Up @@ -620,13 +620,13 @@ export async function createNexusGatewayProxyServer(
name,
{
description:
"Run Nexus run_github_completion_loop() against the server-bound James3014/Nexus-new checkout. Ordinary main drift may create at most 3 non-force PR integration generations and wait at most 2700 seconds total. Affected semantic/authority/test/transport dimensions fail closed for fresh acceptance. Protected main integration delegates to the existing git_merge_pull_request merge core; this action does not implement a second merge authority.",
`Run Nexus run_github_completion_loop() against the server-bound James3014/Nexus-new checkout. Ordinary main drift may create at most ${MAX_COMPLETION_INTEGRATION_GENERATIONS} non-force PR integration generations and wait at most 2700 seconds total. Affected semantic/authority/test/transport dimensions fail closed for fresh acceptance. Protected main integration delegates to the existing git_merge_pull_request merge core; this action does not implement a second merge authority.`,
inputSchema: {
initialEvidence: z.record(z.string(), z.unknown()).describe("Exact accepted nexus.github_orchestration_evidence payload."),
standingGrantRequest: z.record(z.string(), z.unknown()).describe("Exact nexus.standing_grant_request.v1 payload bound to the durable standing grant."),
mergeMethod: z.enum(["merge", "squash", "rebase"]).default("merge"),
ownerConfirmation: z.boolean().describe("Must be exactly true for the host protected merge authority."),
maxGenerations: z.number().int().min(1).max(3).optional(),
maxGenerations: z.number().int().min(1).max(MAX_COMPLETION_INTEGRATION_GENERATIONS).optional(),
maxElapsedSeconds: z.number().int().min(1).max(2700).optional(),
},
annotations: {
Expand Down
Loading