Attest native Copilot permission targets through workspace aliases

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-10-08 12:20:53 -05:00
1 parent e18f56edc9
commit 0526d8e9f4
9 files changed
+44 -14

No files matched your search

+2 -2
View File
@@ -32,12 +32,12 @@
"requiresProviderPolicy": true
},
"copilot": {
"agentProfileVersion": 32,
"agentProfileVersion": 33,
"agentServerPackage": "@github/copilot",
"agentServerVersion": "1.0.88",
"agentRuntimePackage": null,
"agentRuntimeVersion": null,
"commandDigest": "sha256:3dcc8de3034e32001b0a819a614beca345d314266a70a09e0050a9f5f58275fc",
"commandDigest": "sha256:7b459cc43771d4178898be8a362030ac2e4d8356c2a14a82334958959dd54e59",
"requiresProviderPolicy": true
},
"claude": {
@@ -3620,7 +3620,7 @@ mod tests {
descriptor_value["agentRuntimePackage"] = Value::Null;
descriptor_value["agentRuntimeVersion"] = Value::Null;
descriptor_value["commandDigest"] = serde_json::from_str::<Value>(include_str!(
"../../../../test/fixtures/copilot-profile-v32-identity.json"
"../../../../test/fixtures/copilot-profile-v33-identity.json"
))
.unwrap()["commandDigest"]
.clone();
@@ -47,7 +47,7 @@ pub(crate) fn acpx_release_profile(agent: &str) -> Option<AcpxReleaseProfile> {
agent_runtime_package: None,
agent_runtime_version: None,
command_digest:
"sha256:3dcc8de3034e32001b0a819a614beca345d314266a70a09e0050a9f5f58275fc",
"sha256:7b459cc43771d4178898be8a362030ac2e4d8356c2a14a82334958959dd54e59",
requires_provider_policy: true,
},
"claude" => AcpxReleaseProfile {
@@ -1029,7 +1029,7 @@ fn provider_death_expires_input_and_commits_failure_without_querying_the_dead_pr
let mut config = acpx_config(&directory, mode);
let mut payload = prepare_payload_with_mode(&directory, "codex", mode);
let identity: Value = serde_json::from_str(include_str!(
"../../../../test/fixtures/copilot-profile-v32-identity.json"
"../../../../test/fixtures/copilot-profile-v33-identity.json"
))
.unwrap();
let digest = identity["commandDigest"].as_str().unwrap();
@@ -7,7 +7,7 @@ import { COPILOT_VERSION, materializePinnedCopilotBinary, resolveCopilotDistribu
const MAX_ARCHIVE_BYTES = 128 * 1024 * 1024;
const MAX_EXPANDED_BYTES = 384 * 1024 * 1024;
const PROFILE_DIGEST = "sha256:3dcc8de3034e32001b0a819a614beca345d314266a70a09e0050a9f5f58275fc";
const PROFILE_DIGEST = "sha256:7b459cc43771d4178898be8a362030ac2e4d8356c2a14a82334958959dd54e59";
/** Build-time only; outputRoot is the exact runner-owned platform asset directory. */
export async function buildPinnedCopilotDistribution({ outputRoot, platform = process.platform, architecture = process.arch }, { fetchImpl = fetch } = {}) {
@@ -7,9 +7,9 @@ import { fileURLToPath } from "node:url";
import { resolve, dirname } from "node:path";
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const fixture = resolve(root, "test/fixtures/copilot-profile-v32-identity.json");
const fixture = resolve(root, "test/fixtures/copilot-profile-v33-identity.json");
const historical = JSON.parse(readFileSync(resolve(root, "test/fixtures/copilot-profile-v12-identity.json"), "utf8"));
const declaration = { ...historical.declaration, agentProfileVersion: 32 };
const declaration = { ...historical.declaration, agentProfileVersion: 33 };
delete declaration.permissionIdentitySourceSha256;
const sources = {
displayEventSourceSha256: "src/drivers/acpx/copilot-events.ts",
@@ -1,10 +1,35 @@
import { redactPaperclipSemanticValue } from "../../semantic-tools/redaction.js";
import { safeAcpxLocations } from "./safe-locations.js";
import { lstatSync, realpathSync } from "node:fs";
import { basename, dirname, isAbsolute, join } from "node:path";
export const COPILOT_PERMISSION_CONTEXT_CONTRACT = "copilot-edit-permission-context-v1";
const record = (value: unknown): Record<string, unknown> =>
value !== null && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : {};
/** Native providers can spell an absolute workspace path through a host alias
* (macOS /tmp and /private/tmp). Attest the real entry, or its existing parent
* for a new file, before converting it to a display target. */
function aliasedLocation(path: string, cwd: string): string | undefined {
if (!isAbsolute(path)) return;
try {
const canonicalCwd = realpathSync(cwd);
let canonicalPath: string;
try {
lstatSync(path);
canonicalPath = realpathSync(path);
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== "ENOENT") return;
// A dangling symlink must not become an apparently safe create target.
try { lstatSync(path); return; } catch (missing) {
if ((missing as NodeJS.ErrnoException).code !== "ENOENT") return;
}
canonicalPath = join(realpathSync(dirname(path)), basename(path));
}
return safeAcpxLocations([{ path: canonicalPath }], canonicalCwd, "edit")[0]?.path as string | undefined;
} catch { return; }
}
/** Display context only; this does not authorize filesystem access. */
export function safeCopilotEditTarget(value: unknown, workingDirectory: string | undefined): string | undefined {
const call = record(value);
@@ -14,7 +39,12 @@ export function safeCopilotEditTarget(value: unknown, workingDirectory: string |
if (Array.isArray(call.locations) && (call.locations.length > 16 || call.locations.some(x => typeof record(x).path !== "string"))) return;
const paths = [input.path, input.fileName, ...(Array.isArray(call.locations) ? call.locations.map(x => record(x).path) : [])].filter(x => x !== undefined);
if (!paths.length || paths.some(x => typeof x !== "string" || !x || x.trim() !== x || x.length > 2048 || /[\\\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u.test(x))) return;
const safe = paths.flatMap(path => safeAcpxLocations([{ path }], workingDirectory, call.kind, call.title));
const safe = paths.flatMap(path => {
const locations = safeAcpxLocations([{ path }], workingDirectory, call.kind, call.title);
if (locations.length) return locations;
const aliased = aliasedLocation(path as string, workingDirectory);
return aliased ? [{ path: aliased }] : [];
});
const names = safe.map(x => x.path).filter((x): x is string => typeof x === "string" && x.length <= 1024 && x.trim() === x && !x.includes(":"));
if (names.length !== paths.length || new Set(names).size !== 1) return;
const target = names[0];
@@ -48,12 +48,12 @@ export const QUALIFIED_ACPX_PROFILE_DATA = {
"protocolVersion": 1,
"acpxVersion": "0.13.1",
"agent": "copilot",
"agentProfileVersion": 32,
"agentProfileVersion": 33,
"agentServerPackage": "@github/copilot",
"agentServerVersion": "1.0.88",
"agentRuntimePackage": null,
"agentRuntimeVersion": null,
"commandDigest": "sha256:3dcc8de3034e32001b0a819a614beca345d314266a70a09e0050a9f5f58275fc",
"commandDigest": "sha256:7b459cc43771d4178898be8a362030ac2e4d8356c2a14a82334958959dd54e59",
"permissionPolicy": "interactive"
},
"claude": {
@@ -1,9 +1,9 @@
{
"commandDigest": "sha256:3dcc8de3034e32001b0a819a614beca345d314266a70a09e0050a9f5f58275fc",
"commandDigest": "sha256:7b459cc43771d4178898be8a362030ac2e4d8356c2a14a82334958959dd54e59",
"declaration": {
"schema": "paperclip.acpx_profile_declaration.v1",
"agent": "copilot",
"agentProfileVersion": 32,
"agentProfileVersion": 33,
"acpxVersion": "0.13.1",
"agentServerVersion": "1.0.88",
"protocolPolicyRevision": "copilot-agent-manual-v1",
@@ -19,7 +19,7 @@
"messageIdentityContract": "copilot-native-message-id-v1",
"ownedDistributionDelivery": "COPILOT_CLI_DIST_DIR:lease-owned-guarded-inner-distribution:v1",
"permissionContextContract": "copilot-edit-permission-context-v1",
"permissionContextSourceSha256": "fe1bb611bea289d81b1c7c973fbb01b93de133a7caa8fe78b7ff4ee1f485b224",
"permissionContextSourceSha256": "de2d38d702b3c574161af0c0c04790fb6f6347916eb3916ce7a131a8c229121b",
"permissionAdapterSourceSha256": "eebcc1ec9bc8caa8d1edf0cbda74bd69f5c55cf8d65a0503e95798b9979a19dc",
"permissionLocationsSourceSha256": "f39a81da395b6e23aa2a1a45572a28846e5e81bfd3d764d9a456f74a79c752e9",
"permissionRedactionSourceSha256": "ad6770c69269087951790e2e87e22ac088e3d432769adf17ebc865ed512e6c4f",