diff --git a/doc/architecture/runner-copilot-capabilities.md b/doc/architecture/runner-copilot-capabilities.md index 54162551324..2ff06fdf21e 100644 --- a/doc/architecture/runner-copilot-capabilities.md +++ b/doc/architecture/runner-copilot-capabilities.md @@ -41,7 +41,7 @@ answer cannot be delivered. This narrows the blocking-input question for the admitted pinned mode without claiming an upstream question responder exists. The [offline conformance record](../../packages/paperclip-runner/test/fixtures/copilot-policy-conformance-2026-09-29.json) -retains ten attempts and their evidence digests. A protected file outside the +retains every attempt and its evidence digest, including the isolated comparison release. A protected file outside the working directory was denied before its contents reached the fixture model. A durable conversation was closed and loaded, then requested permission for a shell write; `reject_once` prevented the file. The initial attempt to load an @@ -55,7 +55,8 @@ absent. Both native processes use permission request ID 0, proving why response authority must belong to the current connection. These are local native close/load and process-death probes, not final packaged Product qualification. -All these probes use the verified 1.0.88 ARM64 binary, an explicitly constructed +Production-pin probes use the verified 1.0.88 ARM64 binary; an explicitly selected +1.0.89 comparison has separately verified archive integrity. All use an explicitly constructed credential-free environment, `COPILOT_OFFLINE=true`, and a synthetic OpenAI-style model at an ephemeral loopback HTTP server. The synthetic `gpt-4.1` ID does not name an authenticated GitHub model selection. Counts include the setup turn. @@ -69,7 +70,7 @@ answered Cobalt choice, and warm-session continuation. Copilot instead supplied `[terminal marker]` to `paperclip_finish`. Neither the grader nor the terminal marker requirement changed. A final-runtime rerun remains required. -### Detached background work remains a production blocker +### Native detached work is explicitly unsupported The broader 2026-09-29 deterministic probe reproduces early completion for `bash` with `mode: async` and `detach: true`. After an allowed finite two-second @@ -78,13 +79,38 @@ command, Copilot sends a completed tool update naming a detached shell ID, then keeps observing for three seconds after terminal and proves the marker appears late. Both attempts retain their failed settlement assertion. -The preceding standard tool-call notification includes `detach: true`, but the -permission request carries only the command. Native background-task change -notices have empty payloads and expose no task-list or settle control. Neither -notification counts nor a fixed delay establish settlement. The prior live -finite-detached pass exercised a model that waited; it does not qualify this -native lifetime. A governed runner-owned background interface or a versioned -upstream settlement fix is required to support detached work safely. +The preceding standard tool-call notification includes `rawInput.detach: true`, +but the observed permission request carries only the command. The Copilot guard +rejects that update before the SDK can deliver any permission answer, terminates +the provider, and reports `COPILOT_DETACHED_WORK_UNSUPPORTED` with instructions to +run attached or use a managed runtime service. It also rejects detach carried +only in a permission request. Attached asynchronous work retains the normal +permission path. This is policy denial, not completed background settlement. + +A native offline probe invokes the exact production TypeScript tool-update +policy, kills the provider before permission dispatch, and observes no marker +through the command's bounded delay. It makes one loopback fixture request and +sends zero permission responses. Separate actual patched-ACPX stream tests prove +the same rejection before SDK delivery. The native probe intentionally tests +only tool admission: its synthetic model does not satisfy full production model +admission. Neither layer's result is misrepresented as final-pack live evidence. + +The bounded schema audit finds `detach` on `bash`, absent on `read_bash`, +`stop_bash`, and `task`; `write_bash` and PowerShell variants are not advertised +in this ARM64 catalog. Embedded JavaScript references `write_bash` but delegates +schemas to native Rust. The guard is independent of tool name, but this audit +cannot prove every platform/feature-flag variant or shell-created daemon +lifetime. Comprehensive background qualification remains blocked. Empty native +background-task notices and fixed delays cannot establish settlement. + +An isolated exact-integrity 1.0.89 comparison passes the same attached async +scenario and reproduces the detached late marker. Production remains pinned to +1.0.88. As checked on 2026-09-29, upstream #4743 is closed and explicitly concerns +attached async commands, distinguishing deliberate detached services. These +results do not establish that issue remains unfixed. #4537 remains open; the +narrow denied-write/read/recovery probes did not reproduce its permission bypass. +The original detached failures remain recorded as failures of the attempted +runner settlement contract; they are not relabeled as successful settlement. ## Evidence and scope @@ -228,7 +254,7 @@ Daytona claims require the separate live product qualification. | Task file changes / diffs | Standard tools carry locations/diff content for create/edit/str_replace/apply_patch. | Shared ACP tool activity retains bounded `rawOutput`, `inputUpdated` and the first validated relative location. Structured tool `content` diffs/images, `rawInput` and secondary locations are dropped; no complete diff presentation is claimed. | Real denied-create wire contains a diff, but wire presence is not runner/UI preservation. P1 add typed, bounded diff/image content and all validated locations with tool/session provenance; Product file-edit/validation and downloadable-file presentation pass; rich diff rendering remains unqualified. | | Provider workspace files | `session.workspace_file_changed.path` is relative to provider session workspace files, not task cwd. | Validated reference tagged `provider_session_workspace`, resolution required. | Source + traversal tests; P1 safe file retrieval/upload. | | Images / binary artifacts | Prompt image input; native content-addressed binary_asset base64. | Hash/length-validated metadata references; bytes not blindly read from disk or emitted in activity. | Source + unit digest tests; P1 durable artifact storage; >32 KiB payload provider omission remains. | -| Background settlement | Standard prompt waits for idle in tested attached async-shell case; lossy native idle/receipt also exist. | ACP terminal result remains authoritative; raw event cannot end turn. | Offline attached and real-service finite detached commands completed before end_turn; marker verified through cleanup. A deterministic detach:true case now reproduces end_turn before the finite command completes; see the 2026-09-29 blocker. P0 actual background settlement is required. | +| Background settlement | Standard prompt waits for idle in tested attached async-shell case; lossy native idle/receipt also exist. | ACP terminal result remains authoritative; raw event cannot end turn. | Offline attached and real-service finite detached commands completed before end_turn; marker verified through cleanup. A deterministic detach:true case now reproduces end_turn before the finite command completes; see the 2026-09-29 blocker. Native detach is now rejected before permission delivery; comprehensive background qualification remains blocked pending unproven tool/platform variants. | | Compaction/context | Native compaction lifecycle/token counts/context git metadata. | Safe bounded counters/status, immutable workspace binding. | Source + projection tests; raw summary/private custom instructions omitted. | | Goals / remote/schedules | Native autopilot/objectives/remote/schedule facilities; no qualified ACP goal protocol. | Unsupported through this profile; remote disabled. | Source; P2 separate governance review before control exposure. | diff --git a/packages/paperclip-runner/scripts/probe-copilot-acp.py b/packages/paperclip-runner/scripts/probe-copilot-acp.py index 7a5546e17f8..3f35957de58 100644 --- a/packages/paperclip-runner/scripts/probe-copilot-acp.py +++ b/packages/paperclip-runner/scripts/probe-copilot-acp.py @@ -5,20 +5,25 @@ This proves a protocol fixture, never GitHub/model/Daytona qualification. import argparse,json,subprocess,tempfile,pathlib,os,select,time,threading,http.server,hashlib,shutil,contextlib,sys parser=argparse.ArgumentParser(description=__doc__) parser.add_argument('--package-root',required=True) +parser.add_argument('--comparison-version',choices=['1.0.89'],help='Explicit offline comparison only; production remains pinned to 1.0.88') parser.add_argument('--scenario',choices=['deny-write','deny-shell','deny-read','attached-shell','detached-shell','discover-inputs','native-question','native-plan'],default='deny-write') parser.add_argument('--mode',choices=['agent','plan','autopilot'],default='agent') resume_group=parser.add_mutually_exclusive_group() resume_group.add_argument('--resume',action='store_true',help='Close and load the same native session before prompting') resume_group.add_argument('--restart',action='store_true',help='SIGKILL and replace the provider after one allowed seed mutation; load and verify no replay') +parser.add_argument('--tool-policy-node',help='Apply the repository tool policy before native permission dispatch using this Node 24 binary') args=parser.parse_args() +if args.tool_policy_node and args.scenario!='detached-shell':parser.error('--tool-policy-node requires detached-shell') package_root=pathlib.Path(args.package_root).resolve() metadata=json.loads((package_root/'package.json').read_text()) pins={'@github/copilot-darwin-arm64':'a9ff8babb10b7e443182ae96a8bc50a9c826ef1c773e1344c396eb5bf7f512c3','@github/copilot-darwin-x64':'85eb919f6b9b9dd833ce5e326cbf974b3ee2d4a9ac525c59d4ec9c9ec085715b','@github/copilot-linux-x64':'0059754cf78c3f3bf2c9d4564dfa7e9e25f3a3f8f411f2f0cdad9363f5662748'} -assert metadata['version']=='1.0.88' and metadata['name'] in pins +if args.comparison_version:pins={'@github/copilot-darwin-arm64':'97c12874d9adb9738374a9fb8a52cd724b81132ff815140f7e017bac706feba7'} +assert metadata['version']==(args.comparison_version or '1.0.88') and metadata['name'] in pins assert not (package_root/'copilot').is_symlink() assert hashlib.sha256((package_root/'copilot').read_bytes()).hexdigest()==pins[metadata['name']] model_tool_names=[] model_tool_results=[] +model_tool_schemas={} command='sleep 2; printf ACP_SHELL_DONE > settlement.txt' binary=str(package_root/'copilot') @@ -50,16 +55,21 @@ with contextlib.ExitStack() as cleanup: tool_name={'deny-write':'create','deny-shell':'bash','deny-read':'view','attached-shell':'bash','detached-shell':'bash','native-question':'ask_user','native-plan':'exit_plan_mode'}.get(args.scenario) tool_arguments={'deny-write':{'path':str(root/'denied.txt'),'file_text':'MUST NOT EXIST'},'deny-shell':{'command':'printf MUST_NOT_EXIST > denied.txt','description':'Denied command fixture'},'deny-read':{'path':str(outside)},'attached-shell':{'command':command,'description':'Bounded settlement fixture','mode':'async','detach':False},'detached-shell':{'command':command,'description':'Bounded detached settlement fixture','mode':'async','detach':True},'native-question':{'question':'Choose one','choices':['A','B'],'allowFreeform':False},'native-plan':{'summary':'Fixture plan','planContent':'Do fixture work'}}.get(args.scenario) marker_at_prompt_result=False + policy_rejection=None + class PolicyRejected(Exception):pass class Handler(http.server.BaseHTTPRequestHandler): def log_message(self,*args):pass def do_GET(self): self.send_response(200);self.send_header('Content-Type','application/json');self.end_headers();self.wfile.write(json.dumps({'data':[{'id':'gpt-4.1','object':'model','owned_by':'fixture'}]}).encode()) def do_POST(self): - global calls,total_model_calls,model_tool_names,model_tool_results + global calls,total_model_calls,model_tool_names,model_tool_results,model_tool_schemas calls+=1 total_model_calls+=1 data=json.loads(self.rfile.read(int(self.headers.get('Content-Length','0')))) model_tool_names=sorted(set(model_tool_names+[x.get('function',{}).get('name') for x in data.get('tools',[]) if x.get('function',{}).get('name')])) + for tool in data.get('tools',[]): + function=tool.get('function',{}) + if function.get('name') in ('bash','read_bash','write_bash','stop_bash','task','powershell','write_powershell','read_powershell'):model_tool_schemas[function['name']]=function model_tool_results.extend(x.get('content') for x in data.get('messages',[]) if x.get('role')=='tool') if total_model_calls>6:raise RuntimeError('Fixture model exceeded its bounded request count') if data.get('stream'): @@ -85,6 +95,7 @@ with contextlib.ExitStack() as cleanup: responses={};wire=[];permission_responses=[];buf={p.stdout:b'',p.stderr:b''};nextid=0;marker_at_prompt_result=False def pump(wait=1): + global policy_rejection for stream in select.select([p.stdout,p.stderr],[],[],wait)[0]: data=os.read(stream.fileno(),65536) if not data:continue @@ -94,6 +105,13 @@ with contextlib.ExitStack() as cleanup: if not line:continue if stream==p.stderr:continue message=json.loads(line);wire.append(message) + if args.tool_policy_node and message.get('method')=='session/update': + checked=subprocess.run([args.tool_policy_node,str(pathlib.Path(__file__).with_name('probe-copilot-tool-policy.mjs'))],input=json.dumps(message['params']['update']),text=True,capture_output=True,timeout=5,env={'PATH':'/usr/bin:/bin'}) + if checked.returncode: + if checked.returncode!=42 or checked.stdout.strip()!='COPILOT_DETACHED_WORK_UNSUPPORTED':raise RuntimeError('Repository tool policy probe failed unexpectedly') + policy_rejection=checked.stdout.strip() + p.terminate();p.wait(timeout=5) + raise PolicyRejected(policy_rejection) if 'id' in message and 'method' not in message:responses[message['id']]=message if message.get('method')=='session/request_permission': requested_command=message['params'].get('toolCall',{}).get('rawInput',{}).get('command') @@ -143,16 +161,23 @@ with contextlib.ExitStack() as cleanup: while time.monotonic() { it.each(["/yolo on", " /allow-all", "\n/autopilot", "\t/plan", "\ufeff/cwd /elsewhere", "/unknown-future-command", "/custom-skill"])("rejects native command dispatch %s", text => { expect(() => assertCopilotPromptPolicy(text)).toThrowError(expect.objectContaining({ code: "COPILOT_POLICY_VIOLATION", retryable: false })); }); + it.each(["tool_call", "tool_call_update"])("rejects native detach independent of tool name in %s", sessionUpdate => { + for (const tool of ["bash", "write_bash", "powershell", "unknown"]) { + expect(() => assertCopilotSessionUpdatePolicy({ sessionUpdate, tool, rawInput: { detach: true } })) + .toThrowError(expect.objectContaining({ code: "COPILOT_DETACHED_WORK_UNSUPPORTED", retryable: false })); + expect(() => assertCopilotSessionUpdatePolicy({ sessionUpdate, tool, rawInput: { mode: "async", detach: false } })).not.toThrow(); + } + }); it("preserves ordinary prose mentioning commands and natural-language questions", () => { for (const text of ["Explain /plan", "Ask me before proceeding", "Read this file: /workspace/file", "Plan the work\n/yolo on"]) { expect(() => assertCopilotPromptPolicy(text)).not.toThrow(); @@ -75,6 +82,13 @@ describe("Copilot connection authority", () => { expect(() => guard("inbound", { method: "session/update", params: { sessionId: "s", update: { sessionUpdate: "config_option_update", configOptions: safe.configOptions } } })).toThrow(); } }); + it("also denies detachment carried only by permission rawInput", () => { + const guard = createCopilotProtocolGuard("gpt-5.6-luna"); admit(guard); + expect(() => guard("inbound", { id: 0, method: "session/request_permission", params: { sessionId: "s", toolCall: { rawInput: { detach: true } } } })) + .toThrowError(expect.objectContaining({ code: "COPILOT_DETACHED_WORK_UNSUPPORTED" })); + expect(() => guard("outbound", { id: 0, result: { outcome: { outcome: "selected", optionId: "allow_once" } } })) + .toThrowError(expect.objectContaining({ code: "COPILOT_DETACHED_WORK_UNSUPPORTED" })); + }); it("does not turn native unresolved input into delivered answers or successful continuation", () => { for (const type of ["user_input.requested", "exit_plan_mode.requested", "elicitation.requested"]) { const guard = createCopilotProtocolGuard("gpt-5.6-luna"); admit(guard); diff --git a/packages/paperclip-runner/src/drivers/acpx/copilot-policy.ts b/packages/paperclip-runner/src/drivers/acpx/copilot-policy.ts index 51a892cd226..cfc467c5fd9 100644 --- a/packages/paperclip-runner/src/drivers/acpx/copilot-policy.ts +++ b/packages/paperclip-runner/src/drivers/acpx/copilot-policy.ts @@ -2,7 +2,7 @@ export const COPILOT_AGENT_MODE = "https://agentclientprotocol.com/protocol/session-modes#agent"; export class CopilotPolicyViolationError extends Error { - readonly code = "COPILOT_POLICY_VIOLATION"; + readonly code: string = "COPILOT_POLICY_VIOLATION"; readonly retryable = false; constructor() { super("Copilot session configuration or command cannot preserve the admitted permission policy."); @@ -10,6 +10,15 @@ export class CopilotPolicyViolationError extends Error { } } +export class CopilotDetachedWorkUnsupportedError extends CopilotPolicyViolationError { + override readonly code = "COPILOT_DETACHED_WORK_UNSUPPORTED"; + constructor() { + super(); + this.name = "CopilotDetachedWorkUnsupportedError"; + this.message = "Native detached commands are unsupported in this Copilot runner. Run the command attached or use a managed runtime service."; + } +} + /** * Native ACP interprets a leading slash as a CLI command, before inference. * Reject every such command rather than maintaining an incomplete list of @@ -56,9 +65,15 @@ export function assertCopilotConfigPolicy(value: unknown): void { } } +/** Reject native detachment before permission handling, regardless of tool name. */ +export function assertCopilotToolPolicy(value: unknown): void { + if (record(record(value).rawInput).detach === true) throw new CopilotDetachedWorkUnsupportedError(); +} + /** Call synchronously for owned-session updates; any exception aborts authority. */ export function assertCopilotSessionUpdatePolicy(value: unknown): void { const update = record(value); + if (update.sessionUpdate === "tool_call" || update.sessionUpdate === "tool_call_update") assertCopilotToolPolicy(update); if (update.sessionUpdate === "current_mode_update" && update.currentModeId !== COPILOT_AGENT_MODE) { throw new CopilotPolicyViolationError(); } @@ -74,9 +89,9 @@ export function createCopilotProtocolGuard(expectedModel: string): (direction: " const options = record(result).configOptions; return Array.isArray(options) ? record(options.find(option => record(option).id === "model")).currentValue : undefined; }; - let failed = false; + let failure: CopilotPolicyViolationError | undefined; return (direction, value) => { - if (failed) throw new CopilotPolicyViolationError(); + if (failure) throw failure; try { const message = record(value); const params = record(message.params); @@ -118,6 +133,7 @@ export function createCopilotProtocolGuard(expectedModel: string): (direction: " } else if (request.method === "session/close" && request.sessionId) sessions.delete(request.sessionId); } } + if (direction === "inbound" && method === "session/request_permission") assertCopilotToolPolicy(params.toolCall); if (direction === "inbound" && method === "session/update") { // Replay can precede load's response. It still cannot introduce unsafe // configuration, and it cannot grant admission before that response. @@ -134,12 +150,12 @@ export function createCopilotProtocolGuard(expectedModel: string): (direction: " if (params.type === "session.permissions_changed" && data.mode !== "manual") throw new CopilotPolicyViolationError(); if (params.type === "session.mode_changed" && data.newMode !== "interactive" && data.newMode !== "agent") throw new CopilotPolicyViolationError(); } - } catch { - failed = true; + } catch (error) { + failure = error instanceof CopilotPolicyViolationError ? error : new CopilotPolicyViolationError(); sessions.clear(); models.clear(); pending.clear(); - throw new CopilotPolicyViolationError(); + throw failure; } }; } diff --git a/packages/paperclip-runner/src/drivers/acpx/copilot-protocol-guard.test.ts b/packages/paperclip-runner/src/drivers/acpx/copilot-protocol-guard.test.ts index 2cc2bb36650..f2349dd5a1d 100644 --- a/packages/paperclip-runner/src/drivers/acpx/copilot-protocol-guard.test.ts +++ b/packages/paperclip-runner/src/drivers/acpx/copilot-protocol-guard.test.ts @@ -61,4 +61,19 @@ describe("patched ACPX admission guard", () => { expect(c.kill).not.toHaveBeenCalled(); c.inbound.close(); }); + it("blocks detached command before the permission request can authorize a side effect", async () => { + const c = connection(); + await c.writer.write({ id: 0, method: "session/new", params: {} }); + c.inbound.enqueue({ id: 0, result: safe }); await c.reader.read(); + await c.writer.write({ id: 1, method: "session/prompt", params: { sessionId: "s", prompt: [{ type: "text", text: "Run task" }] } }); + const read = c.reader.read(); + c.inbound.enqueue({ method: "session/update", params: { sessionId: "s", update: { + sessionUpdate: "tool_call", toolCallId: "native-detached", kind: "execute", status: "pending", + rawInput: { command: "fixture", mode: "async", detach: true }, + } } }); + c.inbound.enqueue({ id: 0, method: "session/request_permission", params: { sessionId: "s", toolCall: { toolCallId: "native-detached" } } }); + await expect(read).rejects.toMatchObject({ code: "COPILOT_DETACHED_WORK_UNSUPPORTED", message: expect.stringContaining("Run the command attached") }); + expect(c.written).toHaveLength(2); // Only session/new and session/prompt; no permission response. + expect(c.kill).toHaveBeenCalledWith("SIGTERM"); + }); }); diff --git a/packages/paperclip-runner/test/fixtures/copilot-policy-conformance-2026-09-29.json b/packages/paperclip-runner/test/fixtures/copilot-policy-conformance-2026-09-29.json index 13da8dc2b56..5cda9be4296 100644 --- a/packages/paperclip-runner/test/fixtures/copilot-policy-conformance-2026-09-29.json +++ b/packages/paperclip-runner/test/fixtures/copilot-policy-conformance-2026-09-29.json @@ -169,9 +169,89 @@ "postTerminalObservationMs": 3000, "disposition": "provider_behavior_failure", "failedAssertion": "Detached shell must finish before prompt terminal result" + }, + { + "evidenceFile": "detached-shell-policy-denied.json", + "sha256": "a941e02818297025866b76f541a23d078515dcfd4a43bf26421ffd46025ed720", + "providerVersion": "1.0.88", + "scenario": "detached-shell", + "loopbackModelRequests": 1, + "permissionResponseCount": 0, + "markerAtPromptResult": false, + "markerAfterObservation": false, + "policyRejection": "COPILOT_DETACHED_WORK_UNSUPPORTED", + "disposition": "policy_denied_before_effect", + "scope": "exact repository tool-update policy source, not full production admission" + }, + { + "evidenceFile": "detached-shell-policy-denied-final.json", + "sha256": "4a2e91b561300ec6e2bad7ebb9d599ca502692883ffaa3a30d24d606f3c0284b", + "providerVersion": "1.0.88", + "scenario": "detached-shell", + "loopbackModelRequests": 1, + "permissionResponseCount": 0, + "markerAtPromptResult": false, + "markerAfterObservation": false, + "policyRejection": "COPILOT_DETACHED_WORK_UNSUPPORTED", + "disposition": "policy_denied_before_effect", + "scope": "exact repository tool-update policy source, not full production admission" + }, + { + "evidenceFile": "comparison-1.0.89-attached.json", + "sha256": "95c87df86e91483ac67d7f02ffd76cf2c968e4be5c0f79679c0a1e8f289d6b20", + "providerVersion": "1.0.89", + "scenario": "attached-shell", + "loopbackModelRequests": 3, + "permissionResponseCount": 1, + "markerAtPromptResult": true, + "markerAfterObservation": true, + "policyRejection": null, + "disposition": "passed", + "scope": "unguarded native protocol" + }, + { + "evidenceFile": "comparison-1.0.89-detached.json", + "sha256": "0dd99fa9fe72c831f09bebfa99d9effae16fe895964f9ef726462b92ca1cf0f3", + "providerVersion": "1.0.89", + "scenario": "detached-shell", + "loopbackModelRequests": 2, + "permissionResponseCount": 1, + "markerAtPromptResult": false, + "markerAfterObservation": true, + "policyRejection": null, + "disposition": "unsupported_native_lifetime", + "scope": "unguarded native protocol" } ], "emptySessionLoadDiagnosis": "Copilot does not retain an empty conversation for load. Initial resource-not-found retained; second attempt seeds one loopback-only text turn before close/load. It then requests shell permission and rejects the write.", "providerDeathDiagnosis": "One allowed seed mutation appends X. The exact provider is SIGKILLed and replaced against the same private state, then session/load succeeds. X occurs once after load and the next denied write remains absent. Request ID 0 is reused by the new process and requires fresh permission authority.", - "detachedSettlementDiagnosis": "Pinned Copilot returns end_turn and completed tool status for detach:true before the finite shell creates its marker. A separate diagnostic observes three seconds after terminal and proves the marker appears late. Empty background-task notices cannot prove task identity/count or settlement; no ACP task-list control is advertised. Retained failures are not converted into passes." + "detachedSettlementDiagnosis": "Explicit native detach returns end_turn before command completion in both 1.0.88 and isolated 1.0.89. Original failed settlement assertions are retained. This intentional native lifetime is unsupported by the runner, not proof that attached-shell upstream issue #4743 remains open. The production guard denies rawInput.detach:true before permission dispatch; native no-side-effect probe passes. Native Rust schemas/platform/feature variants and shell escape lifetimes remain outside comprehensive qualification.", + "comparisonRelease": { + "package": "@github/copilot-darwin-arm64", + "version": "1.0.89", + "tarball": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.89.tgz", + "integrity": "sha512-CaBNRZ1gRgatEdlgL2l/Ykw+79sOOYhBWFh3ohifYL7XgPBLf4xnXf59WlOTRT3Ojm4RSbbu/qVwBAa/elIdPQ==", + "tarballSha256": "7292cd2b3f0275e90bed9306bee23c5b7b1e22d604d8ea121f67c24f7192ad49", + "executableSha256": "97c12874d9adb9738374a9fb8a52cd724b81132ff815140f7e017bac706feba7" + }, + "toolSchemaEvidence": { + "evidenceFile": "native-tool-schemas.json", + "sha256": "68463a6afc5cf6b247998d76d4094fdc44789909197b2cd7a380b1649c8b4062", + "detachParameterTools": [ + "bash" + ], + "noDetachParameterTools": [ + "read_bash", + "stop_bash", + "task" + ], + "notAdvertised": [ + "write_bash", + "powershell", + "write_powershell", + "read_powershell" + ], + "comprehensiveAcrossPlatformsAndFlags": false + }, + "detachedPolicySourceSha256": "a4be69be43db84052560420a3ca546a665c9165573c24d543eb33d144ba138a3" }