Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
600e323
fix(kimi): K2.8 registry refresh — adjustable thinking, 1M context, k…
yuanyuanlove Sep 21, 2026
3726427
docs: fold the desktop app into a beta section and record install loc…
lidge-jun Sep 21, 2026
cfbe9a9
fix(desktop): let the bootstrap surface stay silent until it has some…
lidge-jun Sep 21, 2026
e4aebd7
feat(release): verify everything before publication and add a resumab…
lidge-jun Sep 21, 2026
a9fb950
Harden durable runtime ownership against stale consent and unsafe rep…
lidge-jun Sep 21, 2026
28d88ab
fix(desktop): keep one local bundle format from destroying the rest (…
lidge-jun Sep 21, 2026
f19b229
fix(cli): warn only for proven stale app servers (#5394)
Ingwannu Sep 21, 2026
ac3df52
fix(responses): repair exec_command input wrappers (#5395)
Ingwannu Sep 21, 2026
403b6b2
test(ci): derive the release-verify fixture's signed set from the upd…
lidge-jun Sep 21, 2026
1687636
fix(runtime): close ownership mutation races (#5426)
lidge-jun Sep 21, 2026
52acf81
fix(cli): probe endpoint liveness in-process so the standalone resolv…
devin-ai-integration[bot] Sep 21, 2026
1e233a4
fix(desktop): ship the startup surface as one page the policy can nam…
lidge-jun Sep 21, 2026
8f94a6f
feat(desktop): open a usage popup from the tray icon (#5452)
lidge-jun Sep 21, 2026
f2ebc5a
fix(desktop): let the tray's left click reach the usage popup (#5462)
lidge-jun Sep 21, 2026
71d02e3
fix(desktop): make the startup surface unable to wait forever (#5451)
lidge-jun Sep 21, 2026
e4ceeb3
docs(devlog): land the app runtime ownership record and close the uni…
lidge-jun Sep 21, 2026
498498d
fix(web-search): combine replay-cache isolation with deadline-safe qu…
luvs01 Sep 21, 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
175 changes: 144 additions & 31 deletions .github/workflows/release.yml

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Release automation needs explicit security review

The workflow changes publication ordering and adds a resumable release path. Repository policy requires explicit security review for this exact head.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ on:
required: false
type: boolean
default: true
resume-after-npm-publish:
description: "Operator attestation: a previous run of this workflow acknowledged npm publication for this exact commit; skip npm publish and complete the GitHub side"
required: false
type: boolean
default: false
expected-sha:
description: "Immutable release commit this dispatch must publish (fail if the branch moved)"
required: true
Expand Down Expand Up @@ -156,9 +161,10 @@ jobs:
tar -czf "../../ocx-${RELEASE_VERSION}-${STANDALONE_TARGET}.tar.gz" ocx gui
fi
cd ../..
# shasum -c in attach-release runs from dist/release, where the artifact download
# lands these files flat; the checksum therefore records the bare file name, which
# sha256sum takes verbatim from its argument.
# The pre-publication verifier resolves every recorded checksum from
# dist/release, where the artifact download lands these files flat; the
# checksum therefore records the bare file name, which sha256sum takes
# verbatim from its argument.
if [[ "$RUNNER_OS" == "Windows" ]]; then sha256sum "ocx-${RELEASE_VERSION}-${STANDALONE_TARGET}.zip" > "ocx-${RELEASE_VERSION}-${STANDALONE_TARGET}.sha256"
else sha256sum "ocx-${RELEASE_VERSION}-${STANDALONE_TARGET}.tar.gz" > "ocx-${RELEASE_VERSION}-${STANDALONE_TARGET}.sha256"
fi
Expand Down Expand Up @@ -424,15 +430,19 @@ jobs:
security delete-keychain "${OPENCODEX_SIGNING_KEYCHAIN}"
fi

attach-release:
# Pre-publication verification. Everything that will be published is checked
# here — expected platform set, every checksum, the updater signatures, and the
# manifest parse-back — and publication consumes this result rather than
# verifying after the fact. Runs on dry-run too: a dry run must prove the same
# chain a real release will rely on.
verify-release:
runs-on: ubuntu-latest
needs: [publish, package-standalone, package-desktop]
if: ${{ inputs.dry-run != true }}
env:
UPDATER_SIGNING_CONFIGURED: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY != '' }}
needs: [validate-dispatch, package-standalone, package-desktop]
timeout-minutes: 10
permissions:
contents: write
contents: read
env:
UPDATER_SIGNING_CONFIGURED: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY != '' }}
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
Expand All @@ -456,24 +466,86 @@ jobs:
merge-multiple: true
path: dist/release

# Generate latest.json only when the updater key is configured; then require
# signatures for all four updater platforms before publishing it.
- name: Generate updater manifest
if: env.UPDATER_SIGNING_CONFIGURED == 'true'
- name: Verify release assets
env:
RELEASE_VERSION: ${{ inputs.version }}
run: |
bun desktop/scripts/updater-manifest.ts \
--version "$RELEASE_VERSION" \
--dir dist/release \
--repo lidge-jun/opencodex \
--out dist/release/latest.json \
--require-all
set -euo pipefail
args=(
--version "$RELEASE_VERSION"
--dir dist/release
--repo "$GITHUB_REPOSITORY"
--sha "$GITHUB_SHA"
--receipt-out verification/receipt.json
)
# Signatures are verified whenever they exist; the manifest is only
# generated when this run holds the updater key, exactly as before.
if [ "$UPDATER_SIGNING_CONFIGURED" = "true" ]; then
args+=(--manifest-out dist/release/latest.json --require-signatures)
fi
bun desktop/scripts/verify-release-assets.ts "${args[@]}"

- name: Upload verified release bundle
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: verified-release
path: dist/release/
if-no-files-found: error
retention-days: 7

- name: Verify the checksum before uploading
- name: Upload verification receipt
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: release-verification-receipt
path: verification/receipt.json
if-no-files-found: error
retention-days: 7

attach-release:
runs-on: ubuntu-latest
needs: [publish, verify-release]
if: ${{ inputs.dry-run != true }}
timeout-minutes: 10
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
persist-credentials: false

- name: Setup project Bun
uses: ./.github/actions/setup-project-bun

- name: Download the verified release bundle
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: verified-release
path: dist/release

- name: Download the verification receipt
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: release-verification-receipt
path: verification

# The bundle is attached exactly as verified: the receipt must name this
# run's version and commit, or nothing uploads.
- name: Require the verification receipt for this commit
env:
RELEASE_VERSION: ${{ inputs.version }}
run: |
cd dist/release
shasum -a 256 -c ./*.sha256
set -euo pipefail
receipt_version="$(bun -e 'console.log(JSON.parse(await Bun.file("verification/receipt.json").text()).version)')"
receipt_sha="$(bun -e 'console.log(JSON.parse(await Bun.file("verification/receipt.json").text()).sha)')"
test "$receipt_version" = "$RELEASE_VERSION" || {
echo "::error::verification receipt names version $receipt_version, not $RELEASE_VERSION"
exit 1
}
test "$receipt_sha" = "$GITHUB_SHA" || {
echo "::error::verification receipt names commit $receipt_sha, not $GITHUB_SHA"
exit 1
}

- name: Attach to the release
env:
Expand All @@ -485,7 +557,7 @@ jobs:
gh release upload "v${RELEASE_VERSION}" dist/release/* --clobber

publish:
needs: [validate-dispatch, package-standalone, package-desktop]
needs: [validate-dispatch, verify-release]
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
Expand Down Expand Up @@ -676,6 +748,7 @@ jobs:
GH_TOKEN: ${{ github.token }}
RELEASE_VERSION: ${{ inputs.version }}
DRY_RUN: ${{ inputs.dry-run }}
RESUME: ${{ inputs.resume-after-npm-publish }}
run: |
set -euo pipefail

Expand All @@ -692,7 +765,9 @@ jobs:
fi

if [ -n "$existing_tag_sha" ]; then
if [ "$dry_run" = "true" ]; then
if [ "$RESUME" = "true" ]; then
echo "::notice::${release_tag} already exists at this commit; resuming"
elif [ "$dry_run" = "true" ]; then
echo "::notice::${release_tag} already exists at this commit; dry-run only"
else
echo "::error::${release_tag} already exists. Refusing to publish a version with pre-existing Git metadata."
Expand All @@ -701,32 +776,46 @@ jobs:
fi

if gh release view "$release_tag" >/dev/null 2>&1; then
if [ "$dry_run" = "true" ]; then
if [ "$RESUME" = "true" ]; then
echo "::notice::GitHub Release ${release_tag} already exists; resuming to complete the attachment"
elif [ "$dry_run" = "true" ]; then
echo "::notice::GitHub Release ${release_tag} already exists; dry-run only"
else
echo "::error::GitHub Release ${release_tag} already exists. Choose the next unused patch version."
exit 1
fi
fi

if [ "$RESUME" = "true" ] && [ "$dry_run" = "true" ]; then
echo "::error::resume-after-npm-publish is a real-publication recovery path and cannot combine with dry-run"
exit 1
fi
if npm view "${pkg_name}@${RELEASE_VERSION}" version >/dev/null 2>&1; then
if [ "$dry_run" = "true" ]; then
if [ "$RESUME" = "true" ]; then
echo "::notice::${pkg_name}@${RELEASE_VERSION} is acknowledged on npm; resuming after the recorded partial publication"
elif [ "$dry_run" = "true" ]; then
echo "::notice::${pkg_name}@${RELEASE_VERSION} already exists on npm; dry-run only"
else
echo "::error::${pkg_name}@${RELEASE_VERSION} already exists on npm. Choose the next unused patch version."
echo "::error::${pkg_name}@${RELEASE_VERSION} already exists on npm. If a previous run acknowledged this publication and failed afterwards, re-dispatch with resume-after-npm-publish: true; otherwise choose the next unused patch version."
exit 1
fi
elif [ "$RESUME" = "true" ]; then
echo "::error::resume-after-npm-publish is set, but ${pkg_name}@${RELEASE_VERSION} is not on npm — there is no acknowledged publication to resume from"
exit 1
fi

- name: Refuse a release the current tag set already outranks
env:
RELEASE_VERSION: ${{ inputs.version }}
DRY_RUN: ${{ inputs.dry-run }}
RESUME: ${{ inputs.resume-after-npm-publish }}
run: |
set -euo pipefail
allow=""
existing_tag_sha="$(git rev-parse -q --verify "refs/tags/v${RELEASE_VERSION}^{commit}" || true)"
if [ "$DRY_RUN" = "true" ] && [ -n "$existing_tag_sha" ] && [ "$existing_tag_sha" = "$GITHUB_SHA" ]; then
# Dry-run re-dispatches and the resume path both legitimately find the tag
# already at this commit; a moved tag is still refused above.
if { [ "$DRY_RUN" = "true" ] || [ "$RESUME" = "true" ]; } && [ -n "$existing_tag_sha" ] && [ "$existing_tag_sha" = "$GITHUB_SHA" ]; then
allow="--allow-existing-tag-at-head"
fi
git tag --list 'v*' | bun scripts/version-line.ts assert-releasable "$RELEASE_VERSION" $allow
Expand Down Expand Up @@ -755,15 +844,25 @@ jobs:
env:
DRY_RUN: ${{ inputs.dry-run }}
NPM_DIST_TAG: ${{ inputs.tag }}
RESUME: ${{ inputs.resume-after-npm-publish }}
RELEASE_VERSION: ${{ inputs.version }}
run: |
set -euo pipefail
if [ "$DRY_RUN" = "true" ]; then
pkg_name="$(node -p "require('./package.json').name")"
if [ "$RESUME" = "true" ]; then
# npm publication was acknowledged by the earlier run and confirmed by the
# preflight above; completing the GitHub side must never republish.
echo "::notice::RESUME — npm publish skipped; publication already acknowledged"
echo "published=true" >> "$GITHUB_OUTPUT"
echo "Publication resumed for ${pkg_name}@${RELEASE_VERSION} at ${GITHUB_SHA} (npm publish skipped; acknowledged by the earlier run)." >> "$GITHUB_STEP_SUMMARY"
elif [ "$DRY_RUN" = "true" ]; then
echo "::notice::DRY RUN — building + packing, not publishing"
npm run prepublishOnly
npm pack --dry-run
else
npm publish --tag "$NPM_DIST_TAG" --access public
echo "published=true" >> "$GITHUB_OUTPUT"
echo "Publication acknowledged for ${pkg_name}@${RELEASE_VERSION} at ${GITHUB_SHA}. If any later step in this run fails, re-dispatch with the same version and expected-sha plus resume-after-npm-publish: true — never republish this version." >> "$GITHUB_STEP_SUMMARY"
fi

# Publication is acknowledged before registry reads, which can lag or fail.
Expand Down Expand Up @@ -805,6 +904,7 @@ jobs:
env:
GH_TOKEN: ${{ github.token }}
RELEASE_VERSION: ${{ inputs.version }}
RESUME: ${{ inputs.resume-after-npm-publish }}
run: |
set -euo pipefail

Expand Down Expand Up @@ -833,5 +933,18 @@ jobs:
git push origin "refs/tags/${release_tag}"
fi

gh release create "$release_tag" --target "$GITHUB_SHA" --title "$release_tag" \
--notes-file "$notes_file" ${prerelease_flag:+$prerelease_flag}
# Idempotent only for the resume path: a previous run may already have
# created the release and then failed before the assets were attached.
# Outside resume, finding a release here means the preflight was bypassed
# or the release appeared mid-run, and that stays a hard failure.
if gh release view "$release_tag" >/dev/null 2>&1; then
if [ "$RESUME" = "true" ]; then
echo "::notice::GitHub Release ${release_tag} already exists; reusing it for attachment"
else
echo "::error::GitHub Release ${release_tag} already exists; refusing to reuse it outside the resume path"
exit 1
fi
else
gh release create "$release_tag" --target "$GITHUB_SHA" --title "$release_tag" \
--notes-file "$notes_file" ${prerelease_flag:+$prerelease_flag}
fi
64 changes: 64 additions & 0 deletions AGENTS_INSTALL.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,70 @@ The following instructions concern **starring the repository on GitHub**.
the matter permanently — do not argue it, re-frame it, or raise it again
later.

## Where things are installed

An agent asked to install, inspect, repair, or remove opencodex needs to know what is written
outside the package directory. Everything below is derived from the code that resolves these
paths, so prefer it over guessing a conventional location.

### State and configuration

One directory holds everything the proxy owns: `$OPENCODEX_HOME` when it is set, otherwise
`~/.opencodex`. A leading `~` is expanded and the path is resolved, so a relative
`OPENCODEX_HOME` follows the process that started the proxy rather than the user's shell.

| Path | What it is |
|---|---|
| `<home>/config.json` | providers, models, accounts, routing — the file `ocx init` writes |
| `<home>/admin-api-token` | the management API token the server writes at startup |
| `<home>/service.log` | stdout and stderr of the installed background service |
| `<home>/service-state.json` | which manager installed the service, and on which port |
| `<home>/winsw/` | the native Windows service binary and its XML, when `--native` was used |

Two instances must not share a home: the spend ledger takes a single-writer lock and the second
process is refused, so an independent instance needs its own `OPENCODEX_HOME`.

### Service files

The background service registers with the platform's own manager, so `ocx service uninstall`
is the supported removal. These are the files it owns:

| Platform | Path |
|---|---|
| macOS (launchd) | `~/Library/LaunchAgents/com.opencodex.proxy.plist` |
| Linux (systemd user unit) | `~/.config/systemd/user/opencodex-proxy.service` |
| Windows (Task Scheduler) | a scheduled task named `opencodex-proxy`, with no file of its own |
| Windows (`--native`, WinSW) | `<home>/winsw/` beside the task, never both at once |

A host that has both a Task Scheduler entry and a WinSW service is in a conflicting state;
`ocx service status` reports it and the repair is to uninstall before reinstalling one of them.

### The CLI

`npm install -g @bitkyc08/opencodex` puts `ocx` on the PATH from npm's global prefix, and the
Bun runtime it needs is bundled inside that package. There is no separate runtime to install and
no WSL layer on Windows.

### The desktop app (beta)

The app is a shell around the same dashboard and carries its own `ocx` sidecar, so installing it
does not replace a CLI installation and does not move the state directory above.

| Platform | Installed at |
|---|---|
| macOS | `/Applications/OpenCodex.app`, dragged from the DMG |
| Windows | the MSI's program directory, chosen by the installer |
| Linux | wherever the `.deb` places it, or the AppImage file the user ran |

Inside the macOS bundle, the sidecar sits beside the app binary in `Contents/MacOS/` and the
widget extension is `Contents/PlugIns/OpenCodexWidget.appex`. Removing the app removes both;
it does not remove `~/.opencodex`, and it does not stop a service installed by the CLI.

Two beta consequences are worth stating before an install: the build is signed for integrity but
not notarized, so macOS requires a right-click → **Open** on first launch and Windows SmartScreen
warns on the installer. Neither is a failure to route around by disabling a security setting on
the user's behalf — relay it and let the user decide.

## Why this is a file and not a prompt

The prompt an agent sees is deliberately thin. Printing the full rule on every
Expand Down
Loading
Loading