Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
37b3a7f
docs: plan Aside profile controls and ownership boundaries
invalid-email-address Sep 5, 2026
52d8d5c
feat(aside): synchronize profiles with independent CLI controls
invalid-email-address Sep 5, 2026
aa46afd
test(aside): align profile refresh outcomes and layout seeds
invalid-email-address Sep 5, 2026
3c2eb3f
fix(aside): centralize profile mutations and preserve backup sources
invalid-email-address Sep 5, 2026
1d4da9f
fix(cli): report empty Aside profile diagnostics
invalid-email-address Sep 5, 2026
ee47f17
test(aside): align aggregate status and privacy-safe fixtures
invalid-email-address Sep 5, 2026
fb14428
fix(aside): register profile routes and CLI surface metadata
invalid-email-address Sep 5, 2026
a695c4b
Merge branch 'codex/grok-native-tool-parity-5598' into codex/aside-pr…
invalid-email-address Sep 5, 2026
14cb508
docs(aside): explain server-owned synchronization requirements
invalid-email-address Sep 5, 2026
76c3ba6
Merge branch 'codex/grok-native-tool-parity-5598' into codex/aside-pr…
invalid-email-address Sep 5, 2026
4778f48
fix(cli): report Aside sync when the proxy is unavailable
invalid-email-address Sep 5, 2026
b7b237f
Merge branch 'codex/grok-native-tool-parity-5598' into codex/aside-pr…
invalid-email-address Sep 5, 2026
0294b72
Merge branch 'codex/grok-native-tool-parity-5598' into codex/aside-pr…
invalid-email-address Sep 5, 2026
cea9d51
fix(aside): tighten profile sync and history contracts
invalid-email-address Sep 5, 2026
e60c1ff
fix(aside): keep recovery diagnostics in the backend stack layer
invalid-email-address Sep 5, 2026
db9c6a0
Merge branch 'codex/grok-native-tool-parity-5598' into codex/aside-pr…
invalid-email-address Sep 5, 2026
1ccd1cb
Merge branch 'codex/grok-native-tool-parity-5598' into codex/aside-pr…
invalid-email-address Sep 5, 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
21 changes: 21 additions & 0 deletions devlog/_plan/260906_aside_profiles/000_research.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Aside profile synchronization roadmap

User scope extension: synchronize all Aside profiles and expose independent profile switches in GUI and CLI. Continue the original Grok catalog/Responses stabilization stack; no local test suites or local typecheck, no release/service deployment. Existing push --no-verify and admin-merge authorization applies to these scoped layers.

Observed installed contract: accounts.json has currentAccountId plus accounts[] with numeric id and name; profileAccountBindings maps browser profiles to accountId. This machine has three account-backed profiles (one cloud, two local), with a models.json only in the current account. Every model catalog lives under the configured Aside root/u/<accountId>/models.json. Browser profilePath is metadata, never a write destination. Multiple bindings sharing one account share one model catalog and therefore one control row. Keep only id/name/current metadata; never serialize sessions, tokens, user IDs, email or subscription metadata from the manifest.

Current owners: config-export.ts asideCurrentAccountId resolves only currentAccountId. registry.ts aside.resolvePaths freezes that current path pair. writer.ts synchronous input supports resolvedPaths but its async freeze recomputes current paths; state.ts has no frozen-pair input. The ownership store is keyed by clientId, so a single root can retain only one Aside account. FileIntegrationPage already owns safe toggle/overwrite/history/restore, and all its resource keys currently use only client ID. CLI is a thin management caller with no profile flag.

Decision: enumerate account-backed profiles; derive paths strictly from numeric IDs under Aside root, not profilePath. Partition each new profile's ownership and journal into <integration-store>/aside-profiles/<id>; retain exactly one stable writable legacy root owner; all sibling writes use independent child stores. Older mixed legacy history remains readable by exact profile path and can be imported into the correct child store only for explicit restore. Freeze the chosen profile's paths for status/write/restore. Do not move user files, change currentAccountId, or copy credentials.

Desired state: add asideProfileSync:{allProfiles?:boolean,profiles?:Record<string,boolean>,legacyProfileId?:number|null} to OcxConfig. Absent defaults to whether a legacy Aside ownership record establishes prior connection. That legacy connection enables all discovered profiles by default, satisfying the user's all-profile request. A per-profile override persists independently. Before modifying a per-profile override, materialize the prior global default so disabling the legacy profile does not flip siblings. Explicit actions persist desired policy before any file writes; a save failure aborts with no file mutation. Bulk intent sets allProfiles and clears overrides, while actual per-profile applied states and refusals remain separate. A failed file mutation leaves visible pending intent, never an all-applied claim. Restore reconciles only its target profile policy with validated prior ownership so Undo cannot be silently reversed by the next sync. Per-profile-only enable when previously disconnected leaves other profiles off. Implicit sync refreshes owned enabled profiles and may safely apply an absent block in an explicitly/legacy-enabled unowned profile; it never overwrites foreign blocks or recreates a manually removed previously-owned block.

Cycle map: docs-only roadmap; 010 backend/profile ownership/API/CLI (foundation and API can be separate dependent PRs within this single implementation unit); 020 GUI controls/QA and final full-stack landing. Every original exact-head CI and merge-ancestry criterion remains open until terminal delivery.

Design Read: a repeated-use integration settings page using the existing monochrome dashboard: --bg white/#212121, --surface white/#262626, --accent #0d0d0d/#ececec, existing --font-ui and ClientMark. Compact profile rows show name/current marker, state, and switch; a global switch and enabled/total count summarize all profiles. Details reuse the existing FileIntegrationPage scoped to a selected profile so history/restore stays available. No new visual framework, assets or motion. DESIGN_VARIANCE2, MOTION_INTENSITY1, densityD5. Loading/error/empty/partial/busy states are explicit; the current browser account never changes when an integration switch changes.

Resource bounds inherited: six-hour window from original goal, no requested token budget, original at-most24 live synthetic provider requests. Profile probes use temporary roots with three profiles; bulk production discovery is bounded to128 account entries. Existing local/GitHub credentials only for authorized repo work. Actual user profile files remain read-only during development. Runtime file writes are tested only in isolated fixtures. C4 ownership/path review is required before production merge; security working notes stay ignored scratch.

## Baseline

`bun .tmp/aside-profiles/baseline.ts` runs only synthetic temp files: manifest has0/1/2, legacy owned0, current model-selection route runs refresh, and configuredAsideProfiles remains1. This reproduces the user report without editing any real profile. Browser profile bindings resolve to three distinct account IDs in the current install.
47 changes: 47 additions & 0 deletions devlog/_plan/260906_aside_profiles/010_profiles_backend_cli.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# 010 Profile data, ownership, API and CLI

Class C4 for controlled multi-file writes; spec-satisfaction repair. Goal: all account-backed Aside profiles receive the selected catalog and can be independently enabled/disabled. Non-goals: login/account switching, browser profile data, credential changes, unowned overwrite without the existing explicit flag, other clients redesign.

NEW src/clients/aside-profiles.ts: typed AsideProfile {id:number,name?:string,current:boolean,configPath:string,detectDir:string}; read configured asideHomeDir accounts.json, validate bounded account array, dedupe safe nonnegative integer IDs, fall back to current-only legacy manifest when accounts is absent, fail on malformed identities. Map only safe metadata; derive root/u/id paths. A numeric query selector must refer to this enumeration. No path from browser profile bindings reaches writes.
MODIFY src/types/config.ts + src/config.ts: asideProfileSync optional object with allProfiles boolean, numeric-key boolean overrides, and optional nullable safe-integer legacyProfileId provenance. Per-field validity must not erase unrelated configuration; preserve unknown future policy fields where existing conventions require. Full field chain: creation in Aside mutation service; persistence via saveConfigPreservingClaudeCode; deserialization in config schema; consumers profile status, explicit toggles and implicit sync; serialization GUI/CLI receives effective enabled per row, not raw credentials.
MODIFY src/integrations/state.ts: optional resolvedPaths in IntegrationStateInput, use it instead of resolving current profile. MODIFY writer.ts freezeIntegrationInput to clone a supplied internal resolved pair, preserving existing resolution otherwise. This is an internal seam only; routes never accept caller-provided paths.
NEW src/integrations/aside-profiles.ts: resolve profile-specific store/path input. Exactly one profile may use the writable legacy root: a matching current legacy ownership record wins; only if no record exists may the newest legacy Aside operation choose it. An unrecognized existing record makes the root unassigned. Persist the resolved legacyProfileId (number or null) before the first explicit mutation, so disabling/reloading cannot reassign it. All other profiles use isolated child stores. Read statuses with same classifier. Model load memoized across profiles. Compute effective default and per-profile override. Explicit enable/disable/overwrite uses existing coordinated writer and mutation-flight exclusivity, serializes profiles, returns per-profile results; persist desired preferences through the caller save seam before file mutation, under the same exclusive operation. On save failure restore the in-memory prior policy and abort before filesystem changes. Report desired enabled separately from actual state and per-profile refusals; do not fabricate all-applied success. Missing/foreign/unsafe/drifted profile remains untouched with explicit refusal. A manual deletion with a surviving ownership record stays absent on implicit refresh. First safe creation in enabled unowned profile uses apply without overwrite. Never switch the active account.
MODIFY src/integrations/owned-refresh.ts optional internal resolvedPaths; MODIFY catalog-refresh.ts Aside fan-out to profile service and preserve per-profile outcome IDs; update CLI explicit sync logs/type projection to identify profiles.

NEW src/server/management/aside-profile-routes.ts: GET /api/client-integrations/aside/profiles returns {profiles:[{profileId,name?,current,enabled,...IntegrationStatus}],allEnabled,enabledCount,total}; GET /aside without profile returns aggregate IntegrationStatus+profiles, PUT /aside without profile acts on all discovered profiles. Existing /aside?profile=<id> handles one explicit profile with same mutation/refusal semantics. Numeric profile parsing is strict, membership checked, non-Aside use rejected. Reuse existing jsonResponse/body parsing/CSRF outer boundary. Return partial failures visibly; do not turn mixed outcomes into a successful all-applied status.
MODIFY integration-routes.ts: route Aside list/status/toggle to profile service; collection projects Aside aggregate while other clients remain unchanged. Bind optional profile scope for journal/delete/restore query paths to the same selected store and frozen paths; no snapshot can restore into another profile. Existing no-profile legacy history remains accessible. Existing test hooks (root store/env/home/io/lock seams) must propagate. New prefs writes use deps.saveConfigPreservingClaudeCode, never bypass fixture isolation.
MODIFY src/cli/integrations.ts: --profile <id> for Aside status/show/list, enable/disable, history/journal and restore/delete equivalents that exist; reject on other clients and malformed IDs. No --profile on Aside enable/disable means all. Route flag through query profile; status prints all per-profile rows, JSON preserves metadata; mixed failure exits nonzero with structured result retained. Update usage/capability source if help registry owns it, and operating docs.

Tests: new profile enumeration/store/writer domain tests registered in both layout manifests; management and CLI tests cover current0+local1+local2, all-enable, individual-off persists through sync, legacy-default all, explicit one-only enable, active-account changes do not retarget a pinned write, unowned/drifted/removed/symlink/missing profile refusals, malformed selectors, unknown ID, partial outcome, per-profile journal/restore isolation and old legacy history. Actual temporary fixtures and original writer/management calls; no live user config mutation.

Verification: standalone temp-root production probe establishes three distinct file outputs and one-off persistence across refresh; remote Bun focused regressions/typecheck/privacy gates; independent ownership/API review. Final exact-head hosted CI and all PR ancestry are terminal obligations, not satisfied by queueing. Candidate new paths source-checked before B. Escalation only for a concrete unresolvable external constraint, not routine design choices.

## Audit-locked operational contracts

- One outer Aside mutation flight owns the complete action, including policy persistence and every coordinated writer call. Its key includes root fingerprint, sorted selected profile IDs, operation/overwrite/restore semantics and a unique operation nonce. Overlap returns busy; no profile ever joins another result. Do not nest refreshOwnedIntegration inside that flight; call coordinated refresh/apply directly after the service's ownership checks. Different profile roots cannot coalesce either.
- Profile status and every writer use the concrete filesystem validation/guard contract recorded in ignored .tmp/aside-profiles/security-scope.md. Frozen path pairs alone are not the boundary. The guard is rechecked immediately before file mutation and is shared with status.
- Restore resolves the operation's exact profile independently of currentAccountId. Before policy persistence validate operation/snapshot availability, target identity and ordinary drift preflight. Desired state after Undo is true only when priorRecord describes the exact snapshot bytes as owned; absent/foreign/conflicted snapshots set a target false override. Global defaults and sibling overrides remain unchanged. Persist that target intent first; writer refuses or restores under the same flight. Cover enable->undo->sync and disable->undo->sync after reload. A later filesystem refusal remains visible as desired/actual mismatch, not success.
- NEW src/integrations/aside-profile-journal.ts (if separation needed): path-filtered profile history combines its writable store and matching legacy operations, deduping operation IDs. Snapshot reads use each operation's source store. A restore of an older sibling legacy operation imports only that immutable operation and its available snapshot into the target child store (same opId, exact priorRecord/configPath, no original deletion), then uses the existing coordinated restore there; it never changes the legacy owner's record. Expired snapshots stay expired. Profile history deletion checks the newest operation within that profile and retires duplicate imported/source copies together so a deleted row cannot reappear. Generic history/restore paths resolve Aside operation scope by exact configPath when no profile query is supplied, and reject an operation whose profile is no longer registered instead of retargeting it.
- Add profileId to journal/API rows, and treat (clientId,profileId/configPath) as history ownership for latest/undo/delete checks. Existing non-Aside behavior stays unchanged.

C4 audit findings and concrete filesystem guard details are kept in ignored scratch; the public roadmap records feature contracts only.

## P implementation interfaces at37b3a7f9b

Delegation is within this one010 cycle with disjoint write sets. Path worker owns clients/aside-profiles.ts and tests/clients/aside-profile-paths.test.ts. Engine worker owns integrations/aside-profile-context.ts, aside-profiles.ts, aside-profile-journal.ts and tests/clients/aside-profiles.test.ts. Main owns type/config schemas, resolved-path seams in state/writer, management routes, CLI, implicit fan-out wiring and route/CLI tests. No worker commits, orchestration, local suites or real profile mutation.

Path module exports AsideProfile {id,name?,current,root,configPath,detectDir}; listAsideProfiles(env?,home?) and guardAsideProfileIO(profile,io,profiles?) plus assertAsideProfileBoundary(profile,profiles?,mutation?). Invalid manifest/selector/path raises ClientPathError with safe text. Engine module exports AsideProfilesInput (config, models array/lazy, port, env/home/store/io, persistConfig?, lockSeams?), AsideProfileState (IntegrationStatus plus profileId/name/current/enabled and optional safe error), AsideProfileList (clientId,profiles,allEnabled,enabledCount,appliedCount,total plus aggregate state fields), listAsideProfileStates, getAsideProfileState(input,id), mutateAsideProfiles(input,{enabled,profileId?,overwriteConflict?}), refreshAsideProfiles. Mutations return {ok,clientId,changed,state,message,results:[WriteOutcome+profileId]}; singleton result stays accessible for the existing refusal serializer.

Journal module exports listAsideOperations(input,profileId?) -> [{profileId,entry,store}], findAsideOperation(input,opId,profileId?) -> row|null, restoreAsideProfile(input,{opId,profileId?,confirmDrift?}) -> WriteOutcome+profileId and deleteAsideOperation(input,{opId,profileId?,principal?}). Main serializes journal metadata using each source store; profile-scoped newest protection and duplicate retirement live in the journal service. Journal discovery can return null for unrecognized non-Aside operations so the existing route handles them.

The context owner centralizes exact scope/store resolution, desired policy, guarded IO and outer flight; engine/journal import it without circular imports. Scope includes a safe ownership-store root as well as the client file target. No writable legacy root may be shared across profiles. Domain errors carry safe code/status for route mapping; no manifest/session payload reaches diagnostics.

## Implementation evidence and review scope

`bun .tmp/aside-profiles/api-cli-probe.ts` passed against an isolated live HTTP management handler and actual CLI: three-profile bulk enable, individual-off after persisted reload/model selection, Undo followed by sync, unrelated settings and metadata privacy. Default unconfigured/disabled Aside now skips implicit fan-out before manifest/catalog discovery.

This C4 backend layer is larger than the default review-size guideline because the new filesystem scope, one-owner store model, reversible desired state, and API/CLI consumers must be assessed as one complete contract; these are new cohesive modules with focused fixtures, not unrelated cleanup. UI implementation remains a separate dependent PR/cycle, and the original Grok work is already four separate reviewed PRs.

## Coordinated client interface amendment

CLI Aside refresh runs through POST /api/client-integrations/aside/sync on the live server, never through the local file writer; MCode/Pi keep their existing paths. Add a deterministic two-process CLI/server coordination regression. Dedicated primary profile paths are /aside/profiles (GET list, PUT bulk), /aside/profiles/<id> (GET/PUT one), /aside/profiles/<id>/journal (GET/DELETE) and /aside/profiles/<id>/restore (POST). CLI and new UI use these paths so unsupported old servers refuse rather than ignore a profile query. The new server may retain validated query compatibility, but Aside can never fall through to a legacy generic writer. Journal source availability and request selector consistency are part of the final regression matrix; detailed review synthesis stays ignored scratch.
Loading
Loading