Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
a9826e8
chore(dependencies): remove package-lock.json file
EremesNG May 17, 2026
15fd296
feat(sticky-sessions): Add persistent sticky session routing
EremesNG May 17, 2026
c9a3435
docs(openspec): Archive sticky session change
EremesNG May 17, 2026
2207d58
chore(openspec): Add project config
EremesNG May 17, 2026
18baf7c
feat(server): add sticky sessions and SPA asset serving
EremesNG May 17, 2026
8bc2fd2
feat(web): add dashboard workspace and app
EremesNG May 17, 2026
ff574eb
docs(openspec): archive dashboard redesign specs
EremesNG May 17, 2026
9169edd
docs(accessibility): add WCAG 2.2 and a11y patterns reference
EremesNG May 17, 2026
c2d9f57
feat(ui): restructure dashboard layout with CSS grid
EremesNG May 17, 2026
dd7e20c
feat(ui): improve AccountHistoryChart Y-axis behavior
EremesNG May 17, 2026
2adade0
fix(api): update usage endpoint and refine rate limit logic
EremesNG May 17, 2026
b720b24
feat(build): switch to pnpm for builds and CI workflows
EremesNG May 17, 2026
803a67c
feat(img): add placeholder for new dashboard asset
EremesNG May 17, 2026
5c42e40
docs(readme): update image path in project overview
EremesNG May 17, 2026
648c2a2
docs(readme): expand feature list and update usage details
EremesNG May 17, 2026
16029be
chore(gitignore): ignore local agent files
EremesNG May 17, 2026
1c95815
fix(limits): clear stale rate-limited state on refresh
EremesNG May 18, 2026
8eea937
fix(store): preserve metadata when overwriting accounts
EremesNG May 26, 2026
17aaef1
fix(auth): log token refresh outcomes
EremesNG May 26, 2026
92e1774
feat(web): refresh selected account from dashboard
EremesNG May 26, 2026
8b3ee65
feat(core): modularize sticky identity and error handling
EremesNG Jun 1, 2026
9285f96
fix(auth): prevent reauth tokens from being clobbered by stale codex …
EremesNG Jun 1, 2026
8eb6e5d
test(auth): cover reauth token sync, sync freshness guard, and refres…
EremesNG Jun 1, 2026
4cac84c
feat(store): split account telemetry into account-metrics.json sidecar
EremesNG Jun 2, 2026
ca510dc
fix(auto-login): stop writing telemetry keys into accounts.json
EremesNG Jun 2, 2026
f1fe970
docs(openspec): add split-account-metrics-store change artifacts
EremesNG Jun 2, 2026
2ed1393
chore(openspec): archive split-account-metrics-store change
EremesNG Jun 2, 2026
1db2cce
feat(metrics): add rateLimitHistory support to metrics-store
EremesNG Jun 2, 2026
7c6d8f3
chore(openspec): archive dashboard two-section UX redesign
EremesNG Jun 2, 2026
c360ca8
feat(sticky-sessions): add advanced identity sources and diagnostics
EremesNG Jun 2, 2026
b0a427c
feat(auth): add manual Codex auth import and active-state APIs
EremesNG Jun 2, 2026
403f5fc
feat(web): add Codex account controls and status UI
EremesNG Jun 2, 2026
4ef329d
chore(build): bump pnpm package manager metadata
EremesNG Jun 2, 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
13 changes: 9 additions & 4 deletions .github/workflows/publish-npm.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,22 @@ jobs:
with:
node-version: "20"
registry-url: "https://registry.npmjs.org"
cache: pnpm
cache-dependency-path: pnpm-lock.yaml

- name: Enable Corepack
run: corepack enable

- name: Install dependencies
run: npm ci
run: pnpm install --frozen-lockfile

- name: Build
run: npm run build
run: pnpm run build

- name: Run unit tests
run: npm run test:unit -- --runInBand
run: pnpm run test:unit -- --runInBand

- name: Publish to npm
run: npm publish --access public
run: npm publish --access public --provenance
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,9 @@ node
auto-login/credentials.json
auto-login/debug_*.png
auto-login/__pycache__/
/.idea/
/dist/
/web/node_modules/
opencode.json
/.playwright-cli/
/.agents/
80 changes: 48 additions & 32 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# opencode-multi-auth-codex

Multi-account OAuth rotation plugin for OpenCode with a local dashboard, force mode, weighted settings, limits probing, and reliability hardening.
<img width="1659" height="888" alt="image" src="https://github.com/user-attachments/assets/c72b4d04-be1b-4222-9094-454c2105336f" />
Multi-account OAuth rotation plugin for OpenCode with a localhost dashboard, force mode, weighted rotation, sticky-session admin, account re-auth, and reliability hardening.
<img width="1659" height="888" alt="image" src="img/img.png" />

## Documentation map

Expand All @@ -15,32 +15,36 @@ Multi-account OAuth rotation plugin for OpenCode with a local dashboard, force m

- Rotates requests across multiple ChatGPT/Codex OAuth accounts.
- Keeps a local account store with migration, validation, and atomic writes.
- Provides a localhost dashboard to manage accounts and limits.
- Provides a localhost React/Vite dashboard to manage accounts, limits, rotation presets, force mode, re-auth, and sticky sessions.
- Supports force mode (pin one alias), account enable/disable, and re-auth.
- Supports settings-driven rotation strategy (`round-robin`, `least-used`, `random`, `weighted-round-robin`).
- Supports settings-driven rotation strategy (`round-robin`, `least-used`, `random`, `weighted-round-robin`) and account weights.
- Probes limits safely and keeps authoritative data quality rules.
- Gates non-core Antigravity features behind a feature flag.
- Gates non-core Antigravity and sticky-session features behind feature flags.

## Current implementation status

- Core phases A-G are implemented in this workspace.
- Validation scripts are available for: unit, integration, web-headless, failure, stress, sandbox, soak.
- The dashboard is a React/Vite app with dedicated Overview, Accounts, Configuration, Operations, and Sticky Session views.
- Validation scripts are available for: unit, integration, web-headless, frontend, failure, stress, sandbox, soak.
- Web hardening fixes are in place:
- localhost-only bind enforcement
- malformed JSON returns deterministic `400` without process crash
- dashboard client script parse issue fixed
- Runtime UI/API coverage includes account re-auth, enabled/disabled gating, rotation presets, and sticky-session admin endpoints.

## Behavior guarantees (latest)

- Rate-limit handling sleeps an alias until reset when reset timing is known (`Retry-After`, rate-limit window reset, or parsed provider reset text), instead of retrying that alias immediately.
- Force mode is strict: when enabled, requests stay pinned to the forced alias and do not silently fall back to other aliases.
- Force mode is time-bound: it pins one alias for up to 24 hours and restores the previous strategy when cleared.
- Rotation strategy control is shown next to Force Mode in the dashboard.
- Strategy changes from dashboard settings are applied to runtime selection logic (not just persisted state/UI display).
- Force Mode and strategy interaction is explicit:
- while Force Mode is ON, strategy changes are saved
- saved strategy becomes active when Force Mode is turned OFF
- Dashboard controls include mouseover help text for Force Mode and rotation strategy definitions.
- Account enable/disable toggle is authoritative for eligibility in rotation.
- Sticky-session routing is opt-in and persisted separately from the main rotation settings.

## Rotation strategy reference

Expand All @@ -53,6 +57,7 @@ Multi-account OAuth rotation plugin for OpenCode with a local dashboard, force m
## Repository structure

- `src/` -> TypeScript source
- `web/` -> React/Vite dashboard workspace
- `dist/` -> compiled output (`tsc` generated)
- `tests/unit/` -> unit tests
- `tests/integration/` -> integration tests
Expand All @@ -62,6 +67,7 @@ Multi-account OAuth rotation plugin for OpenCode with a local dashboard, force m
- `tests/sandbox/` -> sandbox isolation tests
- `tests/soak/` -> soak scaffolding
- `docs/` -> QA and phase documentation (see `docs/README.md` for canonical/historical split)
- `openspec/` -> archived and current design/spec artifacts
- `IMPLEMENTATION_PLAN.md` -> full plan and contracts
- `TEST_EXECUTION_PLAN.md` -> required test order and gates
- `codextesting.md` -> live testing TODO for Codex CLI sessions
Expand All @@ -70,9 +76,10 @@ Multi-account OAuth rotation plugin for OpenCode with a local dashboard, force m
## Requirements

- Node.js 20+
- npm
- pnpm 11.x (use Corepack or install pnpm directly)
- OpenCode CLI
- ChatGPT/Codex OAuth accounts
- Python 3.9+ and Playwright if you use `auto-login/`

## Install and use

Expand Down Expand Up @@ -138,8 +145,9 @@ Update existing installs:
```bash
git clone https://github.com/guard22/opencode-multi-auth-codex.git
cd opencode-multi-auth-codex
npm ci
npm run build
corepack enable
pnpm install --frozen-lockfile
pnpm run build
```

### Quick start
Expand Down Expand Up @@ -299,6 +307,10 @@ Outlook login often shows interstitial pages after password entry:
- `PUT /api/settings/feature-flags`
- `POST /api/settings/reset`
- `POST /api/settings/preset`
- `GET /api/sticky-sessions/config` (feature-flag gated)
- `PUT /api/sticky-sessions/config` (feature-flag gated)
- `GET /api/sticky-sessions/status` (feature-flag gated)
- `POST /api/sticky-sessions/cleanup` (feature-flag gated)
- `POST /api/antigravity/refresh` (feature-flag gated)
- `POST /api/antigravity/refresh-all` (feature-flag gated)

Expand All @@ -320,6 +332,7 @@ Outlook login often shows interstitial pages after password entry:
- `OPENCODE_MULTI_AUTH_TOKEN_FAILURE_COOLDOWN_MS`
- `OPENCODE_MULTI_AUTH_PROBE_EFFORT`
- `OPENCODE_MULTI_AUTH_LIMITS_PROBE_MODELS`
- `OPENCODE_MULTI_AUTH_USAGE_BASE_URL`

### Model mapping and runtime behavior

Expand Down Expand Up @@ -391,6 +404,7 @@ See [docs/gpt-5.4-fast-benchmark.md](./docs/gpt-5.4-fast-benchmark.md) for a con
### Feature flags

- `OPENCODE_MULTI_AUTH_ANTIGRAVITY_ENABLED`
- `OPENCODE_MULTI_AUTH_STICKY_SESSIONS_ENABLED`

### Notifications

Expand All @@ -412,18 +426,19 @@ See [docs/gpt-5.4-fast-benchmark.md](./docs/gpt-5.4-fast-benchmark.md) for a con
## Build and test

```bash
npm ci
npm run lint
npm run build
npx tsc --noEmit

npm run test:unit
npm run test:integration
npm run test:web:headless
npm run test:failure
npm run test:stress
npm run test:sandbox
npm run test:soak:48h
corepack enable
pnpm install --frozen-lockfile
pnpm run verify:static
pnpm run build

pnpm run test:unit
pnpm run test:integration
pnpm run test:web:headless
pnpm run test:frontend
pnpm run test:failure
pnpm run test:stress
pnpm run test:sandbox
pnpm run test:soak:48h
```

Current test script surfaces are scaffolded and active. For true long soak, set a long duration and keep the run alive.
Expand All @@ -442,35 +457,36 @@ Use `codextesting.md` for the Codex CLI live-testing checklist and copy-paste co

## Development notes

- Edit `src/*`, never hand-edit `dist/*`.
- Run `npm run build` after source changes.
- Edit `src/*` and `web/src/*`, never hand-edit `dist/*`.
- Use pnpm scripts for local verification (`pnpm run verify:static`, `pnpm run test:unit`, `pnpm run test:frontend`).
- Keep manual/live tests sandboxed (temp HOME/store/auth paths).

## Release flow

- This plugin is now intended to be installed from npm, so every shipped update should bump `package.json` version and publish a new package version. Reusing the same version on a new commit will leave users stuck on cached installs.
- Prepare the next release by bumping the package version, rebuilding, and publishing:
- Releases are published from `v*` tags through `.github/workflows/publish-npm.yml`.
- The workflow uses pnpm for install/build/test, then publishes to npm with provenance.
- Reusing the same version on a new commit will leave users stuck on cached installs, so bump `package.json` before tagging.
- Prepare the next release by bumping the package version, rebuilding, and tagging:

```bash
npm version 1.2.1 --no-git-tag-version
npm install
npm run build
npm publish --access public
pnpm version X.Y.Z --no-git-tag-version
pnpm install --frozen-lockfile
pnpm run build
```

- After that, cut the git release from `main`:

```bash
git commit -m "chore: release v1.2.1"
git tag v1.2.1
git commit -m "chore: release vX.Y.Z"
git tag vX.Y.Z
git push origin main --follow-tags
```

- Users who want a pinned build can install a specific npm version:

```json
{
"plugin": ["npm:@guard22/opencode-multi-auth-codex@1.2.1"]
"plugin": ["npm:@guard22/opencode-multi-auth-codex@X.Y.Z"]
}
```

Expand Down
28 changes: 21 additions & 7 deletions auto-login/auto_login.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,21 @@
# Timing
BETWEEN_ACCOUNTS_DELAY = 5 # seconds between accounts

ACCOUNT_METRIC_KEYS = {
"lastRefresh",
"lastSeenAt",
"lastActiveUntil",
"lastUsed",
"usageCount",
"rateLimits",
"rateLimitHistory",
"limitStatus",
"limitError",
"lastLimitProbeAt",
"lastLimitErrorAt",
"limitsConfidence",
}


def find_system_browser():
override = os.environ.get("OPENCODE_MULTI_AUTH_BROWSER")
Expand Down Expand Up @@ -197,6 +212,10 @@ def fetch_userinfo_email(access_token):
return None


def strip_metric_fields(account):
return {key: value for key, value in account.items() if key not in ACCOUNT_METRIC_KEYS}


# ── Account store (v2 format compatible with plugin) ───────────────────────
def load_store():
if not STORE_FILE.exists():
Expand All @@ -218,7 +237,7 @@ def save_store(store):
tmp = STORE_FILE.with_suffix(f".tmp-{os.getpid()}-{int(time.time() * 1000)}")
with open(tmp, "w") as f:
json.dump(store, f, indent=2)
tmp.rename(STORE_FILE)
tmp.replace(STORE_FILE)
os.chmod(STORE_FILE, 0o600)


Expand Down Expand Up @@ -250,12 +269,9 @@ def add_account_to_store(tokens):
"accountId": account_id,
"expiresAt": expires_at,
"email": email,
"lastRefresh": datetime.now(timezone.utc).isoformat(),
"lastSeenAt": now,
"addedAt": now,
"source": "opencode",
"authInvalid": False,
"usageCount": 0,
"enabled": True,
}

Expand All @@ -264,11 +280,9 @@ def add_account_to_store(tokens):
for i, acc in enumerate(store["accounts"]):
if acc.get("email") == email:
store["accounts"][i] = {
**acc,
**strip_metric_fields(acc),
**new_account,
"usageCount": acc.get("usageCount", 0),
"addedAt": acc.get("addedAt", now),
"rateLimitHistory": acc.get("rateLimitHistory", []),
}
save_store(store)
return email, i, False
Expand Down
3 changes: 0 additions & 3 deletions dist/auth-sync.d.ts

This file was deleted.

1 change: 0 additions & 1 deletion dist/auth-sync.d.ts.map

This file was deleted.

Loading