Skip to content
This repository was archived by the owner on Aug 25, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
13347b3
fix(browser/security): propagate runtime context
nisavid Aug 14, 2026
0668e25
fix(computer-use/security): propagate authorization context
nisavid Aug 14, 2026
5a06bb8
fix(window-shell): support current app bundle
nisavid Aug 14, 2026
abab1b4
fix(automation/security): harden runtime propagation
nisavid Aug 17, 2026
af43f7f
fix(browser/security): reject decoy patch anchors
nisavid Aug 17, 2026
2009d98
fix(automation/security): reject executable decoy anchors
nisavid Aug 17, 2026
e6c8be3
test(launcher/security): cover endpoint preservation
nisavid Aug 17, 2026
23ae973
fix(window-shell/security): reject constructor decoys
nisavid Aug 17, 2026
cc2abd0
fix(browser/security): preserve for-await scanner context
nisavid Aug 17, 2026
74bc32b
fix(window-shell/security): bind tray retention helper
nisavid Aug 17, 2026
7da33af
fix(browser/security): scan nested template expressions
nisavid Aug 17, 2026
122fb4e
fix(window-shell/security): correlate tray wrapper factory
nisavid Aug 17, 2026
d7f2188
fix(browser/security): distinguish statement block regexes
nisavid Aug 17, 2026
1bafb08
fix(browser/security): classify regex statement boundaries
nisavid Aug 17, 2026
d5b2de9
fix(browser/security): validate staged client syntax
nisavid Aug 17, 2026
0950cb8
test(browser): refresh official client smoke fixture
nisavid Aug 18, 2026
29791a3
test(chrome): refresh official client smoke fixture
nisavid Aug 18, 2026
0f9e25e
fix(app/security): adapt current official bundle contracts
nisavid Aug 18, 2026
0e98442
test(security): avoid dynamic fixture construction
nisavid Aug 18, 2026
322a1ab
fix(browser/security): install trusted RPC runtime
nisavid Aug 18, 2026
235a1ae
fix(nix): refresh official DMG hash
nisavid Aug 18, 2026
efb3d01
fix(nix): bypass default deb unpacking
nisavid Aug 18, 2026
98a53fc
fix(browser/security): reject Node builtin imports
nisavid Aug 18, 2026
ac3f2a7
fix(browser/security): pin trusted client bytes
nisavid Aug 18, 2026
7d34e04
test(browser/security): align trusted client fixtures
nisavid Aug 18, 2026
771d8b1
fix(nix/browser): preserve static node repl
nisavid Aug 18, 2026
cbeaede
fix(nix/browser): validate static node repl shape
nisavid Aug 18, 2026
be98627
fix(app/review): address automation findings
nisavid Aug 18, 2026
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
133 changes: 133 additions & 0 deletions launcher/start.sh.template
Original file line number Diff line number Diff line change
Expand Up @@ -2999,6 +2999,138 @@ recover_unhealthy_running_app() {
rm -f "$APP_PID_FILE" "$WEBVIEW_PID_FILE" "$LAUNCH_ACTION_SOCKET"
}

recover_stale_computer_use_sockets() {
if ! needs_cold_start; then
return 0
fi
if [ "$LAUNCHER_LOCK_HELD" -ne 1 ]; then
echo "WARN: refusing Computer Use socket recovery without the cold-start launcher lock" >&2
return 0
fi
if [ -z "${XDG_RUNTIME_DIR:-}" ]; then
return 0
fi
Comment thread
nisavid marked this conversation as resolved.

if ! python3 - "$XDG_RUNTIME_DIR" "$LAUNCH_ACTION_RUNTIME_DIR" <<'PY'
import errno
import os
import socket
import stat
import sys

runtime_root = os.path.abspath(sys.argv[1])
runtime_dir = os.path.abspath(sys.argv[2])
current_uid = os.getuid()


def private_directory(path):
try:
metadata = os.lstat(path)
except OSError as exc:
print(f"WARN: preserving Computer Use sockets because {path} cannot be inspected: {exc}", file=sys.stderr)
return False
return (
stat.S_ISDIR(metadata.st_mode)
and not stat.S_ISLNK(metadata.st_mode)
and metadata.st_uid == current_uid
and stat.S_IMODE(metadata.st_mode) == 0o700
)


try:
confined = os.path.commonpath((runtime_root, runtime_dir)) == runtime_root
except ValueError:
confined = False
if not confined or not private_directory(runtime_root):
print("WARN: preserving Computer Use sockets because XDG_RUNTIME_DIR is not a trusted private directory", file=sys.stderr)
sys.exit(0)

relative_dir = os.path.relpath(runtime_dir, runtime_root)
cursor = runtime_root
if relative_dir != ".":
for component in relative_dir.split(os.sep):
if component in ("", ".", ".."):
print("WARN: preserving Computer Use sockets because the runtime path is not confined", file=sys.stderr)
sys.exit(0)
cursor = os.path.join(cursor, component)
if not private_directory(cursor):
print("WARN: preserving Computer Use sockets because the app runtime directory is not private", file=sys.stderr)
sys.exit(0)


def recover_socket(name):
path = os.path.join(runtime_dir, name)
if len(os.fsencode(path)) > 100:
print(f"WARN: preserving Computer Use socket with an overlong path: {name}", file=sys.stderr)
return
try:
before = os.lstat(path)
except FileNotFoundError:
return
except OSError as exc:
print(f"WARN: preserving Computer Use socket {name}: {exc}", file=sys.stderr)
return

if not (
stat.S_ISSOCK(before.st_mode)
and before.st_uid == current_uid
and stat.S_IMODE(before.st_mode) == 0o600
):
print(f"WARN: preserving untrusted Computer Use endpoint: {name}", file=sys.stderr)
return

client = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
client.settimeout(0.15)
try:
client.connect(path)
except FileNotFoundError:
return
except OSError as exc:
if exc.errno != errno.ECONNREFUSED:
print(f"WARN: preserving Computer Use socket {name} after probe error: {exc}", file=sys.stderr)
return
else:
print(f"Preserving active Computer Use socket: {name}")
return
finally:
client.close()

try:
after = os.lstat(path)
except FileNotFoundError:
return
except OSError as exc:
print(f"WARN: preserving Computer Use socket {name} after revalidation error: {exc}", file=sys.stderr)
return

if not (
after.st_dev == before.st_dev
and after.st_ino == before.st_ino
and stat.S_ISSOCK(after.st_mode)
and after.st_uid == current_uid
and stat.S_IMODE(after.st_mode) == 0o600
):
print(f"WARN: preserving Computer Use socket changed during recovery: {name}", file=sys.stderr)
return

try:
os.unlink(path)
except FileNotFoundError:
return
except OSError as exc:
print(f"WARN: could not remove stale Computer Use socket {name}: {exc}", file=sys.stderr)
return
print(f"Removed stale Computer Use socket: {name}")


recover_socket("computer-use-authority.sock")
recover_socket("computer-use-cursor.sock")
PY
then
echo "WARN: Computer Use socket recovery failed closed; preserving existing endpoints" >&2
fi
}

send_warm_start_launch_action() {
[ "$WARM_START" -eq 1 ] || return 1
[ -S "$LAUNCH_ACTION_SOCKET" ] || return 1
Expand Down Expand Up @@ -4961,6 +5093,7 @@ log_phase "initial_launch_state_refreshed"

recover_unhealthy_running_app
prepare_launch_state_under_lock
recover_stale_computer_use_sockets

if send_warm_start_launch_action "${LAUNCHER_ARGS[@]}"; then
echo "Sent launch args over warm-start IPC"
Expand Down
9 changes: 8 additions & 1 deletion plugins/openai-bundled/plugins/computer-use/.mcp.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,14 @@
"computer-use": {
"command": "./bin/chatgpt-computer-use-linux",
"args": ["mcp"],
"cwd": "."
"cwd": ".",
"env_vars": [
"CODEX_HOME",
"XDG_RUNTIME_DIR",
"CHATGPT_LINUX_APP_ID",
"CHATGPT_APP_ID",
"CHATGPT_LINUX_INSTANCE_ID"
]
}
}
}
4 changes: 2 additions & 2 deletions port-integrations/conversation-mode/patch.js
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,7 @@ function applyDictationEndpointPatch(source) {
const recorderCreationPattern =
/let ([A-Za-z_$][\w$]*)=new MediaRecorder\(([A-Za-z_$][\w$]*)\);([A-Za-z_$][\w$]*)\.current=\1;let ([A-Za-z_$][\w$]*)=/gu;
const recorderStartPattern =
/([A-Za-z_$][\w$]*)\.onstop=\(\)=>\{([A-Za-z_$][\w$]*)\(\)\},([A-Za-z_$][\w$]*)==null\?\1\.start\(\):\1\.start\(([A-Za-z_$][\w$]*)\),([A-Za-z_$][\w$]*)\.performance\.mark\(`recording_started`\),([A-Za-z_$][\w$]*)\(!0\)/gu;
/([A-Za-z_$][\w$]*)\.onstop=\(\)=>\{([A-Za-z_$][\w$]*)\(\)\},([A-Za-z_$][\w$]*)==null\?\1\.start\(\):\1\.start\(([A-Za-z_$][\w$]*)\),([A-Za-z_$][\w$]*)\(!0\),([A-Za-z_$][\w$]*)\.performance\.mark\(`recording_started`\),([A-Za-z_$][\w$]*)\(!0\)/gu;
const transcriptPattern =
/([A-Za-z_$][\w$]*)\.length>0&&\(([A-Za-z_$][\w$]*)==null\?([A-Za-z_$][\w$]*)\.getInstance\(\)\.dispatchMessage\(`global-dictation-record-history-item`,\{text:\1\}\):\2\.setTranscript\(\1\),([A-Za-z_$][\w$]*)\.performance\.mark\(`transcript_dispatched`\),([A-Za-z_$][\w$]*)\.action===`send`\?([A-Za-z_$][\w$]*)\.onTranscriptSend\(\1\):\6\.onTranscriptInsert\(\1\)\)/gu;
const uniqueMatch = (pattern) => {
Expand Down Expand Up @@ -319,7 +319,7 @@ function applyDictationEndpointPatch(source) {
patched = patched.replace(
recorderStartMatch[0],
() =>
`${recorderVar}.onstop=()=>{${recorderStartMatch[2]}()},${recorderVar}.chatgptLinuxConversationCleanup=globalThis.chatgptLinuxConversationEndpoint?.({stream:${streamVar},stop:()=>{${actionRef}.current=\`send\`;${recorderVar}.state!==\`inactive\`&&${recorderVar}.stop()},isActive:()=>${recorderRefVar}.current===${recorderVar}&&${recorderVar}.state!==\`inactive\`}),${recorderStartMatch[3]}==null?${recorderVar}.start():${recorderVar}.start(${recorderStartMatch[4]}),${recorderStartMatch[5]}.performance.mark(\`recording_started\`),${recorderStartMatch[6]}(!0)`,
`${recorderVar}.onstop=()=>{${recorderStartMatch[2]}()},${recorderVar}.chatgptLinuxConversationCleanup=globalThis.chatgptLinuxConversationEndpoint?.({stream:${streamVar},stop:()=>{${actionRef}.current=\`send\`;${recorderVar}.state!==\`inactive\`&&${recorderVar}.stop()},isActive:()=>${recorderRefVar}.current===${recorderVar}&&${recorderVar}.state!==\`inactive\`}),${recorderStartMatch[3]}==null?${recorderVar}.start():${recorderVar}.start(${recorderStartMatch[4]}),${recorderStartMatch[5]}(!0),${recorderStartMatch[6]}.performance.mark(\`recording_started\`),${recorderStartMatch[7]}(!0)`,
);
return patched.replace(
transcriptMatch[0],
Expand Down
2 changes: 1 addition & 1 deletion port-integrations/conversation-mode/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ const currentDictationAsset = currentAppInitialAsset;
const currentAssistantAsset = "local-conversation-turn-current.js";

const dictationSource =
"function Opt(){let C={current:null},z={current:null},O={current:[]},k={current:null};let pe=async e=>{let{analytics:t,audio:r,handlers:i,recordingPersistence:a}=e;let l=`hello`;l.length>0&&(a==null?_m.getInstance().dispatchMessage(`global-dictation-record-history-item`,{text:l}):a.setTranscript(l),t.performance.mark(`transcript_dispatched`),e.action===`send`?i.onTranscriptSend(l):i.onTranscriptInsert(l))},ge=async()=>{let e=k.current??`insert`,t={performance:{mark(){}}},n={onTranscriptInsert(){},onTranscriptSend(){}};k.current=null;let i=C.current,a=z.current;z.current=null;let o=O.current;if(O.current=[],i&&(i.ondataavailable=null,i.onstop=null),C.current=null,q(),a?.finalize(e===`abort`||o.length===0,100),e===`abort`||o.length===0)return;await pe({action:e,analytics:t,audio:o,handlers:n,recordingPersistence:a})};return{startDictation:async()=>{let a=wpt(null,null),t=await a.stream,n={performance:{mark(){}}};let c=new MediaRecorder(t);C.current=c;let l=Xit({createdAtMs:1});if(z.current=l,O.current=[],c.ondataavailable=e=>{e.data.size>0&&(O.current.push(e.data),l?.appendChunk(e.data,c.mimeType||e.data.type||`audio/webm`))},c.onstop=()=>{ge()},l==null?c.start():c.start(Zit),n.performance.mark(`recording_started`),_(!0),k.current!=null){c.stop();return}}}}function wpt(e,t){let n=!1,r=null,i=()=>{n=!0,r?.getTracks().forEach(e=>{e.stop()}),r=null};return{dispose:i,stream:(e==null?Udt({channelCount:1},t):Gdt({channelCount:1},e,t)).then(e=>(r=e,n&&i(),e))}}";
"function Opt(){let C={current:null},z={current:null},O={current:[]},k={current:null};let pe=async e=>{let{analytics:t,audio:r,handlers:i,recordingPersistence:a}=e;let l=`hello`;l.length>0&&(a==null?_m.getInstance().dispatchMessage(`global-dictation-record-history-item`,{text:l}):a.setTranscript(l),t.performance.mark(`transcript_dispatched`),e.action===`send`?i.onTranscriptSend(l):i.onTranscriptInsert(l))},ge=async()=>{let e=k.current??`insert`,t={performance:{mark(){}}},n={onTranscriptInsert(){},onTranscriptSend(){}};k.current=null;let i=C.current,a=z.current;z.current=null;let o=O.current;if(O.current=[],i&&(i.ondataavailable=null,i.onstop=null),C.current=null,q(),a?.finalize(e===`abort`||o.length===0,100),e===`abort`||o.length===0)return;await pe({action:e,analytics:t,audio:o,handlers:n,recordingPersistence:a})};return{startDictation:async()=>{let a=wpt(null,null),t=await a.stream,n={performance:{mark(){}}};let c=new MediaRecorder(t);C.current=c;let l=Xit({createdAtMs:1});if(z.current=l,O.current=[],c.ondataavailable=e=>{e.data.size>0&&(O.current.push(e.data),l?.appendChunk(e.data,c.mimeType||e.data.type||`audio/webm`))},c.onstop=()=>{ge()},l==null?c.start():c.start(Zit),ue(!0),n.performance.mark(`recording_started`),_(!0),k.current!=null){c.stop();return}}}}function wpt(e,t){let n=!1,r=null,i=()=>{n=!0,r?.getTracks().forEach(e=>{e.stop()}),r=null};return{dispose:i,stream:(e==null?Udt({channelCount:1},t):Gdt({channelCount:1},e,t)).then(e=>(r=e,n&&i(),e))}}";

const currentComposerControlSource =
"function Vka(e){let{isResponseInProgress:x,onStop:T,submitBlockReason:E,voiceControls:A}=e,j=Nn(Bk),M=RZ(),N=Rk(j),P=LEa(j.value,t),{canRetryDictation:B,dictationShortcutLabel:V,isDictating:U,isDictationButtonVisible:W,isDictationSupported:G,isTranscribing:ee,isVoiceFooterVisible:te,recordingDurationMs:ne,retryDictation:K,startDictation:re,stopDictation:ie,realtimeSession:ae,waveformCanvasRef:oe}=A;let je=(0,x7.jsx)(_ka,{conversationId:N,hostId:g,cwdOverride:_}),ke=(0,x7.jsx)(Twe,{isTranscribing:ee,recordingDurationMs:ne,waveformCanvasRef:oe,stopDictation:ie});let Ae=(0,x7.jsx)(Ewe,{idleIcon:U,isVisible:W,disabled:!G,isTranscribing:ee,canRetryDictation:B,shortcutLabel:V,retryDictation:K,startDictation:re,stopDictation:ie});return Ae}";
Expand Down
8 changes: 4 additions & 4 deletions port-integrations/project-group-last-updated-sort/patch.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
"use strict";

const currentGroupSorter =
"function ROn({groups:e,items:t,projectOrder:n}){let r=new Map(t.map(e=>[e.task.key,e.recencyAt]));return bEn(e.map((e,t)=>({group:e,index:t,recencyAt:e.threadKeys.reduce((e,t)=>Math.max(e,r.get(t)??0),e.projectUpdatedAt??0)})).sort((e,t)=>t.recencyAt-e.recencyAt||e.index-t.index).map(({group:e})=>e),n)}";
"function von({groups:e,projectOrder:t}){return sin(e,t)}";
const patchedGroupSorter =
"function ROn({groups:e,items:t,projectOrder:n,sortMode:chatgptLinuxProjectSortMode}){let r=new Map(t.map(e=>[e.task.key,e.recencyAt]));return((chatgptLinuxRecencySortedGroups)=>chatgptLinuxProjectSortMode===`updated_at`?chatgptLinuxRecencySortedGroups:bEn(chatgptLinuxRecencySortedGroups,n))(e.map((e,t)=>({group:e,index:t,recencyAt:e.threadKeys.reduce((e,t)=>Math.max(e,r.get(t)??0),e.projectUpdatedAt??0)})).sort((e,t)=>t.recencyAt-e.recencyAt||e.index-t.index).map(({group:e})=>e))}";
"function von({groups:e,projectOrder:t,items:n,sortMode:r}){if(r!==`updated_at`)return sin(e,t);let i=new Map;for(let e of n??[]){let t=e.task?.key;t!=null&&i.set(t,Math.max(i.get(t)??0,e.recencyAt??0))}return e.map((e,t)=>({group:e,index:t,recencyAt:(e.threadKeys??[]).reduce((e,t)=>Math.max(e,i.get(t)??0),e.projectUpdatedAt??0)})).sort((e,t)=>t.recencyAt-e.recencyAt||e.index-t.index).map(({group:e})=>e)}";

const currentGroupSorterCall =
"M=ROn({groups:k,items:f,projectOrder:cm(t,ru.PROJECT_ORDER)})";
"M=von({groups:k,projectOrder:im(t,tu.PROJECT_ORDER)})";
const patchedGroupSorterCall =
"M=ROn({groups:k,items:f,projectOrder:cm(t,ru.PROJECT_ORDER),sortMode:j})";
"M=von({groups:k,projectOrder:im(t,tu.PROJECT_ORDER),items:f,sortMode:j})";

function countOccurrences(source, needle) {
return source.split(needle).length - 1;
Expand Down
62 changes: 54 additions & 8 deletions port-integrations/project-group-last-updated-sort/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,12 @@ const {

const currentProjectSource = [
"function vEn(e,t){let n=new Set(e.map(e=>e.projectId)),r=(t??[]).filter(e=>n.has(e)),i=new Set(r);return[...e.map(e=>e.projectId).filter(e=>!i.has(e)),...r]}",
"function bEn(e,t){let n=vEn(e,t),r=new Map(n.map((e,t)=>[e,t]));return[...e].sort((e,t)=>(r.get(e.projectId)??2**53-1)-(r.get(t.projectId)??2**53-1))}",
"function ROn({groups:e,items:t,projectOrder:n}){let r=new Map(t.map(e=>[e.task.key,e.recencyAt]));return bEn(e.map((e,t)=>({group:e,index:t,recencyAt:e.threadKeys.reduce((e,t)=>Math.max(e,r.get(t)??0),e.projectUpdatedAt??0)})).sort((e,t)=>t.recencyAt-e.recencyAt||e.index-t.index).map(({group:e})=>e),n)}",
"function sin(e,t){let n=vEn(e,t),r=new Map(n.map((e,t)=>[e,t]));return[...e].sort((e,t)=>(r.get(e.projectId)??2**53-1)-(r.get(t.projectId)??2**53-1))}",
"function von({groups:e,projectOrder:t}){return sin(e,t)}",
"const prioritySortId=`sidebarElectron.sortMenu.priority`;",
"const updatedSortId=`sidebarElectron.sortMenu.updated`;",
"const manualSortId=`sidebarElectron.sortMenu.manual`;",
"const {projectSortMode:j}=t(qw);M=ROn({groups:k,items:f,projectOrder:cm(t,ru.PROJECT_ORDER)});",
"const {projectSortMode:j}=t(Nb);M=von({groups:k,projectOrder:im(t,tu.PROJECT_ORDER)});",
].join("");

function captureWarns(fn) {
Expand Down Expand Up @@ -80,7 +80,7 @@ function withFeatureConfig(enabled, fn) {
function evaluateGroupSorter(source) {
const context = {};
const sorterSource = source.slice(0, source.indexOf("const prioritySortId"));
vm.runInNewContext(`${sorterSource};globalThis.sortProjectGroups=ROn`, context);
vm.runInNewContext(`${sorterSource};globalThis.sortProjectGroups=von`, context);
return context.sortProjectGroups;
}

Expand Down Expand Up @@ -160,19 +160,65 @@ test("non-updated modes preserve the upstream saved project order", () => {
}
});

test("sorting tolerates missing current item and thread metadata", () => {
const patched = applyPatchTwice(currentProjectSource);
const sortProjectGroups = evaluateGroupSorter(patched);
const groups = [
{ projectId: "newer" },
{ projectId: "older", threadKeys: ["older-task"] },
];
const projectOrder = ["older", "newer"];

assert.deepEqual(
Array.from(
sortProjectGroups({ groups, projectOrder, sortMode: "manual" }),
(group) => group.projectId,
),
["older", "newer"],
);
assert.deepEqual(
Array.from(
sortProjectGroups({
groups,
items: [{ task: null, recencyAt: 2 }],
projectOrder,
sortMode: "updated_at",
}),
(group) => group.projectId,
),
["newer", "older"],
);
assert.deepEqual(
Array.from(
sortProjectGroups({
groups,
items: [
{ task: null, recencyAt: 9 },
{ task: { key: "older-task" }, recencyAt: 5 },
{ task: { key: "older-task" }, recencyAt: 3 },
],
projectOrder,
sortMode: "updated_at",
}),
(group) => group.projectId,
),
["older", "newer"],
);
});

test("patch passes the selected project sort mode into the group sorter", () => {
const patched = applyPatchTwice(currentProjectSource);
assert.ok(
patched.includes(
"projectOrder:cm(t,ru.PROJECT_ORDER),sortMode:j",
"projectOrder:im(t,tu.PROJECT_ORDER),items:f,sortMode:j",
),
);
});

test("drift leaves the asset byte-identical", () => {
const source = currentProjectSource.replace(
"function ROn({groups:e,items:t,projectOrder:n})",
"function ROn({groups:e,items:t,projectOrder:n,unknown:o})",
"function von({groups:e,projectOrder:t})",
"function von({groups:e,projectOrder:t,unknown:o})",
);
const { value, warnings } = captureWarns(() =>
applyProjectGroupLastUpdatedSortPatch(source),
Expand All @@ -185,7 +231,7 @@ test("drift leaves the asset byte-identical", () => {

test("missing current call site leaves the asset byte-identical", () => {
const source = currentProjectSource.replace(
"projectOrder:cm(t,ru.PROJECT_ORDER)",
"projectOrder:im(t,tu.PROJECT_ORDER)",
"projectOrder:unknownProjectOrder",
);
const { value, warnings } = captureWarns(() =>
Expand Down
Loading
Loading