instar 1.3.610 → 1.3.612

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.
@@ -0,0 +1,178 @@
1
+ /**
2
+ * WS5.2 R12 — Revocation / de-authorization data-plane executor (Mechanism B, the DEFAULT).
3
+ *
4
+ * "Stop following to machine X" is TWO things: a control-plane mandate revocation (PIN-gated on the
5
+ * Mandates tab; the agent CANNOT revoke — that authority stays with MandateGate) PLUS a real
6
+ * data-plane effect. This module is the data-plane half: it CONSUMES a revocation signal (the
7
+ * control-plane revoke already happened) and computes the honest data-plane outcome. It NEVER
8
+ * self-revokes the mandate and is NOT a new blocking authority — it is a deterministic planner +
9
+ * honest-state surface over INJECTED side-effect deps (logout, slot delete, SubscriptionPool.remove,
10
+ * attention emit, pending store). No direct I/O in the logic; every effect is injected so each is
11
+ * independently unit-testable (mirrors AccountFollowMeService / Grants / Orchestrator style).
12
+ *
13
+ * R12 has three honesty-load-bearing branches:
14
+ *
15
+ * (i) COOPERATIVE, still-paired target ONLINE (R12.i): the data-plane effect is local & total —
16
+ * the target logs the account OUT of its config-home, deletes the per-account slot, and
17
+ * SubscriptionPool.remove(accountId) fires on the target. End state: `removed`.
18
+ *
19
+ * (ii) DE-PAIRED / HOSTILE / non-cooperative holder (S8 / R12.i corrected): a B-minted credential
20
+ * is an independently-refreshable real OAuth login; instar CANNOT force the logout remotely.
21
+ * The HONEST revocation path is provider-side de-authorization. We surface a phone-first
22
+ * instruction ("machine X still holds its own live login for account Y — de-authorize/rotate
23
+ * at <provider> to kill it"), NEVER a false "removed everywhere". End state:
24
+ * `provider-rotation-required`. (When a machine de-pairs, MachineStatus flips to 'revoked' —
25
+ * that flip is the caller's; this module READS that status and refuses to claim a wipe.)
26
+ *
27
+ * (iii) OFFLINE-honest pending, BOUNDED (R12.iii): a still-paired target that is OFFLINE at revoke
28
+ * time gets its control-plane revoke immediately, but the data-plane wipe becomes a DURABLE
29
+ * pending action fired when it reconnects ("write it down durably, fire on return", the
30
+ * cross-machine-secret-sync pattern). Dashboard shows `revocation-pending`, NEVER `removed`.
31
+ * The pending wipe is NOT unbounded: after a fixed, operator-tunable reconnect-deadline it
32
+ * ESCALATES to a LOUD `revocation-FAILED — rotate at provider NOW` aggregated HIGH attention
33
+ * item (resume-queue give-up discipline). Honest end state of an offline-forever / terminated
34
+ * VM is "rotate at provider", never a silently-aging "pending".
35
+ *
36
+ * FAIL CLOSED on uncertainty: if we cannot positively confirm the cooperative-online wipe path
37
+ * (unknown reachability, revoked status, the wipe effect threw), we NEVER report `removed` — we fall
38
+ * to pending or provider-rotation-required. Deny-by-default for the optimistic "removed" claim.
39
+ *
40
+ * Mechanism A (sealed-transport) is dark/refused-for-Anthropic; per R9 it is out of scope here
41
+ * beyond the minimal `mechanism` discriminator — a Mechanism-A revoke ALSO requires the wipe verb +
42
+ * an unconditional provider-rotation prompt (a delivered credential cannot be un-delivered), so this
43
+ * module flags `providerRotationRequired: true` for any Mechanism-A revoke regardless of branch.
44
+ */
45
+ export type RevocationMechanism = 're-mint' | 'credential-transport';
46
+ /** Target reachability/cooperation posture at revoke time — drives the R12 branch. */
47
+ export type TargetPosture = 'cooperative-online' | 'offline' | 'revoked';
48
+ /** Honest end-state of a revocation's DATA plane (never conflate with the control-plane revoke). */
49
+ export type RevocationDataState = 'removed' | 'revocation-pending' | 'revocation-failed' | 'provider-rotation-required';
50
+ export interface RevocationRequest {
51
+ accountId: string;
52
+ /** Operator-facing account email (shown in honest operator messaging). */
53
+ accountEmail: string;
54
+ targetMachineId: string;
55
+ /** Operator-facing nickname of the target machine (shown in honest operator messaging). */
56
+ targetMachineNickname: string;
57
+ /** Provider name for the provider-side de-authorization instruction (e.g. 'Anthropic'). */
58
+ provider: string;
59
+ /** The mandate id whose control-plane revoke triggered this data-plane effect (audit). */
60
+ mandateId: string;
61
+ /** Default 're-mint' (Mechanism B). 'credential-transport' ⇒ unconditional provider rotation. */
62
+ mechanism?: RevocationMechanism;
63
+ }
64
+ /** A phone-first provider-side de-authorization instruction — a real instruction, NOT a "handled" claim. */
65
+ export interface ProviderRotationInstruction {
66
+ kind: 'provider-rotation-required';
67
+ accountId: string;
68
+ accountEmail: string;
69
+ targetMachineId: string;
70
+ targetMachineNickname: string;
71
+ provider: string;
72
+ /** Plain-English, honest message surfaced to the operator (never implies instar removed it). */
73
+ message: string;
74
+ }
75
+ /** A LOUD aggregated attention item for an offline target that gave up (R12.iii give-up). */
76
+ export interface RevocationFailedAttention {
77
+ /** Stable id so the item dedups per (account,target) — one running item, not a flood (P17). */
78
+ id: string;
79
+ title: string;
80
+ body: string;
81
+ priority: 'high';
82
+ source: 'agent';
83
+ }
84
+ /** A durable pending-wipe record — fired on the target's reconnect; bounded by the deadline. */
85
+ export interface PendingWipeRecord {
86
+ accountId: string;
87
+ targetMachineId: string;
88
+ mandateId: string;
89
+ provider: string;
90
+ accountEmail: string;
91
+ targetMachineNickname: string;
92
+ mechanism: RevocationMechanism;
93
+ /** When the control-plane revoke fired — the deadline clock starts here. */
94
+ revokedAt: number;
95
+ /** Absolute deadline; after this a still-pending wipe escalates to revocation-failed. */
96
+ deadlineAt: number;
97
+ }
98
+ /** Durable persistence seam for pending wipes (production: JSON/SQLite; tests: in-memory). */
99
+ export interface PendingWipeStore {
100
+ /** Upsert keyed on `${accountId}::${targetMachineId}`. */
101
+ put(record: PendingWipeRecord): void;
102
+ /** Resolve (the wipe fired) — remove the pending record. */
103
+ remove(accountId: string, targetMachineId: string): void;
104
+ get(accountId: string, targetMachineId: string): PendingWipeRecord | undefined;
105
+ all(): PendingWipeRecord[];
106
+ }
107
+ export declare function inMemoryPendingWipeStore(): PendingWipeStore;
108
+ /** Outcome of an attempted cooperative-online data-plane wipe (each step injected). */
109
+ export interface CooperativeWipeResult {
110
+ loggedOut: boolean;
111
+ slotDeleted: boolean;
112
+ poolRemoved: boolean;
113
+ }
114
+ export interface AccountFollowMeRevocationDeps {
115
+ /** Master gate: the whole feature is dark unless this is true (single-machine / flag-off ⇒ no-op). */
116
+ enabled: () => boolean;
117
+ /**
118
+ * Execute the cooperative data-plane wipe ON the (online, still-paired) target: framework logout
119
+ * against that CLAUDE_CONFIG_DIR + delete the per-account slot + SubscriptionPool.remove(accountId).
120
+ * MUST throw or return all-false on any failure — we then fail closed (never claim `removed`).
121
+ */
122
+ cooperativeWipe: (req: RevocationRequest) => CooperativeWipeResult;
123
+ /** Durable pending-wipe ledger (offline path). */
124
+ pendingStore: PendingWipeStore;
125
+ /** Raise ONE aggregated LOUD attention item (offline give-up). */
126
+ emitRevocationFailed: (item: RevocationFailedAttention) => void;
127
+ /** Reconnect-deadline before an offline pending wipe escalates to revocation-failed (ms). */
128
+ reconnectDeadlineMs: () => number;
129
+ now?: () => number;
130
+ log?: (msg: string) => void;
131
+ }
132
+ export type RevocationOutcome = {
133
+ state: RevocationDataState;
134
+ accountId: string;
135
+ targetMachineId: string;
136
+ /** True whenever provider-side rotation is the (only) complete revocation — caller surfaces it. */
137
+ providerRotationRequired: boolean;
138
+ /** Present for the de-paired/hostile and Mechanism-A paths — a real phone-first instruction. */
139
+ providerRotation?: ProviderRotationInstruction;
140
+ /** Present on the cooperative-online success path. */
141
+ wipe?: CooperativeWipeResult;
142
+ reason: string;
143
+ };
144
+ export declare class AccountFollowMeRevocation {
145
+ private readonly deps;
146
+ private readonly now;
147
+ constructor(deps: AccountFollowMeRevocationDeps);
148
+ private rotationInstruction;
149
+ /**
150
+ * Revoke account access on `targetMachineId` given the control-plane revoke already fired. The
151
+ * `posture` (computed by the caller from authoritative registry state — online flag, MachineStatus)
152
+ * selects the R12 branch. Returns the HONEST data-plane outcome; never claims `removed` it can't
153
+ * confirm. A no-op (`provider-rotation-required` with a no-op message) when the feature is dark.
154
+ */
155
+ revoke(req: RevocationRequest, posture: TargetPosture): RevocationOutcome;
156
+ /**
157
+ * A previously-offline target reconnected — fire its durable pending wipe (R12.iii). Drives the
158
+ * SAME cooperative wipe as the online path; on success the pending record is cleared and we report
159
+ * `removed`. On failure/partial we keep the record (it may still escalate at the deadline).
160
+ * A no-op if no pending record exists for (account, target) or the feature is dark.
161
+ */
162
+ onTargetReconnect(accountId: string, targetMachineId: string): RevocationOutcome | null;
163
+ /**
164
+ * Sweep durable pending wipes whose reconnect-deadline has passed and escalate each to a LOUD
165
+ * `revocation-FAILED — rotate at provider NOW` aggregated HIGH attention item (R12.iii give-up).
166
+ * Each escalated record is removed from the pending store (its honest end-state is now provider
167
+ * rotation, never a silently-aging "pending"). Returns the records that escalated. Caller runs
168
+ * this on a cadence (resume-queue drainer style). No-op when the feature is dark.
169
+ */
170
+ sweepDeadlines(): RevocationFailedAttention[];
171
+ /**
172
+ * Honest dashboard state for an (account, target) pair: a live pending record reports
173
+ * `revocation-pending` (or `revocation-failed` once past its deadline), else null (no pending
174
+ * revocation in flight). Read surface — never mutates.
175
+ */
176
+ pendingStateFor(accountId: string, targetMachineId: string): RevocationDataState | null;
177
+ }
178
+ //# sourceMappingURL=AccountFollowMeRevocation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AccountFollowMeRevocation.d.ts","sourceRoot":"","sources":["../../src/core/AccountFollowMeRevocation.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,279 @@
1
+ /**
2
+ * WS5.2 R12 — Revocation / de-authorization data-plane executor (Mechanism B, the DEFAULT).
3
+ *
4
+ * "Stop following to machine X" is TWO things: a control-plane mandate revocation (PIN-gated on the
5
+ * Mandates tab; the agent CANNOT revoke — that authority stays with MandateGate) PLUS a real
6
+ * data-plane effect. This module is the data-plane half: it CONSUMES a revocation signal (the
7
+ * control-plane revoke already happened) and computes the honest data-plane outcome. It NEVER
8
+ * self-revokes the mandate and is NOT a new blocking authority — it is a deterministic planner +
9
+ * honest-state surface over INJECTED side-effect deps (logout, slot delete, SubscriptionPool.remove,
10
+ * attention emit, pending store). No direct I/O in the logic; every effect is injected so each is
11
+ * independently unit-testable (mirrors AccountFollowMeService / Grants / Orchestrator style).
12
+ *
13
+ * R12 has three honesty-load-bearing branches:
14
+ *
15
+ * (i) COOPERATIVE, still-paired target ONLINE (R12.i): the data-plane effect is local & total —
16
+ * the target logs the account OUT of its config-home, deletes the per-account slot, and
17
+ * SubscriptionPool.remove(accountId) fires on the target. End state: `removed`.
18
+ *
19
+ * (ii) DE-PAIRED / HOSTILE / non-cooperative holder (S8 / R12.i corrected): a B-minted credential
20
+ * is an independently-refreshable real OAuth login; instar CANNOT force the logout remotely.
21
+ * The HONEST revocation path is provider-side de-authorization. We surface a phone-first
22
+ * instruction ("machine X still holds its own live login for account Y — de-authorize/rotate
23
+ * at <provider> to kill it"), NEVER a false "removed everywhere". End state:
24
+ * `provider-rotation-required`. (When a machine de-pairs, MachineStatus flips to 'revoked' —
25
+ * that flip is the caller's; this module READS that status and refuses to claim a wipe.)
26
+ *
27
+ * (iii) OFFLINE-honest pending, BOUNDED (R12.iii): a still-paired target that is OFFLINE at revoke
28
+ * time gets its control-plane revoke immediately, but the data-plane wipe becomes a DURABLE
29
+ * pending action fired when it reconnects ("write it down durably, fire on return", the
30
+ * cross-machine-secret-sync pattern). Dashboard shows `revocation-pending`, NEVER `removed`.
31
+ * The pending wipe is NOT unbounded: after a fixed, operator-tunable reconnect-deadline it
32
+ * ESCALATES to a LOUD `revocation-FAILED — rotate at provider NOW` aggregated HIGH attention
33
+ * item (resume-queue give-up discipline). Honest end state of an offline-forever / terminated
34
+ * VM is "rotate at provider", never a silently-aging "pending".
35
+ *
36
+ * FAIL CLOSED on uncertainty: if we cannot positively confirm the cooperative-online wipe path
37
+ * (unknown reachability, revoked status, the wipe effect threw), we NEVER report `removed` — we fall
38
+ * to pending or provider-rotation-required. Deny-by-default for the optimistic "removed" claim.
39
+ *
40
+ * Mechanism A (sealed-transport) is dark/refused-for-Anthropic; per R9 it is out of scope here
41
+ * beyond the minimal `mechanism` discriminator — a Mechanism-A revoke ALSO requires the wipe verb +
42
+ * an unconditional provider-rotation prompt (a delivered credential cannot be un-delivered), so this
43
+ * module flags `providerRotationRequired: true` for any Mechanism-A revoke regardless of branch.
44
+ */
45
+ export function inMemoryPendingWipeStore() {
46
+ const map = new Map();
47
+ const key = (a, t) => `${a}::${t}`;
48
+ return {
49
+ put: (r) => { map.set(key(r.accountId, r.targetMachineId), r); },
50
+ remove: (a, t) => { map.delete(key(a, t)); },
51
+ get: (a, t) => map.get(key(a, t)),
52
+ all: () => [...map.values()],
53
+ };
54
+ }
55
+ export class AccountFollowMeRevocation {
56
+ deps;
57
+ now;
58
+ constructor(deps) {
59
+ this.deps = deps;
60
+ this.now = deps.now ?? Date.now;
61
+ }
62
+ rotationInstruction(req, lead) {
63
+ return {
64
+ kind: 'provider-rotation-required',
65
+ accountId: req.accountId,
66
+ accountEmail: req.accountEmail,
67
+ targetMachineId: req.targetMachineId,
68
+ targetMachineNickname: req.targetMachineNickname,
69
+ provider: req.provider,
70
+ message: `${lead} "${req.targetMachineNickname}" still holds its own live, refreshable login for ` +
71
+ `${req.accountEmail}. instar cannot revoke it remotely — de-authorize that session/device ` +
72
+ `or rotate the credential at ${req.provider} to kill it.`,
73
+ };
74
+ }
75
+ /**
76
+ * Revoke account access on `targetMachineId` given the control-plane revoke already fired. The
77
+ * `posture` (computed by the caller from authoritative registry state — online flag, MachineStatus)
78
+ * selects the R12 branch. Returns the HONEST data-plane outcome; never claims `removed` it can't
79
+ * confirm. A no-op (`provider-rotation-required` with a no-op message) when the feature is dark.
80
+ */
81
+ revoke(req, posture) {
82
+ const mechanism = req.mechanism ?? 're-mint';
83
+ // Dark / single-machine ⇒ strict no-op (deny-by-default; never act).
84
+ if (!this.deps.enabled()) {
85
+ return {
86
+ state: 'provider-rotation-required',
87
+ accountId: req.accountId,
88
+ targetMachineId: req.targetMachineId,
89
+ providerRotationRequired: false,
90
+ reason: 'feature-disabled',
91
+ };
92
+ }
93
+ // Mechanism A relocated a real credential — provider rotation is ALWAYS required regardless of
94
+ // branch (a delivered credential cannot be un-delivered, R12.ii). We still attempt the wipe for
95
+ // a cooperative-online A target, but the rotation instruction is unconditional.
96
+ const mechARotation = mechanism === 'credential-transport';
97
+ // (ii) De-paired / hostile / MachineStatus==='revoked' — provider-side ONLY (S8/R12.i corrected).
98
+ if (posture === 'revoked') {
99
+ const rotation = this.rotationInstruction(req, 'Machine');
100
+ this.deps.log?.(`[account-follow-me] revoke ${req.accountId}→${req.targetMachineId}: de-paired/hostile — ` +
101
+ `provider-side de-authorization required (no remote wipe possible)`);
102
+ return {
103
+ state: 'provider-rotation-required',
104
+ accountId: req.accountId,
105
+ targetMachineId: req.targetMachineId,
106
+ providerRotationRequired: true,
107
+ providerRotation: rotation,
108
+ reason: 'de-paired-non-cooperative',
109
+ };
110
+ }
111
+ // (iii) Offline still-paired target — durable pending wipe, fired on reconnect (BOUNDED).
112
+ if (posture === 'offline') {
113
+ const t = this.now();
114
+ const record = {
115
+ accountId: req.accountId,
116
+ targetMachineId: req.targetMachineId,
117
+ mandateId: req.mandateId,
118
+ provider: req.provider,
119
+ accountEmail: req.accountEmail,
120
+ targetMachineNickname: req.targetMachineNickname,
121
+ mechanism,
122
+ revokedAt: t,
123
+ deadlineAt: t + this.deps.reconnectDeadlineMs(),
124
+ };
125
+ this.deps.pendingStore.put(record);
126
+ this.deps.log?.(`[account-follow-me] revoke ${req.accountId}→${req.targetMachineId}: target offline — ` +
127
+ `durable pending wipe (deadline +${this.deps.reconnectDeadlineMs()}ms)`);
128
+ return {
129
+ state: 'revocation-pending',
130
+ accountId: req.accountId,
131
+ targetMachineId: req.targetMachineId,
132
+ // For Mechanism A, rotate at provider NOW even while pending (the blob already landed).
133
+ providerRotationRequired: mechARotation,
134
+ providerRotation: mechARotation
135
+ ? this.rotationInstruction(req, 'Offline machine')
136
+ : undefined,
137
+ reason: 'target-offline-pending',
138
+ };
139
+ }
140
+ // (i) Cooperative, still-paired, ONLINE target — local & total wipe. Fail closed on any error.
141
+ let wipe;
142
+ try {
143
+ wipe = this.deps.cooperativeWipe(req);
144
+ }
145
+ catch (err) {
146
+ // The wipe threw — we CANNOT claim removed. Fail closed to a durable pending retry.
147
+ const t = this.now();
148
+ this.deps.pendingStore.put({
149
+ accountId: req.accountId,
150
+ targetMachineId: req.targetMachineId,
151
+ mandateId: req.mandateId,
152
+ provider: req.provider,
153
+ accountEmail: req.accountEmail,
154
+ targetMachineNickname: req.targetMachineNickname,
155
+ mechanism,
156
+ revokedAt: t,
157
+ deadlineAt: t + this.deps.reconnectDeadlineMs(),
158
+ });
159
+ this.deps.log?.(`[account-follow-me] revoke ${req.accountId}→${req.targetMachineId}: cooperative wipe THREW ` +
160
+ `(${err instanceof Error ? err.message : String(err)}) — fail-closed to pending`);
161
+ return {
162
+ state: 'revocation-pending',
163
+ accountId: req.accountId,
164
+ targetMachineId: req.targetMachineId,
165
+ providerRotationRequired: mechARotation,
166
+ providerRotation: mechARotation ? this.rotationInstruction(req, 'Machine') : undefined,
167
+ reason: 'cooperative-wipe-error',
168
+ };
169
+ }
170
+ const fullyWiped = wipe.loggedOut && wipe.slotDeleted && wipe.poolRemoved;
171
+ if (!fullyWiped) {
172
+ // Partial wipe — fail closed: do NOT claim removed; keep a durable pending so it retries.
173
+ const t = this.now();
174
+ this.deps.pendingStore.put({
175
+ accountId: req.accountId,
176
+ targetMachineId: req.targetMachineId,
177
+ mandateId: req.mandateId,
178
+ provider: req.provider,
179
+ accountEmail: req.accountEmail,
180
+ targetMachineNickname: req.targetMachineNickname,
181
+ mechanism,
182
+ revokedAt: t,
183
+ deadlineAt: t + this.deps.reconnectDeadlineMs(),
184
+ });
185
+ this.deps.log?.(`[account-follow-me] revoke ${req.accountId}→${req.targetMachineId}: partial wipe ` +
186
+ `(logout=${wipe.loggedOut} slot=${wipe.slotDeleted} pool=${wipe.poolRemoved}) — pending`);
187
+ return {
188
+ state: 'revocation-pending',
189
+ accountId: req.accountId,
190
+ targetMachineId: req.targetMachineId,
191
+ providerRotationRequired: mechARotation,
192
+ providerRotation: mechARotation ? this.rotationInstruction(req, 'Machine') : undefined,
193
+ wipe,
194
+ reason: 'cooperative-wipe-partial',
195
+ };
196
+ }
197
+ // Confirmed local & total. The pending record (if any earlier offline/error attempt) is cleared.
198
+ this.deps.pendingStore.remove(req.accountId, req.targetMachineId);
199
+ this.deps.log?.(`[account-follow-me] revoke ${req.accountId}→${req.targetMachineId}: removed (logout + slot + pool)`);
200
+ return {
201
+ state: mechARotation ? 'provider-rotation-required' : 'removed',
202
+ accountId: req.accountId,
203
+ targetMachineId: req.targetMachineId,
204
+ // Even a clean Mechanism-A wipe still requires provider rotation (blob was delivered).
205
+ providerRotationRequired: mechARotation,
206
+ providerRotation: mechARotation ? this.rotationInstruction(req, 'Machine') : undefined,
207
+ wipe,
208
+ reason: mechARotation ? 'wiped-but-mechanism-a-rotate' : 'wiped-local-and-total',
209
+ };
210
+ }
211
+ /**
212
+ * A previously-offline target reconnected — fire its durable pending wipe (R12.iii). Drives the
213
+ * SAME cooperative wipe as the online path; on success the pending record is cleared and we report
214
+ * `removed`. On failure/partial we keep the record (it may still escalate at the deadline).
215
+ * A no-op if no pending record exists for (account, target) or the feature is dark.
216
+ */
217
+ onTargetReconnect(accountId, targetMachineId) {
218
+ if (!this.deps.enabled())
219
+ return null;
220
+ const record = this.deps.pendingStore.get(accountId, targetMachineId);
221
+ if (!record)
222
+ return null;
223
+ const req = {
224
+ accountId: record.accountId,
225
+ accountEmail: record.accountEmail,
226
+ targetMachineId: record.targetMachineId,
227
+ targetMachineNickname: record.targetMachineNickname,
228
+ provider: record.provider,
229
+ mandateId: record.mandateId,
230
+ mechanism: record.mechanism,
231
+ };
232
+ // Reconnected ⇒ treat as cooperative-online and run the same wipe (revoke clears the record on success).
233
+ return this.revoke(req, 'cooperative-online');
234
+ }
235
+ /**
236
+ * Sweep durable pending wipes whose reconnect-deadline has passed and escalate each to a LOUD
237
+ * `revocation-FAILED — rotate at provider NOW` aggregated HIGH attention item (R12.iii give-up).
238
+ * Each escalated record is removed from the pending store (its honest end-state is now provider
239
+ * rotation, never a silently-aging "pending"). Returns the records that escalated. Caller runs
240
+ * this on a cadence (resume-queue drainer style). No-op when the feature is dark.
241
+ */
242
+ sweepDeadlines() {
243
+ if (!this.deps.enabled())
244
+ return [];
245
+ const t = this.now();
246
+ const escalated = [];
247
+ for (const record of this.deps.pendingStore.all()) {
248
+ if (record.deadlineAt > t)
249
+ continue; // still within the deadline — stays pending.
250
+ const item = {
251
+ id: `agent:account-follow-me-revoke-failed:${record.accountId}::${record.targetMachineId}`,
252
+ title: `Revocation FAILED on "${record.targetMachineNickname}" — rotate at ${record.provider} NOW`,
253
+ body: `I revoked access for ${record.accountEmail} on "${record.targetMachineNickname}", but that ` +
254
+ `machine never reconnected to confirm its copy was destroyed. Its login may still be live. ` +
255
+ `Rotate or de-authorize the credential at ${record.provider} now to be certain.`,
256
+ priority: 'high',
257
+ source: 'agent',
258
+ };
259
+ this.deps.emitRevocationFailed(item);
260
+ this.deps.pendingStore.remove(record.accountId, record.targetMachineId);
261
+ this.deps.log?.(`[account-follow-me] revoke ${record.accountId}→${record.targetMachineId}: pending wipe ` +
262
+ `EXCEEDED deadline — escalated to revocation-FAILED (rotate at ${record.provider})`);
263
+ escalated.push(item);
264
+ }
265
+ return escalated;
266
+ }
267
+ /**
268
+ * Honest dashboard state for an (account, target) pair: a live pending record reports
269
+ * `revocation-pending` (or `revocation-failed` once past its deadline), else null (no pending
270
+ * revocation in flight). Read surface — never mutates.
271
+ */
272
+ pendingStateFor(accountId, targetMachineId) {
273
+ const record = this.deps.pendingStore.get(accountId, targetMachineId);
274
+ if (!record)
275
+ return null;
276
+ return record.deadlineAt > this.now() ? 'revocation-pending' : 'revocation-failed';
277
+ }
278
+ }
279
+ //# sourceMappingURL=AccountFollowMeRevocation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AccountFollowMeRevocation.js","sourceRoot":"","sources":["../../src/core/AccountFollowMeRevocation.ts"],"names":[],"mappings":"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"}
@@ -21,6 +21,7 @@
21
21
  */
22
22
  import { PendingLoginStore, type PendingLogin, type LoginFlowKind, type LoginProvider } from './PendingLoginStore.js';
23
23
  import { type EnsureInteractiveReadyResult } from './ensureInteractiveReady.js';
24
+ import type { IdentityOracle } from './CredentialLocationLedger.js';
24
25
  /** The public artifact a framework login yields (no secret). */
25
26
  export interface LoginArtifact {
26
27
  verificationUrl: string;
@@ -52,6 +53,26 @@ export interface EnrollmentWizardConfig {
52
53
  * Injectable for hermetic tests; production uses the real util.
53
54
  */
54
55
  ensureReady?: (configHome: string) => EnsureInteractiveReadyResult;
56
+ /**
57
+ * WS5.2 §5.3/S7 — identity oracle used by `completeFollowMe` to read the freshly-minted
58
+ * login's account email from its config-home slot, for validation against operator
59
+ * expectation. Absent ⇒ the follow-me gate fails CLOSED (the account is HELD, never
60
+ * auto-selected). The plain `complete()` path never touches it.
61
+ */
62
+ oracle?: IdentityOracle;
63
+ /**
64
+ * WS5.2 §5.3/S7 — emit a HIGH attention item when a follow-me completion is HELD
65
+ * (surprise/mismatched/unverifiable email). Best-effort; absence does not change the
66
+ * fail-closed verdict (the account is still NOT selected), only whether the operator
67
+ * is paged.
68
+ */
69
+ emitAttention?: (item: {
70
+ id: string;
71
+ title: string;
72
+ body: string;
73
+ priority: 'high';
74
+ source: 'agent';
75
+ }) => void;
55
76
  }
56
77
  export interface StartEnrollmentInput {
57
78
  id: string;
@@ -62,12 +83,17 @@ export interface StartEnrollmentInput {
62
83
  kind?: LoginFlowKind;
63
84
  /** The new account's CLAUDE_CONFIG_DIR — isolates the login to its own slot. */
64
85
  configHome?: string;
86
+ /** WS5.2 §5.3/S7 — the operator-expected account email (follow-me path only). Carried
87
+ * onto the pending login so `completeFollowMe` can validate the minted account. */
88
+ expectedEmail?: string;
65
89
  }
66
90
  export declare class EnrollmentWizard {
67
91
  private readonly store;
68
92
  private readonly driveLogin;
69
93
  private readonly logger;
70
94
  private readonly ensureReady;
95
+ private readonly oracle?;
96
+ private readonly emitAttention?;
71
97
  constructor(cfg: EnrollmentWizardConfig);
72
98
  /** Default flow kind per provider: Codex/OpenAI = device-code (its endorsed
73
99
  * flow); everyone else = url-code-paste (the phone-friendly Claude path). */
@@ -106,6 +132,30 @@ export declare class EnrollmentWizard {
106
132
  * logged, never blocks completion (the launch paths re-ensure defensively).
107
133
  */
108
134
  complete(id: string): PendingLogin | null;
135
+ /**
136
+ * WS5.2 §5.3 step 3 / S7 — the FOLLOW-ME completion path. Completes the pending login
137
+ * (reusing the sync `complete()` so claude-code interactive-readiness still runs), then
138
+ * validates the freshly-minted account's email against the operator's expectation BEFORE
139
+ * the account is allowed to become selectable. Only a verified match returns 'validated'
140
+ * (the caller — the route — then adds it to the SubscriptionPool); everything else FAILS
141
+ * CLOSED to 'held' (a HIGH attention item is emitted, the account is NOT added to the pool).
142
+ *
143
+ * Fail-closed by construction: a missing oracle, an unreadable/missing config-home, an
144
+ * unavailable identity probe, or a missing operator-expected email all resolve to 'held'
145
+ * — an account is NEVER auto-selected unless its email provably matches what the operator
146
+ * approved (this is the FOLLOW-ME path, so the email gate ALWAYS runs).
147
+ */
148
+ completeFollowMe(id: string, targetMachineNickname: string): Promise<{
149
+ outcome: 'validated';
150
+ login: PendingLogin;
151
+ email: string;
152
+ } | {
153
+ outcome: 'held';
154
+ login: PendingLogin;
155
+ reason: string;
156
+ } | {
157
+ outcome: 'not-found';
158
+ }>;
109
159
  /** The phone surface: still-valid logins awaiting approval. */
110
160
  pending(): PendingLogin[];
111
161
  }
@@ -1 +1 @@
1
- {"version":3,"file":"EnrollmentWizard.d.ts","sourceRoot":"","sources":["../../src/core/EnrollmentWizard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EACL,iBAAiB,EACjB,KAAK,YAAY,EACjB,KAAK,aAAa,EAClB,KAAK,aAAa,EACnB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAEL,KAAK,4BAA4B,EAClC,MAAM,6BAA6B,CAAC;AAErC,gEAAgE;AAChE,MAAM,WAAW,aAAa;IAC5B,eAAe,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,8EAA8E;AAC9E,MAAM,MAAM,WAAW,GAAG,CAAC,GAAG,EAAE;IAC9B,QAAQ,EAAE,aAAa,CAAC;IACxB,SAAS,EAAE,YAAY,CAAC,WAAW,CAAC,CAAC;IACrC,IAAI,EAAE,aAAa,CAAC;IACpB;6DACyD;IACzD,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,KAAK,OAAO,CAAC,aAAa,CAAC,CAAC;AAE7B,MAAM,WAAW,sBAAsB;IACrC,KAAK,EAAE,iBAAiB,CAAC;IACzB,UAAU,EAAE,WAAW,CAAC;IACxB,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE;QAAE,GAAG,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;QAAC,IAAI,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,IAAI,CAAA;KAAE,CAAC;IACjE;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,4BAA4B,CAAC;CACpE;AAED,MAAM,WAAW,oBAAoB;IACnC,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,aAAa,CAAC;IACxB,SAAS,EAAE,YAAY,CAAC,WAAW,CAAC,CAAC;IACrC,6EAA6E;IAC7E,IAAI,CAAC,EAAE,aAAa,CAAC;IACrB,gFAAgF;IAChF,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAoB;IAC1C,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAc;IACzC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA0D;IACjF,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAuD;gBAEvE,GAAG,EAAE,sBAAsB;IAOvC;kFAC8E;IAC9E,MAAM,CAAC,WAAW,CAAC,QAAQ,EAAE,aAAa,GAAG,aAAa;IAI1D;;;;;;;;;OASG;IACH,MAAM,CAAC,UAAU,CAAC,IAAI,EAAE,aAAa,GAAG,MAAM,GAAG,SAAS;IAW1D;;;;OAIG;IACG,KAAK,CAAC,KAAK,EAAE,oBAAoB,GAAG,OAAO,CAAC,YAAY,CAAC;IAwB/D;;;;;;OAMG;IACG,cAAc,IAAI,OAAO,CAAC,YAAY,EAAE,CAAC;IA4B/C;;;;;;;OAOG;IACH,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI;IAazC,+DAA+D;IAC/D,OAAO,IAAI,YAAY,EAAE;CAG1B"}
1
+ {"version":3,"file":"EnrollmentWizard.d.ts","sourceRoot":"","sources":["../../src/core/EnrollmentWizard.ts"],"names":[],"mappings":"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"}