Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
261063a
fix: preserve CLI table and review policy gates
ChefGroep Sep 8, 2026
d133d0e
fix(ci): use supported Bun cache input
ChefGroep Sep 8, 2026
1e75cbb
fix(client): make isolated artifact builds cross-platform
ChefGroep Sep 8, 2026
a67ce99
fix: support macOS live checkout checks
ChefGroep Sep 8, 2026
3b09e3b
fix: gate patch releases on complete platform verification
ChefGroep Sep 8, 2026
7dcaf85
fix(ci): verify the shipped Bun runtime
ChefGroep Sep 8, 2026
b1ce5b9
fix(ci): distinguish jq bindings from shell expansion
ChefGroep Sep 8, 2026
8a39346
test: use portable shell shebangs in bounded git fixtures
ChefGroep Sep 8, 2026
16e7a9e
fix: accept macOS system var alias in client shim
ChefGroep Sep 8, 2026
9b12020
test: allow client artifact builds on Windows
ChefGroep Sep 8, 2026
4b45745
test: declare runtime fixture imports before path constants
ChefGroep Sep 8, 2026
1f18cbd
📝 Add docstrings to `fix/release-review-followup-20260908`
coderabbitai[bot] Sep 8, 2026
1dcf7ac
fix: preserve live checkout guard executable bit
ChefGroep Sep 8, 2026
2ee6076
fix(client): canonicalize trusted aliases before containment
ChefGroep Sep 8, 2026
b3db530
test(client): share immutable launcher artifact across behavior cases
ChefGroep Sep 8, 2026
6022ddc
Add regression tests for release and checkout safeguards
coderabbitai[bot] Sep 8, 2026
1470870
fix(ci): run every test batch while preserving failures
ChefGroep Sep 8, 2026
38d5b66
test: use native Windows release helper fixtures
ChefGroep Sep 8, 2026
df28f60
test: cache native release helper fixtures
ChefGroep Sep 8, 2026
0ed16a5
merge: preserve concurrent regression coverage and portable fixtures
ChefGroep Sep 8, 2026
ab55b78
test: cancel delayed SSE fixture timers
ChefGroep Sep 8, 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
18 changes: 9 additions & 9 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,13 @@ jobs:
# and admission suites pushed the serial macOS/Windows jobs into this ceiling even though the
# root tests themselves remained green. Slow platforms therefore separate root tests from GUI
# quality work, and Windows root tests are deterministically balanced over two shards.
# ponytail: Linux-only on push/PR (saves macOS 10x / Windows 2x minutes); full matrix on release tags.
# Linux-only on push/PR; manual pre-release verification and release tags run every platform.
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
include: ${{ startsWith(github.ref, 'refs/tags/v')
include:
${{ (github.event_name == 'workflow_dispatch' || startsWith(github.ref, 'refs/tags/v'))
&& fromJSON('[{"name":"ubuntu-latest","os":"ubuntu-latest","run_tests":true,"run_quality":true,"run_typecheck":true,"shard_index":0,"shard_count":1},{"name":"macos-latest","os":"macos-latest","run_tests":true,"run_quality":false,"run_typecheck":true,"shard_index":0,"shard_count":1},{"name":"macos-quality","os":"macos-latest","run_tests":false,"run_quality":true,"run_typecheck":false,"shard_index":0,"shard_count":1},{"name":"windows-latest","os":"windows-latest","run_tests":true,"run_quality":false,"run_typecheck":true,"shard_index":0,"shard_count":2},{"name":"windows-latest shard 2/2","os":"windows-latest","run_tests":true,"run_quality":false,"run_typecheck":false,"shard_index":1,"shard_count":2},{"name":"windows-quality","os":"windows-latest","run_tests":false,"run_quality":true,"run_typecheck":false,"shard_index":0,"shard_count":1}]')
|| fromJSON('[{"name":"ubuntu-latest","os":"ubuntu-latest","run_tests":true,"run_quality":true,"run_typecheck":true,"shard_index":0,"shard_count":1}]') }}
steps:
Expand All @@ -71,9 +72,8 @@ jobs:
- name: Setup Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
with:
bun-version: 1.3.14
cache: true
cache-bin: true
bun-version: 1.4.0
no-cache: false

- name: Cache GUI node_modules
if: ${{ matrix.run_quality || matrix.run_tests }}
Expand Down Expand Up @@ -119,7 +119,7 @@ jobs:
exit 0
fi

# Bun 1.3.14 on Windows intermittently panics under Worker spawn/terminate churn in
# Bun on Windows can panic under Worker spawn/terminate churn in
# storage policy tests ("Internal assertion failure" / "Bun has crashed"). Retry the
# current shard once on that runtime crash only; ordinary assertion failures still fail.
out="$(mktemp)"
Expand Down Expand Up @@ -170,11 +170,11 @@ jobs:
name: npm-global ${{ matrix.os }}
runs-on: ${{ matrix.os }}
timeout-minutes: 8
# ponytail: Linux-only on push/PR; full OS matrix on release tags (matrix.os below is conditional).
# Match the runtime matrix: manual pre-release checks and tags cover every OS.
strategy:
fail-fast: false
matrix:
os: ${{ startsWith(github.ref, 'refs/tags/v') && fromJSON('["ubuntu-latest", "windows-latest", "macos-latest"]') || fromJSON('["ubuntu-latest"]') }}
os: ${{ (github.event_name == 'workflow_dispatch' || startsWith(github.ref, 'refs/tags/v')) && fromJSON('["ubuntu-latest", "windows-latest", "macos-latest"]') || fromJSON('["ubuntu-latest"]') }}
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
Expand Down Expand Up @@ -217,7 +217,7 @@ jobs:
- name: Setup Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
with:
bun-version: 1.3.14
bun-version: 1.4.0

- name: Install dependencies
run: bun install --frozen-lockfile
Expand Down
25 changes: 20 additions & 5 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -157,22 +157,37 @@ jobs:
;;
esac

ci_url="$(
ci_id="$(
gh run list \
--workflow ci.yml \
--commit "$GITHUB_SHA" \
--event workflow_dispatch \
--status success \
--limit 10 \
--json conclusion,headSha,url,workflowName \
--jq '.[0].url // ""'
--json databaseId \
--jq '.[0].databaseId // ""'
)"

if [ -z "$ci_url" ]; then
echo "::error::No successful Cross-platform CI run found for ${GITHUB_SHA}. Wait for CI to pass before releasing."
if [ -z "$ci_id" ]; then
echo "::error::No successful full-platform pre-release CI run found for ${GITHUB_SHA}. Dispatch ci.yml on main and wait for success before releasing."
gh run list --workflow ci.yml --commit "$GITHUB_SHA" --limit 10 || true
exit 1
fi

# A Linux-only push run is not cross-platform release evidence. Require
# every runtime, quality and installation leg on the exact release SHA.
if [ "$(gh run view "$ci_id" --json headSha --jq '.headSha')" != "$GITHUB_SHA" ]; then
echo "::error::CI run ${ci_id} does not match the release SHA"
exit 1
fi
# shellcheck disable=SC2016 # $run/$passed/$name are jq bindings, not shell variables.
matrix_ok="$(gh run view "$ci_id" --json headSha,conclusion,jobs --jq \
'. as $run | [.jobs[] | select(.conclusion == "success") | .name] as $passed | ($run.conclusion == "success" and (["ubuntu-latest", "macos-latest", "macos-quality", "windows-latest", "windows-latest shard 2/2", "windows-quality", "npm-global ubuntu-latest", "npm-global macos-latest", "npm-global windows-latest", "Security audit", "Lint GitHub Actions"] | all(.[]; . as $name | $passed | index($name) != null)))')"
if [ "$matrix_ok" != "true" ]; then
echo "::error::CI run ${ci_id} lacks successful required platform jobs"
exit 1
fi
ci_url="$(gh run view "$ci_id" --json url --jq '.url')"
echo "Cross-platform CI passed for ${GITHUB_SHA}: ${ci_url}"

# Notes / service baseline:
Expand Down
20 changes: 10 additions & 10 deletions docs-site/src/content/docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,16 +195,16 @@ Operational dashboard features are also available without a browser. These comma
identity-checked running proxy (including a fallback runtime port) and reuse the same management
routes, validation, live configuration, and catalog refresh side effects as the GUI.

| Resource | Commands |
| --------------- | ---------------------------------------- |
| Routing | `ocx combo ...` or `ocx route combo ...` |
| Agent policy | `ocx agent injection | effort | subagents | fallback | sidecar ...` |
| Observability | `ocx observe logs | usage | storage | memory | cache | debug ...` |
| API admission | `ocx access key | endpoints | models | test ...` |
| Claude Code | `ocx claude config status | set ...` |
| Grok Build | `ocx grok status | exclude | include | set | clear | apply ...` |
| Runtime control | `ocx system status | settings | startup | diagnostics | sync | update ...` |
| Offline config | `ocx config show | get | set | unset | validate | export | import ...` |
| Resource | Commands |
| --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Routing | `ocx combo ...` or `ocx route combo ...` |
| Agent policy | `ocx agent injection ...`, `ocx agent effort ...`, `ocx agent subagents ...`, `ocx agent fallback ...`, `ocx agent sidecar ...` |
| Observability | `ocx observe logs ...`, `ocx observe usage ...`, `ocx observe storage ...`, `ocx observe memory ...`, `ocx observe cache ...`, `ocx observe debug ...` |
| API admission | `ocx access key ...`, `ocx access endpoints ...`, `ocx access models ...`, `ocx access test ...` |
| Claude Code | `ocx claude config status`, `ocx claude config set ...` |
| Grok Build | `ocx grok status`, `ocx grok exclude ...`, `ocx grok include ...`, `ocx grok set ...`, `ocx grok clear ...`, `ocx grok apply ...` |
| Runtime control | `ocx system status`, `ocx system settings ...`, `ocx system startup ...`, `ocx system diagnostics ...`, `ocx system sync ...`, `ocx system update ...` |
| Offline config | `ocx config show`, `ocx config get ...`, `ocx config set ...`, `ocx config unset ...`, `ocx config validate`, `ocx config export ...`, `ocx config import ...` |

List/status is the default where unambiguous. Use `--json` for structured snapshots and
`ocx observe logs --follow --jsonl` for a streaming request-log feed. Destructive removal/import,
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@groeponline/opencodex",
"version": "1.4.0",
"version": "1.4.1",
"description": "GroepOnline fork — Universal provider proxy for OpenAI Codex & Claude Code. Use any LLM with Codex CLI/App/SDK and Claude Code.",
"type": "module",
"main": "./bin/package-main.mjs",
Expand Down
23 changes: 20 additions & 3 deletions scripts/assert-live-checkout-safe.sh
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,38 @@ fi
checkout=$1
target=${2-}

# GNU coreutils exposes `timeout`, while macOS commonly exposes it as
# `gtimeout` when coreutils is installed (and otherwise has no equivalent).
# Perl is part of macOS and preserves an alarm across exec. Never drop the
# deadline just because GNU coreutils is absent.
run_git() {
if command -v timeout >/dev/null 2>&1; then
timeout 10s git "$@"
elif command -v gtimeout >/dev/null 2>&1; then
gtimeout 10s git "$@"
elif command -v perl >/dev/null 2>&1; then
perl -e 'alarm shift; exec @ARGV or exit 127' 10 git "$@"
else
echo "assert-live-checkout-safe: no bounded process runner available" >&2
return 127
fi
}

if [[ ! -d $checkout ]]; then
echo "assert-live-checkout-safe: not a directory: $(basename -- "$checkout")" >&2
exit 2
fi

cd "$checkout"

if ! timeout 10s git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
if ! run_git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
echo "assert-live-checkout-safe: not a git checkout: $(basename -- "$checkout")" >&2
exit 2
fi

# Porcelain only as a boolean. Do not print the listing (paths can be sensitive).
porcelain=$(
timeout 10s git status --porcelain 2>/dev/null
run_git status --porcelain 2>/dev/null
) || {
echo "assert-live-checkout-safe: git status probe failed" >&2
exit 2
Expand All @@ -36,7 +53,7 @@ if [[ -n $porcelain ]]; then
fi

if [[ -n $target ]]; then
if ! timeout 10s git merge-base --is-ancestor HEAD "$target"; then
if ! run_git merge-base --is-ancestor HEAD "$target"; then
echo "assert-live-checkout-safe: HEAD is not an ancestor of $target (would drop live-only commits)" >&2
exit 1
fi
Expand Down
92 changes: 83 additions & 9 deletions scripts/build-client-artifact.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
mkdirSync,
mkdtempSync,
readFileSync,
realpathSync,
renameSync,
rmSync,
writeFileSync,
Expand All @@ -18,11 +19,41 @@ const ROOT = fileURLToPath(new URL("..", import.meta.url));
const sha256 = (data: string | Uint8Array) =>
createHash("sha256").update(data).digest("hex");

/**
* Determines whether a path is the trusted macOS `/var` alias for `/private/var`.
*
* @param path - The logical path to evaluate
* @param physicalTarget - The path resolved by the filesystem
* @param platform - The operating-system platform to evaluate
* @returns `true` if the values represent the macOS `/var` to `/private/var` alias, `false` otherwise
*/
export function isTrustedDarwinSystemPathAlias(
path: string,
physicalTarget: string,
platform = process.platform,
) {
return (
platform === "darwin" &&
path === "/var" &&
physicalTarget === "/private/var"
);
}

/**
* Ensures that an existing component of a path is not a symbolic link, except for the trusted macOS `/var` alias.
*
* Missing path components are allowed.
*
* @param path - The path whose components to inspect
*/
function assertNoSymlinkPathComponents(path: string) {
let current = resolve(path);
while (true) {
try {
if (lstatSync(current).isSymbolicLink()) {
if (
lstatSync(current).isSymbolicLink() &&
!isTrustedDarwinSystemPathAlias(current, realpathSync(current))
) {
throw new Error(
`Destination path traverses a symlink; refusing publication: ${current}`,
);
Expand Down Expand Up @@ -119,6 +150,14 @@ function normalizeGeneratedBundleSourceComments(
return new TextEncoder().encode(normalized);
}

/**
* Creates an isolated build worktree at a source revision with frozen dependencies installed.
*
* @param sourceRoot - The Git repository containing the source revision
* @param sourceSha - The commit SHA to check out
* @returns The path to the isolated build worktree
* @throws If worktree creation or dependency installation fails
*/
function prepareIsolatedBuildRoot(
sourceRoot: string,
sourceSha: string,
Expand All @@ -127,14 +166,14 @@ function prepareIsolatedBuildRoot(
try {
git(
sourceRoot,
"clone",
"--shared",
"--no-checkout",
"worktree",
"add",
"--detach",
"--force",
"--quiet",
sourceRoot,
buildRoot,
sourceSha,
);
git(buildRoot, "checkout", "--detach", "--quiet", sourceSha);
const install = Bun.spawnSync(
[
process.execPath,
Expand All @@ -152,11 +191,25 @@ function prepareIsolatedBuildRoot(
}
return buildRoot;
} catch (error) {
Bun.spawnSync(["git", "worktree", "remove", "--force", buildRoot], {
cwd: sourceRoot,
stdout: "pipe",
stderr: "pipe",
});
rmSync(buildRoot, { recursive: true, force: true });
throw error;
}
}

function removeIsolatedBuildRoot(sourceRoot: string, buildRoot: string) {
Bun.spawnSync(["git", "worktree", "remove", "--force", buildRoot], {
cwd: sourceRoot,
stdout: "pipe",
stderr: "pipe",
});
rmSync(buildRoot, { recursive: true, force: true });
}

// The remote wrapper owns all mutation and lifecycle behavior. Direct bundle use
// stops before the CLI's auto-repair hooks can run.
export const CLIENT_GUARD = `
Expand Down Expand Up @@ -207,6 +260,13 @@ export const CODEX_CLIENT_SHIM = [
' [ -n "$path_component" ] || continue',
' path_part="${path_part}/${path_component}"',
' if [ -L "$path_part" ]; then',
" # macOS exposes /var as the system-owned alias of /private/var. It is the",
" # sole symlink component allowed here; every other link remains forbidden.",
' if [ "$path_part" = "/var" ] && [ "$(uname -s)" = "Darwin" ] && [ "$(cd -P -- "$path_part" && pwd -P)" = "/private/var" ]; then',
" # Canonicalize even a missing child before native-home containment.",
' client_home="/private${client_home}"',
" continue",
" fi",
' echo "OCX client-only: refusing symlinked Codex home path $path_part" >&2',
" exit 78",
" fi",
Expand Down Expand Up @@ -280,6 +340,10 @@ export const CODEX_CLIENT_POWERSHELL_SHIM = [
" }",
" return [System.IO.Path]::GetFullPath($current)",
"}",
"function Test-TrustedDarwinSystemPathAlias([string]$Path) {",
" if (-not $IsMacOS -or $Path -ne '/var') { return $false }",
" try { return [string]::Equals((Resolve-PhysicalPath $Path), '/private/var', [System.StringComparison]::Ordinal) } catch { return $false }",
"}",
"$clientHomeRaw = if ($env:OCX_CLIENT_CODEX_HOME) { $env:OCX_CLIENT_CODEX_HOME } else { Join-Path $homeDir '.codex-ocx' }",
"if (-not [System.IO.Path]::IsPathRooted($clientHomeRaw)) { [Console]::Error.WriteLine('OCX client-only: OCX_CLIENT_CODEX_HOME must be absolute'); exit 78 }",
"$clientHomeCandidate = [System.IO.Path]::GetFullPath($clientHomeRaw)",
Expand All @@ -291,7 +355,10 @@ export const CODEX_CLIENT_POWERSHELL_SHIM = [
" $current = Join-Path $current $part",
" $item = Get-Item -Force -LiteralPath $current -ErrorAction SilentlyContinue",
" if ($null -ne $item) {",
" if ($item.Attributes -band [System.IO.FileAttributes]::ReparsePoint) { return $true }",
" if ($item.Attributes -band [System.IO.FileAttributes]::ReparsePoint) {",
" if (Test-TrustedDarwinSystemPathAlias $current) { continue }",
" return $true",
" }",
" }",
" }",
" return $false",
Expand Down Expand Up @@ -320,6 +387,13 @@ export const CODEX_CLIENT_POWERSHELL_SHIM = [
"",
].join("\r\n");

/**
* Builds and publishes a client artifact from clean, committed runtime inputs.
*
* @param destination - Destination directory for the new artifact candidate
* @param root - Runtime source repository to build from
* @returns The generated artifact manifest
*/
export async function buildClientArtifact(destination: string, root = ROOT) {
const builderDirty = git(
ROOT,
Expand Down Expand Up @@ -371,7 +445,7 @@ export async function buildClientArtifact(destination: string, root = ROOT) {
try {
staging = mkdtempSync(join(publicationParent, ".ocx-client-build-"));
} catch (error) {
rmSync(buildRoot, { recursive: true, force: true });
removeIsolatedBuildRoot(root, buildRoot);
throw error;
}
try {
Expand Down Expand Up @@ -452,7 +526,7 @@ export async function buildClientArtifact(destination: string, root = ROOT) {
return manifest;
} finally {
rmSync(staging, { recursive: true, force: true });
rmSync(buildRoot, { recursive: true, force: true });
removeIsolatedBuildRoot(root, buildRoot);
}
}

Expand Down
Loading
Loading