Skip to content
Merged
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
4 changes: 4 additions & 0 deletions .github/workflows/workbuddy-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,7 @@ jobs:
env:
CGO_ENABLED: '1'
run: go test -race -timeout 120s ./internal/workbuddy ./sdk/auth ./internal/watcher/synthesizer
- name: Test native executor and SDK retry integration under the race detector
env:
CGO_ENABLED: '1'
run: go test -race -timeout 180s ./internal/runtime/executor ./internal/runtime/executor/helps ./sdk/cliproxy/auth ./sdk/cliproxy -run TestWorkBuddy
65 changes: 65 additions & 0 deletions .github/workflows/workbuddy-image.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
name: WorkBuddy isolated image

on:
workflow_dispatch:
push:
branches: [main, feat/workbuddy-proxy]
paths:
- 'internal/workbuddy/**'
- 'internal/runtime/executor/workbuddy*'
- 'internal/runtime/executor/helps/workbuddy*'
- 'sdk/cliproxy/**'
- 'internal/config/**'
- 'go.mod'
- 'go.sum'
- 'Dockerfile'
- '.github/workflows/workbuddy-image.yml'

permissions:
contents: read
packages: write

jobs:
build:
if: github.repository == 'CorgiCortex/CLIProxyAPI'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
cache: true
- name: Verify native WorkBuddy contracts
env:
CGO_ENABLED: '1'
run: |
go test -race -timeout 180s ./internal/workbuddy ./sdk/auth ./internal/watcher/synthesizer
go test -race -timeout 180s ./internal/runtime/executor ./internal/runtime/executor/helps ./sdk/cliproxy/auth ./sdk/cliproxy -run TestWorkBuddy
go build -o test-output ./cmd/server
rm test-output
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build immutable test-service image
id: image
uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64
push: true
tags: ghcr.io/corgicortex/cliproxyapi-wb:sha-${{ github.sha }}
build-args: |
VERSION=workbuddy-${{ github.sha }}
COMMIT=${{ github.sha }}
cache-from: type=gha,scope=workbuddy
cache-to: type=gha,mode=max,scope=workbuddy
- name: Record image receipt
env:
IMAGE_DIGEST: ${{ steps.image.outputs.digest }}
run: |
echo "Image: ghcr.io/corgicortex/cliproxyapi-wb:sha-${GITHUB_SHA}" >> "$GITHUB_STEP_SUMMARY"
echo "Digest: ${IMAGE_DIGEST}" >> "$GITHUB_STEP_SUMMARY"
echo "Deployment requires a separate GitOps image pin." >> "$GITHUB_STEP_SUMMARY"
1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ RUN mkdir /CLIProxyAPI
COPY --from=builder ./app/CLIProxyAPI /CLIProxyAPI/CLIProxyAPI

COPY config.example.yaml /CLIProxyAPI/config.example.yaml
COPY internal/workbuddy/NOTICE /usr/share/doc/cliproxyapi/WORKBUDDY-NOTICE

WORKDIR /CLIProxyAPI

Expand Down
121 changes: 121 additions & 0 deletions docs/workbuddy-native.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,125 @@ upstream acceptance, current account entitlement, award amounts or actual
anti-abuse behavior. Those checks remain pending until a test account is
provided.

## Implementation status

The credential/transport foundation is merged. Core execution is being developed
in `feat/workbuddy-proxy`; deployment is still pending. Offline coverage now
includes the native executor, OpenAI Chat/Responses, Claude and Gemini response
translation, SSE/nonstream collection, parallel tool calls, usage accounting,
401 refresh with persistence before replay, and SDK-scoped retry decisions.
HTTP errors and error envelopes inside HTTP 200 streams share the risk classifier.
A truncated stream is an error; once a chunk was emitted it cannot be replayed
on another credential. Only a pre-output connection failure gets one retry.

The static model catalog is an offline snapshot. Opt-in discovery reads
`/v3/config`, prefers the CLI agent's model list and merges observed metadata
with the snapshot. Empty responses cannot erase the catalog. Discovery persists
per-account results and republishes model registrations with the existing SDK
publication fences. Neither source establishes entitlement to run a model.
Native credential `models` explicitly restrict the published models.

The governor and transport pools are shared across executor replacements.
Configuration reloads preserve cooldowns, restrictions and usage. Changing the
state directory requires a restart; the old directory must be migrated explicitly.
Only one process may own a state directory. The isolated deployment will use a
single replica and its own PVC.

Model entitlement error `11158` is retained as region/model evidence for 24 hours,
including the reported user tier when known. As in the reference, this is not a
blanket dispatch ban: a free account's refusal cannot establish whether a paid
account may use the model. A later successful check clears the mark; older
in-flight completions cannot erase newer evidence. Model configuration errors
`11129` and `11150` also stop rotation without penalizing the account.

Conversation affinity keeps three recent eligible accounts per caller/session
for two hours. Conversations beyond 400 messages use stable account pages
(default size three); cooldowns do not change page membership. Native selection
still honors SDK pins, exclusions, priority and the final governor check.

Content rejection can replace the built-in prompt with a neutral prompt until
CST midnight, with one retry per request. Custom prompts are excluded. Optional
exit backoff shares failures by origin/proxy and caps waiting at five seconds
per logical request. Optional product switching persists the new identity before
retrying and permits at most four switches per account/model per minute.
Restrictions and durable free-model caps survive identity changes. Optional paid
fallback applies only to international DeepSeek Flash when every enabled account
in that region has an active free-model cap; paid budgets are checked again.
All rebuilt requests pass through the final payload-rule barrier exactly once.

Background operations now share chat's governor, connection pools and limiter:

- Model discovery and balance observations run in six-hour slots. Resource
pagination must complete before publishing totals. Missing numeric fields
remain unknown, including the enterprise endpoint's unreported remainder.
- CN personal sign-in runs after 09:00 CST. An explicit already-checked-in
response is completion evidence, with no invented reward amount.
- Growth reconciliation runs after 09:00 and 21:00 CST. It verifies individual
acceptance results and claims only vendor-observed completion. Desktop,
subscription, donation and other real actions are not fabricated as telemetry.
- Optional quota scans use the three known free-cap models as applicable to the
realm. Reset probes wait for the recorded reset time; both execute through the
native chat path with paid fallback and product switching disabled. Each cycle
admits at most 16 probes, including at most six quota scans, with 400 ms spacing.
New quota pairs start after five seconds and are staggered by 15 seconds up to
one hour; the schedule survives restart. Successful scans remain at least six
hours apart even across a scheduling-slot boundary. Registered cap windows are
handled only by reset probes until recovery is observed.
- Atomic job receipts precede actions. Completed slots survive restart; ordinary
failures retry after 15 minutes, at most six times per slot. Reset failures
retry after at least 15 seconds, at most 20 times per reset window, subject to
any longer governor cooldown. Reload and shutdown cancel active maintenance.

Background jobs default to disabled. They record only observed credits and
rewards. Inference `usage.credit` feeds the paid daily budget and local debits
against the last observed balance, protecting the reserve between refreshes.
The persisted balance snapshot retains the vendor's original observation.

Automated international web activity and actual task-specific growth actions
remain follow-up work. The isolated deployment and real-account validation are
still pending; this implementation must not be described as fully validated
upstream parity.

An explicit v8 configuration enables the provider:

```yaml
oauth:
auth-dir: /data/workbuddy-auth
providers:
workbuddy:
enabled: true
state-dir: /data/workbuddy-state
max-concurrency: 32
per-caller-concurrency: 2
queue-seconds: 30
jobs:
enabled: false
model-discovery: false
balance-refresh: false
checkin: false
growth: false
quota-scan: false
reset-probes: false
limits:
daily-tokens: 0
daily-credits: 0
reserve-credits: 0
```

Zero budgets disable that local limit. Regional limit blocks replace the global
budget block for that region. A positive credit reserve requires an observed
balance, so requests remain gated while balance is unknown. Tokens and credits
are recorded from upstream observations only; credits are never inferred from
token counts. Concurrent requests can complete beyond a configured budget.
Queue deadlines end at admission and never become upstream network deadlines.
The general management HTTP tunnel is unsupported for this provider; arbitrary
requests must not bypass its risk governor or export credentials to another host.

The `WorkBuddy isolated image` workflow publishes only
`ghcr.io/corgicortex/cliproxyapi-wb:sha-<full-commit>` after offline race checks.
It never updates the existing DockerHub image or deploys a workload. Deployment
requires a separately reviewed GitOps pin.

## Credential setup

Use a separate configuration and auth directory for this test service. Never
Expand Down Expand Up @@ -87,6 +206,8 @@ contact WorkBuddy or inspect any existing account files.
```sh
go test ./internal/workbuddy ./sdk/auth ./internal/watcher/synthesizer \
./internal/cmd ./cmd/server
go test ./internal/runtime/executor ./internal/runtime/executor/helps \
./sdk/cliproxy/auth ./sdk/cliproxy ./internal/config -run TestWorkBuddy
go build -o test-output ./cmd/server
rm test-output
```
Expand Down
2 changes: 2 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,8 @@ type Config struct {
// Antigravity configures provider-wide Antigravity request behavior.
Antigravity AntigravityConfig `yaml:"antigravity" json:"antigravity"`

WorkBuddy WorkBuddyConfig `yaml:"workbuddy" json:"workbuddy"`

// Devin configures provider-wide Devin request behavior.
Devin DevinConfig `yaml:"devin" json:"devin"`

Expand Down
1 change: 1 addition & 0 deletions internal/config/config_v8.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ func buildV8Paths() []configPath {
{"antigravity-signature-cache-enabled", "oauth.providers.antigravity.signature-cache-enabled"},
{"antigravity-signature-bypass-strict", "oauth.providers.antigravity.signature-bypass-strict"},
{"quota-exceeded.antigravity-credits", "oauth.providers.antigravity.antigravity-credits"},
{"workbuddy", "oauth.providers.workbuddy"},
{"xai", "upstream.xai"}, {"devin", "oauth.providers.devin"},
{"disable-image-generation", "multimedia.disable-image-generation"}, {"gpt-image-2-base-model", "multimedia.gpt-image-2-base-model"},
{"video-result-auth-cache-ttl", "multimedia.video-result-auth-cache-ttl"},
Expand Down
45 changes: 45 additions & 0 deletions internal/config/workbuddy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package config

// WorkBuddyConfig is opt-in. State belongs to a dedicated provider volume and
// must never be shared between independent server processes.
type WorkBuddyConfig struct {
Jobs WorkBuddyJobs `yaml:"jobs" json:"jobs"`
AffinityByPrefix *bool `yaml:"affinity-by-prefix" json:"affinity-by-prefix"`
AffinityMaxMessages *int `yaml:"affinity-max-messages" json:"affinity-max-messages"`
AffinityPageSize *int `yaml:"affinity-page-size" json:"affinity-page-size"`
EgressBackoff bool `yaml:"egress-backoff" json:"egress-backoff"`
AutoSwitchProduct bool `yaml:"auto-switch-product" json:"auto-switch-product"`
AutoSwitchProducts []string `yaml:"auto-switch-products" json:"auto-switch-products"`
PaidFallback bool `yaml:"paid-fallback" json:"paid-fallback"`
Prompt WorkBuddyPrompt `yaml:"prompt" json:"prompt"`
Enabled bool `yaml:"enabled" json:"enabled"`
StateDir string `yaml:"state-dir" json:"state-dir"`
MaxConcurrency int `yaml:"max-concurrency" json:"max-concurrency"`
PerCallerConcurrency int `yaml:"per-caller-concurrency" json:"per-caller-concurrency"`
QueueSeconds int `yaml:"queue-seconds" json:"queue-seconds"`
Limits WorkBuddyLimits `yaml:"limits" json:"limits"`
RealmLimits map[string]WorkBuddyLimits `yaml:"realm-limits" json:"realm-limits"`
}

type WorkBuddyJobs struct {
Enabled bool `yaml:"enabled" json:"enabled"`
ModelDiscovery bool `yaml:"model-discovery" json:"model-discovery"`
BalanceRefresh bool `yaml:"balance-refresh" json:"balance-refresh"`
Checkin bool `yaml:"checkin" json:"checkin"`
Growth bool `yaml:"growth" json:"growth"`
QuotaScan bool `yaml:"quota-scan" json:"quota-scan"`
ResetProbes bool `yaml:"reset-probes" json:"reset-probes"`
}

type WorkBuddyLimits struct {
ReserveCredits float64 `yaml:"reserve-credits" json:"reserve-credits"`
DailyTokens int64 `yaml:"daily-tokens" json:"daily-tokens"`
DailyCredits float64 `yaml:"daily-credits" json:"daily-credits"`
ModelTokens map[string]int64 `yaml:"model-tokens" json:"model-tokens"`
}

type WorkBuddyPrompt struct {
Mode string `yaml:"mode" json:"mode"`
Text string `yaml:"text" json:"text"`
File string `yaml:"file" json:"file"`
}
38 changes: 38 additions & 0 deletions internal/config/workbuddy_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package config

import "testing"

func TestWorkBuddyV8Configuration(t *testing.T) {
cfg, err := ParseConfigBytes([]byte(`version: 8
oauth:
auth-dir: /private/workbuddy-auth
providers:
workbuddy:
enabled: true
state-dir: /private/workbuddy-state
max-concurrency: 8
per-caller-concurrency: 2
queue-seconds: 15
limits:
reserve-credits: 10
daily-tokens: 10000
model-tokens:
fixture-model: 5000
realm-limits:
cn:
daily-tokens: 2000
`))
if err != nil {
t.Fatal(err)
}
if !cfg.WorkBuddy.Enabled || cfg.WorkBuddy.StateDir != "/private/workbuddy-state" || cfg.WorkBuddy.PerCallerConcurrency != 2 || cfg.WorkBuddy.Limits.ModelTokens["fixture-model"] != 5000 || cfg.WorkBuddy.RealmLimits["cn"].DailyTokens != 2000 {
t.Fatalf("configuration not mapped: %+v", cfg.WorkBuddy)
}
defaultCfg, err := ParseConfigBytes([]byte("version: 8\n"))
if err != nil {
t.Fatal(err)
}
if defaultCfg.WorkBuddy.Enabled {
t.Fatal("provider enabled implicitly")
}
}
Loading
Loading