@agentpulselabs/cli-plugin-pulse 0.2.0 → 1.0.0

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.
Files changed (69) hide show
  1. package/README.md +69 -32
  2. package/package.json +10 -18
  3. package/src/commands/pulse/doctor.js +14 -0
  4. package/src/commands/pulse/hub-write.js +26 -0
  5. package/src/commands/pulse/install.js +14 -0
  6. package/src/commands/pulse/login.js +20 -0
  7. package/src/commands/pulse/mirror.js +27 -0
  8. package/src/commands/pulse/pair.js +38 -0
  9. package/src/commands/pulse/restart.js +14 -0
  10. package/src/commands/pulse/start.js +6 -18
  11. package/src/commands/pulse/status.js +6 -6
  12. package/src/commands/pulse/stop.js +24 -0
  13. package/src/daemon/main.js +36 -0
  14. package/src/lib/BaseCommand.js +23 -23
  15. package/src/lib/agentSessions.js +279 -0
  16. package/src/lib/application.js +656 -0
  17. package/src/lib/backoff.js +8 -0
  18. package/src/lib/budgets.js +85 -0
  19. package/src/lib/cliDefaults.js +20 -0
  20. package/src/lib/cloudAuth.js +189 -0
  21. package/src/lib/contract.sha256 +2 -0
  22. package/src/lib/daemonComposition.js +338 -0
  23. package/src/lib/daemonLock.js +55 -0
  24. package/src/lib/daemonLoop.js +69 -0
  25. package/src/lib/daemonProcess.js +366 -0
  26. package/src/lib/daemonRuntime.js +820 -0
  27. package/src/lib/drivers.js +280 -0
  28. package/src/lib/errors.js +19 -0
  29. package/src/lib/graphWindows.js +149 -0
  30. package/src/lib/http.js +84 -0
  31. package/src/lib/hubActions.js +74 -0
  32. package/src/lib/identity.js +102 -0
  33. package/src/lib/intents.js +590 -0
  34. package/src/lib/ledger.js +535 -0
  35. package/src/lib/lifecycle.js +123 -0
  36. package/src/lib/localHub.js +217 -0
  37. package/src/lib/localHubWriter.js +69 -0
  38. package/src/lib/mirror-contract.json +4299 -0
  39. package/src/lib/mirrorContract.js +513 -0
  40. package/src/lib/mirrorDaemon.js +127 -0
  41. package/src/lib/mirrorService.js +358 -0
  42. package/src/lib/mirrorSource.js +455 -0
  43. package/src/lib/mirrorSync.js +353 -0
  44. package/src/lib/oauthLogin.js +132 -0
  45. package/src/lib/ownerLock.js +99 -0
  46. package/src/lib/pairedConfig.js +40 -0
  47. package/src/lib/pairingLifecycle.js +179 -0
  48. package/src/lib/pathRedaction.js +89 -0
  49. package/src/lib/planContract.js +141 -0
  50. package/src/lib/priorityLane.js +80 -0
  51. package/src/lib/processOwnership.js +252 -0
  52. package/src/lib/processSupervisor.js +376 -0
  53. package/src/lib/redaction.js +128 -0
  54. package/src/lib/rootPolicy.js +372 -0
  55. package/src/lib/rulePacks.js +162 -0
  56. package/src/lib/secretStore.js +169 -0
  57. package/src/lib/selfRestart.js +261 -0
  58. package/src/lib/serviceManager.js +358 -0
  59. package/src/lib/storage.js +131 -0
  60. package/src/lib/subprocess.js +45 -0
  61. package/src/lib/tokenStore.js +97 -0
  62. package/src/lib/transport.js +286 -0
  63. package/src/lib/transportContract.js +37 -0
  64. package/LICENSE +0 -21
  65. package/src/commands/pulse/gate.js +0 -25
  66. package/src/commands/pulse/next.js +0 -14
  67. package/src/commands/pulse/submit.js +0 -21
  68. package/src/commands/pulse/verify.js +0 -21
  69. package/src/runPulse.js +0 -58
@@ -0,0 +1,279 @@
1
+ import path from 'node:path';
2
+ import { HubError } from './errors.js';
3
+ import { ensureOwnerOnlyDirectory, readOwnerOnlyJson, writeOwnerOnlyJson } from './storage.js';
4
+
5
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
6
+ const PROVIDERS = new Set(['cursor', 'claude']);
7
+ const BINDING_FIELDS = [
8
+ 'agentSessionId',
9
+ 'agentId',
10
+ 'deliveryId',
11
+ 'streamId',
12
+ 'rootId',
13
+ 'provider',
14
+ ];
15
+
16
+ function invalid(message = 'Agent session binding is invalid') {
17
+ return new HubError('HUB_AGENT_SESSION_INVALID', message);
18
+ }
19
+
20
+ function bindingMismatch() {
21
+ return new HubError(
22
+ 'HUB_AGENT_SESSION_BINDING_MISMATCH',
23
+ 'Continuation does not exactly match its durable cloud and local session binding'
24
+ );
25
+ }
26
+
27
+ function sameFields(left, right, fields) {
28
+ return fields.every((field) => left?.[field] === right?.[field]);
29
+ }
30
+
31
+ function assertBinding(value) {
32
+ if (
33
+ !value ||
34
+ Object.keys(value).length !== BINDING_FIELDS.length ||
35
+ !BINDING_FIELDS.every((field) => Object.hasOwn(value, field)) ||
36
+ !UUID.test(String(value.agentSessionId || '')) ||
37
+ !UUID.test(String(value.agentId || '')) ||
38
+ !UUID.test(String(value.deliveryId || '')) ||
39
+ !UUID.test(String(value.streamId || '')) ||
40
+ typeof value.rootId !== 'string' ||
41
+ value.rootId.length < 1 ||
42
+ value.rootId.length > 80 ||
43
+ !PROVIDERS.has(value.provider)
44
+ ) {
45
+ throw invalid();
46
+ }
47
+ return value;
48
+ }
49
+
50
+ function assertLocalSession(value) {
51
+ if (
52
+ !value ||
53
+ !UUID.test(String(value.agentId || '')) ||
54
+ !UUID.test(String(value.deliveryId || '')) ||
55
+ !UUID.test(String(value.streamId || '')) ||
56
+ typeof value.rootId !== 'string' ||
57
+ value.rootId.length < 1 ||
58
+ value.rootId.length > 80 ||
59
+ !PROVIDERS.has(value.provider) ||
60
+ typeof value.providerSessionId !== 'string' ||
61
+ value.providerSessionId.length < 1 ||
62
+ value.providerSessionId.length > 180 ||
63
+ typeof value.model !== 'string' ||
64
+ value.model.length < 1 ||
65
+ value.model.length > 100 ||
66
+ !['read', 'write'].includes(value.mode) ||
67
+ !['architect', 'coder', 'reviewer', 'auditor'].includes(value.role) ||
68
+ !['active', 'stopped'].includes(value.state) ||
69
+ (value.worktreeId !== null &&
70
+ (typeof value.worktreeId !== 'string' ||
71
+ value.worktreeId.length < 1 ||
72
+ value.worktreeId.length > 180)) ||
73
+ (value.mode === 'write' && typeof value.worktreeId !== 'string') ||
74
+ (value.processIdentity !== undefined &&
75
+ value.processIdentity !== null &&
76
+ (typeof value.processIdentity !== 'object' ||
77
+ typeof value.processIdentity.processRef !== 'string' ||
78
+ !Number.isInteger(value.processIdentity.pid) ||
79
+ !Number.isInteger(value.processIdentity.pgid) ||
80
+ typeof value.processIdentity.bootId !== 'string' ||
81
+ typeof value.processIdentity.startToken !== 'string'))
82
+ ) {
83
+ throw invalid();
84
+ }
85
+ }
86
+
87
+ export class AgentSessionStore {
88
+ #queue = Promise.resolve();
89
+
90
+ static async open(directory) {
91
+ await ensureOwnerOnlyDirectory(directory);
92
+ const file = path.join(directory, 'agent-sessions.json');
93
+ const data = await readOwnerOnlyJson(file, {
94
+ schemaVersion: 2,
95
+ pendingStarts: {},
96
+ sessions: {},
97
+ startBindings: {},
98
+ });
99
+ if (
100
+ data.schemaVersion !== 2 ||
101
+ !data.pendingStarts ||
102
+ typeof data.pendingStarts !== 'object' ||
103
+ !data.sessions ||
104
+ typeof data.sessions !== 'object' ||
105
+ !data.startBindings ||
106
+ typeof data.startBindings !== 'object'
107
+ ) {
108
+ throw new HubError('HUB_AGENT_SESSION_STORE_INVALID', 'Agent session store is invalid');
109
+ }
110
+ if (
111
+ Object.keys(data.sessions).length > 1_000 ||
112
+ Object.keys(data.pendingStarts).length > 100 ||
113
+ Object.keys(data.startBindings).length > 1_000 ||
114
+ Buffer.byteLength(JSON.stringify(data), 'utf8') > 2 * 1024 * 1024
115
+ ) {
116
+ throw new HubError('HUB_AGENT_SESSION_STORE_INVALID', 'Agent session store exceeds safety bounds');
117
+ }
118
+ for (const [id, session] of Object.entries(data.sessions)) {
119
+ assertBinding({
120
+ agentSessionId: id,
121
+ agentId: session.agentId,
122
+ deliveryId: session.deliveryId,
123
+ streamId: session.streamId,
124
+ rootId: session.rootId,
125
+ provider: session.provider,
126
+ });
127
+ assertLocalSession(session);
128
+ }
129
+ for (const [jobId, session] of Object.entries(data.pendingStarts)) {
130
+ if (!UUID.test(jobId)) throw invalid();
131
+ assertLocalSession(session);
132
+ }
133
+ for (const [jobId, binding] of Object.entries(data.startBindings)) {
134
+ if (!UUID.test(jobId)) throw invalid();
135
+ assertBinding(binding);
136
+ }
137
+ return new AgentSessionStore(file, data);
138
+ }
139
+
140
+ constructor(file, data) {
141
+ this.file = file;
142
+ this.data = data;
143
+ }
144
+
145
+ get(id) {
146
+ const value = Object.hasOwn(this.data.sessions, id) ? this.data.sessions[id] : null;
147
+ return value ? structuredClone(value) : null;
148
+ }
149
+
150
+ list() {
151
+ return Object.entries(this.data.sessions).map(([id, value]) => ({
152
+ id,
153
+ ...structuredClone(value),
154
+ }));
155
+ }
156
+
157
+ async #mutate(operation) {
158
+ const pending = this.#queue.then(async () => {
159
+ const draft = structuredClone(this.data);
160
+ const result = operation(draft);
161
+ if (Buffer.byteLength(JSON.stringify(draft), 'utf8') > 2 * 1024 * 1024) {
162
+ throw new HubError('HUB_AGENT_SESSION_STORE_FULL', 'Agent session store exceeds safety bounds');
163
+ }
164
+ await writeOwnerOnlyJson(this.file, draft);
165
+ this.data = draft;
166
+ return structuredClone(result);
167
+ });
168
+ this.#queue = pending.catch(() => {});
169
+ return pending;
170
+ }
171
+
172
+ async recordPendingStart(jobId, value) {
173
+ if (!UUID.test(String(jobId || ''))) throw invalid('Start job id is invalid');
174
+ assertLocalSession(value);
175
+ return this.#mutate((draft) => {
176
+ const existing = draft.pendingStarts[jobId];
177
+ if (existing && JSON.stringify(existing) !== JSON.stringify(value)) throw bindingMismatch();
178
+ if (!existing && Object.keys(draft.pendingStarts).length >= 100) {
179
+ throw new HubError('HUB_AGENT_SESSION_STORE_FULL', 'Pending start store is full');
180
+ }
181
+ draft.pendingStarts[jobId] = structuredClone(value);
182
+ return value;
183
+ });
184
+ }
185
+
186
+ async applyStartAcknowledgement(jobId, acknowledgement) {
187
+ if (
188
+ !UUID.test(String(jobId || '')) ||
189
+ acknowledgement?.job?.id !== jobId ||
190
+ acknowledgement.job.type !== 'agent.start' ||
191
+ !['succeeded', 'failed', 'cancelled'].includes(acknowledgement.job.state)
192
+ ) {
193
+ throw invalid('Agent start acknowledgement is invalid');
194
+ }
195
+ return this.#mutate((draft) => {
196
+ const pending = draft.pendingStarts[jobId];
197
+ if (acknowledgement.job.state !== 'succeeded') {
198
+ if (acknowledgement.sessionBinding !== undefined) throw bindingMismatch();
199
+ delete draft.pendingStarts[jobId];
200
+ return null;
201
+ }
202
+ const binding = assertBinding(acknowledgement.sessionBinding);
203
+ const priorBinding = draft.startBindings[jobId];
204
+ if (priorBinding) {
205
+ if (!sameFields(priorBinding, binding, BINDING_FIELDS)) throw bindingMismatch();
206
+ const priorSession = draft.sessions[binding.agentSessionId];
207
+ if (!priorSession) throw bindingMismatch();
208
+ return priorSession;
209
+ }
210
+ if (!pending || !sameFields(pending, binding, BINDING_FIELDS.slice(1))) {
211
+ throw bindingMismatch();
212
+ }
213
+ const existing = draft.sessions[binding.agentSessionId];
214
+ const session = {
215
+ ...structuredClone(pending),
216
+ agentId: binding.agentId,
217
+ deliveryId: binding.deliveryId,
218
+ streamId: binding.streamId,
219
+ rootId: binding.rootId,
220
+ provider: binding.provider,
221
+ };
222
+ if (existing && JSON.stringify(existing) !== JSON.stringify(session)) throw bindingMismatch();
223
+ if (!existing && Object.keys(draft.sessions).length >= 1_000) {
224
+ throw new HubError('HUB_AGENT_SESSION_STORE_FULL', 'Agent session store is full');
225
+ }
226
+ draft.sessions[binding.agentSessionId] = session;
227
+ draft.startBindings[jobId] = structuredClone(binding);
228
+ delete draft.pendingStarts[jobId];
229
+ return session;
230
+ });
231
+ }
232
+
233
+ async rejectStartSettlement(jobId) {
234
+ if (!UUID.test(String(jobId || ''))) throw invalid('Start job id is invalid');
235
+ return this.#mutate((draft) => {
236
+ if (!Object.hasOwn(draft.pendingStarts, jobId)) return null;
237
+ delete draft.pendingStarts[jobId];
238
+ return null;
239
+ });
240
+ }
241
+
242
+ resolveContinuation(job) {
243
+ if (!['agent.turn', 'agent.stop'].includes(job?.type)) return null;
244
+ const binding = assertBinding(job.sessionBinding);
245
+ const session = this.get(binding.agentSessionId);
246
+ const expectedCapability = `agent.${binding.provider}.invoke`;
247
+ if (
248
+ !session ||
249
+ job.arguments?.agentSessionId !== binding.agentSessionId ||
250
+ job.arguments?.rootId !== binding.rootId ||
251
+ job.delivery_id !== binding.deliveryId ||
252
+ job.stream_id !== binding.streamId ||
253
+ job.root_id !== binding.rootId ||
254
+ job.capability !== expectedCapability ||
255
+ (session.state !== 'active' && !(job.type === 'agent.stop' && session.state === 'stopped')) ||
256
+ !sameFields(session, binding, BINDING_FIELDS.slice(1))
257
+ ) {
258
+ throw bindingMismatch();
259
+ }
260
+ return {
261
+ provider: binding.provider,
262
+ cloudSessionId: binding.agentSessionId,
263
+ session,
264
+ };
265
+ }
266
+
267
+ async put(id, value) {
268
+ if (!UUID.test(String(id || ''))) throw invalid();
269
+ assertLocalSession(value);
270
+ return this.#mutate((draft) => {
271
+ const existing = draft.sessions[id];
272
+ if (!existing || !sameFields(existing, value, BINDING_FIELDS.slice(1))) {
273
+ throw bindingMismatch();
274
+ }
275
+ draft.sessions[id] = structuredClone(value);
276
+ return value;
277
+ });
278
+ }
279
+ }