Skip to content
Draft
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
9 changes: 9 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,15 @@ Text chunks are at most 64 KiB; total text evidence, including indexes, is at mo
bounded pages; binary changes contain metadata instead of encoded content.
These are internal limits, not configurable model context limits.

Copilot publication and review sessions receive runner-owned tools automatically
when captured evidence is present. The tools list changed paths in bounded pages,
read one manifest-backed patch or untracked-content chunk, and search captured
text for a case-sensitive literal with bounded matches. References come only
from the current invocation's verified manifest; arbitrary filesystem paths,
missing pages or chunks, and modified artifacts fail explicitly. List and read
responses report unread chunks so the agent can disclose incomplete inspection.
Binary entries are returned as metadata. No configuration or opt-in is required.

Review output includes `complete` and `limitations`. Incomplete reviews preserve
partial findings locally and block normal review publication. Prompt content,
output contract and evidence identities are retained with each response attempt.
Expand Down
6 changes: 5 additions & 1 deletion docs/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,11 @@ providers. Stage task text cannot remove those checks. Both retain their existin
inspection permissions: Codex's read-only sandbox and Copilot's read-only
permission handler (`approve-once` for reads, `reject` for non-read requests).
Managed human-approval requirements remain denied. No shell permission is added
for Copilot.
for Copilot. Copilot publication and review additionally receive three
runner-owned tools for listing, chunk-reading and literal-searching only their
current verified evidence manifest. Tool calls and explicit failures flow into
the invocation event log. The tools do not grant shell, write or arbitrary path
access. Codex tool access and evidence presentation are unchanged.

Evidence indexes use absolute paths outside the checkout. Controlled tests check
schema mapping, outside-directory reads and denied write/shell requests. The
Expand Down
1 change: 1 addition & 0 deletions src/adapters/agents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ export class SDKAgent implements AgentAdapter {
outputSchema: invocation.outputSchema,
readOnly: invocation.readOnly,
timeoutMs: invocation.timeoutMs ?? defaultStageTimeoutMs,
evidence: invocation.evidence,
},
invocation,
);
Expand Down
100 changes: 100 additions & 0 deletions src/adapters/evidence-tools.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import type { Tool, ToolInvocation } from "@github/copilot-sdk";
import type { ChangeEvidence } from "../evidence.js";
import { EvidenceQuery } from "../evidence-query.js";
import type { Emit } from "./sdk-protocol.js";

type ToolOperation = (
input: Record<string, unknown>,
signal?: AbortSignal,
) => Promise<unknown>;

export function createEvidenceTools(
evidence: ChangeEvidence,
emit: Emit,
): Tool[] {
const query = new EvidenceQuery(evidence);
const handler =
(toolName: string, operation: ToolOperation) =>
async (input: unknown, invocation: ToolInvocation) => {
emit("event", {
type: "evidence.tool.started",
data: { toolName, arguments: input },
});
try {
const result = await operation(
input as Record<string, unknown>,
invocation.signal,
);
emit("event", {
type: "evidence.tool.completed",
data: { toolName },
});
return result;
} catch (error) {
emit("event", {
type: "evidence.tool.failed",
data: { toolName, error: String(error) },
});
throw error;
}
};

return [
{
name: "evidence_list_changes",
description:
"List only this invocation's captured changes in bounded pages. Returns manifest-backed references, change kinds, binary metadata, available chunks, and unread chunks. Start here and paginate until nextPage is null.",
parameters: {
type: "object",
properties: {
page: { type: "integer", minimum: 1, default: 1 },
pageSize: { type: "integer", minimum: 1, maximum: 50, default: 25 },
},
additionalProperties: false,
},
handler: handler("evidence_list_changes", (args, signal) =>
query.list(args as { page?: number; pageSize?: number }, signal),
),
skipPermission: true,
defer: "never",
},
{
name: "evidence_read_change",
description:
"Read one bounded patch or untracked-content chunk selected by a reference and chunk ordinal returned by evidence_list_changes. Never accepts filesystem paths. Continue until remainingUnreadChunks is zero or report the inspection limit.",
parameters: {
type: "object",
properties: {
reference: { type: "string", pattern: "^change-[0-9]+$" },
chunk: { type: "integer", minimum: 0 },
},
required: ["reference", "chunk"],
additionalProperties: false,
},
handler: handler("evidence_read_change", (args, signal) =>
query.read(args as { reference: string; chunk: number }, signal),
),
skipPermission: true,
defer: "never",
},
{
name: "evidence_search",
description:
"Search only this invocation's captured text for a case-sensitive literal term. Returns bounded manifest references, chunk ordinals, line and column locations, previews, and fully searched versus unsearched change and chunk counts. Binary changes are counted but not searched.",
parameters: {
type: "object",
properties: {
term: { type: "string", minLength: 1, maxLength: 256 },
limit: { type: "integer", minimum: 1, maximum: 50, default: 20 },
},
required: ["term"],
additionalProperties: false,
},
handler: handler("evidence_search", (args, signal) =>
query.search(args as { term: string; limit?: number }, signal),
),
skipPermission: true,
defer: "never",
},
];
}
7 changes: 7 additions & 0 deletions src/adapters/sdk-protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import type {
} from "@openai/codex-sdk";
import type { AgentProfile } from "../config.js";
import { defaultStageTimeoutMs } from "../defaults.js";
import type { ChangeEvidence } from "../evidence.js";
import { createEvidenceTools } from "./evidence-tools.js";

export interface WorkerInput {
provider: "codex" | "copilot";
Expand All @@ -18,6 +20,7 @@ export interface WorkerInput {
readOnly: boolean;
processFile?: string;
timeoutMs?: number;
evidence?: ChangeEvidence;
}
export type Emit = (type: string, value: unknown) => void;
export interface CodexClient {
Expand Down Expand Up @@ -104,6 +107,10 @@ export async function runCopilot(
infiniteSessions: input.profile.context
? { enabled: true, ...input.profile.context }
: undefined,
tools:
input.readOnly && input.evidence
? createEvidenceTools(input.evidence, emit)
: undefined,
onPermissionRequest: async (request) =>
request.managedApprovalRequired ||
(input.readOnly && request.kind !== "read")
Expand Down
1 change: 1 addition & 0 deletions src/domain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ export interface AgentInvocation {
readOnly: boolean;
signal: AbortSignal;
timeoutMs?: number;
evidence?: import("./evidence.js").ChangeEvidence;
session: (id: string) => Promise<void>;
event: (event: unknown) => Promise<void>;
}
Expand Down
Loading
Loading