instar 1.3.537 → 1.3.539
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 +133 -2
- package/dist/commands/server.js.map +1 -1
- package/dist/core/CredentialAuditEmit.d.ts +82 -0
- package/dist/core/CredentialAuditEmit.d.ts.map +1 -0
- package/dist/core/CredentialAuditEmit.js +132 -0
- package/dist/core/CredentialAuditEmit.js.map +1 -0
- package/dist/core/CredentialLocationGate.d.ts +99 -0
- package/dist/core/CredentialLocationGate.d.ts.map +1 -0
- package/dist/core/CredentialLocationGate.js +127 -0
- package/dist/core/CredentialLocationGate.js.map +1 -0
- package/dist/core/CredentialManualLevers.d.ts +57 -0
- package/dist/core/CredentialManualLevers.d.ts.map +1 -0
- package/dist/core/CredentialManualLevers.js +0 -0
- package/dist/core/CredentialManualLevers.js.map +1 -0
- package/dist/core/CredentialRestoreEnrollment.d.ts +76 -0
- package/dist/core/CredentialRestoreEnrollment.d.ts.map +1 -0
- package/dist/core/CredentialRestoreEnrollment.js +76 -0
- package/dist/core/CredentialRestoreEnrollment.js.map +1 -0
- package/dist/core/CredentialSwapExecutor.d.ts +10 -0
- package/dist/core/CredentialSwapExecutor.d.ts.map +1 -1
- package/dist/core/CredentialSwapExecutor.js +19 -0
- package/dist/core/CredentialSwapExecutor.js.map +1 -1
- package/dist/core/InUseAccountResolver.d.ts +28 -1
- package/dist/core/InUseAccountResolver.d.ts.map +1 -1
- package/dist/core/InUseAccountResolver.js +34 -1
- package/dist/core/InUseAccountResolver.js.map +1 -1
- package/dist/core/QuotaPoller.d.ts +22 -0
- package/dist/core/QuotaPoller.d.ts.map +1 -1
- package/dist/core/QuotaPoller.js +42 -8
- package/dist/core/QuotaPoller.js.map +1 -1
- package/dist/core/SessionManager.d.ts +19 -0
- package/dist/core/SessionManager.d.ts.map +1 -1
- package/dist/core/SessionManager.js +37 -5
- package/dist/core/SessionManager.js.map +1 -1
- package/dist/core/types.d.ts +7 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/monitoring/AccountSwitcher.d.ts.map +1 -1
- package/dist/monitoring/AccountSwitcher.js +12 -1
- package/dist/monitoring/AccountSwitcher.js.map +1 -1
- package/dist/monitoring/CredentialProvider.d.ts +32 -1
- package/dist/monitoring/CredentialProvider.d.ts.map +1 -1
- package/dist/monitoring/CredentialProvider.js +32 -2
- package/dist/monitoring/CredentialProvider.js.map +1 -1
- package/dist/server/AgentServer.d.ts +1 -0
- package/dist/server/AgentServer.d.ts.map +1 -1
- package/dist/server/AgentServer.js +1 -0
- package/dist/server/AgentServer.js.map +1 -1
- package/dist/server/CapabilityIndex.d.ts.map +1 -1
- package/dist/server/CapabilityIndex.js +16 -0
- package/dist/server/CapabilityIndex.js.map +1 -1
- package/dist/server/routes.d.ts +20 -0
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +228 -0
- package/dist/server/routes.js.map +1 -1
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +48 -48
- package/upgrades/1.3.538.md +51 -0
- package/upgrades/1.3.539.md +29 -0
- package/upgrades/side-effects/ws52-step6-census-rerouting.md +143 -0
- package/upgrades/side-effects/ws52-step7-routes-audit.md +39 -0
- package/upgrades/1.3.537.md +0 -27
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CredentialAuditEmit — the SINGLE secret-scrub chokepoint for live credential re-pointing
|
|
3
|
+
* (spec §2.9, build-plan §7).
|
|
4
|
+
*
|
|
5
|
+
* Every `logs/credential-swaps.jsonl` audit write, every `/credentials/*` HTTP response body,
|
|
6
|
+
* and every attention-item this feature constructs routes through ONE `scrub(record)` funnel.
|
|
7
|
+
* The invariant the spec names — "no field of any persisted, audited, notified, or HTTP-served
|
|
8
|
+
* surface of this feature may contain token material" — is enforced STRUCTURALLY here, not by
|
|
9
|
+
* "remember to scrub at each of N callsites".
|
|
10
|
+
*
|
|
11
|
+
* ── Why a single chokepoint (spec §2.9, round-3) ──
|
|
12
|
+
* Node's `JSON.parse` error is position-only and does not echo bytes, so the real leak vector is
|
|
13
|
+
* developer-authored interpolation: a `${raw}`-bearing log line, a `security`/keychain stderr, a
|
|
14
|
+
* fetch error string that carries a token FRAGMENT inside a free-text `reason`/`error`/`message`.
|
|
15
|
+
* A FORBIDDEN_CREDENTIAL_FIELDS field-name scan does not catch a token hiding in free text. So
|
|
16
|
+
* this funnel deep-walks EVERY string in a record and runs each through `redactToken`-backed
|
|
17
|
+
* scrubbing before the record reaches any persisted/served/notified surface.
|
|
18
|
+
*
|
|
19
|
+
* ── The redaction (reuses CredentialProvider.redactToken — NOT re-authored) ──
|
|
20
|
+
* `redactToken(t)` → `[TOKEN:abc1****]`. We apply it to any token-shaped run: the `sk-ant-…`
|
|
21
|
+
* family (access/oauth/refresh tokens), and any long high-entropy base64url-ish run that could
|
|
22
|
+
* be a bearer/secret. Account ids, slot paths, emails-by-id, and ordinary prose pass through
|
|
23
|
+
* unchanged — the feature references accounts BY ID only, so a healthy record loses nothing.
|
|
24
|
+
*
|
|
25
|
+
* This module performs NO IO and NO credential access. It is a pure, deterministic, machine-local
|
|
26
|
+
* transform (Phase C: redaction is local-deterministic, no peer dependency).
|
|
27
|
+
*/
|
|
28
|
+
/**
|
|
29
|
+
* Scrub token material out of a single string. Reuses `redactToken` (the project's one redactor)
|
|
30
|
+
* on every token-shaped run. Idempotent: a `[TOKEN:…]` marker contains no token-shaped run that
|
|
31
|
+
* survives a second pass.
|
|
32
|
+
*/
|
|
33
|
+
export declare function scrubString(s: string): string;
|
|
34
|
+
/**
|
|
35
|
+
* Deep-scrub an arbitrary JSON-able value: walks objects/arrays and scrubs every string leaf.
|
|
36
|
+
* Object KEYS are passed through unchanged (a key is a field name, never token material — and the
|
|
37
|
+
* §2.9 field-name scan covers forbidden keys separately). Non-string/number/bool/null leaves
|
|
38
|
+
* (functions, symbols) are dropped to a string form so nothing un-serializable rides along.
|
|
39
|
+
*
|
|
40
|
+
* THE chokepoint: `CredentialAuditEmit.scrub(record)` calls this. Every audit write, every
|
|
41
|
+
* `/credentials/*` response body, and every attention-item field passes through here.
|
|
42
|
+
*/
|
|
43
|
+
export declare function scrub<T>(record: T): T;
|
|
44
|
+
/**
|
|
45
|
+
* The audit-emit handle the host wires into the swap executor + the routes. ONE instance per
|
|
46
|
+
* process; every emit path (jsonl, response, attention) calls the corresponding method, and each
|
|
47
|
+
* method scrubs BEFORE the value reaches its surface. Constructing the handle with an injected
|
|
48
|
+
* `writeLine` (the jsonl sink) and `emitAttention` keeps it IO-free + unit-testable.
|
|
49
|
+
*/
|
|
50
|
+
export interface CredentialAuditEmitDeps {
|
|
51
|
+
/** Append one already-stringified JSON line to `logs/credential-swaps.jsonl`. */
|
|
52
|
+
writeLine?: (line: string) => void;
|
|
53
|
+
/** Deliver one attention item (typically telegram.createAttentionItem). */
|
|
54
|
+
emitAttention?: (item: CredentialAttentionItem) => void | Promise<void>;
|
|
55
|
+
now?: () => string;
|
|
56
|
+
}
|
|
57
|
+
export interface CredentialAttentionItem {
|
|
58
|
+
id: string;
|
|
59
|
+
title: string;
|
|
60
|
+
summary: string;
|
|
61
|
+
description?: string;
|
|
62
|
+
category: string;
|
|
63
|
+
priority: 'URGENT' | 'HIGH' | 'NORMAL' | 'LOW';
|
|
64
|
+
sourceContext?: string;
|
|
65
|
+
}
|
|
66
|
+
export declare class CredentialAuditEmit {
|
|
67
|
+
private readonly writeLine?;
|
|
68
|
+
private readonly emitAttentionFn?;
|
|
69
|
+
private readonly now;
|
|
70
|
+
constructor(deps?: CredentialAuditEmitDeps);
|
|
71
|
+
/** Surface 1 — the jsonl audit write. Scrubs the record, stamps it, appends one line. */
|
|
72
|
+
audit(record: Record<string, unknown>): void;
|
|
73
|
+
/**
|
|
74
|
+
* Surface 2 — the `/credentials/*` HTTP response body. Returns the SCRUBBED body the route
|
|
75
|
+
* sends. The route MUST send `emit.response(body)`, never a raw body — this is the wiring the
|
|
76
|
+
* §2.9 chokepoint test asserts.
|
|
77
|
+
*/
|
|
78
|
+
response<T>(body: T): T;
|
|
79
|
+
/** Surface 3 — the attention item. Scrubs every field, then delivers (best-effort). */
|
|
80
|
+
attention(item: CredentialAttentionItem): Promise<void>;
|
|
81
|
+
}
|
|
82
|
+
//# sourceMappingURL=CredentialAuditEmit.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CredentialAuditEmit.d.ts","sourceRoot":"","sources":["../../src/core/CredentialAuditEmit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAkBH;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAS7C;AAED;;;;;;;;GAQG;AACH,wBAAgB,KAAK,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,GAAG,CAAC,CAErC;AAgBD;;;;;GAKG;AACH,MAAM,WAAW,uBAAuB;IACtC,iFAAiF;IACjF,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,2EAA2E;IAC3E,aAAa,CAAC,EAAE,CAAC,IAAI,EAAE,uBAAuB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACxE,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,uBAAuB;IACtC,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,QAAQ,GAAG,MAAM,GAAG,QAAQ,GAAG,KAAK,CAAC;IAC/C,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAyB;IACpD,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAC,CAA0D;IAC3F,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;gBAEvB,IAAI,GAAE,uBAA4B;IAM9C,yFAAyF;IACzF,KAAK,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IAa5C;;;;OAIG;IACH,QAAQ,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,GAAG,CAAC;IAIvB,uFAAuF;IACjF,SAAS,CAAC,IAAI,EAAE,uBAAuB,GAAG,OAAO,CAAC,IAAI,CAAC;CAW9D"}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CredentialAuditEmit — the SINGLE secret-scrub chokepoint for live credential re-pointing
|
|
3
|
+
* (spec §2.9, build-plan §7).
|
|
4
|
+
*
|
|
5
|
+
* Every `logs/credential-swaps.jsonl` audit write, every `/credentials/*` HTTP response body,
|
|
6
|
+
* and every attention-item this feature constructs routes through ONE `scrub(record)` funnel.
|
|
7
|
+
* The invariant the spec names — "no field of any persisted, audited, notified, or HTTP-served
|
|
8
|
+
* surface of this feature may contain token material" — is enforced STRUCTURALLY here, not by
|
|
9
|
+
* "remember to scrub at each of N callsites".
|
|
10
|
+
*
|
|
11
|
+
* ── Why a single chokepoint (spec §2.9, round-3) ──
|
|
12
|
+
* Node's `JSON.parse` error is position-only and does not echo bytes, so the real leak vector is
|
|
13
|
+
* developer-authored interpolation: a `${raw}`-bearing log line, a `security`/keychain stderr, a
|
|
14
|
+
* fetch error string that carries a token FRAGMENT inside a free-text `reason`/`error`/`message`.
|
|
15
|
+
* A FORBIDDEN_CREDENTIAL_FIELDS field-name scan does not catch a token hiding in free text. So
|
|
16
|
+
* this funnel deep-walks EVERY string in a record and runs each through `redactToken`-backed
|
|
17
|
+
* scrubbing before the record reaches any persisted/served/notified surface.
|
|
18
|
+
*
|
|
19
|
+
* ── The redaction (reuses CredentialProvider.redactToken — NOT re-authored) ──
|
|
20
|
+
* `redactToken(t)` → `[TOKEN:abc1****]`. We apply it to any token-shaped run: the `sk-ant-…`
|
|
21
|
+
* family (access/oauth/refresh tokens), and any long high-entropy base64url-ish run that could
|
|
22
|
+
* be a bearer/secret. Account ids, slot paths, emails-by-id, and ordinary prose pass through
|
|
23
|
+
* unchanged — the feature references accounts BY ID only, so a healthy record loses nothing.
|
|
24
|
+
*
|
|
25
|
+
* This module performs NO IO and NO credential access. It is a pure, deterministic, machine-local
|
|
26
|
+
* transform (Phase C: redaction is local-deterministic, no peer dependency).
|
|
27
|
+
*/
|
|
28
|
+
import { redactToken } from '../monitoring/CredentialProvider.js';
|
|
29
|
+
/**
|
|
30
|
+
* The `sk-ant-…` token family Anthropic issues (oauth access `sk-ant-oat…`, api-key `sk-ant-api…`,
|
|
31
|
+
* refresh `sk-ant-ort…`, etc.). Matched first so the canonical leak vector is always redacted.
|
|
32
|
+
*/
|
|
33
|
+
const SK_ANT_RE = /sk-ant-[A-Za-z0-9_-]{8,}/g;
|
|
34
|
+
/**
|
|
35
|
+
* A long high-entropy token-shaped run (≥32 chars of base64url/hex alphabet) that is NOT a
|
|
36
|
+
* recognizable non-secret. This catches a bearer/secret that does NOT carry the `sk-ant-` prefix
|
|
37
|
+
* (a keychain stderr could echo a raw blob). Deliberately conservative on the low end so ordinary
|
|
38
|
+
* ids / slot hashes / ISO timestamps are never mangled (an 8-hex slot suffix is far under 32).
|
|
39
|
+
*/
|
|
40
|
+
const LONG_TOKEN_RE = /\b[A-Za-z0-9_-]{32,}\b/g;
|
|
41
|
+
/**
|
|
42
|
+
* Scrub token material out of a single string. Reuses `redactToken` (the project's one redactor)
|
|
43
|
+
* on every token-shaped run. Idempotent: a `[TOKEN:…]` marker contains no token-shaped run that
|
|
44
|
+
* survives a second pass.
|
|
45
|
+
*/
|
|
46
|
+
export function scrubString(s) {
|
|
47
|
+
if (!s)
|
|
48
|
+
return s;
|
|
49
|
+
let out = s.replace(SK_ANT_RE, (m) => redactToken(m));
|
|
50
|
+
out = out.replace(LONG_TOKEN_RE, (m) => {
|
|
51
|
+
// Never re-redact an already-emitted marker, and never touch the redactor's own output.
|
|
52
|
+
if (m.startsWith('TOKEN'))
|
|
53
|
+
return m;
|
|
54
|
+
return redactToken(m);
|
|
55
|
+
});
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Deep-scrub an arbitrary JSON-able value: walks objects/arrays and scrubs every string leaf.
|
|
60
|
+
* Object KEYS are passed through unchanged (a key is a field name, never token material — and the
|
|
61
|
+
* §2.9 field-name scan covers forbidden keys separately). Non-string/number/bool/null leaves
|
|
62
|
+
* (functions, symbols) are dropped to a string form so nothing un-serializable rides along.
|
|
63
|
+
*
|
|
64
|
+
* THE chokepoint: `CredentialAuditEmit.scrub(record)` calls this. Every audit write, every
|
|
65
|
+
* `/credentials/*` response body, and every attention-item field passes through here.
|
|
66
|
+
*/
|
|
67
|
+
export function scrub(record) {
|
|
68
|
+
return deepScrub(record);
|
|
69
|
+
}
|
|
70
|
+
function deepScrub(v) {
|
|
71
|
+
if (typeof v === 'string')
|
|
72
|
+
return scrubString(v);
|
|
73
|
+
if (Array.isArray(v))
|
|
74
|
+
return v.map((x) => deepScrub(x));
|
|
75
|
+
if (v && typeof v === 'object') {
|
|
76
|
+
const out = {};
|
|
77
|
+
for (const [k, val] of Object.entries(v)) {
|
|
78
|
+
out[k] = deepScrub(val);
|
|
79
|
+
}
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
82
|
+
// number | boolean | null | undefined | bigint pass through unchanged (no token material).
|
|
83
|
+
return v;
|
|
84
|
+
}
|
|
85
|
+
export class CredentialAuditEmit {
|
|
86
|
+
writeLine;
|
|
87
|
+
emitAttentionFn;
|
|
88
|
+
now;
|
|
89
|
+
constructor(deps = {}) {
|
|
90
|
+
this.writeLine = deps.writeLine;
|
|
91
|
+
this.emitAttentionFn = deps.emitAttention;
|
|
92
|
+
this.now = deps.now ?? (() => new Date().toISOString());
|
|
93
|
+
}
|
|
94
|
+
/** Surface 1 — the jsonl audit write. Scrubs the record, stamps it, appends one line. */
|
|
95
|
+
audit(record) {
|
|
96
|
+
if (!this.writeLine)
|
|
97
|
+
return;
|
|
98
|
+
const scrubbed = scrub({ at: this.now(), ...record });
|
|
99
|
+
try {
|
|
100
|
+
this.writeLine(JSON.stringify(scrubbed) + '\n');
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
// @silent-fallback-ok — the audit jsonl is observability; a write failure (full disk, fs
|
|
104
|
+
// hiccup) must never throw into a swap/route. The actual credential operation is the
|
|
105
|
+
// load-bearing action and is unaffected; the next emit retries. (No token can leak here —
|
|
106
|
+
// the value was already scrubbed before the failing write.)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Surface 2 — the `/credentials/*` HTTP response body. Returns the SCRUBBED body the route
|
|
111
|
+
* sends. The route MUST send `emit.response(body)`, never a raw body — this is the wiring the
|
|
112
|
+
* §2.9 chokepoint test asserts.
|
|
113
|
+
*/
|
|
114
|
+
response(body) {
|
|
115
|
+
return scrub(body);
|
|
116
|
+
}
|
|
117
|
+
/** Surface 3 — the attention item. Scrubs every field, then delivers (best-effort). */
|
|
118
|
+
async attention(item) {
|
|
119
|
+
if (!this.emitAttentionFn)
|
|
120
|
+
return;
|
|
121
|
+
const scrubbed = scrub(item);
|
|
122
|
+
try {
|
|
123
|
+
await this.emitAttentionFn(scrubbed);
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
// @silent-fallback-ok — attention delivery is best-effort; the swap/quarantine safety action
|
|
127
|
+
// it announces is already durably recorded in the ledger regardless of whether the notice
|
|
128
|
+
// lands. The item was already scrubbed before this call, so a delivery failure leaks nothing.
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
//# sourceMappingURL=CredentialAuditEmit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CredentialAuditEmit.js","sourceRoot":"","sources":["../../src/core/CredentialAuditEmit.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CredentialLocationGate — the SINGLE re-routing chokepoint for the §2.2 consumer census
|
|
3
|
+
* (Step 6 of live credential re-pointing).
|
|
4
|
+
*
|
|
5
|
+
* Spec: docs/specs/live-credential-repointing-rebalancer.md §2.2 (the consumer census table).
|
|
6
|
+
*
|
|
7
|
+
* ── What this is ──
|
|
8
|
+
* Every place in the live code that today treats a pool account's enrollment `configHome` as the
|
|
9
|
+
* LIVE location of its credential (the QuotaPoller token read / 401-refresh / email auto-patch /
|
|
10
|
+
* needs-reauth attribution; spawn placement + session attribution; the InUseAccountResolver
|
|
11
|
+
* default badge) is re-routed through ONE gate object instead of reaching into the ledger
|
|
12
|
+
* directly. Centralizing the gate keeps the load-bearing safety contract in a single auditable
|
|
13
|
+
* place — every consumer inherits the same flag-gating, back-compat-on-unknown, and
|
|
14
|
+
* fail-open-loud posture by construction (Structure > Willpower).
|
|
15
|
+
*
|
|
16
|
+
* ── The load-bearing safety contract (spec §2.2; build-prompt invariants 1, 5) ──
|
|
17
|
+
* 1. FLAG-GATED. With `enabled:false` (always, while the feature ships dark) every read returns
|
|
18
|
+
* the caller's `enrollmentHome` (or null for tenant reads) — byte-for-byte today's behavior.
|
|
19
|
+
* 2. BACK-COMPAT ON UNKNOWN. With the flag ON but the ledger holding no record for that
|
|
20
|
+
* account/slot (never-seeded), the gate ALSO falls back to today's behavior. The ledger only
|
|
21
|
+
* ever ADDS truth, never removes the working path.
|
|
22
|
+
* 3. SYNC + FAIL-OPEN-LOUD. `ledger.slotOf/tenantOf` are in-memory sync reads that return null in
|
|
23
|
+
* UNKNOWN mode (corrupt on-disk state). This gate NEVER throws into a hot path (QuotaPoller /
|
|
24
|
+
* spawn): on UNKNOWN mode it returns the enrollment fallback AND raises ONE deduped HIGH
|
|
25
|
+
* attention item naming the degradation, then the caller continues on today's behavior.
|
|
26
|
+
*
|
|
27
|
+
* The gate performs NO credential writes. It is a pure read-router over the ledger + flag.
|
|
28
|
+
*/
|
|
29
|
+
import type { CredentialLocationLedger } from './CredentialLocationLedger.js';
|
|
30
|
+
/** Attention-item shape (mirrors CredentialLocationLedger.CredentialLedgerAttentionInput). */
|
|
31
|
+
export interface CredentialGateAttentionInput {
|
|
32
|
+
id: string;
|
|
33
|
+
title: string;
|
|
34
|
+
summary: string;
|
|
35
|
+
description?: string;
|
|
36
|
+
category: string;
|
|
37
|
+
priority: 'URGENT' | 'HIGH' | 'NORMAL' | 'LOW';
|
|
38
|
+
sourceContext?: string;
|
|
39
|
+
}
|
|
40
|
+
export interface CredentialLocationGateDeps {
|
|
41
|
+
/**
|
|
42
|
+
* Reads the live `subscriptionPool.credentialRepointing.enabled` flag. Read EACH call (not
|
|
43
|
+
* cached) so a restartless config flip is honored on the next read — the same restartless
|
|
44
|
+
* posture the rest of the feature uses.
|
|
45
|
+
*/
|
|
46
|
+
isEnabled: () => boolean;
|
|
47
|
+
/** The machine-local ledger (the single source of truth for "which account is in which slot"). */
|
|
48
|
+
ledger: CredentialLocationLedger;
|
|
49
|
+
/** Emit a deduped HIGH attention item on UNKNOWN-mode degradation (best-effort). */
|
|
50
|
+
emitAttention?: (item: CredentialGateAttentionInput) => void | Promise<void>;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Re-routes the §2.2 census consumers' `configHome`-as-location reads through the ledger.
|
|
54
|
+
*
|
|
55
|
+
* Construct ONE per process and hand it to each consumer (QuotaPoller, SessionManager spawn,
|
|
56
|
+
* InUseAccountResolver). The consumers stay dumb — they ask the gate "where does account X live
|
|
57
|
+
* now?" and "who tenants slot Y now?" and otherwise behave exactly as today.
|
|
58
|
+
*/
|
|
59
|
+
export declare class CredentialLocationGate {
|
|
60
|
+
private readonly isEnabledFn;
|
|
61
|
+
private readonly ledger;
|
|
62
|
+
private readonly emitAttention?;
|
|
63
|
+
/** Dedupe the UNKNOWN-mode attention item so a read storm raises it once per process. */
|
|
64
|
+
private unknownAttentionRaised;
|
|
65
|
+
constructor(deps: CredentialLocationGateDeps);
|
|
66
|
+
/** Live flag read — true only when re-pointing is enabled. */
|
|
67
|
+
isEnabled(): boolean;
|
|
68
|
+
/**
|
|
69
|
+
* The config-home SLOT where `accountId`'s credential CURRENTLY lives.
|
|
70
|
+
*
|
|
71
|
+
* - flag OFF → `enrollmentHome` (today's behavior, byte-identical).
|
|
72
|
+
* - flag ON + ledger has a record → the ledger's current slot for the account.
|
|
73
|
+
* - flag ON + ledger UNKNOWN/empty → `enrollmentHome` (back-compat) AND, if UNKNOWN mode,
|
|
74
|
+
* one deduped HIGH attention item (fail-open-LOUD). NEVER throws.
|
|
75
|
+
*
|
|
76
|
+
* The returned slot is in the SAME shape the caller passed (a config-home path); it is NOT
|
|
77
|
+
* canonicalized here so a caller that does its own `expandHome` keeps working unchanged.
|
|
78
|
+
*/
|
|
79
|
+
slotForAccount(accountId: string, enrollmentHome: string): string;
|
|
80
|
+
/**
|
|
81
|
+
* The pool account id CURRENTLY tenanting `slot` per the ledger, or null.
|
|
82
|
+
*
|
|
83
|
+
* - flag OFF → null (caller keeps its existing attribution path).
|
|
84
|
+
* - flag ON + ledger has a record → the ledger's tenant account id.
|
|
85
|
+
* - flag ON + ledger UNKNOWN/empty → null (back-compat) AND, if UNKNOWN mode, one deduped HIGH
|
|
86
|
+
* attention item (fail-open-LOUD). NEVER throws.
|
|
87
|
+
*
|
|
88
|
+
* `slot` is matched against the ledger as-passed; pass the same shape the ledger stores (the
|
|
89
|
+
* consumers use `~/.claude` for the default badge, matching seedFromOracle's slot keys).
|
|
90
|
+
*/
|
|
91
|
+
tenantForSlot(slot: string): string | null;
|
|
92
|
+
/**
|
|
93
|
+
* True iff `slot` (any spelling) canonicalizes to the DEFAULT `~/.claude` home. Used by the
|
|
94
|
+
* swap-commit cache-bust: only a swap touching the default slot need bust the in-use badge.
|
|
95
|
+
*/
|
|
96
|
+
static touchesDefaultHome(slot: string): boolean;
|
|
97
|
+
private raiseUnknownModeAttention;
|
|
98
|
+
}
|
|
99
|
+
//# sourceMappingURL=CredentialLocationGate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CredentialLocationGate.d.ts","sourceRoot":"","sources":["../../src/core/CredentialLocationGate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,+BAA+B,CAAC;AAG9E,8FAA8F;AAC9F,MAAM,WAAW,4BAA4B;IAC3C,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,QAAQ,GAAG,MAAM,GAAG,QAAQ,GAAG,KAAK,CAAC;IAC/C,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,0BAA0B;IACzC;;;;OAIG;IACH,SAAS,EAAE,MAAM,OAAO,CAAC;IACzB,kGAAkG;IAClG,MAAM,EAAE,wBAAwB,CAAC;IACjC,oFAAoF;IACpF,aAAa,CAAC,EAAE,CAAC,IAAI,EAAE,4BAA4B,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9E;AAED;;;;;;GAMG;AACH,qBAAa,sBAAsB;IACjC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAgB;IAC5C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA2B;IAClD,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAC,CAA+D;IAC9F,yFAAyF;IACzF,OAAO,CAAC,sBAAsB,CAAS;gBAE3B,IAAI,EAAE,0BAA0B;IAM5C,8DAA8D;IAC9D,SAAS,IAAI,OAAO;IAIpB;;;;;;;;;;OAUG;IACH,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,MAAM;IASjE;;;;;;;;;;OAUG;IACH,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAQ1C;;;OAGG;IACH,MAAM,CAAC,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;IAIhD,OAAO,CAAC,yBAAyB;CAuBlC"}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CredentialLocationGate — the SINGLE re-routing chokepoint for the §2.2 consumer census
|
|
3
|
+
* (Step 6 of live credential re-pointing).
|
|
4
|
+
*
|
|
5
|
+
* Spec: docs/specs/live-credential-repointing-rebalancer.md §2.2 (the consumer census table).
|
|
6
|
+
*
|
|
7
|
+
* ── What this is ──
|
|
8
|
+
* Every place in the live code that today treats a pool account's enrollment `configHome` as the
|
|
9
|
+
* LIVE location of its credential (the QuotaPoller token read / 401-refresh / email auto-patch /
|
|
10
|
+
* needs-reauth attribution; spawn placement + session attribution; the InUseAccountResolver
|
|
11
|
+
* default badge) is re-routed through ONE gate object instead of reaching into the ledger
|
|
12
|
+
* directly. Centralizing the gate keeps the load-bearing safety contract in a single auditable
|
|
13
|
+
* place — every consumer inherits the same flag-gating, back-compat-on-unknown, and
|
|
14
|
+
* fail-open-loud posture by construction (Structure > Willpower).
|
|
15
|
+
*
|
|
16
|
+
* ── The load-bearing safety contract (spec §2.2; build-prompt invariants 1, 5) ──
|
|
17
|
+
* 1. FLAG-GATED. With `enabled:false` (always, while the feature ships dark) every read returns
|
|
18
|
+
* the caller's `enrollmentHome` (or null for tenant reads) — byte-for-byte today's behavior.
|
|
19
|
+
* 2. BACK-COMPAT ON UNKNOWN. With the flag ON but the ledger holding no record for that
|
|
20
|
+
* account/slot (never-seeded), the gate ALSO falls back to today's behavior. The ledger only
|
|
21
|
+
* ever ADDS truth, never removes the working path.
|
|
22
|
+
* 3. SYNC + FAIL-OPEN-LOUD. `ledger.slotOf/tenantOf` are in-memory sync reads that return null in
|
|
23
|
+
* UNKNOWN mode (corrupt on-disk state). This gate NEVER throws into a hot path (QuotaPoller /
|
|
24
|
+
* spawn): on UNKNOWN mode it returns the enrollment fallback AND raises ONE deduped HIGH
|
|
25
|
+
* attention item naming the degradation, then the caller continues on today's behavior.
|
|
26
|
+
*
|
|
27
|
+
* The gate performs NO credential writes. It is a pure read-router over the ledger + flag.
|
|
28
|
+
*/
|
|
29
|
+
import { credentialSlotKey } from './OAuthRefresher.js';
|
|
30
|
+
/**
|
|
31
|
+
* Re-routes the §2.2 census consumers' `configHome`-as-location reads through the ledger.
|
|
32
|
+
*
|
|
33
|
+
* Construct ONE per process and hand it to each consumer (QuotaPoller, SessionManager spawn,
|
|
34
|
+
* InUseAccountResolver). The consumers stay dumb — they ask the gate "where does account X live
|
|
35
|
+
* now?" and "who tenants slot Y now?" and otherwise behave exactly as today.
|
|
36
|
+
*/
|
|
37
|
+
export class CredentialLocationGate {
|
|
38
|
+
isEnabledFn;
|
|
39
|
+
ledger;
|
|
40
|
+
emitAttention;
|
|
41
|
+
/** Dedupe the UNKNOWN-mode attention item so a read storm raises it once per process. */
|
|
42
|
+
unknownAttentionRaised = false;
|
|
43
|
+
constructor(deps) {
|
|
44
|
+
this.isEnabledFn = deps.isEnabled;
|
|
45
|
+
this.ledger = deps.ledger;
|
|
46
|
+
this.emitAttention = deps.emitAttention;
|
|
47
|
+
}
|
|
48
|
+
/** Live flag read — true only when re-pointing is enabled. */
|
|
49
|
+
isEnabled() {
|
|
50
|
+
return this.isEnabledFn();
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* The config-home SLOT where `accountId`'s credential CURRENTLY lives.
|
|
54
|
+
*
|
|
55
|
+
* - flag OFF → `enrollmentHome` (today's behavior, byte-identical).
|
|
56
|
+
* - flag ON + ledger has a record → the ledger's current slot for the account.
|
|
57
|
+
* - flag ON + ledger UNKNOWN/empty → `enrollmentHome` (back-compat) AND, if UNKNOWN mode,
|
|
58
|
+
* one deduped HIGH attention item (fail-open-LOUD). NEVER throws.
|
|
59
|
+
*
|
|
60
|
+
* The returned slot is in the SAME shape the caller passed (a config-home path); it is NOT
|
|
61
|
+
* canonicalized here so a caller that does its own `expandHome` keeps working unchanged.
|
|
62
|
+
*/
|
|
63
|
+
slotForAccount(accountId, enrollmentHome) {
|
|
64
|
+
if (!this.isEnabledFn())
|
|
65
|
+
return enrollmentHome;
|
|
66
|
+
// ledger.slotOf is a sync in-memory read; null in UNKNOWN mode or when never-seeded.
|
|
67
|
+
const slot = this.ledger.slotOf(accountId);
|
|
68
|
+
if (slot)
|
|
69
|
+
return slot;
|
|
70
|
+
if (this.ledger.isUnknownMode())
|
|
71
|
+
this.raiseUnknownModeAttention();
|
|
72
|
+
return enrollmentHome;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* The pool account id CURRENTLY tenanting `slot` per the ledger, or null.
|
|
76
|
+
*
|
|
77
|
+
* - flag OFF → null (caller keeps its existing attribution path).
|
|
78
|
+
* - flag ON + ledger has a record → the ledger's tenant account id.
|
|
79
|
+
* - flag ON + ledger UNKNOWN/empty → null (back-compat) AND, if UNKNOWN mode, one deduped HIGH
|
|
80
|
+
* attention item (fail-open-LOUD). NEVER throws.
|
|
81
|
+
*
|
|
82
|
+
* `slot` is matched against the ledger as-passed; pass the same shape the ledger stores (the
|
|
83
|
+
* consumers use `~/.claude` for the default badge, matching seedFromOracle's slot keys).
|
|
84
|
+
*/
|
|
85
|
+
tenantForSlot(slot) {
|
|
86
|
+
if (!this.isEnabledFn())
|
|
87
|
+
return null;
|
|
88
|
+
const tenant = this.ledger.tenantOf(slot);
|
|
89
|
+
if (tenant)
|
|
90
|
+
return tenant;
|
|
91
|
+
if (this.ledger.isUnknownMode())
|
|
92
|
+
this.raiseUnknownModeAttention();
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* True iff `slot` (any spelling) canonicalizes to the DEFAULT `~/.claude` home. Used by the
|
|
97
|
+
* swap-commit cache-bust: only a swap touching the default slot need bust the in-use badge.
|
|
98
|
+
*/
|
|
99
|
+
static touchesDefaultHome(slot) {
|
|
100
|
+
return credentialSlotKey(slot) === credentialSlotKey('~/.claude');
|
|
101
|
+
}
|
|
102
|
+
raiseUnknownModeAttention() {
|
|
103
|
+
if (this.unknownAttentionRaised || !this.emitAttention)
|
|
104
|
+
return;
|
|
105
|
+
this.unknownAttentionRaised = true;
|
|
106
|
+
try {
|
|
107
|
+
void this.emitAttention({
|
|
108
|
+
id: 'credential-location-gate-unknown-mode',
|
|
109
|
+
title: 'Credential location ledger UNKNOWN — consumers fell back to enrollment homes',
|
|
110
|
+
summary: 'A census consumer (quota poll / spawn placement / in-use badge) read the credential ' +
|
|
111
|
+
'location ledger while it was in UNKNOWN mode (corrupt on-disk state). The read fell ' +
|
|
112
|
+
'back to enrollment-home behavior (no path broke), but credential re-pointing is not ' +
|
|
113
|
+
'authoritative until the ledger is re-seeded via the identity oracle.',
|
|
114
|
+
category: 'credential-repointing',
|
|
115
|
+
priority: 'HIGH',
|
|
116
|
+
sourceContext: 'credential-location-gate',
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
// @silent-fallback-ok — the attention emitter is best-effort; a delivery failure must never
|
|
121
|
+
// throw into the QuotaPoller / spawn hot path. The actual safety behavior (the enrollment-
|
|
122
|
+
// home fallback) is already in force regardless of whether the notice was delivered, so the
|
|
123
|
+
// path's correctness does not depend on this emit.
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
//# sourceMappingURL=CredentialLocationGate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CredentialLocationGate.js","sourceRoot":"","sources":["../../src/core/CredentialLocationGate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAGH,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AA0BxD;;;;;;GAMG;AACH,MAAM,OAAO,sBAAsB;IAChB,WAAW,CAAgB;IAC3B,MAAM,CAA2B;IACjC,aAAa,CAAgE;IAC9F,yFAAyF;IACjF,sBAAsB,GAAG,KAAK,CAAC;IAEvC,YAAY,IAAgC;QAC1C,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC,SAAS,CAAC;QAClC,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC;QAC1B,IAAI,CAAC,aAAa,GAAG,IAAI,CAAC,aAAa,CAAC;IAC1C,CAAC;IAED,8DAA8D;IAC9D,SAAS;QACP,OAAO,IAAI,CAAC,WAAW,EAAE,CAAC;IAC5B,CAAC;IAED;;;;;;;;;;OAUG;IACH,cAAc,CAAC,SAAiB,EAAE,cAAsB;QACtD,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE;YAAE,OAAO,cAAc,CAAC;QAC/C,qFAAqF;QACrF,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;QAC3C,IAAI,IAAI;YAAE,OAAO,IAAI,CAAC;QACtB,IAAI,IAAI,CAAC,MAAM,CAAC,aAAa,EAAE;YAAE,IAAI,CAAC,yBAAyB,EAAE,CAAC;QAClE,OAAO,cAAc,CAAC;IACxB,CAAC;IAED;;;;;;;;;;OAUG;IACH,aAAa,CAAC,IAAY;QACxB,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE;YAAE,OAAO,IAAI,CAAC;QACrC,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;QAC1C,IAAI,MAAM;YAAE,OAAO,MAAM,CAAC;QAC1B,IAAI,IAAI,CAAC,MAAM,CAAC,aAAa,EAAE;YAAE,IAAI,CAAC,yBAAyB,EAAE,CAAC;QAClE,OAAO,IAAI,CAAC;IACd,CAAC;IAED;;;OAGG;IACH,MAAM,CAAC,kBAAkB,CAAC,IAAY;QACpC,OAAO,iBAAiB,CAAC,IAAI,CAAC,KAAK,iBAAiB,CAAC,WAAW,CAAC,CAAC;IACpE,CAAC;IAEO,yBAAyB;QAC/B,IAAI,IAAI,CAAC,sBAAsB,IAAI,CAAC,IAAI,CAAC,aAAa;YAAE,OAAO;QAC/D,IAAI,CAAC,sBAAsB,GAAG,IAAI,CAAC;QACnC,IAAI,CAAC;YACH,KAAK,IAAI,CAAC,aAAa,CAAC;gBACtB,EAAE,EAAE,uCAAuC;gBAC3C,KAAK,EAAE,8EAA8E;gBACrF,OAAO,EACL,sFAAsF;oBACtF,sFAAsF;oBACtF,sFAAsF;oBACtF,sEAAsE;gBACxE,QAAQ,EAAE,uBAAuB;gBACjC,QAAQ,EAAE,MAAM;gBAChB,aAAa,EAAE,0BAA0B;aAC1C,CAAC,CAAC;QACL,CAAC;QAAC,MAAM,CAAC;YACP,4FAA4F;YAC5F,2FAA2F;YAC3F,4FAA4F;YAC5F,mDAAmD;QACrD,CAAC;IACH,CAAC;CACF"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CredentialManualLevers — the detective-control bookkeeping for the manual swap/set-default
|
|
3
|
+
* levers (spec §2.4 manual levers + §0.g force budget). Machine-local, in-memory, pure.
|
|
4
|
+
*
|
|
5
|
+
* The levers stay agent-callable (single-operator autonomy directive), so the control is
|
|
6
|
+
* DETECTIVE + structural + non-suppressible, not a PIN gate:
|
|
7
|
+
* - per-PAIR cooldown — a manual swap respects the per-pair cooldown by default; `force:true`
|
|
8
|
+
* overrides it, and the override is FLAGGED in the audit + notification.
|
|
9
|
+
* - force budget (§0.g) — the `force:true` bypass carries its OWN bounded budget:
|
|
10
|
+
* `maxForcedManualSwapsPerWindow`. Exhaustion refuses further FORCED
|
|
11
|
+
* manual swaps until the window rolls — `force` is not the one uncapped
|
|
12
|
+
* bypass left in the spec. A non-forced swap is never blocked by it.
|
|
13
|
+
*
|
|
14
|
+
* Every guard here makes "no safe action available" a SURFACED refusal (a named reason returned to
|
|
15
|
+
* the caller), never a silent drop and never an unbounded retry (§0.g(c)).
|
|
16
|
+
*/
|
|
17
|
+
export interface CredentialManualLeversDeps {
|
|
18
|
+
/** Per-pair cooldown ms (≥1× poll interval). */
|
|
19
|
+
pairCooldownMs?: number;
|
|
20
|
+
/** §0.g force budget: max FORCED manual swaps per window. */
|
|
21
|
+
maxForcedPerWindow?: number;
|
|
22
|
+
/** §0.g force budget window length ms. */
|
|
23
|
+
forcedWindowMs?: number;
|
|
24
|
+
now?: () => number;
|
|
25
|
+
}
|
|
26
|
+
/** A cooldown/budget verdict — allow, or a named refusal (surfaced, never silent). */
|
|
27
|
+
export type LeverVerdict = {
|
|
28
|
+
allowed: true;
|
|
29
|
+
forced: boolean;
|
|
30
|
+
} | {
|
|
31
|
+
allowed: false;
|
|
32
|
+
reason: string;
|
|
33
|
+
code: 'pair-cooldown' | 'force-budget-exhausted';
|
|
34
|
+
};
|
|
35
|
+
export declare class CredentialManualLevers {
|
|
36
|
+
private readonly pairCooldownMs;
|
|
37
|
+
private readonly maxForcedPerWindow;
|
|
38
|
+
private readonly forcedWindowMs;
|
|
39
|
+
private readonly now;
|
|
40
|
+
/** pairKey → last actuation epoch ms. */
|
|
41
|
+
private readonly lastActuation;
|
|
42
|
+
/** Forced-swap timestamps within the rolling window. */
|
|
43
|
+
private forcedTimestamps;
|
|
44
|
+
constructor(deps?: CredentialManualLeversDeps);
|
|
45
|
+
/**
|
|
46
|
+
* Evaluate whether a manual swap of (slotA, slotB) may proceed. `force` overrides the per-pair
|
|
47
|
+
* cooldown BUT is itself bounded by the force budget. Read-only — call `recordSwap` after a
|
|
48
|
+
* swap actually commits so the cooldown/budget advance only on real actuation.
|
|
49
|
+
*/
|
|
50
|
+
evaluateSwap(slotA: string, slotB: string, force: boolean): LeverVerdict;
|
|
51
|
+
/** Record an actually-committed swap: advances the pair cooldown and (if forced) the budget. */
|
|
52
|
+
recordSwap(slotA: string, slotB: string, forced: boolean): void;
|
|
53
|
+
/** Forced swaps remaining in the current window (for observability). */
|
|
54
|
+
forcedBudgetRemaining(): number;
|
|
55
|
+
private pruneForcedWindow;
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=CredentialManualLevers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CredentialManualLevers.d.ts","sourceRoot":"","sources":["../../src/core/CredentialManualLevers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAMH,MAAM,WAAW,0BAA0B;IACzC,gDAAgD;IAChD,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,6DAA6D;IAC7D,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,0CAA0C;IAC1C,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAED,sFAAsF;AACtF,MAAM,MAAM,YAAY,GACpB;IAAE,OAAO,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,OAAO,CAAA;CAAE,GAClC;IAAE,OAAO,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,eAAe,GAAG,wBAAwB,CAAA;CAAE,CAAC;AAOzF,qBAAa,sBAAsB;IACjC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAS;IACxC,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAS;IAC5C,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAS;IACxC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;IAEnC,yCAAyC;IACzC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAA6B;IAC3D,wDAAwD;IACxD,OAAO,CAAC,gBAAgB,CAAgB;gBAE5B,IAAI,GAAE,0BAA+B;IAOjD;;;;OAIG;IACH,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,YAAY;IA8BxE,gGAAgG;IAChG,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,GAAG,IAAI;IAS/D,wEAAwE;IACxE,qBAAqB,IAAI,MAAM;IAK/B,OAAO,CAAC,iBAAiB;CAI1B"}
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CredentialManualLevers.js","sourceRoot":"","sources":["../../src/core/CredentialManualLevers.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CredentialRestoreEnrollment — the teardown lever (spec §2.8, build-plan §7).
|
|
3
|
+
*
|
|
4
|
+
* `POST /credentials/restore-enrollment` moves every slot back to its enrollment layout (N
|
|
5
|
+
* ordinary §2.3 swaps back), then the operator darks the feature — at which point ledger ==
|
|
6
|
+
* enrollment and raw `configHome` reads are truthful again (the ordered-rollback the spec names).
|
|
7
|
+
*
|
|
8
|
+
* This module owns the ONE piece the plain swap executor does not: the per-slot DECISION of
|
|
9
|
+
* whether a slot's current blob may be EXCHANGED back into a healthy enrollment slot, or must be
|
|
10
|
+
* PARKED ONE-DIRECTIONALLY (moved out / the slot vacated for operator re-auth, NEVER exchanged in).
|
|
11
|
+
*
|
|
12
|
+
* ── Why a quarantine bypass that retains everything else (spec §0.g + §2.8) ──
|
|
13
|
+
* restore-enrollment is a TEARDOWN, not a balancing move, so it carries a QUARANTINE BYPASS — it
|
|
14
|
+
* must operate ON quarantined slots (the degraded state rollback exists for), or §2.3 step-1 would
|
|
15
|
+
* hard-block it exactly when it is needed. Per §0.g a bypass drops ONLY the named guard: it RETAINS
|
|
16
|
+
* 1. parse (an unparseable blob is parked, never exchanged)
|
|
17
|
+
* 2. refresh-token-present (a refresh-token-less blob is parked, never exchanged)
|
|
18
|
+
* 3. IDENTITY-COHERENCE (access-tenant == refresh-lineage; the §2.7 Frankenstein gap)
|
|
19
|
+
*
|
|
20
|
+
* ── The Frankenstein gap (spec §2.8 round-4) ──
|
|
21
|
+
* "Parses + has a refresh token" is NOT sufficient. A legacy `AccountSwitcher` slip can leave
|
|
22
|
+
* B's access token grafted onto A's PRESERVED refresh token: the blob parses, carries a refresh
|
|
23
|
+
* token, and the oracle (on B's still-valid access token) resolves it to B — yet on first refresh
|
|
24
|
+
* the client exchanges A's refresh token and silently resurrects A (a §0.d violation). So before
|
|
25
|
+
* any exchange, the blob's access-token identity (oracle) MUST equal its refresh-token lineage's
|
|
26
|
+
* expected account. A blob that fails coherence — Frankenstein, revoked-grant, access/refresh
|
|
27
|
+
* tenant mismatch — is parked ONE-DIRECTIONALLY exactly like an unparseable one, NEVER exchanged
|
|
28
|
+
* into a healthy slot (a teardown that exchanges garbage into a good slot, then post-commit-verify
|
|
29
|
+
* quarantines THAT slot, would spread corruption during the exact degraded state rollback is for).
|
|
30
|
+
*
|
|
31
|
+
* This module is the DECISION (coherence classification + the park plan). It performs NO keychain
|
|
32
|
+
* writes itself — the route drives the actual exchange through the §2.3 CredentialSwapExecutor and
|
|
33
|
+
* the actual park through the ledger quarantine + an attention item. Pure + injectable.
|
|
34
|
+
*/
|
|
35
|
+
import type { ClaudeOauth } from './OAuthRefresher.js';
|
|
36
|
+
/** The expected refresh-lineage tenant of a slot's blob, and the oracle's access-token identity. */
|
|
37
|
+
export interface CoherenceProbe {
|
|
38
|
+
/**
|
|
39
|
+
* The account the blob's ACCESS token resolves to via the identity oracle, or null when the
|
|
40
|
+
* oracle could not confirm it (timeout/401/5xx/etc — §2.11 unavailable, never a guess).
|
|
41
|
+
*/
|
|
42
|
+
accessTenant: string | null;
|
|
43
|
+
/**
|
|
44
|
+
* The account the blob's REFRESH-token lineage is EXPECTED to belong to. When the oracle is
|
|
45
|
+
* reachable this is derived from the recorded provenance; when it is down the spec's named cheap
|
|
46
|
+
* proxy applies: the ledger's expected tenant for the slot. null = no expectation available.
|
|
47
|
+
*/
|
|
48
|
+
refreshLineage: string | null;
|
|
49
|
+
}
|
|
50
|
+
export type CoherenceVerdict = {
|
|
51
|
+
coherent: true;
|
|
52
|
+
tenant: string;
|
|
53
|
+
} | {
|
|
54
|
+
coherent: false;
|
|
55
|
+
reason: string;
|
|
56
|
+
park: 'one-directional';
|
|
57
|
+
};
|
|
58
|
+
/** A parsed blob to classify (the raw is round-tripped by the caller; we only read fields). */
|
|
59
|
+
export interface RestoreBlob {
|
|
60
|
+
raw: string;
|
|
61
|
+
oauth: ClaudeOauth | null;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Classify a slot's current blob for restore-enrollment. Returns a COHERENT verdict (safe to
|
|
65
|
+
* EXCHANGE back to its enrollment slot) ONLY when ALL THREE retained preconditions hold:
|
|
66
|
+
* parse + refresh-token-present + identity-coherence (accessTenant === refreshLineage).
|
|
67
|
+
* Any failure → an INCOHERENT verdict with `park: 'one-directional'` and a named reason — the
|
|
68
|
+
* caller must vacate the slot for operator re-auth and NEVER exchange this blob into a healthy slot.
|
|
69
|
+
*
|
|
70
|
+
* The oracle-down cheap proxy is the caller's job: it supplies `refreshLineage` from the ledger's
|
|
71
|
+
* expected tenant for the slot, and `accessTenant` may be null (oracle unavailable). When BOTH are
|
|
72
|
+
* known and equal → coherent; when the oracle is down (accessTenant null) we CANNOT certify
|
|
73
|
+
* coherence → one-directional park (the safe direction: never exchange an unverifiable blob).
|
|
74
|
+
*/
|
|
75
|
+
export declare function classifyRestoreCoherence(blob: RestoreBlob, probe: CoherenceProbe): CoherenceVerdict;
|
|
76
|
+
//# sourceMappingURL=CredentialRestoreEnrollment.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CredentialRestoreEnrollment.d.ts","sourceRoot":"","sources":["../../src/core/CredentialRestoreEnrollment.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAEvD,oGAAoG;AACpG,MAAM,WAAW,cAAc;IAC7B;;;OAGG;IACH,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B;;;;OAIG;IACH,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/B;AAED,MAAM,MAAM,gBAAgB,GACxB;IAAE,QAAQ,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAClC;IAAE,QAAQ,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,iBAAiB,CAAA;CAAE,CAAC;AAEjE,+FAA+F;AAC/F,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,WAAW,GAAG,IAAI,CAAC;CAC3B;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,WAAW,EAAE,KAAK,EAAE,cAAc,GAAG,gBAAgB,CA4BnG"}
|