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.
- package/dist/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +19 -0
- package/dist/commands/server.js.map +1 -1
- package/dist/core/AccountFollowMeRevocation.d.ts +178 -0
- package/dist/core/AccountFollowMeRevocation.d.ts.map +1 -0
- package/dist/core/AccountFollowMeRevocation.js +279 -0
- package/dist/core/AccountFollowMeRevocation.js.map +1 -0
- package/dist/core/EnrollmentWizard.d.ts +50 -0
- package/dist/core/EnrollmentWizard.d.ts.map +1 -1
- package/dist/core/EnrollmentWizard.js +52 -0
- package/dist/core/EnrollmentWizard.js.map +1 -1
- package/dist/core/PendingLoginStore.d.ts +8 -0
- package/dist/core/PendingLoginStore.d.ts.map +1 -1
- package/dist/core/PendingLoginStore.js +1 -0
- package/dist/core/PendingLoginStore.js.map +1 -1
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +51 -0
- package/dist/server/routes.js.map +1 -1
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +46 -46
- package/upgrades/1.3.611.md +22 -0
- package/upgrades/1.3.612.md +28 -0
- package/upgrades/side-effects/ws52-account-follow-me-email-gate.md +70 -0
- package/upgrades/side-effects/ws52-account-follow-me-revocation.md +122 -0
|
@@ -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;
|
|
1
|
+
{"version":3,"file":"EnrollmentWizard.d.ts","sourceRoot":"","sources":["../../src/core/EnrollmentWizard.ts"],"names":[],"mappings":"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"}
|