Deny native Copilot detached work before permission dispatch

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-09-29 09:58:05 -05:00
1 parent fcb1421685
commit f9cd8dd256
7 files changed
+207 -21

No files matched your search

+37 -11
View File
@@ -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. |
@@ -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()<observation_deadline:pump(.1)
else:
for _ in range(3):pump(.1)
except PolicyRejected:
# No permission response was sent. Wait past the fixture delay to prove no effect.
time.sleep(3)
finally:
failure=sys.exc_info()[1]
try:
marker_exists=(root/('settlement.txt' if args.scenario in ('attached-shell','detached-shell') else 'denied.txt')).exists()
report={'schema':'paperclip.copilot-acp-evidence/v1','harnessVersion':'1.0.88','package':metadata['name'],'executableSha256':pins[metadata['name']],'modelSource':'deterministic loopback fixture, COPILOT_OFFLINE=true; not a live model qualification','scenario':args.scenario,'mode':args.mode,'resumed':args.resume or args.restart,'providerKilledAndReplaced':args.restart,'replayMutationCount':len((root/'replay-count.txt').read_text()) if (root/'replay-count.txt').exists() else None,'costUsd':0,'filesystemMarkerExistedAtPromptResult':marker_at_prompt_result,'filesystemMarkerExistedAfterPrompt':marker_exists,'modelCalls':total_model_calls,'modelToolNames':model_tool_names,'modelToolResults':model_tool_results,'permissionResponses':permission_responses,'wire':wire}
report={'schema':'paperclip.copilot-acp-evidence/v1','harnessVersion':metadata['version'],'package':metadata['name'],'executableSha256':pins[metadata['name']],'modelSource':'deterministic loopback fixture, COPILOT_OFFLINE=true; not a live model qualification','scenario':args.scenario,'toolPolicyScope':'repository tool-update admission only' if args.tool_policy_node else None,'toolPolicyRejection':policy_rejection,'mode':args.mode,'resumed':args.resume or args.restart,'providerKilledAndReplaced':args.restart,'replayMutationCount':len((root/'replay-count.txt').read_text()) if (root/'replay-count.txt').exists() else None,'costUsd':0,'filesystemMarkerExistedAtPromptResult':marker_at_prompt_result,'filesystemMarkerExistedAfterPrompt':marker_exists,'modelCalls':total_model_calls,'modelToolNames':model_tool_names,'modelToolSchemas':model_tool_schemas,'modelToolResults':model_tool_results,'permissionResponses':permission_responses,'wire':wire}
serialized=json.dumps(report,indent=2).replace(str(root),'/fixture/workspace').replace(str(root).lstrip('/'),'fixture/workspace').replace(binary,'/fixture/verified/copilot')
print(serialized)
except Exception:
if failure is None:raise
if args.tool_policy_node:
assert policy_rejection=='COPILOT_DETACHED_WORK_UNSUPPORTED' and not permission_responses and not marker_exists, 'Detached policy failed to prevent native side effects'
sys.exit(0)
if not session or 'result' not in session or responses.get(final_prompt_id,{}).get('result',{}).get('stopReason')!='end_turn':
raise RuntimeError('Copilot did not complete the fixture turn')
if marker_at_prompt_result != (args.scenario in ('attached-shell','detached-shell')) or marker_exists != marker_at_prompt_result:
@@ -0,0 +1,10 @@
// Credential-free native probe bridge to the exact production policy source.
import { readFileSync } from "node:fs";
import { assertCopilotSessionUpdatePolicy, CopilotDetachedWorkUnsupportedError } from "../src/drivers/acpx/copilot-policy.ts";
try {
assertCopilotSessionUpdatePolicy(JSON.parse(readFileSync(0, "utf8")));
} catch (error) {
if (!(error instanceof CopilotDetachedWorkUnsupportedError)) throw error;
process.stdout.write(error.code);
process.exitCode = 42;
}
@@ -17,6 +17,13 @@ describe("Copilot native policy fence", () => {
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);
@@ -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;
}
};
}
@@ -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");
});
});
@@ -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"
}