-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathchain.js
More file actions
68 lines (61 loc) · 2.96 KB
/
Copy pathchain.js
File metadata and controls
68 lines (61 loc) · 2.96 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
// Chain Mode (DESIGN §13.3; ceiling locked, CLAUDE.md #8).
// One grant auto-approves N declared single-auth gates.
// ANY surprise kills the chain; rest confirm manually.
// Destructive stays human-only. Never grows toward Auto Mode.
import { randomUUID } from 'node:crypto';
const TTL_MS = 10 * 60_000; /* grant lifetime */
const CAPABILITY_SET = 'debug'; /* same routing as prompt-queue */
export class ChainRegistry {
constructor({ dispatcher, audit, settings }) {
this.dispatcher = dispatcher;
this.audit = audit;
this.settings = settings;
this.active = null; /* { chainId, count, used, summary, expiresAt } */
dispatcher.subscribe('claude.chain.cancel', (p) => {
if (this.active && p?.chainId === this.active.chainId) this.kill('gm-cancel');
});
}
// Claude offers; GM answers one single-confirm card.
async offer({ count, summary, tabId }) {
if (!this.settings.get('chainOffers')) return { refused: true, reason: 'chain-offers-disabled' };
if (this.active) return { refused: true, reason: 'chain-already-active' };
const max = this.settings.get('chainMaxLength');
if (!Number.isInteger(count) || count < 2 || count > max) {
return { refused: true, reason: `count must be an integer 2..${max}` };
}
const chainId = `chain-${randomUUID()}`;
const decision = await this.dispatcher.requestConfirmation({
capabilitySet: CAPABILITY_SET, opId: chainId, kind: 'chain', level: 'single', tabId,
summary: `CHAIN MODE - approve ${count} single-auth gates as one batch:\n${summary}`,
});
if (!decision.approved) {
this.audit.log('chain.declined', { chainId, reason: decision.reason });
return { refused: true, reason: decision.reason };
}
this.active = { chainId, count, used: 0, summary, expiresAt: Date.now() + TTL_MS, tabId };
this.audit.log('chain.grant', { chainId, count, summary, tabId });
this._notify({ event: 'grant', chainId, count, text: summary, tabId });
return { chainId, count, expiresInSeconds: TTL_MS / 1000 };
}
// Gate rides the chain; false = confirm manually.
consume(chainId, gateSummary) {
const a = this.active;
if (!a || a.chainId !== chainId) return false;
if (Date.now() > a.expiresAt) { this.kill('ttl-expired'); return false; }
a.used++;
this.audit.log('chain.gate', { chainId, n: a.used, of: a.count, summary: gateSummary });
this._notify({ event: 'gate', chainId, n: a.used, count: a.count, text: gateSummary, tabId: a.tabId });
if (a.used >= a.count) this.kill('count-exhausted'); /* Nth gate still rides */
return true;
}
kill(reason) {
if (!this.active) return;
const { chainId, used, count, tabId } = this.active;
this.active = null;
this.audit.log('chain.end', { chainId, used, count, reason });
this._notify({ event: 'end', chainId, n: used, count, text: reason, tabId });
}
_notify(params) {
this.dispatcher.notifyBridge({ capabilitySet: CAPABILITY_SET, method: 'claude.chain', params });
}
}