atomantic / atomantic/PortOS

Turn provider records into presets derived from a service, with an additive migration and generated samples

Open
#7,565 1 comment 0 reactions 0 assignees View on GitHub
blocked effort:high enhancement model:medium plan planner:fable-5-1
Dominant language
JavaScript
Stars
38
Forks
31
Avg merge
22m
Merged PRs (30d)
988

Description

Part of #7561 (epic).
Blocked by #7564

## Problem / Goal

Provider records are the only thing a selector can name, and each one duplicates its service's endpoint, key, env block and model list. Turn records into **presets**: a named (harness, method, service) tuple plus selection defaults and overrides, with connection-owned fields derived from the service on every save and still materialized into `data/providers.json` so older releases keep executing them.

## Context

- Record fields and their ownership are catalogued in `aiToolkit/providers.js:655 buildProviderRecord` and `internal/providerModes.js` (`MODE_GROUPED_KEYS = endpoint, apiKey, envVars, credentialBootstrap`; `PROVIDER_MODE_OVERRIDE_KEYS = args, headlessArgs, tuiPromptDelayMs`).
- The projection machinery exists: `withoutConnectionOwnedFields`/`withConnectionOwnedFields`/`projectConnectionOwnedFields` (`providerConnections.js`), `planGraphReconciliation` + `applyReconciliation` (projected/pending snapshots, external-change refusal).
- `providerConnectionProfile` classifies every existing record; `CONNECTION_ISOLATION_REASONS` name why one cannot be mapped (unknown harness, dynamic env, external config, …).
- 55 shipped samples in `aiToolkit/defaults/providers.sample.json` (mirrored by `data.reference/providers.json`); `providerRouteRecipes.sampleParity.test.js` pins recipe↔sample agreement.
- Migration rules (root AGENTS.md): numbered script in `scripts/migrations/`, gated on the presence of the INPUT (`data/providers.json`), never on output absence; no derived `data.reference` seed; applied-list gives re-run safety.

## Proposed approach

1. **Fields (additive).** `providerSchema` gains optional `harnessId` (enum of `PROVIDER_HARNESS_IDS`), `method` (`cli|tui|api`, must equal `type`), `serviceId` (service slug), `catalogNarrowing: string[]|null` (subset of the service catalog to offer; `null` = whole catalog), `credentialBootstrapId` (slug of a configured bootstrap app, `cli`/`tui` only). A record with all three is a **derived preset**; without them a **legacy preset**.
2. **Save path.** On create/update of a derived preset, the service instance is loaded, `materializeRoute` produces the connection-owned fields, and they are written into the record (endpoint, apiKey, envVars, secretEnvVars, markers, `models` = catalog ∩ narrowing). When `credentialBootstrapId` is set, the inline `credentialBootstrap` object is written from the app row (with the harness name from its map) exactly as today; a preset carrying a hand-written inline object and no id is a legacy bootstrap and stays as is. User-editable per-preset fields stay: name, args/headlessArgs/tuiPromptDelayMs/timeout, defaultModel, tier pins, effort, fallback, generation params, consent flags, unknown custom fields. Editing a connection-owned field directly on a derived preset is refused with a pointer to the service (the editor hides them). Service edits (child 2) re-project every derived preset via the existing pending/projected snapshot flow.
3. **Migration `NNN-provider-presets-structural-fields.js`.** Input-gated on `data/providers.json`. For each record: `harnessForProvider` → `harnessId` (`direct` for `api`), `method = type`, `providerConnectionProfile` → find the service instance whose definition/transports/credentials contain the profile (`routeBelongsOnConnection`) → `serviceId`; when none exists and the profile has no isolation reasons, create the instance (child 2 API, slug from definition id) and assign. Records with isolation reasons stay legacy, untouched. An inline `credentialBootstrap` is matched against configured bootstrap apps by `(command, args, argsSeparator, setupCommand)` equality and, on a match, gets `credentialBootstrapId`; otherwise it is left inline (the migration never creates a bootstrap app). Write only the additive keys; never touch args/env/models/pins/consent/`activeProvider`/fallbacks. Private report of counts; log aggregate counts only.
4. **"Save as preset."** `POST /api/providers/presets` accepts `{compositeId, id?, name?, model?, effort?}`; the composite's `+` suffix becomes `credentialBootstrapId`; mints a record id from the composite (`pi-tui-nvidia-nim-free`, `claude-cli-anthropic-corp-auth`, uniquified) and materializes it enabled. `GET /api/providers` returns presets exactly as today plus the three new fields.
5. **Samples.** Replace the hand-maintained 55-record sample file with a generator (`scripts/generate-provider-samples.js`, content-addressed, `positionInvariance` test) that emits the same ids from `(harnessId, method, serviceDefinition)` tuples plus per-sample overrides, so a new service definition gets its presets for free; keep `data.reference/providers.json` byte-stable for this slice (generator output must equal the current file before any new tuple is added).
6. Downgrade proof: fixture test that the previous release's `providers.js` parser (`PRIOR_*` fixture pattern already used for codex/antigravity catalogs) accepts the projected file with the new keys present.

## Acceptance criteria

- [ ] Migration on the 55-sample fixture assigns `harnessId/method/serviceId` to every mappable record and leaves each isolated record legacy with its reason; a diff of the file before/after shows only added keys.
- [ ] Editing a service's endpoint re-projects every derived preset on it and the projection survives a simulated crash between DB commit and file write (existing `planGraphReconciliation` tests extended for presets).
- [ ] `POST /api/providers/presets` from `pi.tui@nvidia-nim-free` yields a record that runs identically to the composite (same argv/env in the spawn test).
- [ ] A legacy preset (custom command with `$VAR` env) is still selectable, executable and fully editable, and the editor offers "Convert to derived preset" only when `providerConnectionProfile` reports no reasons.
- [ ] Sample generator reproduces `data.reference/providers.json` byte-for-byte and is position-invariant.
- [ ] Prior-release parser fixture accepts the new file; `activeProvider` remains a record id.

## Out of scope

Selector and page UI (child 5/6); deleting or renaming any shipped sample id.

Contributor guide

Open the contributing guide

Research direction

Start with root AGENTS.md for migration rules, then read aiToolkit/providers.js buildProviderRecord, internal/providerModes.js, and providerConnections.js. Run the existing providerRouteRecipes.sampleParity.test.js and planGraphReconciliation tests before tracing the preset save and migration paths. Done means the migration, projection, preset endpoint, sample generator, and prior-release parser fixture satisfy the listed acceptance criteria without changing legacy behavior.

Written by the indexing model from the issue text.

Assessment

Tech stack
javascript
Domain
api, backend, testing
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
38/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.