instar 1.3.522 → 1.3.524
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +56 -0
- package/dist/commands/server.js.map +1 -1
- package/dist/config/ConfigDefaults.d.ts.map +1 -1
- package/dist/config/ConfigDefaults.js +13 -1
- package/dist/config/ConfigDefaults.js.map +1 -1
- package/dist/core/CredentialIdentityOracle.d.ts +59 -0
- package/dist/core/CredentialIdentityOracle.d.ts.map +1 -0
- package/dist/core/CredentialIdentityOracle.js +89 -0
- package/dist/core/CredentialIdentityOracle.js.map +1 -0
- package/dist/core/CredentialLocationLedger.d.ts +187 -0
- package/dist/core/CredentialLocationLedger.d.ts.map +1 -0
- package/dist/core/CredentialLocationLedger.js +351 -0
- package/dist/core/CredentialLocationLedger.js.map +1 -0
- package/dist/core/CredentialWriteFunnel.d.ts +77 -0
- package/dist/core/CredentialWriteFunnel.d.ts.map +1 -0
- package/dist/core/CredentialWriteFunnel.js +128 -0
- package/dist/core/CredentialWriteFunnel.js.map +1 -0
- package/dist/core/MeshRpc.d.ts +5 -0
- package/dist/core/MeshRpc.d.ts.map +1 -1
- package/dist/core/MeshRpc.js +8 -0
- package/dist/core/MeshRpc.js.map +1 -1
- package/dist/core/StoreSnapshot.d.ts +455 -0
- package/dist/core/StoreSnapshot.d.ts.map +1 -0
- package/dist/core/StoreSnapshot.js +786 -0
- package/dist/core/StoreSnapshot.js.map +1 -0
- package/dist/core/devGatedFeatures.d.ts.map +1 -1
- package/dist/core/devGatedFeatures.js +5 -0
- package/dist/core/devGatedFeatures.js.map +1 -1
- package/dist/core/stateSyncConfig.js +1 -1
- package/dist/core/stateSyncConfig.js.map +1 -1
- package/dist/core/storeSnapshotBuild.worker.d.ts +2 -0
- package/dist/core/storeSnapshotBuild.worker.d.ts.map +1 -0
- package/dist/core/storeSnapshotBuild.worker.js +32 -0
- package/dist/core/storeSnapshotBuild.worker.js.map +1 -0
- package/dist/core/types.d.ts +35 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/package.json +1 -1
- package/scripts/lint-no-direct-llm-http.js +6 -0
- package/src/data/builtin-manifest.json +2 -2
- package/upgrades/1.3.523.md +31 -0
- package/upgrades/1.3.524.md +31 -0
- package/upgrades/side-effects/hlc-step3-snapshot-tail.md +133 -0
- package/upgrades/side-effects/live-credential-repointing-increment-a-foundation.md +75 -0
- package/upgrades/side-effects/live-credential-repointing-increment-a-funnel-primitive.md +66 -0
- package/upgrades/side-effects/live-credential-repointing-increment-a-ledger.md +71 -0
- package/upgrades/side-effects/live-credential-repointing-increment-a-oracle.md +68 -0
|
@@ -59,6 +59,12 @@ const ALLOWLIST = new Set([
|
|
|
59
59
|
// so there is nothing for burn-detection to attribute. Same class as the
|
|
60
60
|
// anthropic-headless usageMeterProvider above.
|
|
61
61
|
'src/core/QuotaPoller.ts',
|
|
62
|
+
// Live credential re-pointing (spec live-credential-repointing-rebalancer.md §2.3/§2.11):
|
|
63
|
+
// the CredentialIdentityOracle calls the READ-ONLY /api/oauth/profile endpoint to read
|
|
64
|
+
// which account a slot's credential belongs to — identity bookkeeping, NOT an LLM
|
|
65
|
+
// inference call, so there is nothing for burn-detection to attribute. Same class as
|
|
66
|
+
// QuotaPoller's /api/oauth/usage call above.
|
|
67
|
+
'src/core/CredentialIdentityOracle.ts',
|
|
62
68
|
]);
|
|
63
69
|
|
|
64
70
|
/**
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "./builtin-manifest.schema.json",
|
|
3
3
|
"schemaVersion": 1,
|
|
4
|
-
"generatedAt": "2026-06-
|
|
5
|
-
"instarVersion": "1.3.
|
|
4
|
+
"generatedAt": "2026-06-13T11:34:57.191Z",
|
|
5
|
+
"instarVersion": "1.3.524",
|
|
6
6
|
"entryCount": 201,
|
|
7
7
|
"entries": {
|
|
8
8
|
"hook:session-start": {
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- assembled-by: assemble-next-md -->
|
|
4
|
+
<!-- bump: patch -->
|
|
5
|
+
|
|
6
|
+
## What Changed
|
|
7
|
+
|
|
8
|
+
The **snapshot-then-tail** join/recover path for cross-machine memory stores — so a returning / compacted / long-dark machine never replays a peer's journal from genesis. This step builds the GENERIC substrate ONLY; it adds no concrete store kind (that lands with the first store, WS2.1). Per `docs/specs/multi-machine-replicated-store-foundation.md` §6 / §8.2.
|
|
9
|
+
|
|
10
|
+
- **Single-origin snapshots** (`src/core/StoreSnapshot.ts`) — a peer materializes the current state of the records **it itself authored** (`origin === serving machine`, the first-hop anti-forgery invariant enforced at build AND at the receive door, so a compromised peer can never smuggle a record under another machine's name). A multi-origin store is recovered by snapshot-then-tailing each origin separately.
|
|
11
|
+
- **Seq-watermark VECTOR + the cutover** — the snapshot carries a per-`(origin, kind)` sequence watermark (not a scalar — a scalar would silently lose a lagging stream's record). `applySnapshotCutover()` seeds the applier's cursor to that watermark, then tails the UNCHANGED `buildServeBatch` seq transport, so the no-gap / no-double-apply guarantee is inherited from the existing seq-contiguity. The hybrid logical clock is demoted to a belt-and-suspenders duplicate filter. Re-running the whole snapshot-then-tail is idempotent.
|
|
12
|
+
- **Tombstone safety** — a per-key deleted-keys high-water seed blocks a stale pre-delete edit from resurrecting a key after the tombstone record itself rotates out.
|
|
13
|
+
- **Off-event-loop build + bounded cache** — `StoreSnapshotEngine` runs the whole-store materialization in a worker thread (the instar#1069 discipline, mirroring `CartographerSweepEngine`); `SnapshotCache` is a fixed-ceiling LRU ring (count AND bytes, NOT pool-scaled) with a `cacheLossCounter`, and `SnapshotRebuildBreaker` bounds rebuild storms from a flapping peer. An over-cap (truncated) build is a HARD REFUSAL — never a silent partial — so a consumer can never seed the cursor past dropped records.
|
|
14
|
+
- **Mesh verb + config** — a new `state-snapshot` read/observe verb (`src/core/MeshRpc.ts`) pulls the snapshot over the existing authenticated mesh RPC (no LAN/broadcast — scales to N cloud machines). `DEFAULT_MAX_CACHE_BYTES` reconciled 32 MiB → 64 MiB to match spec §8.2.
|
|
15
|
+
|
|
16
|
+
Pure MECHANISM, dark by default (`multiMachine.stateSync.*`, default false). The only refusal surfaces are at the receive door (anti-forgery) and the build door (rebuild breaker / truncation refusal); neither blocks a user-initiated action. A single-machine install is a strict no-op (no peer to pull from, nothing to materialize).
|
|
17
|
+
|
|
18
|
+
## What to Tell Your User
|
|
19
|
+
|
|
20
|
+
None — internal substrate (no user-facing surface). The cross-machine memory features that USERS will notice (preferences/relationships following them across machines) land in later WS2.x steps that consume this foundation.
|
|
21
|
+
|
|
22
|
+
## Summary of New Capabilities
|
|
23
|
+
|
|
24
|
+
None — internal substrate. New internal modules: `StoreSnapshot.ts` (single-origin snapshot + cutover + cache + rebuild breaker), `storeSnapshotBuild.worker.ts` (off-loop build worker), the `state-snapshot` mesh verb. All dark by default; no new user-facing API surface.
|
|
25
|
+
|
|
26
|
+
## Evidence
|
|
27
|
+
|
|
28
|
+
- `tests/unit/StoreSnapshot.test.ts` — single-origin anti-forgery (cross-origin entries dropped at build; wire snapshot with any foreign-origin record rejected wholesale), per-`(origin,kind)` watermark-vector correctness, seq-driven cutover completeness across a simulated multi-origin pool + idempotent re-apply, tombstone-resurrection drop, cache LRU eviction + `cacheLossCounter` (count + byte ceilings), rebuild breaker bounded across windows (reset + cooldown), and truncation-refusal (cutover throws, serve returns `build-truncated` + does not cache). Green.
|
|
29
|
+
- `tests/integration/store-snapshot-mesh.test.ts` — the REAL compiled off-event-loop worker (dist) builds a bounded snapshot and a 60k-record build keeps main-loop lag < 250 ms (the instar#1069 proof); the `state-snapshot` verb flows through the full mesh dispatcher (verify → RBAC → handler) and serves a single-origin snapshot; the Step-3 substrate (empty registry) answers `no-entries` (strict no-op). Green.
|
|
30
|
+
- `tests/unit/MeshRpc.test.ts` — `state-snapshot` is read/observe RBAC class (any registered peer; self-binding, no router/owner role). Green.
|
|
31
|
+
- Gates: `tsc --noEmit` clean; `no-silent-fallbacks` green (untrusted-wire-reject catches tagged); `lint-dev-agent-dark-gate` green; `docs-coverage --check` exit 0 (StoreSnapshot sections added to `multi-machine.md` + `under-the-hood.md`).
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- assembled-by: assemble-next-md -->
|
|
4
|
+
<!-- bump: patch -->
|
|
5
|
+
|
|
6
|
+
## What Changed
|
|
7
|
+
|
|
8
|
+
Added the foundation of **live credential re-pointing** (Increment A, Steps 1–2) — a new, **dark-by-default** subsystem that will let the agent rebalance which subscription account's credential sits in which config home *without restarting any session* (the change is picked up on the next API call, exactly like `/login`). This ships behind a destructive dark gate: `subscriptionPool.credentialRepointing` defaults to `enabled: false` + `dryRun: true` for **everyone, including dev agents**, and going live requires a deliberate two-flag flip.
|
|
9
|
+
|
|
10
|
+
This shipment is foundation only — it has **zero runtime behavior** and writes **no credentials**:
|
|
11
|
+
|
|
12
|
+
- The config block, type, and `DARK_GATE_EXCLUSIONS` registration (category `destructive`).
|
|
13
|
+
- `CredentialLocationLedger` — the durable, machine-local bookkeeping core (`state/credential-locations.json`) recording which account's credential lives in which config-home slot. It enforces the one-home-per-credential invariant, refuses every mutation in unknown-mode (corrupt state → fail-closed for moves, fail-open-LOUD for reads with a HIGH attention item), and seeds/recovers from an identity oracle that refuses to guess on an ambiguous or unknown account.
|
|
14
|
+
|
|
15
|
+
The identity-oracle implementation, the staged swap executor, the consumer re-routing, and the HTTP routes are later steps in this increment; the autonomous "use-it-or-lose-it" drain balancer is Increment B.
|
|
16
|
+
|
|
17
|
+
## What to Tell Your User
|
|
18
|
+
|
|
19
|
+
Nothing yet — this feature ships **dark and does nothing** in this release. It reserves the switch and lays the bookkeeping core for a future capability: using your weekly subscription allowances before they reset by quietly moving an idle account's credential between config homes (no session restart, conversation preserved). When the rest is built and you choose to turn it on, you'll get a separate, explicit heads-up — it stays off until then.
|
|
20
|
+
|
|
21
|
+
## Summary of New Capabilities
|
|
22
|
+
|
|
23
|
+
⚗️ **Experimental / dark** — no user-facing behavior in this release:
|
|
24
|
+
- `subscriptionPool.credentialRepointing` config block (off + dry-run for everyone; two-flag flip to ever go live).
|
|
25
|
+
- Internal `CredentialLocationLedger` bookkeeping core (not yet wired to any consumer).
|
|
26
|
+
|
|
27
|
+
## Evidence
|
|
28
|
+
|
|
29
|
+
- 69 unit tests green (52 dark-gate + golden line-map; 17 ledger covering every decision boundary: never-seeded, all four seed outcomes, unknown-mode, one-home invariant, journal pruning, persistence round-trip).
|
|
30
|
+
- `tsc --noEmit`, the dev-agent dark-gate lint, no-empty-catch, and repo-invariants all clean.
|
|
31
|
+
- Spec converged across 5 review rounds (material findings 50→22→12→5→0), approved 2026-06-12.
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# Side-Effects Review — HLC Foundation Step 3 (snapshot-then-tail)
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `hlc-step3-snapshot-tail`
|
|
4
|
+
**Date:** `2026-06-13`
|
|
5
|
+
**Author:** `echo`
|
|
6
|
+
**Second-pass reviewer:** `echo (Phase-5 reviewer subagent)`
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
Implements Component 4 (snapshot-then-tail) of the multi-machine replicated-store foundation (`docs/specs/multi-machine-replicated-store-foundation.md` §6). GENERIC substrate only — there is NO concrete store kind (preferences/relationships are later consumers). New `src/core/StoreSnapshot.ts` exports: `materializeSnapshot()` (single-origin materialization, §6.1/§6.2), the per-`(origin,kind)` seq-watermark VECTOR (§6.6), `applySnapshotCutover()` (seeds `lastHeldSeq = snapshotSeq` then rides the UNCHANGED `buildServeBatch` seq transport — §6.3, HLC demoted to secondary dedup §6.4), the deleted-keys high-water seed (§6.5 tombstone safety), `SnapshotCache` (fixed-ceiling LRU ring + `cacheLossCounter`, §8.2), `SnapshotRebuildBreaker` (§6.3 rebuild-storm bound), `StoreSnapshotEngine` (orchestrates an OFF-event-loop worker build mirroring `CartographerSweepEngine`, instar#1069), and `validateWireSnapshot()` (the receiver anti-forgery gate). New `src/core/storeSnapshotBuild.worker.ts` is the trivial worker entrypoint. `src/core/MeshRpc.ts` gains a `state-snapshot` read/observe verb. `src/commands/server.ts` constructs the engine + registers the dark-gated mesh handler. `src/core/stateSyncConfig.ts` + `src/config/ConfigDefaults.ts` reconcile `DEFAULT_MAX_CACHE_BYTES` 32 MiB → 64 MiB to match spec §8.2. Ships dark/additive behind `multiMachine.stateSync.*` (default false); a single-machine agent is a strict no-op.
|
|
11
|
+
|
|
12
|
+
## Decision-point inventory
|
|
13
|
+
|
|
14
|
+
- `validateWireSnapshot()` (receive-door anti-forgery) — **add** — rejects (returns null) any wire snapshot whose top-level/record/watermark `origin !== authenticated sender` (single-origin §6.1); the caller quarantines as untrusted-origin. Protects data, never blocks a user.
|
|
15
|
+
- `materializeSnapshot()` cross-origin drop — **add** — drops (counts) any own-stream entry whose `machine !== origin` at build time. Defense-in-depth, not a user gate.
|
|
16
|
+
- `applySnapshotCutover()` HLC-max merge + resurrection guard — **add** — a snapshot record is the winner only if HLC-greater than the present record AND not below the deleted-keys high-water. Mechanism, no user surface.
|
|
17
|
+
- `SnapshotRebuildBreaker.shouldRebuild()` — **add** — a build-side rate decision (serve cache / refuse) that bounds rebuild storms. Protects the holder's CPU; never touches user data.
|
|
18
|
+
- `MeshRpc` `state-snapshot` RBAC — **add (pass-through)** — read/observe class: any registered peer may issue it (same as journal-sync/preferences-sync). The single-origin invariant is the authority, not a role.
|
|
19
|
+
- `buildServeBatch` seq-contiguity — **pass-through** — the no-gap/no-double-apply guarantee is BORROWED unchanged from the existing applier; this change adds NO new gap-detection.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## 1. Over-block
|
|
24
|
+
|
|
25
|
+
**What legitimate inputs does this change reject that it shouldn't?**
|
|
26
|
+
|
|
27
|
+
The only block surface is `validateWireSnapshot()` at the receive door. It rejects a whole snapshot if any record's `origin` disagrees with the authenticated sender. A legitimate snapshot is ALWAYS single-origin by construction (the holder serves only its own authored records — the engine passes its own machine id as origin, never a peer field), so a well-formed snapshot from an honest peer is never rejected. A malformed/forged snapshot SHOULD be rejected — that is the §6.1 anti-forgery purpose. No legitimate input is over-blocked: a multi-origin store is recovered by snapshot-then-tailing EACH origin separately, each pull single-origin (§6.1), so the rejection of a cross-origin snapshot never blocks a real recovery.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## 2. Under-block
|
|
32
|
+
|
|
33
|
+
**What failure modes does this still miss?**
|
|
34
|
+
|
|
35
|
+
**[RESOLVED by the Phase-5 second-pass review — truncation-under-seed gap trap.]** The reviewer found a real correctness trap that the first-pass review missed: `materializeSnapshot()` truncates the records set when over `maxSnapshotBytes` but keeps the FULL `snapshotSeq` watermark. If a truncated snapshot were applied, the cutover would seed `lastHeldSeq = snapshotSeq` while the snapshot is MISSING records at-or-below that seq — and the subsequent tail (`seq > snapshotSeq`) would never replay them, a SILENT GAP the seq-contiguity cannot catch (it starts above them). Originally the code only FLAGGED `truncated` and no caller refused it, while `server.ts` armed `maxSnapshotBytes = maxCacheBytes` (64 MiB) — so a future WS2.1 consumer would have inherited an armed under-seed. **Folded fix (in THIS PR, where the trap is introduced):** the `truncated` flag now travels ON the `StoreSnapshot` itself (not just the serve-result envelope); `StoreSnapshotEngine.serveSnapshot` REFUSES a truncated build with `build-truncated` (never caches/serves it — the caller falls back to a from-genesis tail, the complete path), and `applySnapshotCutover` THROWS on a truncated snapshot (a structural backstop even against a buggy/old holder that serves one anyway). `validateWireSnapshot` carries the flag off the wire so the backstop holds end-to-end. Three new tests assert it (`tests/unit/StoreSnapshot.test.ts`: cutover throws on truncated, serve returns `build-truncated` + does not cache, wire carries the flag). The real fix for an over-cap store is its per-kind retention bound (§8), not a silent partial.
|
|
36
|
+
|
|
37
|
+
Beyond that: `validateWireSnapshot()` enforces origin === sender but does NOT cryptographically per-record-sign (the spec §6.1/§11.1 names per-record signing as the heavier "alternative B," explicitly NOT chosen). So a COMPROMISED peer M can still forge arbitrary records under `origin = M` (its OWN namespace) — bounded exactly as the steady-state tail's first-hop binding already allows, and the operator's recourse is rollback-unmerge (§7.4, a later step). This is the documented threat-model boundary (§11.1: "a compromised peer is bounded to corrupting records under ITS OWN origin"), not a gap this step introduces. The §6.4 secondary HLC-identity dedup is belt-and-suspenders only — it does not catch a record with a NEW (recordKey, origin, hlc) identity that is semantically a duplicate; that is correct, because the seq-contiguity (the primary mechanism) already handles it. No remaining under-block beyond the spec-acknowledged boundary.
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## 3. Level-of-abstraction fit
|
|
42
|
+
|
|
43
|
+
**Is this at the right layer?**
|
|
44
|
+
|
|
45
|
+
Correct layer. The cutover deliberately RIDES the existing `JournalSyncApplier` seq-contiguity (lastHeldSeq+1) and `buildServeBatch` serve path rather than re-implementing gap detection — `applySnapshotCutover()` is injected with `CutoverApplierSeams` so it never duplicates the applier's logic; the real wiring binds those seams to the applier's PeerMeta. The off-event-loop build mirrors the established `CartographerSweepEngine`/`cartographerDetect.worker.ts` pattern (instar#1069) rather than inventing a new threading model. The cache + breaker are bounded primitives mirroring the quarantine ring's `lossCounter`. Nothing here re-implements a primitive that exists; it composes the existing transport.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## 4. Signal vs authority compliance
|
|
50
|
+
|
|
51
|
+
**Required reference:** `docs/signal-vs-authority.md`
|
|
52
|
+
|
|
53
|
+
**Does this change hold blocking authority with brittle logic?**
|
|
54
|
+
|
|
55
|
+
- [x] No — this change has no block/allow surface over USER actions (it is pure mechanism: it orders, validates, caches, materializes, and un-merges; it never actuates and never decides a conflict winner).
|
|
56
|
+
|
|
57
|
+
The two refusals it does have — the receive-door anti-forgery rejection (`validateWireSnapshot`) and the build-side rebuild breaker — are both deterministic structural checks that protect the user's data / the holder's CPU, neither blocks a user-initiated action (spec §14: "No gate in this foundation blocks a user-initiated action"). The single-origin invariant is a STRUCTURAL property (origin === authenticated sender), not brittle content-pattern logic. The conflict-winner decision (the one judgment call) is explicitly deferred UP to the operator in a later step (`POST /state/resolve-conflict`, §7.3), not decided here.
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## 5. Interactions
|
|
62
|
+
|
|
63
|
+
**Does this interact with existing checks, recovery paths, or infrastructure?**
|
|
64
|
+
|
|
65
|
+
- **Shadowing:** The cutover seeds `PeerMeta.lastHeldSeq` then defers to the UNCHANGED applier serve/apply path — it does not shadow the existing seq-contiguity; it places the cursor and lets the existing rule run. The `seedLastHeldSeq` seam never LOWERS an already-advanced cursor (idempotent re-cutover does not rewind), so it cannot shadow steady-state replication progress.
|
|
66
|
+
- **Double-fire:** Re-running the whole snapshot-then-tail is safe (§6.3 step 5): the §6.4 HLC-identity dedup + the existing seq `duplicate` drop (seq ≤ lastHeldSeq) prevent double-apply. The unit test asserts a re-applied snapshot yields `applied: 0, dedupSkipped: N`.
|
|
67
|
+
- **Races:** The build runs on a worker thread and posts a single bounded result; the engine settles once (mirrors the cartographer worker's settle-once guard). The cache + breaker are main-thread, single-writer. No shared mutable state crosses the thread boundary (the worker receives a plain-object copy of `entriesByKind`).
|
|
68
|
+
- **Feedback loops:** A flapping peer that keeps requesting a rebuild is served the cache (within the min-interval) and rate-limited by the breaker (frequency cap → cooldown), so the request→rebuild loop is bounded across windows (the §12 #14 sustained-flapping invariant, covered by the breaker unit test).
|
|
69
|
+
|
|
70
|
+
In the Step-3 substrate the registry is EMPTY → the engine's `loadOwnEntries` returns no contributing kinds → the handler answers `no-entries` and the caller falls back to a from-genesis tail (the legacy behavior). So this change interacts with NOTHING at runtime until a concrete store (WS2.1) registers a kind.
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## 6. External surfaces
|
|
75
|
+
|
|
76
|
+
**Does this change anything visible outside the immediate code path?**
|
|
77
|
+
|
|
78
|
+
- **Other agents on the same machine:** none.
|
|
79
|
+
- **Other users of the install base:** none — dark by default; with no `stateSync.<store>.enabled` flag set the engine has nothing to materialize and the mesh handler answers `no-entries`. Default config preserves today's behavior exactly.
|
|
80
|
+
- **External systems:** the `state-snapshot` pull rides the EXISTING authenticated mesh RPC (Cloudflare tunnel, no LAN/broadcast); no new external endpoint, no new network posture.
|
|
81
|
+
- **Persistent state:** none added by this step (the deleted-keys high-water + namespaced storage are consumed via injected seams; the real persistence lands with the consumer PR). The config reconcile (`maxCacheBytes` 32→64 MiB) backfills via `applyDefaults`/`migrateConfig` add-missing semantics (Migration Parity) — an operator's explicit value is never overwritten.
|
|
82
|
+
- **Timing/runtime conditions:** the build timeout (120 s default) + worker heap ceiling (1536 MB) bound the build; a timeout returns `build-timeout` (the caller falls back), never a hang.
|
|
83
|
+
- **Operator surface (Mobile-Complete Operator Actions):** no operator-facing actions added in this step. The future operator surfaces (conflict resolution, rollback-unmerge) land with later steps and will carry their own phone-completable surfaces. "No operator-facing actions" — valid here.
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## 7. Multi-machine posture (Cross-Machine Coherence)
|
|
88
|
+
|
|
89
|
+
**When this agent runs on MORE THAN ONE machine, what is this feature's posture?**
|
|
90
|
+
|
|
91
|
+
**proxied-on-read** — this IS a cross-machine feature by design. A recovering machine PULLS a single-origin snapshot from a live holder of each origin's stream (the `state-snapshot` mesh verb), the holder builds it off-loop and may serve a cached copy. The merged read is the §7 union across per-origin namespaces (a later step); this step provides the snapshot PULL + cutover that populates one origin's namespace. Single-origin (§6.1) is the multi-machine security boundary: `origin === authenticated sender` holds end-to-end so a compromised peer cannot smuggle a foreign-origin record across the machine boundary.
|
|
92
|
+
|
|
93
|
+
- **User-facing notices:** none in this step (no one-voice gating needed).
|
|
94
|
+
- **Durable state on topic transfer:** the snapshot cache is machine-local + rebuildable (an eviction is a recompute, never a correctness loss), so it does not strand on transfer; the rollback-unmerge `dropOrigin` hook is provided for the later un-merge step.
|
|
95
|
+
- **URLs surviving machine boundaries:** none generated.
|
|
96
|
+
|
|
97
|
+
Phase-C (N machines, not 2): the snapshot is single-origin and the union is across N origins; the cache ceiling is a FIXED constant (NOT pool-scaled, §8.2) so a large pool rebuilds more often rather than growing the cache unboundedly; the rebuild breaker is per-(peer, origin, store) so N peers requesting the same snapshot are independent. The transport is mesh RPC (no LAN/broadcast). Scales to N cloud machines by construction.
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## 8. Rollback cost
|
|
102
|
+
|
|
103
|
+
**If this turns out wrong in production, what's the back-out?**
|
|
104
|
+
|
|
105
|
+
Pure code change, dark by default — revert and ship a patch. No persistent state is written by this step (the seams are injected; the real persistence lands with the consumer). The config reconcile (`maxCacheBytes` 32→64 MiB) is a default bump; reverting it is another default change (no migration needed — `applyDefaults` add-missing leaves an operator's explicit value untouched either way). No user-visible regression during the rollback window because nothing user-facing is enabled. The `multiMachine.stateSync.<store>.enabled` per-store flags (all default false) are the kill switch — turning every store off returns to byte-for-byte today's behavior.
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## Conclusion
|
|
110
|
+
|
|
111
|
+
This review confirms the change is pure, dark-by-default foundation mechanism with a single structural anti-forgery gate (`validateWireSnapshot`) that protects data and never blocks a user. The three requested adversarial lenses were applied: (1) **distributed-correctness / snapshot-cutover** — the no-gap/no-double-apply guarantee is borrowed unchanged from the existing seq-contiguity, HLC is correctly demoted to secondary dedup, and the watermark is a VECTOR (not a scalar) so a lagging stream is never silently excluded; idempotent re-cutover is asserted. (2) **cache-bounds / DoS** — the cache is a FIXED-ceiling LRU ring (count AND bytes, not pool-scaled) with a visible loss counter, and the per-peer rebuild breaker bounds rebuild storms across windows. (3) **integration-purity / Phase-C** — the build runs off the event loop in a worker (instar#1069), the transport is authenticated mesh RPC (no LAN), and every primitive is N-machine-correct. No design changes were required by the review. Clear to ship as dark/additive foundation.
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## Second-pass review (if required)
|
|
116
|
+
|
|
117
|
+
**Reviewer:** echo (Phase-5 reviewer subagent)
|
|
118
|
+
**Independent read of the artifact: concern raised → resolved in this PR.**
|
|
119
|
+
|
|
120
|
+
The reviewer independently confirmed the artifact's core conclusions against the code: the watermark IS a genuine per-`(origin,kind)` vector (not a scalar — BLOCKER-1 honored); HLC IS demoted to secondary dedup (`tailCursorAfterCutover` returns `snapshotSeq`, never HLC); re-cutover is idempotent; the tombstone high-water is seeded before puts apply (correct ordering, blocks resurrection); the cache is fixed-ceiling (count AND bytes, not pool-scaled) with `cacheLossCounter` bumping only on real LRU eviction (not on supersede); the breaker resets+cooldowns across windows; the build runs off the event loop in a real worker with a minimal secret-free env; single-origin is enforced end-to-end (materialize drops cross-origin AND `validateWireSnapshot` rejects it); the server handler passes the holder's OWN machine id as origin; the transport is authenticated mesh RPC; and there is no signal-vs-authority violation (no user-action gate).
|
|
121
|
+
|
|
122
|
+
**Concern raised:** the truncation-under-seed gap trap (detailed in §2 above) — a truncated snapshot kept the full `snapshotSeq` but no caller refused it, and `server.ts` armed `maxSnapshotBytes` to 64 MiB, so a future WS2.1 consumer would have seeded `lastHeldSeq` past dropped records, creating a silent sub-watermark gap that contradicts the "no-gap guarantee borrowed from seq-contiguity." The reviewer recommended closing the contract structurally in this PR (where the trap is introduced).
|
|
123
|
+
|
|
124
|
+
**Resolution (folded in this PR):** `truncated` now travels on the `StoreSnapshot`; `serveSnapshot` refuses with `build-truncated` (never caches/serves); `applySnapshotCutover` throws on a truncated snapshot; `validateWireSnapshot` carries the flag — a structural, end-to-end refusal so a consumer cannot under-seed by construction. Three new tests lock it. Concern resolved.
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## Evidence pointers
|
|
129
|
+
|
|
130
|
+
- Unit: `tests/unit/StoreSnapshot.test.ts` (26 tests — materialization/anti-forgery, cutover/idempotency, tombstone-resurrection drop, wire-validation forgery rejection, cache LRU + lossCounter, breaker across windows, engine orchestration). Green.
|
|
131
|
+
- Integration: `tests/integration/store-snapshot-mesh.test.ts` (5 tests — REAL dist off-loop worker build + event-loop-lag<250ms proof; full mesh dispatcher verify→rbac→handler round-trip; substrate no-entries no-op). Green.
|
|
132
|
+
- RBAC ratchet: `tests/unit/MeshRpc.test.ts` (state-snapshot read/observe RBAC). Green.
|
|
133
|
+
- Gates: `tsc --noEmit` clean; `no-silent-fallbacks` green (3 untrusted-wire-reject catches tagged `@silent-fallback-ok`); `lint-dev-agent-dark-gate` green (16); `feature-delivery-completeness` green (95); `docs-coverage --check` exit 0 (StoreSnapshot sections added to multi-machine.md + under-the-hood.md).
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Side-Effects Review — Live credential re-pointing (Increment A, Step 1: config + dark-gate foundation)
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `live-credential-repointing-increment-a-foundation`
|
|
4
|
+
**Date:** `2026-06-13`
|
|
5
|
+
**Author:** `echo`
|
|
6
|
+
**Second-pass reviewer:** `not required` (no decision logic; see §4)
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
First commit of the approved live-credential-repointing build (Increment A, ships dark). This is the **foundation only** — it reserves the switch and the config shape with **zero runtime behavior**. Files touched:
|
|
11
|
+
|
|
12
|
+
- `src/core/types.ts` — adds the `subscriptionPool.credentialRepointing` config type (`enabled`, `dryRun`, `manualLeversEnabled`, and the Increment-B `balancer` knobs, present so the shape is stable from A).
|
|
13
|
+
- `src/config/ConfigDefaults.ts` — adds the `subscriptionPool.credentialRepointing` default block with a literal `enabled: false` + `dryRun: true` + `manualLeversEnabled: true`, appended at the end of `SHARED_DEFAULTS` (after `topicProfiles`).
|
|
14
|
+
- `src/core/devGatedFeatures.ts` — adds a `DARK_GATE_EXCLUSIONS` entry, `category: 'destructive'`, `configPath: subscriptionPool.credentialRepointing.enabled`, so the gate resolves OFF + dry-run for EVERYONE including the dev agent. (Deliberately NOT `DEV_GATED_FEATURES`, which would resolve LIVE-with-writes on Echo — the rev-2 blocking finding.)
|
|
15
|
+
- `tests/unit/credential-repointing-dark-gate.test.ts` — 4 tests proving the registry entry/category, dark-on-dev, dark-on-fleet, and the lint pairing invariant.
|
|
16
|
+
- `tests/unit/lint-dev-agent-dark-gate.test.ts` — adds the new `enabled: false` path to the golden line-map EXPECTED (recomputed via the attributor; appended at end so it shifts no prior entry).
|
|
17
|
+
- Spec + companions: `docs/specs/live-credential-repointing-rebalancer.{md,eli16.md,build-plan.md}` + `docs/specs/reports/...convergence.md`.
|
|
18
|
+
|
|
19
|
+
**Decision points the change interacts with:** the dark-feature gate (`resolveDevAgentGate` / `DARK_GATE_EXCLUSIONS`). This commit REGISTERS the feature in that gate as a destructive dark feature; it adds no new gate logic of its own.
|
|
20
|
+
|
|
21
|
+
## Decision-point inventory
|
|
22
|
+
|
|
23
|
+
- `DARK_GATE_EXCLUSIONS` (src/core/devGatedFeatures.ts) — **add** (one registry row) — registers `subscriptionPool.credentialRepointing.enabled` as a `destructive` dark feature so it resolves OFF + dry-run for all agents. No code path reads the flag yet; the swap/oracle/route logic that will consume it lands in steps 2–10.
|
|
24
|
+
|
|
25
|
+
The change adds **no** message-filtering, dispatch, session-lifecycle, or block/allow decision point. It is config + types + a single gate-registry row + tests + docs.
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## 1. Over-block
|
|
30
|
+
|
|
31
|
+
**No block/allow surface — over-block not applicable.** The foundation gates nothing at runtime; it only declares a config default and a dark-gate registry row. The eventual feature (later steps) writes credentials and is itself dark + dry-run until a deliberate two-flag flip.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## 2. Under-block
|
|
36
|
+
|
|
37
|
+
**No block/allow surface — under-block not applicable.** Nothing is being filtered or rejected by this commit.
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## 3. Level-of-abstraction fit
|
|
42
|
+
|
|
43
|
+
Correct layer. Config defaults belong in `ConfigDefaults.ts`; the type belongs in `types.ts`; the dark-feature classification belongs in the existing `DARK_GATE_EXCLUSIONS` registry (the same place the worktree-reaper and mcp-process-reaper destructive features are registered). The commit REUSES the existing dark-gate machinery rather than inventing a parallel switch — exactly the precedent the rev-2 review demanded (a `DEV_GATED_FEATURES` omit-enabled would have resolved LIVE on Echo). No lower primitive is being re-implemented.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## 4. Signal vs authority compliance
|
|
48
|
+
|
|
49
|
+
Compliant — and the question is largely vacuous for this commit because it adds **no authority and no signal logic**. It registers a destructive feature as dark. The full feature's signal-vs-authority posture (the identity oracle is a *signal*; oracle-unavailable → quarantine-never-repair, never a destructive "repair"; the per-slot write funnel is a structural single-mover, not a brittle check with blocking authority) was settled across the 5 converged review rounds and is in `lessons-engaged`. Because this commit introduces no decision logic, **second-pass review is not required** under Phase 5 (no block/allow on messaging/dispatch, no session-lifecycle mutation, no new gate/sentinel/watchdog decision — only a registry row in an existing gate).
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## 5. Interactions
|
|
54
|
+
|
|
55
|
+
- **Dark-gate golden line-map test:** the new `enabled: false` literal is picked up by the `attributeEnabledFalsePaths` attributor. Handled — EXPECTED in `lint-dev-agent-dark-gate.test.ts` recomputed; the entry was appended at the END of `SHARED_DEFAULTS` so it shifts no prior line and only ADDS `'1015'`. Verified: 52/52 unit tests pass.
|
|
56
|
+
- **applyDefaults deep-merge:** `subscriptionPool.credentialRepointing` is add-missing-only; an operator's existing `subscriptionPool` values are never overwritten. No shadowing of any sibling default.
|
|
57
|
+
- No double-fire / race surface — there is no runtime code in this commit.
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## 6. External surfaces
|
|
62
|
+
|
|
63
|
+
Nothing visible to other agents, users, or systems. The feature is dark for everyone; no routes, no notices, no spawn-time behavior ship in this commit. `GET /credentials/*` routes are NOT added yet (step 7). The config block backfills onto existing agents via `migrateConfig` add-missing (wired in step 9, not this commit) — until then existing agents simply lack the (dark, inert) block, which changes no behavior.
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## 7. Multi-machine posture (Cross-Machine Coherence)
|
|
68
|
+
|
|
69
|
+
**Machine-local BY DESIGN, by necessity** — credentials live in each machine's own keychain/config homes, and the spec's core invariant ("exactly one home per credential") is per-machine because the keychain is per-machine. The ledger (`state/credential-locations.json`, step 2) and swap engine are deliberately machine-local; a swap on machine A must never reach into machine B's keychain. The existing multi-machine handoff "swap-in-flight" guard is composed-with (not replaced) in the later swap-executor step (rev-1 finding). For THIS foundation commit there is no cross-machine surface at all — it is config + a gate row. The full feature's multi-machine posture is documented in the spec §2.3/§2.10.
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## 8. Rollback cost
|
|
74
|
+
|
|
75
|
+
Near-zero. The feature is dark (`enabled:false` + `dryRun:true`) and has no runtime consumers in this commit, so reverting is a plain `git revert` of a config/types/test/docs commit with no data migration and no agent-state repair. Even after later steps land, going live requires a deliberate two-flag flip; backing out is flipping `enabled` back to false (live read at the chokepoint).
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# Side-Effects Review — Live credential re-pointing (Increment A, Step 4a: CredentialWriteFunnel primitive)
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `live-credential-repointing-increment-a-funnel-primitive`
|
|
4
|
+
**Date:** `2026-06-13`
|
|
5
|
+
**Author:** `echo`
|
|
6
|
+
**Second-pass reviewer:** `not required` (pure in-process lock primitive, no consumers, no writes — see §4)
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
Adds `src/core/CredentialWriteFunnel.ts` (spec §2.2 "Concurrency model" / "Bounded under the lock") — the in-process serialization primitive for credential writes: `withSlotLock(slot, fn)` (per-slot lock), `withSlotLocks(slots, fn)` (canonical-ordered multi-slot, deadlock-free), and `withSingleMover(fn)` (machine-local single-mover mutex for swaps). Acquisition is try-lock-WITH-TIMEOUT — a slow holder degrades to a SKIPPED result with a named reason, never a wedged slot. Plus `tests/unit/credential-write-funnel.test.ts` (8 tests).
|
|
11
|
+
|
|
12
|
+
This is Step 4a: the PRIMITIVE only. It is **not yet wired** to any writer and the forbidding lint is **not yet added** — that is Step 4b (the lint can't land until every existing writer is routed through the funnel, or it breaks the build). So this commit changes no runtime behavior and writes no credentials.
|
|
13
|
+
|
|
14
|
+
## Decision-point inventory
|
|
15
|
+
|
|
16
|
+
- `CredentialWriteFunnel.withSlotLock` / `withSlotLocks` / `withSingleMover` — **add** — serialize concurrent credential writes; on contention they SKIP (bounded), they never block indefinitely. No authority over agent behavior; pure concurrency control with no consumers yet.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## 1. Over-block
|
|
21
|
+
|
|
22
|
+
**No block/allow surface — over-block not applicable.** The only "refusal" is a try-lock timeout / single-mover-busy SKIP, which is the bounded-wait safety contract (§2.2): a write that can't get the lock in time is reported skipped so the caller degrades gracefully (e.g. the QuotaPoller refresh returns NO-SNAPSHOT) rather than wedging the slot. There are no consumers yet, so nothing is actually skipped in this commit.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## 2. Under-block
|
|
27
|
+
|
|
28
|
+
**No block/allow surface — under-block not applicable.** The funnel cannot serialize a writer that does not route through it — that is precisely what the Step-4b lint exists to prevent, and is called out as the next commit. Within this primitive, the timeout/no-deadlock-after-skip and throw-releases-lock paths are unit-tested.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## 3. Level-of-abstraction fit
|
|
33
|
+
|
|
34
|
+
Correct layer. A `src/core` concurrency primitive, self-contained (the existing `withLock` helpers are cross-PROCESS file locks; this is the IN-process per-slot serialization the spec calls for). It mirrors the SafeGitExecutor / SafeFsExecutor single-funnel precedent the spec names. It bounds only ACQUISITION; the caller bounds its own inner `await` (e.g. a refresh fetch carries its own `AbortSignal.timeout`) — documented in the module header.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## 4. Signal vs authority compliance
|
|
39
|
+
|
|
40
|
+
Compliant — it is mechanism, not authority. It holds no policy and gates no agent behavior; it serializes writes and reports contention as a bounded skip. It cannot become a "brittle check with blocking authority" because it makes no allow/deny decision about content — only about lock availability, with a deterministic bounded outcome. **Second-pass review: not required** under Phase 5 — no consumers, no writes, no messaging/dispatch/session decision. The point where the funnel gains real authority over credential writes is **Step 4b** (routing the four writers + the forbidding lint) and **Step 5** (the swap executor) — both will carry the second-pass review.
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## 5. Interactions
|
|
45
|
+
|
|
46
|
+
- **No consumers yet** — nothing calls the funnel in this commit, so it cannot race, shadow, or double-fire against any existing path. Routing the four in-process writers (swap executor, QuotaPoller 401-refresh, OAuthRefresher/EnrollmentWizard, KeychainCredentialProvider.writeCredentials) is Step 4b.
|
|
47
|
+
- **All state in-memory** — a process restart clears any crash-stale lock/mutex state by construction (the spec's stated recovery for the single-mover).
|
|
48
|
+
- Lock order is documented (single-mover → slot locks ordered by path → ledger write) and `withSlotLocks` enforces the canonical order so two multi-slot ops can't deadlock on opposite orders (unit-tested).
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## 6. External surfaces
|
|
53
|
+
|
|
54
|
+
None. No routes, no notices, no network, no filesystem, no keychain. Pure in-process promise/timer machinery. Nothing visible to other agents/users/systems.
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## 7. Multi-machine posture (Cross-Machine Coherence)
|
|
59
|
+
|
|
60
|
+
**Machine-local BY DESIGN** — credential writes happen against THIS machine's keychain, so the serialization that protects them is inherently per-process/per-machine. The single-mover mutex is explicitly "machine-local" (spec §2.2): it serializes swaps within one machine; cross-machine coordination of a topic move is the existing handoff guard's job (composed-with in Step 5), not this lock's. There is no shared state to replicate.
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## 8. Rollback cost
|
|
65
|
+
|
|
66
|
+
Near-zero. New file + new test only; no consumers, no writes, no migration. Plain `git revert`. Because nothing uses the funnel yet, a revert leaves no behavior change.
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# Side-Effects Review — Live credential re-pointing (Increment A, Step 2: CredentialLocationLedger)
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `live-credential-repointing-increment-a-ledger`
|
|
4
|
+
**Date:** `2026-06-13`
|
|
5
|
+
**Author:** `echo`
|
|
6
|
+
**Second-pass reviewer:** `not required` (no wired consumers, no keychain writes, no live decision surface — see §4)
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
Adds `src/core/CredentialLocationLedger.ts` (spec §2.2) — the durable, machine-local bookkeeping core that records, per config-home SLOT, which pool account's credential currently lives there. Plus `tests/unit/credential-location-ledger.test.ts` (17 tests). This is Step 2 of Increment A; it is **not wired into any consumer yet** (the §2.2 census re-routing is Step 6) and it performs **no keychain writes** (the staged swap executor is Step 5, the write funnel is Step 4). The identity oracle it seeds from is the injected `IdentityOracle` interface — Step 3 implements it against `api.anthropic.com/api/oauth/profile`.
|
|
11
|
+
|
|
12
|
+
Decision points the module embodies (all internal bookkeeping postures, not message/dispatch/session decisions):
|
|
13
|
+
- **Unknown-mode** on corrupt on-disk state: fail-closed for moves (every mutation throws), fail-open-LOUD for reads (return null + one HIGH attention item). Recovery = a fresh oracle re-seed.
|
|
14
|
+
- **Seed-never-guess**: a probed email mapping to ≥2 accounts (ambiguous) or 0 accounts (unknown) REFUSES auto-assignment + quarantines the slot + raises attention.
|
|
15
|
+
- **One-home-per-credential** invariant: re-pointing a slot evicts both the slot's prior tenant and any stale assignment of the same account elsewhere.
|
|
16
|
+
|
|
17
|
+
## Decision-point inventory
|
|
18
|
+
|
|
19
|
+
- `CredentialLocationLedger.assertMutable` — **add** — refuses every mutation while in unknown mode (fail-closed). No external authority; throws a typed error to the (future) caller.
|
|
20
|
+
- `CredentialLocationLedger.seedFromOracle` — **add** — refuse-to-guess on ambiguous/unknown email; quarantine on oracle-unavailable. Produces signals (attention items, quarantine flags), never blocks anything outside the ledger.
|
|
21
|
+
- `slotOf` / `tenantOf` — **add** — pure in-memory reads; return null in unknown/never-seeded mode so callers fall back to today's enrollment-home behavior.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## 1. Over-block
|
|
26
|
+
|
|
27
|
+
**No block/allow surface — over-block not applicable.** The only "refusals" are (a) mutations while corrupt (fail-closed, the safe direction — the alternative is moving a credential on guessed state) and (b) refusing to auto-assign a slot whose tenant can't be uniquely resolved (the alternative is guessing the wrong account, which would route a session to someone else's credential). Both refusals are deliberately conservative; neither rejects a legitimate user input (there is no user input — it's internal bookkeeping).
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## 2. Under-block
|
|
32
|
+
|
|
33
|
+
**Limited block surface.** The ledger does not attempt to detect a credential that the *client itself* rotated out from under it (the §2.3 source-slot CAS + identity audit, Step 5, owns that). A slot whose on-disk blob silently changed tenant between probes would show a stale assignment until the next scheduled audit probe (§2.4). This is by design — the ledger is the record, the audit probe is the divergence detector — and is documented in §2.11. The unknown-mode trigger only fires on *unparseable / wrong-shape* state, not on a semantically-stale-but-valid ledger; staleness is the audit's job.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## 3. Level-of-abstraction fit
|
|
38
|
+
|
|
39
|
+
Correct layer. This is a `src/core` durable-state module mirroring `SubscriptionPool` (atomic tmp+rename save, narrow injected deps). It REUSES the established patterns rather than inventing new ones: the save/load shape from `SubscriptionPool.save`, the injected-attention-callback pattern from `AgentWorktreeDetector`, and an injected oracle interface so the network-touching implementation lives one layer out (Step 3). It does not re-implement keychain access (deferred to the write funnel) or HTTP (deferred to the oracle).
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## 4. Signal vs authority compliance
|
|
44
|
+
|
|
45
|
+
Compliant. The ledger is a **record + signal producer**, not an authority over agent behavior. Its strongest action is to *refuse its own mutation* (fail-closed) and to *raise an attention signal* — it never blocks an outbound message, a session spawn, or a dispatch. The identity oracle is registered conceptually as a HIGH-criticality state detector (§2.2 RULE 3.1) whose fallback is fail-closed (no answer → quarantine, never guess) — exactly the signal-vs-authority posture `docs/signal-vs-authority.md` prescribes for a brittle external probe. **Second-pass review: not required** under Phase 5 — there is no block/allow on messaging/dispatch, no session-lifecycle mutation, no wired gate/sentinel/watchdog, and no keychain write in this module. The genuinely high-risk decision logic (the staged swap executor and the live consumer re-routing) lands in Steps 4–6 and WILL carry a second-pass review there.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## 5. Interactions
|
|
50
|
+
|
|
51
|
+
- **No wired consumers yet** — the §2.2 census (QuotaPoller, SessionManager spawn, InUseAccountResolver, etc.) is re-routed in Step 6. Until then nothing reads this ledger, so it cannot shadow or race any existing check. The `state/credential-locations.json` file is new and owned solely by this module.
|
|
52
|
+
- **Single-writer** — `version` is a journal sequence under the server-process single-writer assumption (the per-slot write funnel + single-mover mutex are Step 4). This module does not itself spawn writers.
|
|
53
|
+
- **Journal pruning** keeps all in-flight + last 50 terminal entries; an in-flight entry is never pruned (so crash-recovery, Step 5, can always find an interrupted swap).
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## 6. External surfaces
|
|
58
|
+
|
|
59
|
+
None visible to other agents/users/systems in this commit. No routes (`/credentials/*` is Step 7), no notices except the two internal attention items (unknown-mode, seed-refusal) which only fire on a genuine degradation and are deduped by stable id. The feature remains dark (Step 1's gate); nothing constructs this ledger at runtime yet.
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## 7. Multi-machine posture (Cross-Machine Coherence)
|
|
64
|
+
|
|
65
|
+
**Machine-local BY DESIGN** — credentials live in each machine's own keychain/config homes, so "which account is in which home" is inherently per-machine. The ledger file is `state/credential-locations.json`, machine-local, NOT replicated — replicating it would be actively wrong (machine B's keychain layout differs). A swap is always machine-local; cross-machine coordination of a topic move is handled by the existing handoff guard (composed-with in Step 5, not here). The attention items it raises are per-machine signals. There is no cross-machine read surface to proxy because the answer is only meaningful for the machine asking.
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## 8. Rollback cost
|
|
70
|
+
|
|
71
|
+
Near-zero. New file + new test + a doc progress-log line; no wired consumers, no migration, no keychain mutation. Plain `git revert`. The `state/credential-locations.json` file is only ever created once the (dark) feature constructs the ledger — which nothing does yet — so even a deployed revert leaves no orphaned state.
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Side-Effects Review — Live credential re-pointing (Increment A, Step 3: CredentialIdentityOracle)
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `live-credential-repointing-increment-a-oracle`
|
|
4
|
+
**Date:** `2026-06-13`
|
|
5
|
+
**Author:** `echo`
|
|
6
|
+
**Second-pass reviewer:** `not required` (pure read+classify signal producer, fail-closed, no authority, no writes — see §4)
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
Adds `src/core/CredentialIdentityOracle.ts` (spec §2.3 verify / §2.11) — the implementation of the `IdentityOracle` interface the ledger (Step 2) already depends on. Given a config-home slot, it reads the slot's current credential blob (reusing `readClaudeOauth` from OAuthRefresher — no hand-rolled keychain access), takes the OAuth access token, and asks the read-only `GET /api/oauth/profile` endpoint which account that token belongs to. Returns the raw probed email, or an `unavailable` result on any failure. Pool-mapping (email→accountId) stays in the ledger.
|
|
11
|
+
|
|
12
|
+
Also: adds `src/core/CredentialIdentityOracle.ts` to the `lint-no-direct-llm-http.js` ALLOWLIST (the profile call is read-only identity bookkeeping, not an LLM inference call — same class as QuotaPoller's `/api/oauth/usage`), and `tests/unit/credential-identity-oracle.test.ts` (9 tests).
|
|
13
|
+
|
|
14
|
+
Not wired into any runtime construction yet — the ledger is constructed with this oracle at the route/server layer in Step 7. No keychain WRITES (the refresh-before-profile optimization for an expired token is tracked to Step 4/5 when the write funnel exists; until then an expired token classifies `unavailable`, the safe direction).
|
|
15
|
+
|
|
16
|
+
## Decision-point inventory
|
|
17
|
+
|
|
18
|
+
- `CredentialIdentityOracle.resolveSlotTenant` — **add** — classifies a slot probe as confirmed-email or unavailable. Signal producer only; returns a value, blocks nothing. Fail-closed: every uncertain outcome → `unavailable` (never a guessed/mismatched identity).
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## 1. Over-block
|
|
23
|
+
|
|
24
|
+
**No block/allow surface — over-block not applicable.** The oracle rejects nothing; it returns either an email or `unavailable`. The conservative direction (treat any non-2xx / parse-failure / missing-email as `unavailable`) means a momentarily-flaky probe yields "can't tell" → the ledger quarantines and re-probes, never a wrong assignment. That is the intended safety bias.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## 2. Under-block
|
|
29
|
+
|
|
30
|
+
**Limited.** The oracle does not currently refresh an expired access token before probing (the spec optimization needs the Step-4 write funnel). The effect is a possible spurious `unavailable` on a slot whose token just expired — which is safe (quarantine + re-probe), never a wrong identity. Tracked to Step 4/5. It also cannot detect a token that is valid but belongs to a DIFFERENT account than expected — that's exactly the point: it reports the REAL owner; the ledger/audit compares against expectation (§2.11 divergence).
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## 3. Level-of-abstraction fit
|
|
35
|
+
|
|
36
|
+
Correct layer. A `src/core` detector that REUSES the established per-slot blob read (`readClaudeOauth`, OAuthRefresher) and mirrors the existing profile-call shape (QuotaCollector.oauthGet). It does not re-implement keychain access or invent a second profile-fetch convention. The fetch lives behind a bounded timeout and an injectable `fetchImpl` for tests, matching `refreshClaudeToken`'s dependency-injection style.
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## 4. Signal vs authority compliance
|
|
41
|
+
|
|
42
|
+
Compliant — textbook signal producer. It reads credential reality and emits a classification; it holds no authority and performs no mutation. Per the spec's RULE 3.1 registration, it is a HIGH-criticality state detector whose fallback is fail-closed (no answer → `unavailable`, never guessed). `docs/signal-vs-authority.md` prescribes exactly this for a brittle external probe. **Second-pass review: not required** under Phase 5 — no block/allow on messaging/dispatch, no session-lifecycle, no gate/sentinel/watchdog, no write. Every classification branch is unit-tested. The high-risk WRITE logic (the staged swap executor) is Step 5 and will carry a second-pass review.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## 5. Interactions
|
|
47
|
+
|
|
48
|
+
- **Lint allowlist** — adding the file to `lint-no-direct-llm-http.js` ALLOWLIST is the only cross-file effect; it is narrowly justified (read-only OAuth identity endpoint, same class as the already-listed QuotaPoller `/usage`). It does not weaken the lint for any other file.
|
|
49
|
+
- **No runtime construction yet** — nothing builds this oracle at runtime in this commit, so it cannot race or shadow any existing credential reader. The ledger (Step 2) holds it as an injected interface; the server wires the concrete instance in Step 7.
|
|
50
|
+
- It is READ-only against the keychain (via `readClaudeOauth`); it never competes with the QuotaPoller refresh-write or the (future) swap executor.
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## 6. External surfaces
|
|
55
|
+
|
|
56
|
+
One external call: `GET https://api.anthropic.com/api/oauth/profile` with the slot's own access token — the same read-only endpoint the official client and QuotaCollector already call, bounded by a 10s timeout. No new outbound surface to other agents/users. No routes added (Step 7). The token is sent only as a Bearer header to Anthropic's own endpoint and is never logged.
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## 7. Multi-machine posture (Cross-Machine Coherence)
|
|
61
|
+
|
|
62
|
+
**Machine-local BY DESIGN** — the oracle probes credentials in THIS machine's keychain/config homes (a slot only exists on the machine whose keychain holds it). Identity is resolved against the live local credential, so the probe is meaningful only on the machine asking. No replication, no proxied read; another machine's oracle answers about its own slots. This matches the ledger's machine-local posture (Step 2).
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## 8. Rollback cost
|
|
67
|
+
|
|
68
|
+
Near-zero. New file + new test + one allowlist line; no runtime construction, no writes, no migration. Plain `git revert`. Because nothing constructs the oracle at runtime yet, a revert leaves no orphaned behavior.
|