# Factory GPT Sol Provider

audience: AI coding agents first.

## Decision

Keep Factory execution engine unchanged: `coding_agent: pi`. Register ChatGPT Sol browser transport as Pi provider `gpt`. Select effort through model ID:

```yaml
coding_agent: pi
model: gpt/sol-web-pro
thinking: off
```

Supported models: `gpt/sol-web-medium|high|xhigh|pro`. Wrapper default: `pro`.

Owner approved mediated host tools. Sol has no direct machine channel; Pi receives standard tool calls and executes them under existing Factory capabilities, permissions, worktree isolation, timeout lifecycle, retries, and gates.

NEVER:

- invoke `/gpt-orchestrator` as model endpoint;
- add `ask-gpt --factory`;
- execute model-authored command JSON directly;
- add `coding_agent: gpt` or remove existing `claude_code` schema value;
- duplicate Pi tool/session loop.

## Corrected assumptions

| Initial idea | Evidence | Decision |
|---|---|---|
| `/gpt-orchestrator` is a model | Skill coordinates multiple ask-gpt artifact jobs and landing. No provider API. | Reuse safety doctrine only; no nested orchestrator. |
| `ask-gpt --factory` supplies agency | `ask-gpt` is one-shot prompt/artifact client. `solwebd` already exposes stateful OpenAI Chat Completions + tool calls. | Keep ask-gpt unchanged. |
| Attachments are required for repository context | Owner allows mediated tools. Sol provider path already carries system/user messages, schemas, and bounded tool results. | Pi reads context on demand; no repo ZIP. |
| Factory needs command JSON | `protocol.py` already validates one model JSON block and emits standard OpenAI `tool_calls`; Pi validates tool schemas. | Reuse existing protocol. Final prose remains final prose. |
| GPT has “no shell/network access” | Sol can cause Pi to invoke enabled shell/network-capable tools. | Describe truthful boundary: mediated access under existing Pi/Factory capabilities. |

## Viable approaches

### Approach 1: Native Pi provider — selected

Register `gpt/sol-web-*`; run existing `agent_pi` path.

| Dimension | Assessment |
|---|---|
| Robustness | One tool/session/timeout/permission/gate authority. |
| Long-term | Browser transport isolated in gptbridge; Factory remains provider-agnostic. |
| Scalability | Existing Sol seat pool, queue, and account-wide dispatch limiter. |
| Performance | No nested Claude Code/CCR process; measured warm Sol turns 10–20s. |
| Reversibility | Remove provider/presets/wrapper; existing Pi agents remain unchanged. |
| Infra cost | Existing local browser seats and ChatGPT quota. |

**Weakness:** ChatGPT web UI/quota remain unstable external dependencies. Integration guarantees bounded, visible failure—not uninterrupted availability.

### Approach 2: `sw.sh` external runner — fallback only

Run Claude Code → CCR → Sol and adapt its output into Factory.

| Dimension | Assessment |
|---|---|
| Robustness | Existing tested path, but nests execution/session/timeout ownership. |
| Long-term | Two permission and observability models must stay aligned. |
| Scalability | Same browser capacity plus extra processes. |
| Performance | Extra runtime/routing overhead. |
| Reversibility | Removable adapter. |
| Infra cost | Existing services; higher maintenance cost. |

**Weakness:** Factory currently loses structured tool events behind wrapper logs.

**Recommended: Approach 1** — minimal correct seam. Use Approach 2 only if deterministic proof shows Pi fundamentally cannot express required OpenAI tool/error semantics. A correctable `solwebd` standards defect MUST be fixed once; it does not justify fallback.

## User-facing wrapper

Create canonical front door:

```text
factory-gpt [--effort medium|high|xhigh|pro] <adw> <prompt-or-path>
```

Contract:

1. Default effort: `pro`.
2. Map effort to shipped preset `gpt-sol-<effort>`.
3. Run local `solwebctl ensure --json` before Factory.
4. Require workstation co-location with authenticated browser profile and Pi process.
5. Invoke installed `factory --preset gpt-sol-<effort> ...` foreground; preserve exit code/signals.
6. Emit no secret, invented fallback, implicit effort downgrade, remote dispatch, or direct browser fallback.
7. Unsupported effort, unhealthy provider, wrong topology, or absent preset fails before Factory session creation.

Shipped presets bind selected Factory seats to `model: gpt/sol-web-<effort>` and `thinking: off`. Existing preset merge contract owns seat selection. No runtime config generator.

## Architecture

```text
factory-gpt
  -> solwebctl ensure
  -> factory --preset gpt-sol-<effort>
  -> agents.execute
  -> agent_pi.run
  -> Pi provider gpt/openai-completions
  -> solwebd /v1/chat/completions
  -> ChatGPT Sol browser conversation
  <- standard tool_call
  <- Pi validates + executes tool locally
  -> bounded tool result
  -> final Report JSON text
  -> Factory parse, gates, permission enforcement, receipt
```

## Components

### Managed Pi provider

Source of truth: `modules/workstation/pi/agent/models.json`, already symlinked to `~/.pi/agent/models.json` and composed into account-specific `PI_CODING_AGENT_DIR` trees.

Provider contract:

```json
{
  "providers": {
    "gpt": {
      "baseUrl": "http://127.0.0.1:8791/v1",
      "apiKey": "!solwebctl token",
      "api": "openai-completions",
      "authHeader": true,
      "headers": {"X-Overdeck-Client-Session": "$OVERDECK_PI_CLIENT_SESSION"},
      "models": [
        {
          "id": "sol-web-pro",
          "name": "ChatGPT Sol Web Pro",
          "reasoning": false,
          "input": ["text"],
          "cost": {"input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0},
          "contextWindow": 120000,
          "maxTokens": 16000
        }
      ]
    }
  }
}
```

Repeat model entry exactly for `sol-web-medium|high|xhigh`; only `id`/`name` differ. Rules:

- Omit `instant` from Factory presets/provider acceptance.
- `solwebctl token` reads existing mode-0600 `~/.overdeck/gptbridge/solwebd.token`; stdout contains token only; non-regular file, wrong ownership/mode, empty value, or non-local topology fails. Pi config-value command resolution consumes stdout internally.
- Numeric zero cost is Pi schema metadata only. Factory MUST mark billing `unavailable`; never present zero as measured cost.
- `reasoning: false` and `thinking: off`; effort already lives in model ID.
- Text only. Repository/image/file access remains mediated through Pi tools; no OpenAI image input claim.
- Preserve tool names, JSON Schemas, call IDs, and tool-result IDs.
- `contextWindow=120000`, `maxTokens=16000`, and initial 104000 estimated-input threshold are adapter safety settings, not ChatGPT product-limit claims; accumulated assistant output reduces later input budget.
- `pi --list-models` MUST resolve all four provider/model pairs in default and account-materialized Pi directories.

Start kill gate with static registration. If standard `openai-completions` cannot inject dynamic session header or preserve typed stream failures, replace static `gpt` registration with one managed Pi provider extension carrying same model entries. Scope extension to request metadata/error normalization only; NEVER create another tool loop.

### Sol conversation identity

Current `translate.session_key()` hashes system text, first user text, tool names, and effort. This can collide for identical concurrent Factory tasks.

Required wire contract:

```text
source: PiRequest.session_id
env: OVERDECK_PI_CLIENT_SESSION
value: v1.<base64url-no-padding(SHA-256(UTF-8 session_id))>
header: X-Overdeck-Client-Session
validation: ^v1\.[A-Za-z0-9_-]{43}$
```

Rules:

1. `agent_pi.run` computes value and adds `OVERDECK_PI_CLIENT_SESSION` only to Pi child environment. Empty/non-UTF-8 source or invalid derived value fails before spawn.
2. Pi `gpt` provider reads env and sends exact `X-Overdeck-Client-Session`; never sends raw Factory session ID.
3. `solwebd` validates header before body processing and includes value in `translate.session_key(messages, tools, effort, client_session_id)`.
4. Header remains optional for existing CCR/`sw.sh`/ask-gpt callers; headerless requests keep current content-derived key. Factory wrapper/provider MUST send it.
5. Same Pi session + effort resumes same Sol conversation. Different Pi sessions remain distinct with byte-identical prompts/tools.
6. Retries preserve identity. Compaction/replay preserves client identity but opens fresh browser conversation when reconciler requires full replay.
7. Logs/UI may show derived value. Header and `OVERDECK_PI_CLIENT_SESSION` are redacted from provider dumps; raw Factory session stays in existing safe session field.
8. If standard Pi provider cannot read env and inject dynamic header, kill-gate rung 2 requires scoped provider extension. Missing header in Factory compatibility test is fatal; NEVER fall back to collision-prone keying.

### Context occupancy

Separate three concepts:

- billing: unavailable;
- Pi context occupancy: deterministic conservative estimate;
- Sol browser delivered context: stateful cumulative transcript, not repeated OpenAI request body.

Estimator contract:

```text
estimate_tokens(text) = len(text.encode("utf-8"))
estimated_input = sum(estimate_tokens(exact rendered system/user/tool/schema text))
                  + 16 * message_count + 8 * tool_definition_count
estimated_output = estimate_tokens(visible assistant text)
```

One token per UTF-8 byte deliberately overestimates common BPE tokenization and stays tokenizer-independent. Include system text, every user message, tool schema/name/description, tool call arguments, tool results, visible assistant output, and protocol framing overhead. Exclude HTTP/OpenAI JSON reserialization, hidden browser thinking, and historical client messages not re-rendered into stateful browser conversation.

Required behavior:

1. `solwebd` records cumulative estimated input/output for text actually rendered in browser conversation: preamble/full replay once, then deltas.
2. Model metadata uses `contextWindow=120000`, `maxTokens=16000`; reject new send when `cumulative_estimated_input + cumulative_estimated_output + reserved_output > 120000`. Available new-input budget is `104000 - cumulative_estimated_output`; 104000 applies only before any accumulated assistant output.
3. Existing successful 145KB preamble proves only a lower bound. The 104000-byte-equivalent threshold is a deliberately conservative adapter ceiling below that observation, not claimed product limit.
4. Return estimate to Pi in usage-compatible fields plus explicit `usage_estimated=true`; Factory billing/cost remains `unavailable`.
5. Pi compacts before/after normalized `context_length_exceeded` according to installed behavior proven by kill gate. `ReplayNeeded` opens fresh browser conversation and sends compacted full history.
6. Mandatory system rules/tool schemas exceeding 104000 fail explicitly; never truncate them.
7. Keep existing `MAX_RESULT_CHARS=60000` per-tool-result bound.
8. Installed proof uses scripted/fake Sol endpoint only. Raw live Sol model probe remains forbidden by standing model-test policy.

If deterministic kill gate proves Pi compaction cannot consume estimated usage/overflow with these values, stop at decision ladder; do not silently change threshold or claim compatibility.

### Typed provider failures

End-to-end type:

```text
ProviderFailure = {
  kind: capped | unavailable | protocol | context_overflow | timeout | cancelled,
  detail: string,
  retry_after_seconds?: int,
  resume_at?: string
}
```

Rules:

1. `solwebd` preserves typed failure payload through blocking and streaming responses. Current streaming tail drops `resume_at`/`Retry-After`; fix server conformance.
2. Pi provider preserves failure kind/metadata without classifying cap/unavailable as context overflow.
3. `agent_pi` records typed provider failure in `agent_attempt_end` and terminal event payload.
4. Factory has no deferred-provider scheduler today. Cap terminates visibly with `resume_at`; NEVER claim automatic deferral.
5. Existing Factory retry policy MUST NOT immediately retry capped, logged-out, protocol-broken, or provider-unavailable turns.
6. Client cancellation terminates Pi/process group immediately and records `cancelled`. `solwebd` cannot cancel an in-flight Marionette call; it detects disconnected writer, emits no later client event, and releases seat no later than configured `TURN_TIMEOUT=300s`. No host tool can execute after Pi exits.
7. No fallback to Codex, another effort, direct ask-gpt, fresh duplicate session, or external runner.

### Canonical `solwebctl`

```text
solwebctl enroll --current-profile --json -> ProviderIdentity
solwebctl ensure --json [--seats N] -> ProviderHealth
solwebctl health --json -> ProviderHealth
solwebctl token -> bearer token only
solwebctl stop --json -> ProviderHealth

ProviderIdentity = {
  expected_account: string,
  profile_dir: string,
  machine_id_sha256: string
}

ProviderHealth = {
  ok: bool,
  state: ready | starting | logged_out | capped | degraded,
  endpoint: string,
  host: string,
  account: string | null,
  seats: int,
  queue: int,
  active: int,
  models: string[],
  authenticated: bool,
  pid: int | null,
  process_start_ticks: int | null,
  instance_generation: string | null,
  version: string,
  cap_resume_at?: string,
  detail?: string
}
```

Identity source of truth: mode-0600, user-owned `~/.overdeck/gptbridge/identity.json`. `enroll --current-profile` reads authenticated email through existing `chat.authenticated_account()`, canonicalizes profile path, hashes exact `/etc/machine-id` bytes with SHA-256, and atomically pins all three. It refuses logged-out profile, non-local profile, existing differing identity, wrong owner/mode, or ambiguous machine identity. Deployment never creates/rewrites enrollment.

Rules:

- Bind `127.0.0.1` only; bearer auth on every route.
- Serialize startup. Concurrent ensure calls launch at most one daemon.
- `ensure` requires enrolled identity. Compare current machine hash/profile path to enrollment and live `chat.authenticated_account()` to `expected_account` before `ready`.
- Runtime state file stores daemon `pid`, Linux `/proc/<pid>/stat` start ticks, and random UUID `instance_generation`. `solwebctl` accepts process ownership only when all match health response and executable resolves to installed `solwebd`; stale PID files fail closed and may be replaced only after proving process absent.
- `version` is installed gptbridge build/commit identity. Required Factory models `{sol-web-medium, sol-web-high, sol-web-xhigh, sol-web-pro}` MUST be a subset of advertised models. `sol-web-instant` remains allowed for legacy non-Factory consumers but excluded from Factory registration/presets.
- `ok=true` iff `state=ready`, identity matches, authenticated, local process ownership matches, required models present, and `seats >= requested minimum`.
- `state=capped` always has `ok=false` and `cap_resume_at`; `starting|logged_out|degraded` also have `ok=false`.
- Existing healthy daemon with `seats >= N` succeeds. Insufficient running capacity fails; NEVER kill/restart active daemon implicitly.
- `factory-gpt` always calls `ensure --seats 1`; existing larger pool is accepted. Presets do not imply browser-seat count. Capacity tuning remains explicit `solwebctl ensure --seats N` outside wrapper.
- Missing token/enrollment, wrong account/topology, malformed health, logged-out profile, cap, ownership mismatch, or startup timeout fails before Factory.
- Direct Pi path MUST NOT start CCR.
- Refactor `sw.sh` internal startup to use `solwebctl`; preserve headerless external contract.

### Queue and concurrency

Use server queue only. Health is observation, never reservation.

- No Factory seat-admission claim; health checks are TOCTOU.
- `solwebd` atomically owns seat acquisition and per-session serialization.
- Browser seats are shared across efforts.
- Global 60–75s dispatch stamp remains account-wide source of send spacing.
- Queue depth, active requests, dispatch wait, and cap state MUST be visible in `/factory`.
- Same session serializes. Independent sessions may queue concurrently.
- One capped seat blocks new account dispatches until recorded `resume_at`.

### Protocol truth

Current `protocol.py` behavior is authoritative:

- exactly one valid fenced JSON block with known tool + object arguments → `ToolCall`;
- zero fenced blocks → `Final(prose)`;
- multiple/invalid/unknown blocks → `Malformed`;
- one same-conversation repair for `Malformed`; second failure → typed protocol error.

Final prose must contain Factory Report JSON for phase completion. Factory’s existing Report parser/correction loop owns that contract.

## Security

Trust boundary: Sol has mediated host access through enabled Pi tools.

1. Pi remains sole tool executor.
2. `AgentConfig.tools` remains capability allowlist; GPT presets MUST NOT broaden it.
3. Shell tool may execute commands and reach network if current environment permits. This change adds no sandbox claim.
4. Factory write enforcement occurs after phase sends, not after each tool action. Existing isolated worktree and protected paths bound recovery/detection, not per-action prevention.
5. Never execute final model prose or generated command JSON.
6. Provider token MUST NOT be automatically propagated through argv, prompts, traces, provider dumps, or Pi tool subprocess environment; Pi config resolution and HTTP client may hold token in-process. Derived `OVERDECK_PI_CLIENT_SESSION` is non-secret and inherited by Pi tool subprocesses; tools MUST NOT treat it as authority. Same-user tools can invoke `solwebctl token` or read user state; strong secret isolation requires separate OS identity/sandbox and is outside scope.
7. Local HTTP endpoint never binds externally.
8. GPT-backed Factory agents remain barred from `modules/harness/factory/**`.
9. Stronger per-tool sandbox/network/write prevention is separate scope; do not imply it exists.

## Topology and installation

- Workstation only: Pi, `solwebd`, authenticated browser profile, token, and Factory process co-located.
- Buildbox/k3s/remote execution MUST fail preflight. Never copy browser profile/token to remote hosts.
- Pi provider source: `modules/workstation/pi/agent/models.json`; deployed symlink remains canonical.
- Provider extension, if kill gate proves necessary: repository-owned Pi agent extension under workstation module; deployed through existing workstation config path.
- Service control source: `modules/gptbridge/bin/solwebctl`; installed through gptbridge/Overdeck deployment, never copied ad hoc.
- Wrapper source: repository-owned Factory/harness bin path; installed by `packaging/deploy-local.sh`.
- Account-materialized Pi directories MUST inherit provider models/extensions while preserving account-specific auth.

## Failure handling

| Failure | Required outcome |
|---|---|
| Provider absent/unready | Preflight fails before Factory session. |
| Logged out/wrong account | Typed `unavailable`; no prompt, no retry storm. |
| Usage cap | Typed `capped` + `resume_at`; terminal visible failure. |
| Browser failure | One existing seat rebuild; then typed `unavailable`. |
| Protocol drift | One repair; then typed `protocol`. |
| Unknown tool/invalid args | Reject before execution; return bounded correction. |
| Tool failure | Return exact bounded tool result; same session repairs/reports blocker. |
| Context ceiling | Typed overflow; Pi compacts/replays; mandatory context never truncated. |
| Identical concurrent prompts | Distinct client session IDs; distinct conversations. |
| Factory kill | Existing process-group kill stops local tool loop. Browser request may retain seat only until bounded turn timeout; no later host tool can execute. Trace delayed seat release. |
| Missing usage | Context estimate visible; billing/cost `unavailable`. |
| Remote topology | Fail preflight; no remote browser/token fallback. |

## Observability

`/factory` MUST show:

- declared `coding_agent=pi`, provider/model, effort suffix, host, account;
- Pi session ID + safe Sol session identity;
- daemon readiness, seats, active, queue, dispatch wait, cap `resume_at`;
- each existing Pi tool event with bounded arguments/result/duration/status;
- protocol repair, Report correction, gate attempt;
- typed provider failure, timeout kind, stderr/provider log path;
- context occupancy as estimate;
- billing/cost as unavailable.

Reuse existing event types. Add payload fields first. Add DB columns only for required query/filter semantics; shipped tables change through `MIGRATIONS`. UI changes follow `od-ui-dev` and existing deck-ui components.

## Kill gate — before Factory mutations

Use temporary `PI_CODING_AGENT_DIR`, installed Pi binary, scripted authenticated `solwebd`, and fake browser. Prove:

1. provider discovery for all four model IDs in default + account-composed dirs;
2. bearer authentication; token absent from argv/prompts/traces/tool environment while same-user read access is documented, not falsely denied;
3. streaming keepalives;
4. tool call → tool result → final Report;
5. two byte-identical concurrent prompts with different Pi session IDs remain isolated;
6. same-session retry resumes correctly;
7. `429`, `503`, `502`, timeout, cancellation, and exact retry counts;
8. cap metadata survives streaming into exact Factory-visible receipt;
9. context estimate triggers Pi compaction; compacted replay reaches fresh browser conversation;
10. zero billing usage never displays measured zero cost.

Decision ladder:

1. Sol server violates OpenAI semantics → fix `solwebd` once; rerun.
2. Standard Pi provider drops required metadata but Pi extension can preserve it → add scoped provider extension; rerun.
3. Pi fundamentally cannot complete tool/session/error contract → record evidence; revise plan to Approach 2.
4. NEVER patch around failed semantics with `ask-gpt --factory` or command JSON.

## Tests and gates

- gptbridge: service control, readiness, typed streaming failures, client identity, context estimate, queue/cap.
- Factory: session metadata injection, typed attempt receipt, no immediate provider retry, preset/wrapper mapping, remote preflight failure.
- Full fake-browser Factory loop: mutate isolated fixture, run test tool, final Report, gates, permission receipt.
- Regression:

```text
python3 -m pytest modules/gptbridge/tests/ -q
python3 -m pytest modules/harness/factory/tests/ -q
```

Run project-native checks for provider extension and wrapper contract tests. Address every warning/error/notice.

Standing model-test policy forbids raw live Sol verification. Primary correctness uses scripted fake browser/provider. Installed proof covers symlinks, provider discovery, readiness, wrapper default/mapping, config validation, and deterministic fake-endpoint Factory run.

## Delivery boundary

Factory agents cannot modify `modules/harness/factory/**`. NEVER weaken protection.

1. Non-Factory worker implements producer-side Factory changes/tests.
2. Factory may implement gptbridge/workstation provider work only with Factory machinery excluded from writes.
3. Integrate in one controlled worktree.
4. Run all gates.
5. Install local infrastructure first; invoke installed wrapper/readiness/fake endpoint.
6. Land/deploy through project procedure.

## Architecture Decisions

Keep:

- managed Pi provider: deep required transport seam;
- client session metadata: required isolation invariant;
- `solwebctl`: medium boundary with Factory wrapper + `sw.sh` consumers;
- existing `agent_pi`, `solwebd`, `translate.py`, `protocol.py`: deep reused boundaries;
- `factory-gpt` wrapper + static effort presets: stable CLI/default without Factory schema change.

Reject:

- `agent_gpt.py`: provider is not coding-agent backend;
- new backend/plugin abstraction: YAGNI;
- `ask-gpt --factory`: duplicate one-shot/agent protocols;
- attachment packager: mediated tools make it unnecessary;
- command/action JSON: existing standard tool protocol owns it;
- nested `/gpt-orchestrator`: conflicting orchestration/completion authority;
- Factory health-based seat admission: TOCTOU;
- automatic cap deferral: scheduler does not exist;
- direct `sw.sh` runner: fallback only after kill-gate proof of fundamental Pi incompatibility.
