Skip to content
Open
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
2 changes: 2 additions & 0 deletions Launch Codexify.cmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
@echo off
start "" /min "%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe" -NoLogo -NoProfile -STA -ExecutionPolicy Bypass -WindowStyle Hidden -File "%~dp0scripts\windows-launcher.ps1"
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,12 @@ standard installation. A running background service is restarted after the
verified replacement; a stopped service stays stopped. The command reports when
the installation is already up to date and never downgrades a newer version.

For a native Windows launcher that reuses your saved setup, double-click
`Launch Codexify.cmd` in this checkout. It starts chat, the OpenAI tunnel and the
browser companion, with live status and an optional Windows sign-in shortcut.
See the [Windows launcher guide](docs/WINDOWS_LAUNCHER.md) to install desktop and
Start menu shortcuts.

### 2. Connect to ChatGPT

```sh
Expand Down
3 changes: 3 additions & 0 deletions START CODEXIFY.cmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
@echo off
powershell.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -File "%~dp0scripts\windows-start-local.ps1"
if errorlevel 1 pause
6 changes: 6 additions & 0 deletions docs/REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -743,6 +743,12 @@ The supervisor also creates its ordinary server child with `CREATE_NO_WINDOW`. T
prevents the otherwise empty console window without changing the Interactive
logon principal required for tunnel and network access.

For a read-only Windows autowork snapshot, run
`./scripts/windows-autowork-status.ps1`. It reports server/browser readiness,
supervisor count, free disk space, active goals, last tool activity, and retry
errors. Use `-ChatPrefix` to follow particular conversation IDs. State reads
allow atomic replacement, so monitoring does not block goal updates.

The Windows supervisor begins liveness monitoring after the server first answers
`/health` successfully. Six consecutive failed probes (with a five-second pause
between them and a three-second request timeout) restart the contained server process tree. A slow
Expand Down
55 changes: 55 additions & 0 deletions docs/WINDOWS_LAUNCHER.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Windows launcher

Double-click **Launch Codexify.cmd** in the repository to open the native launcher.
It starts the installed Codexify service, its saved OpenAI tunnel(s), and the
ChatGPT browser companion when browser automation is enabled. Your private local
chat opens in the default browser once startup succeeds.

To install a desktop and Start menu shortcut:

```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\install-windows-launcher.ps1 -Launch
```

The installer copies the launcher into `%USERPROFILE%\.codexify\launcher`, so the
shortcuts keep working if this checkout moves. Run the installer again to update
those copies. It requires no administrator rights and uses Windows PowerShell 5.1
and WPF, which are included with Windows.

The launcher always uses the existing `%USERPROFILE%\.codexify\bin\codexify.exe`
and `%USERPROFILE%\.codexify\codexify.config.json`, rather than the example config
in this checkout. Existing tunnel key references, projects, chat history and the
browser profile are reused. A first-time installation still needs
`codexify quickstart`. The launcher does not provision a new tunnel or connector.

- **Start all** starts the installed service and restores the browser session.
Existing running services and ready/busy browser sessions are reused. It checks
the local chat and tunnel health before reporting a successful connection.
- **Open chat** opens the authenticated local chat using the persistent owner
token in `owner-chat.json`. That token is never displayed in the launcher.
- **Stop services** stops the service (including its supervised tunnels), then
the browser companion. Use this when you intend to interrupt running work.
- **Browser** shows the existing ChatGPT browser window. **Sign in** opens its
sign-in surface when the saved session has expired.
- **Launch at Windows sign-in** adds/removes only this launcher's per-user Startup
shortcut. The existing Codexify service's own login setting is independent.
- **Open chat after startup** is remembered in `launcher\preferences.json`.

Closing the launcher leaves the services running. Opening another copy brings the
existing launcher to the front. Periodic status checks and service commands run in
a background runspace so the window stays responsive. Startup failures remain
visible in the activity area; the service logs are under `.codexify\logs`.
Command output draining is bounded after the CLI exits, so a persistent browser
that inherits output handles cannot leave startup waiting indefinitely. Live
health checks still decide whether the chat and tunnel are connected.

For a status-only launch, pass `-NoAutoStart` to `windows-launcher.ps1`. For a
non-operational visual preview, pass `-PreviewPath C:\path\preview.png`. Preview
mode starts no services and writes no preferences or shortcuts. `-CapturePath`
saves the actual window after its first completed action and leaves it open.

Run the isolated launcher checks with:

```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\test-windows-launcher.ps1
```
30 changes: 30 additions & 0 deletions scripts/install-windows-launcher.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
[CmdletBinding()]
param([switch]$Launch, [switch]$StartWithWindows)

Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
. (Join-Path $PSScriptRoot 'windows-launcher-core.ps1')
$context = New-LauncherContext
$null = Get-LauncherSettings $context
$null = New-Item -ItemType Directory -Path $context.LauncherDirectory -Force
foreach ($name in @('windows-launcher.ps1', 'windows-launcher-core.ps1', 'windows-launcher.xaml')) {
$source = Join-Path $PSScriptRoot $name
$destination = Join-Path $context.LauncherDirectory $name
if ([IO.Path]::GetFullPath($source) -ine [IO.Path]::GetFullPath($destination)) {
Copy-Item -LiteralPath $source -Destination $destination -Force
}
}
$installedScript = Join-Path $context.LauncherDirectory 'windows-launcher.ps1'
$desktopLink = Join-Path ([Environment]::GetFolderPath('DesktopDirectory')) 'Codexify Launcher.lnk'
$menuLink = Join-Path ([Environment]::GetFolderPath('Programs')) 'Codexify Launcher.lnk'
Set-LauncherShortcut $desktopLink $installedScript $context.Executable
Set-LauncherShortcut $menuLink $installedScript $context.Executable
if ($StartWithWindows) {
$startupLink = Join-Path ([Environment]::GetFolderPath('Startup')) 'Codexify Launcher.lnk'
Set-LauncherShortcut $startupLink $installedScript $context.Executable
}
[pscustomobject]@{ Launcher = $installedScript; DesktopShortcut = $desktopLink; StartMenuShortcut = $menuLink }
if ($Launch) {
$powershell = Join-Path $env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe'
Start-Process -FilePath $powershell -ArgumentList (Get-LauncherShortcutArguments $installedScript) -WindowStyle Hidden
}
112 changes: 112 additions & 0 deletions scripts/test-windows-agent-update-guards.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
#Requires -Version 7.0
$ErrorActionPreference='Stop'
. (Join-Path $PSScriptRoot 'windows-agent-update-guards.ps1')
$app='C:\fixture\.codexify'
$cua='C:\fixture\runtimes\cua_node'
function Row($Id,$Parent,$Name,$Exe,$Command){[pscustomobject]@{ProcessId=$Id;ParentProcessId=$Parent;Name=$Name;ExecutablePath=$Exe;CommandLine=$Command}}
function Check($Rows,$Expected,$Label){
$actual=@(Get-AgentUpdateWorkProcesses -Processes $Rows -ServerId 10 -BrowserId 20 -AppRoot $app -CuaRuntimeRoot $cua -SystemRoot 'C:\Windows'|ForEach-Object ProcessId|Sort-Object)
if(($actual -join ',') -ne (@($Expected|Sort-Object) -join ',')){throw "$Label failed: expected $Expected, got $actual"}
}
$base=@(
(Row 11 10 'node.exe' "$cua\v1\bin\node.exe" "node.exe C:/runtime/@oai/cua-repl/bin/cua-repl.mjs"),
(Row 12 10 'node_repl.exe' "$cua\v1\bin\node_repl.exe" "`"$cua\v1\bin\node_repl.exe`""),
(Row 13 11 'node_repl.exe' "$cua\v1\bin\node_repl.exe" "`"$cua\v1\bin\node_repl.exe`""),
(Row 14 10 'tunnel-client-runtime.exe' "$app\openai-tunnel\v1\tunnel-client-runtime.exe" 'tunnel-client-runtime.exe run --control-plane.tunnel-id fixture'),
(Row 15 10 'conhost.exe' 'C:\Windows\System32\conhost.exe' 'conhost.exe'),
(Row 20 10 'codexify-browser-companion.exe' "$app\browser\runtime\v1\app\codexify-browser-companion.exe" 'browser'),
(Row 21 20 'chrome.exe' "$app\browser\runtime\v1\chrome.exe" 'chrome'),
(Row 22 21 'powershell.exe' 'C:\Windows\powershell.exe' 'private renderer priority helper')
)
Check $base @() 'Known idle infrastructure'
Check ($base+@((Row 30 10 'powershell.exe' 'C:\Windows\powershell.exe' 'cargo build'),(Row 31 30 'cargo.exe' 'C:\cargo.exe' 'cargo build'),(Row 32 31 'rustc.exe' 'C:\rustc.exe' 'rustc'))) @(30,31,32) 'Yielded build remains busy'
Check ($base+@((Row 40 12 'node.exe' "$cua\v1\bin\node.exe" 'node project-server.mjs'))) @(40) 'User command through idle Node host'
Check ($base+@((Row 50 999 'cargo.exe' 'C:\cargo.exe' 'unrelated build'))) @() 'Unrelated project process'
Check @((Row 20 10 'codexify-browser-companion.exe' 'C:\project\other.exe' 'other'),(Row 21 20 'cargo.exe' 'C:\cargo.exe' 'build')) @(20,21) 'Unverified browser subtree is busy'
Check ($base+@((Row 60 10 'custom-build.exe' 'C:\project\custom-build.exe' 'custom'))) @(60) 'Unknown native child is busy'
$self=Get-CimInstance Win32_Process -Filter ('ProcessId='+$PID)
if(-not(Test-AgentUpdateProcessRunning $self)){throw 'Current test process must be detected as running'}
$fixture=Start-Process -FilePath (Get-Command pwsh).Source -ArgumentList @('-NoProfile','-Command','exit 0') -WindowStyle Hidden -PassThru
try {
$row=[pscustomobject]@{ProcessId=$fixture.Id;CreationDate=$fixture.StartTime}
if(-not $fixture.WaitForExit(10000)){throw 'Fixture process did not finish'}
if(Test-AgentUpdateProcessRunning $row){throw 'An exited process with a retained handle is not active work'}
} finally {$fixture.Dispose()}
if(-not(Test-AgentUpdateBrowserStopped ([pscustomobject]@{state='stopped';running=$false}) @())){throw 'Verified stopped status must be accepted'}
if(Test-AgentUpdateBrowserStopped ([pscustomobject]@{state='stopped';running=$false}) @([pscustomobject]@{ProcessId=999})){throw 'Stopped status must not hide a live private process'}
$idle=@{kind='codexify-browser-companion';status='idle';authenticated=$true;accountFingerprint='fixture';activeActionId=$null}
if(-not(Test-AgentUpdateBrowserIdle ([pscustomobject]$idle))){throw 'Verified idle legacy window must be accepted'}
foreach($change in @(@{status='busy'},@{authenticated=$false},@{activeActionId='accepted-send'},@{kind='other-service'})){
$probe=$idle.Clone();foreach($key in $change.Keys){$probe[$key]=$change[$key]}
if(Test-AgentUpdateBrowserIdle ([pscustomobject]$probe)){throw 'A busy, warming or unrelated window was accepted for update'}
}
$pool=$idle.Clone();$pool.activeActionIds=@('another-page-send');$pool.actionCapacity=4;$pool.availableActionSlots=3
if(Test-AgentUpdateBrowserIdle ([pscustomobject]$pool)){throw 'A free pool slot must not hide a Send owned by another page'}
$pool.activeActionIds=@()
if(Test-AgentUpdateBrowserIdle ([pscustomobject]$pool)){throw 'Unavailable pool capacity must defer updates even without a visible action ID'}
$pool.availableActionSlots=4
if(-not(Test-AgentUpdateBrowserIdle ([pscustomobject]$pool))){throw 'A completely idle verified pool must be accepted'}
$stopped=[pscustomobject]@{generationRunning=$false;observedActionPages=1}
if(-not(Test-AgentUpdateBrowserQuiescent ([pscustomobject]$idle) $stopped)){throw 'A verified idle single page with no generation must pass'}
foreach($diagnostics in @($null,[pscustomobject]@{},[pscustomobject]@{generationRunning=$true;observedActionPages=1})){
if(Test-AgentUpdateBrowserQuiescent ([pscustomobject]$idle) $diagnostics){throw 'Running or unknown generation must defer the update'}
}
if(Test-AgentUpdateBrowserQuiescent ([pscustomobject]$pool) $stopped){throw 'Inspecting only one of four pages is insufficient'}
$allStopped=[pscustomobject]@{generationRunning=$false;observedActionPages=4}
if(-not(Test-AgentUpdateBrowserQuiescent ([pscustomobject]$pool) $allStopped)){throw 'All idle verified pages must pass'}

$tempRoot=[IO.Path]::GetFullPath([IO.Path]::GetTempPath()).TrimEnd('\')+'\'
$fixtureRoot=Join-Path $tempRoot ('codexify-navigation-stop-test-'+[Guid]::NewGuid().ToString('N'))
$originalProfile=$env:USERPROFILE
try {
$env:USERPROFILE=$fixtureRoot
$fixtureScripts=Join-Path $fixtureRoot 'scripts'
$fixtureArtifacts=Join-Path $fixtureRoot 'browser-companion/artifacts'
$fixtureApp=Join-Path $fixtureRoot '.codexify'
$browserRoot=Join-Path $fixtureApp 'browser'
$resources=Join-Path $browserRoot 'runtime/2.3.15/app/resources'
$null=New-Item -ItemType Directory -Path $fixtureScripts,$fixtureArtifacts,$resources,(Join-Path $fixtureApp 'backups') -Force
foreach($name in @('windows-apply-navigation-stall-patch.ps1','windows-agent-update-guards.ps1')){
Copy-Item -LiteralPath (Join-Path $PSScriptRoot $name) -Destination $fixtureScripts
}
@'
$script:fixtureDiagnosticCalls=0
function Invoke-StartLocalCli { throw 'Unexpected native lifecycle command in stopped browser fixture' }
function Invoke-RestMethod {
param($Uri,$Headers,$TimeoutSec)
if($Uri -like '*/v1/health'){
return [pscustomobject]@{kind='codexify-browser-companion';status='idle';authenticated=$true;accountFingerprint='fixture'}
}
if($Uri -notlike '*/v1/diagnostics/presentation'){throw 'Unexpected fixture request'}
$script:fixtureDiagnosticCalls++
if($script:fixtureDiagnosticCalls -eq 2){[IO.File]::WriteAllText($lifecyclePath,'{"automaticStart":false}')}
[pscustomobject]@{generationRunning=$false;observedActionPages=1}
}
'@ | Set-Content -LiteralPath (Join-Path $fixtureScripts 'windows-start-local-core.ps1')
$archive=Join-Path $resources 'app.asar'
$candidate=Join-Path $fixtureArtifacts 'candidate.asar'
[IO.File]::WriteAllText($archive,'fixture installed archive')
[IO.File]::WriteAllText($candidate,'fixture candidate archive')
$previousHash=(Get-FileHash -LiteralPath $archive).Hash
@{version='2.3.15';live=$archive;candidate=$candidate;previousSha256=$previousHash;sha256=(Get-FileHash -LiteralPath $candidate).Hash} |
ConvertTo-Json | Set-Content -LiteralPath (Join-Path $fixtureArtifacts 'navigation-stall-ready.json')
$lifecycle=Join-Path $browserRoot 'lifecycle.json'
$fixtureUpdater=Join-Path $fixtureScripts 'windows-apply-navigation-stall-patch.ps1'
[IO.File]::WriteAllText($lifecycle,'{"automaticStart":false}')
$result=& $fixtureUpdater | ConvertFrom-Json
if($result.reason -ne 'browser_explicitly_stopped'){throw 'Explicitly stopped browser with no descriptor must defer'}
[IO.File]::WriteAllText($lifecycle,'{"automaticStart":true}')
[IO.File]::WriteAllText((Join-Path $browserRoot 'descriptor.json'),'{"endpoint":"http://127.0.0.1:1","token":"fixture"}')
[IO.File]::WriteAllText((Join-Path $fixtureApp 'codexify.config.json'),'{}')
$result=& $fixtureUpdater | ConvertFrom-Json
if($result.reason -ne 'browser_explicitly_stopped'){throw 'An explicit stop during updater preflight must defer'}
if((Get-FileHash -LiteralPath $archive).Hash -ne $previousHash){throw 'Deferral changed the installed archive'}
} finally {
$env:USERPROFILE=$originalProfile
if(Test-Path -LiteralPath $fixtureRoot){
$resolved=(Resolve-Path -LiteralPath $fixtureRoot).Path
if(-not $resolved.StartsWith($tempRoot,[StringComparison]::OrdinalIgnoreCase) -or (Split-Path -Leaf $resolved) -notlike 'codexify-navigation-stop-test-*'){throw 'Unsafe fixture cleanup path'}
Remove-Item -LiteralPath $resolved -Recurse -Force
}
}
'Passed 26 updater process, idle-window, generation and explicit-stop checks'
34 changes: 34 additions & 0 deletions scripts/test-windows-autowork-state-files.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
$ErrorActionPreference = 'Stop'
. (Join-Path $PSScriptRoot 'windows-autowork-state-files.ps1')
$base = [IO.Path]::GetFullPath([IO.Path]::GetTempPath()).TrimEnd('\') + '\'
$root = Join-Path $base ('codexify-monitor-test-' + [guid]::NewGuid().ToString('N'))
$null = New-Item -ItemType Directory -Path $root
function Transcript-Id([string]$Path) {
$sha = [Security.Cryptography.SHA256]::Create()
try { -join ($sha.ComputeHash([Text.Encoding]::UTF8.GetBytes($Path)) | ForEach-Object { $_.ToString('x2') }) }
finally { $sha.Dispose() }
}
try {
$key = 'a' * 64
$real = Join-Path $root ('projects\registered\chats\' + $key)
$fake = Join-Path $root ('projects\.tmp-old-fixture\chats\' + ('b' * 64))
$nested = Join-Path $root ('projects\registered\fixtures\nested\chats\' + ('c' * 64))
foreach ($dir in @($real,$fake,$nested)) {
$null = New-Item -ItemType Directory -Path $dir -Force
[IO.File]::WriteAllText((Join-Path $dir 'CHAT.md'), '# fixture')
[IO.File]::WriteAllText((Join-Path $dir 'automation.json'), '{"goal":{"status":"active"}}')
}
$inventory = [pscustomobject]@{chats=@([pscustomobject]@{id=(Transcript-Id (Join-Path $real 'CHAT.md'))})}
$files = @(Select-AutoworkStateFiles $root $inventory)
if ($files.Count -ne 1 -or $files[0].FullName -ne (Join-Path $real 'automation.json')) { throw 'Unregistered or nested test state entered the monitor' }
$nativePath = $root + '/projects\registered\chats\' + $key + '\CHAT.md'
$inventory.chats[0].id = Transcript-Id $nativePath
if (@(Select-AutoworkStateFiles $root $inventory).Count -ne 1) { throw 'Native mixed-separator owner ID was lost' }
$inventory.chats = @()
if (@(Select-AutoworkStateFiles $root $inventory).Count -ne 0) { throw 'Empty live inventory fell back to historical goals' }
Write-Output 'PASS: registered chats, fake active fixtures, nested fixtures, native path spelling, empty inventory'
} finally {
$resolved = [IO.Path]::GetFullPath($root)
if (-not $resolved.StartsWith($base,[StringComparison]::OrdinalIgnoreCase) -or (Split-Path -Leaf $resolved) -notlike 'codexify-monitor-test-*') { throw 'Unsafe test cleanup path' }
Remove-Item -LiteralPath $resolved -Recurse -Force
}
Loading