instar 1.3.611 → 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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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "instar",
3
- "version": "1.3.611",
3
+ "version": "1.3.612",
4
4
  "description": "Coherence infrastructure for self-evolving AI agents — on the Claude Code or Codex subscription you already have.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "$schema": "./builtin-manifest.schema.json",
3
3
  "schemaVersion": 1,
4
- "generatedAt": "2026-06-17T11:52:44.525Z",
5
- "instarVersion": "1.3.611",
4
+ "generatedAt": "2026-06-17T12:01:22.612Z",
5
+ "instarVersion": "1.3.612",
6
6
  "entryCount": 201,
7
7
  "entries": {
8
8
  "hook:session-start": {
@@ -0,0 +1,28 @@
1
+ # Upgrade Guide — vNEXT
2
+
3
+ <!-- assembled-by: assemble-next-md -->
4
+ <!-- bump: patch -->
5
+
6
+ ## What Changed
7
+
8
+ WS5.2 Account Follow-Me, R12 — the honest revocation data-plane. Revoking "stop following to machine X" is a PIN-gated mandate revoke (control plane — the agent cannot do it) PLUS a real data-plane effect. This PR builds that data-plane effect as a pure, injectable executor (`src/core/AccountFollowMeRevocation.ts`) covering the three R12 branches honestly:
9
+
10
+ - **Cooperative, online target → `removed`.** The target logs the account out of its config-home, deletes the per-account slot, and `SubscriptionPool.remove(accountId)` fires. Local and total.
11
+ - **De-paired / hostile holder → `provider-rotation-required`.** A re-minted (Mechanism B) login is an independently-refreshable real OAuth credential; instar CANNOT force a logout on a machine that left the pair. The honest path is a phone-first instruction to de-authorize / rotate at the provider — never a false "removed everywhere".
12
+ - **Offline target → bounded `revocation-pending` → `revocation-failed`.** The wipe becomes a durable pending action fired on reconnect (`onTargetReconnect`). The dashboard shows `revocation-pending`, not `removed`. After an operator-tunable reconnect-deadline, `sweepDeadlines()` escalates it to a LOUD `revocation-FAILED — rotate at provider NOW` aggregated HIGH attention item — never a silently-aging "pending".
13
+
14
+ Fail-closed throughout: a partial or throwing cooperative wipe falls to `revocation-pending` (never a false `removed`); Mechanism A (dark / refused for Anthropic) always flags `provider-rotation-required` because a delivered credential cannot be un-delivered; flag-off / single-machine is a strict no-op. The module is pure/injectable — it touches no shared production file (the destructive effects — framework logout, slot delete, `SubscriptionPool.remove`) are injected seams.
15
+
16
+ ## What to Tell Your User
17
+
18
+ Nothing to do — this is internal multi-machine account-sharing groundwork, shipped off by default. It guarantees that when you stop sharing an account with one of your machines, the system tells you the TRUTH about what was removed: it never claims a credential was destroyed everywhere when a departed or offline machine still holds its own login — in that case it surfaces a clear "rotate at the provider" instruction instead. No user-facing surface in this release.
19
+
20
+ ## Summary of New Capabilities
21
+
22
+ Internal: an honest revocation data-plane for Account Follow-Me — cooperative-target total wipe, provider-rotation instruction for a de-paired/hostile holder, and a bounded offline `revocation-pending → revocation-FAILED` escalation. The system can never claim a credential was destroyed when it wasn't. Dark behind `multiMachine.accountFollowMe`; server-shell wiring (consuming the mesh revoke, scheduling the deadline sweep, dashboard render) is a tracked follow-on increment.
23
+
24
+ ## Evidence
25
+
26
+ - 15 unit tests (`tests/unit/account-followme-revocation.test.ts`): both sides of every boundary — cooperative-online↔revoked↔offline, within-deadline↔past-deadline, clean↔partial↔throwing wipe, Mechanism B↔A, dark↔enabled, reconnect-with-record↔no-record, selective multi-record sweep; an explicit assertion that the message never contains "removed" on non-removed paths. `npx tsc --noEmit` clean.
27
+ - Side-effects review + mandatory independent second-pass security review (concurred): verified `removed` is reachable only after a fully-successful cooperative wipe, all three branches selected without fall-through, the offline give-up is bounded and loud, Mechanism A never claims success without provider-rotation, signal-vs-authority compliant (data-plane executor, not a new blocking authority), injected seams only. Artifact: `upgrades/side-effects/ws52-account-follow-me-revocation.md`.
28
+ - Spec: `docs/specs/ws52-account-follow-me-security.md` R12 (converged, approved).
@@ -0,0 +1,122 @@
1
+ # Side-Effects Review — WS5.2 Account Follow-Me, PR4 (R12 revocation data-plane)
2
+
3
+ **Version / slug:** `ws52-account-follow-me-revocation`
4
+ **Date:** `2026-06-17`
5
+ **Author:** Echo (autonomous)
6
+ **Second-pass reviewer:** pending (high-risk: credentials, revocation honesty, cross-machine offline state)
7
+ **Spec:** `docs/specs/ws52-account-follow-me-security.md` — R12 (revocation), R9/S7/S8 (mechanism gating, departed-holder), R4b (de-pair key-rotation context)
8
+ **Status:** logic built + unit-tested (15 tests; tsc clean). This PR ships the PURE data-plane executor + honest-state surface; the server-shell wiring (consume the control-plane revoke signal, run the cooperative wipe with real deps, schedule `sweepDeadlines`, dashboard `revocation-pending`/`revocation-failed` render) is a follow-up wiring increment.
9
+
10
+ ## Summary of the change
11
+
12
+ PR4 builds the R12 revocation DATA plane for Mechanism B (the default). "Stop following to machine X" is two halves: a PIN-gated control-plane mandate revoke (MandateGate — out of scope, the agent cannot revoke) PLUS a real data-plane effect. This module CONSUMES the already-fired control-plane revoke and computes the HONEST data-plane outcome over three branches: (i) cooperative-online target → local & total wipe (logout + slot delete + `SubscriptionPool.remove`) → `removed`; (ii) de-paired/hostile/`MachineStatus==='revoked'` holder → no remote wipe is possible, surface a phone-first provider-side de-authorization instruction → `provider-rotation-required`, NEVER a false "removed"; (iii) offline still-paired target → durable pending wipe fired on reconnect, bounded by an operator-tunable reconnect-deadline that ESCALATES to a LOUD `revocation-FAILED — rotate at provider NOW` aggregated HIGH attention item after expiry → `revocation-pending` then `revocation-failed`. Mechanism A (dark/refused-for-Anthropic) is handled minimally per R9: any A revoke flags `providerRotationRequired: true` unconditionally (a delivered credential cannot be un-delivered).
13
+
14
+ Files added (logic only, no I/O, dark behind `multiMachine.accountFollowMe`):
15
+ - `src/core/AccountFollowMeRevocation.ts` — the executor + honest-state surface (pure, injectable, fail-closed).
16
+ - `tests/unit/account-followme-revocation.test.ts` — 15 unit tests, both sides of every boundary.
17
+
18
+ ## Decision-point inventory
19
+
20
+ - `AccountFollowMeRevocation.revoke(req, posture)` — **add** — selects the R12 branch from the caller-computed target posture and returns the honest data-plane outcome; deny-by-default for the optimistic `removed` claim.
21
+ - `AccountFollowMeRevocation.onTargetReconnect(...)` — **add** — fires a durable pending wipe when an offline target returns (R12.iii).
22
+ - `AccountFollowMeRevocation.sweepDeadlines()` — **add** — give-up discipline: escalates past-deadline pending wipes to a LOUD aggregated attention item (R12.iii).
23
+ - `AccountFollowMeRevocation.pendingStateFor(...)` — **add** — read-only honest dashboard state (`revocation-pending` / `revocation-failed` / null).
24
+
25
+ ---
26
+
27
+ ## 1. Over-block
28
+
29
+ Not a block/allow gate — it is a revocation executor. The closest "over-block" surface is the cooperative-wipe fail-closed path: a PARTIAL or THROWING wipe is reported as `revocation-pending` (not `removed`), which keeps a pending record alive and could surface a `revocation-failed` escalation for a wipe that ACTUALLY succeeded partially (e.g. logout succeeded, pool-remove threw transiently). That is the SAFE direction — the worst outcome is the operator is told to rotate at the provider when they technically didn't have to. We never claim `removed` we can't fully confirm; an over-escalation toward "rotate at provider" is intentional honesty, not a defect.
30
+
31
+ ---
32
+
33
+ ## 2. Under-block
34
+
35
+ The module cannot reach a hostile/de-paired holder — by design (S8: instar genuinely cannot force the logout). The honest end-state there is provider-side rotation; the module never PRETENDS otherwise, so the under-block (a live credential still out there) is SURFACED, not silently missed. The remaining real-world gap is operator inaction: if the operator never rotates at the provider after a `provider-rotation-required` / `revocation-failed` instruction, the credential stays live — but that is outside any code's reach and is exactly why the attention item is HIGH priority and aggregated-but-persistent.
36
+
37
+ ---
38
+
39
+ ## 3. Level-of-abstraction fit
40
+
41
+ Correct layer. This is a pure data-plane EXECUTOR + honest-state surface, mirroring PR1's `AccountFollowMeGrants` / PR2's `AccountFollowMeOrchestrator` / `AccountFollowMeService` injectable-deps style. It owns NO authority — the control-plane revoke authority stays with the existing PIN-gated MandateGate; this module consumes that decision and runs the side effects (each injected: `cooperativeWipe`, `pendingStore`, `emitRevocationFailed`). It does NOT re-implement `SubscriptionPool.remove` or framework logout — those are injected effects the server-shell wires to the real implementations. The give-up cadence reuses the resume-queue's deadline-escalation discipline conceptually (it does not depend on ResumeQueue code).
42
+
43
+ ---
44
+
45
+ ## 4. Signal vs authority compliance
46
+
47
+ **Required reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
48
+
49
+ - [x] No — this change has no NEW block/allow authority. It is the data-plane executor of a decision the existing PIN-gated MandateGate already made; the only "gate" it honors is its own `enabled()` dark-flag (no-op when off).
50
+
51
+ The module never blocks a message or an operation. It computes a revocation OUTCOME and emits an honest-state signal (`revocation-pending` / `revocation-failed` / `provider-rotation-required`). The blocking authority (whether a revoke happens at all) is upstream and PIN-gated. Every uncertain path FAILS CLOSED toward "rotate at provider" — the safe, honest direction — never toward a false "removed".
52
+
53
+ ---
54
+
55
+ ## 5. Interactions
56
+
57
+ - **Shadowing:** none — there is no existing revocation data-plane to shadow. It runs AFTER the control-plane mandate revoke (upstream), consuming its result.
58
+ - **Double-fire:** the pending store is keyed on `${accountId}::${targetMachineId}`, so a re-revoke or a reconnect+sweep race cannot double-escalate the same pair — `sweepDeadlines` removes the record on escalation, and a successful `revoke`/`onTargetReconnect` removes it on `removed`. An idempotent upsert means re-running `revoke` for the same offline pair refreshes (not duplicates) the record.
59
+ - **Races:** the in-memory store is single-threaded per Node event loop; the production durable store must preserve the same upsert/remove semantics. A reconnect firing concurrently with a deadline sweep: whichever runs first wins — if the wipe lands first the record is removed and the sweep no-ops; if the sweep escalates first the reconnect's `onTargetReconnect` finds no record and no-ops (the operator already got the rotate-now item). Both orderings are honest.
60
+ - **Feedback loops:** none — `sweepDeadlines` only ever removes records and emits attention items; it never re-enqueues.
61
+
62
+ ---
63
+
64
+ ## 6. External surfaces
65
+
66
+ - **Other agents / mesh:** none directly — this module is local. The server-shell that wires it will consume the control-plane revoke (already mesh-delivered) and run the framework logout on the LOCAL target; this logic has no mesh egress.
67
+ - **Persistent state:** introduces a durable pending-wipe ledger (the `PendingWipeStore` seam; production: JSON/SQLite). Records are small (no credentials, no tokens — only account id, machine id, mandate id, provider name, operator email, nickname, two timestamps). PII note: operator `email` + machine nickname land at-rest in this ledger, same posture as the §6.1a meta projection (email is "never a secret"); no credential field exists in the record shape.
68
+ - **Operator surface (Mobile-Complete):** the operator-facing outputs are (a) the phone-first `ProviderRotationInstruction` message and (b) the HIGH `RevocationFailedAttention` item — both plain-English, both surfaceable via the existing attention-queue → Telegram topic path (no new operator UI primitive). The control-plane revoke itself is the existing PIN-gated Mandates-tab revoke (already phone-complete). No API-only operator action is introduced.
69
+ - **External systems:** the honest end-state explicitly POINTS the operator at the provider (Anthropic) to de-authorize/rotate — it does not call any provider API itself (correct: instar cannot rotate a Claude login on the operator's behalf).
70
+
71
+ ---
72
+
73
+ ## 6b. Operator-surface quality
74
+
75
+ No operator-surface markup file (`dashboard/*.js`, `*.html`, approval/grant/secret-drop form) is touched by THIS PR — it ships pure logic + tests. The dashboard `revocation-pending` / `revocation-failed` render is a follow-up wiring increment and will carry its own 6b review (leads with the honest state + the rotate-at-provider action; revoke control stays the demoted, PIN-gated Mandates control). Not applicable to this commit.
76
+
77
+ ---
78
+
79
+ ## 7. Multi-machine posture
80
+
81
+ **machine-local executor of a cross-machine effect, with proxied-on-read honest state.** This IS a multi-machine feature: it revokes an account on a DIFFERENT machine. Posture per surface:
82
+ - The cooperative wipe runs ON the target machine (the server-shell drives the framework logout against that machine's `CLAUDE_CONFIG_DIR` + its local `SubscriptionPool.remove`) — machine-local effect, triggered by the mesh-delivered control-plane revoke.
83
+ - The durable pending-wipe ledger is held on the REVOKING (operator-authority) machine and fires on the target's reconnect — durable state that must not strand. It is bounded (the deadline give-up) precisely so an offline-forever / terminated VM never leaves a silently-aging pending record.
84
+ - The honest dashboard state (`revocation-pending` / `revocation-failed`) is a read surface; pool-wide visibility is served via the existing `?scope=pool` merged reads (proxied-on-read), so the operator sees the true state from any machine.
85
+ - User-facing notices: the `RevocationFailedAttention` item is aggregated per (account,target) with a stable id (P17 — one running item, never a flood). One-voice gating is the existing attention-queue's job.
86
+
87
+ Single-machine / flag-off ⇒ strict no-op (`enabled()` returns false → `revoke` returns `feature-disabled`, `sweep`/`reconnect` no-op).
88
+
89
+ ---
90
+
91
+ ## 8. Rollback cost
92
+
93
+ Low. Pure code change behind `multiMachine.accountFollowMe` (dark on fleet). Revert the PR and ship a patch — no live credential is written or destroyed by THIS logic layer (the destructive effects are injected and only wired in the follow-up server-shell increment). The only persistent state is the pending-wipe ledger; on rollback an orphaned ledger is inert (nothing reads it once the module is gone) and can be deleted with no migration. No user-visible regression during the rollback window — the feature is dark.
94
+
95
+ ---
96
+
97
+ ## Conclusion
98
+
99
+ This review produced a pure, fail-closed revocation data-plane that mirrors the PR1/PR2 injectable style and is honest by construction: it never reports `removed` it cannot confirm, surfaces provider-side rotation as the ONLY complete answer for a departed/hostile holder, and bounds the offline pending state with a loud give-up. No authority is added — the PIN-gated MandateGate remains the only revoke authority. The build is clear to proceed to the server-shell wiring increment; the dashboard render of the honest states will carry its own 6b operator-surface review. Flagged for second-pass review given the credential/revocation-honesty risk class.
100
+
101
+ ---
102
+
103
+ ## Second-pass review (if required)
104
+
105
+ **Reviewer:** Independent reviewer subagent (2026-06-17) — **CONCUR**. Verified against R12 + the actual code (read in full), ran the tests (15/15) + `tsc --noEmit` (exit 0).
106
+
107
+ - **(a) Fail-closed / honesty** — `removed` is reachable from exactly ONE place: after `fullyWiped = loggedOut && slotDeleted && poolRemoved` AND `mechanism !== 'credential-transport'`. Every other path returns `revocation-pending` / `revocation-failed` / `provider-rotation-required`. A throw → pending; partial wipe → pending; offline → pending; de-paired → provider-rotation. The cardinal R12 sin (a false "removed everywhere") is structurally impossible; a test directly asserts the message never contains "removed" on the non-removed paths.
108
+ - **(b) Branch coverage** — ordered, exhaustive posture switch (dark no-op → `revoked` → `offline` → cooperative-online wipe); closed 3-member `TargetPosture` union; the module trusts (does not re-derive) the caller-supplied authoritative posture.
109
+ - **(c) Offline→pending→give-up** — bounded operator-tunable deadline; `sweepDeadlines()` emits one P17-deduped HIGH attention item then removes the record (no silent aging); `onTargetReconnect()` re-runs the real wipe and clears on success; `pendingStateFor()` honestly reports `revocation-failed` past deadline.
110
+ - **(d) Mechanism A** — `credential-transport` forces `providerRotationRequired:true` on every branch; even a clean A-wipe returns `provider-rotation-required`, never `removed` (a delivered credential is never claimed un-delivered).
111
+ - **(e) Signal vs authority** — pure deterministic planner over injected seams; does NOT revoke the mandate (that stays the PIN-gated MandateGate's control-plane authority); it is the data-plane executor + honest-state read surface, not a new blocking authority.
112
+ - **(f) Injected seams** — zero imports; `cooperativeWipe` / `pendingStore` / `emitRevocationFailed` / `enabled` / `reconnectDeadlineMs` all injected. No direct touch of SubscriptionPool/types/server. (Server-shell wiring is a tracked follow-on increment, mirroring PR1's primitive-layer pattern.)
113
+ - **(g)** Tests cover both sides of every decision boundary with realistic inputs.
114
+
115
+ No changes required to merge.
116
+
117
+ ---
118
+
119
+ ## Evidence pointers
120
+
121
+ - `tests/unit/account-followme-revocation.test.ts` — 15 tests, both sides of every boundary (cooperative vs hostile, online vs offline, pending vs escalated-failed, clean vs partial/throwing wipe, Mechanism B vs A, dark no-op). `npx vitest run` → 15 passed.
122
+ - `npx tsc --noEmit` → exit 0.