groove-dev 0.27.77 → 0.27.78
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/CLAUDE.md +0 -7
- package/MOE_TRAINING_PIPELINE.md +216 -12
- package/moe-training/DEPLOY_CENTRAL_COMMAND.md +413 -0
- package/moe-training/client/consent.js +96 -0
- package/moe-training/client/envelope-builder.js +56 -0
- package/moe-training/client/index.js +10 -0
- package/moe-training/client/parsers/claude-code.js +110 -0
- package/moe-training/client/parsers/codex.js +80 -0
- package/moe-training/client/parsers/gemini.js +80 -0
- package/moe-training/client/parsers/grok.js +16 -0
- package/moe-training/client/parsers/index.js +20 -0
- package/moe-training/client/scrubber.js +126 -0
- package/moe-training/client/session-attestation.js +114 -0
- package/moe-training/client/step-classifier.js +51 -0
- package/moe-training/client/trajectory-capture.js +227 -0
- package/moe-training/client/transmission-queue.js +93 -0
- package/moe-training/package-lock.json +1266 -0
- package/moe-training/package.json +20 -0
- package/moe-training/server/enrichment.js +24 -0
- package/moe-training/server/index.js +119 -0
- package/moe-training/server/ledger.js +110 -0
- package/moe-training/server/routes/ingest.js +96 -0
- package/moe-training/server/routes/sessions.js +43 -0
- package/moe-training/server/routes/stats.js +31 -0
- package/moe-training/server/scoring.js +63 -0
- package/moe-training/server/session-registry.js +156 -0
- package/moe-training/server/stats.js +129 -0
- package/moe-training/server/stitcher.js +69 -0
- package/moe-training/server/storage.js +147 -0
- package/moe-training/server/verifier.js +102 -0
- package/moe-training/shared/constants.js +30 -0
- package/moe-training/shared/crypto.js +45 -0
- package/moe-training/shared/envelope-schema.js +220 -0
- package/moe-training/test/client/consent.test.js +121 -0
- package/moe-training/test/client/envelope-builder.test.js +107 -0
- package/moe-training/test/client/parsers/claude-code.test.js +119 -0
- package/moe-training/test/client/parsers/codex.test.js +83 -0
- package/moe-training/test/client/parsers/gemini.test.js +99 -0
- package/moe-training/test/client/scrubber.test.js +133 -0
- package/moe-training/test/client/session-attestation-security.test.js +95 -0
- package/moe-training/test/client/step-classifier.test.js +88 -0
- package/moe-training/test/integration/handshake.test.js +260 -0
- package/moe-training/test/server/ingest-security.test.js +166 -0
- package/moe-training/test/server/ledger.test.js +131 -0
- package/moe-training/test/server/scoring.test.js +242 -0
- package/moe-training/test/server/session-registry.test.js +125 -0
- package/moe-training/test/server/stitcher.test.js +157 -0
- package/moe-training/test/server/verifier.test.js +232 -0
- package/moe-training/test/shared/crypto.test.js +87 -0
- package/moe-training/test/shared/envelope-schema.test.js +351 -0
- package/node_modules/@groove-dev/cli/package.json +1 -1
- package/node_modules/@groove-dev/daemon/package.json +1 -1
- package/node_modules/@groove-dev/daemon/src/agent-loop.js +48 -5
- package/node_modules/@groove-dev/daemon/src/api.js +77 -0
- package/node_modules/@groove-dev/daemon/src/index.js +61 -0
- package/node_modules/@groove-dev/daemon/src/journalist.js +64 -21
- package/node_modules/@groove-dev/daemon/src/process.js +199 -0
- package/node_modules/@groove-dev/daemon/src/providers/grok.js +15 -0
- package/node_modules/@groove-dev/daemon/src/state.js +20 -1
- package/node_modules/@groove-dev/gui/dist/assets/{index-BbmPDhuW.js → index-BJgEJ9lZ.js} +1677 -1677
- package/node_modules/@groove-dev/gui/dist/index.html +1 -1
- package/node_modules/@groove-dev/gui/package.json +1 -1
- package/node_modules/@groove-dev/gui/src/stores/groove.js +32 -0
- package/node_modules/@groove-dev/gui/src/views/settings.jsx +167 -1
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/daemon/package.json +1 -1
- package/packages/daemon/src/agent-loop.js +48 -5
- package/packages/daemon/src/api.js +77 -0
- package/packages/daemon/src/index.js +61 -0
- package/packages/daemon/src/journalist.js +64 -21
- package/packages/daemon/src/process.js +199 -0
- package/packages/daemon/src/providers/grok.js +15 -0
- package/packages/daemon/src/state.js +20 -1
- package/packages/gui/dist/assets/{index-BbmPDhuW.js → index-BJgEJ9lZ.js} +1677 -1677
- package/packages/gui/dist/index.html +1 -1
- package/packages/gui/package.json +1 -1
- package/packages/gui/src/stores/groove.js +32 -0
- package/packages/gui/src/views/settings.jsx +167 -1
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
|
+
|
|
3
|
+
import { describe, it } from 'node:test';
|
|
4
|
+
import assert from 'node:assert/strict';
|
|
5
|
+
import { validateEnvelope, STEP_TYPES } from '../../shared/envelope-schema.js';
|
|
6
|
+
|
|
7
|
+
const VALID_HMAC = 'a'.repeat(64);
|
|
8
|
+
const VALID_APP_HASH = 'b'.repeat(64);
|
|
9
|
+
const VALID_CONTRIBUTOR = 'c'.repeat(32);
|
|
10
|
+
|
|
11
|
+
function validEnvelope() {
|
|
12
|
+
return {
|
|
13
|
+
envelope_id: 'env_test-123',
|
|
14
|
+
session_id: 'sess_test-456',
|
|
15
|
+
chunk_sequence: 0,
|
|
16
|
+
contributor_id: VALID_CONTRIBUTOR,
|
|
17
|
+
attestation: { session_hmac: VALID_HMAC, sequence: 0, app_version_hash: VALID_APP_HASH },
|
|
18
|
+
metadata: {
|
|
19
|
+
model_engine: 'claude-opus-4-6',
|
|
20
|
+
provider: 'claude-code',
|
|
21
|
+
agent_role: 'backend',
|
|
22
|
+
agent_id: 'backend-1',
|
|
23
|
+
task_complexity: 'medium',
|
|
24
|
+
team_size: 2,
|
|
25
|
+
groove_version: '0.27.0',
|
|
26
|
+
},
|
|
27
|
+
trajectory_log: [
|
|
28
|
+
{ step: 1, type: 'thought', timestamp: Date.now() / 1000, content: 'thinking...', token_count: 10 },
|
|
29
|
+
{ step: 2, type: 'action', timestamp: Date.now() / 1000, tool: 'Read', content: 'reading file' },
|
|
30
|
+
],
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
describe('envelope-schema', () => {
|
|
35
|
+
it('valid envelope passes validation', () => {
|
|
36
|
+
const result = validateEnvelope(validEnvelope());
|
|
37
|
+
assert.equal(result.valid, true);
|
|
38
|
+
assert.equal(result.errors.length, 0);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('null envelope fails', () => {
|
|
42
|
+
const result = validateEnvelope(null);
|
|
43
|
+
assert.equal(result.valid, false);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('missing session_id fails', () => {
|
|
47
|
+
const env = validEnvelope();
|
|
48
|
+
delete env.session_id;
|
|
49
|
+
const result = validateEnvelope(env);
|
|
50
|
+
assert.equal(result.valid, false);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('missing metadata.provider fails', () => {
|
|
54
|
+
const env = validEnvelope();
|
|
55
|
+
delete env.metadata.provider;
|
|
56
|
+
const result = validateEnvelope(env);
|
|
57
|
+
assert.equal(result.valid, false);
|
|
58
|
+
assert.ok(result.errors.some((e) => e.includes('provider')));
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('invalid step type fails', () => {
|
|
62
|
+
const env = validEnvelope();
|
|
63
|
+
env.trajectory_log[0].type = 'invalid_type';
|
|
64
|
+
const result = validateEnvelope(env);
|
|
65
|
+
assert.equal(result.valid, false);
|
|
66
|
+
assert.ok(result.errors.some((e) => e.includes('invalid_type')));
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('missing step number fails', () => {
|
|
70
|
+
const env = validEnvelope();
|
|
71
|
+
delete env.trajectory_log[0].step;
|
|
72
|
+
const result = validateEnvelope(env);
|
|
73
|
+
assert.equal(result.valid, false);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('missing timestamp fails', () => {
|
|
77
|
+
const env = validEnvelope();
|
|
78
|
+
delete env.trajectory_log[0].timestamp;
|
|
79
|
+
const result = validateEnvelope(env);
|
|
80
|
+
assert.equal(result.valid, false);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('all STEP_TYPES are valid', () => {
|
|
84
|
+
for (const type of STEP_TYPES) {
|
|
85
|
+
const env = validEnvelope();
|
|
86
|
+
env.trajectory_log = [{ step: 1, type, timestamp: Date.now() / 1000 }];
|
|
87
|
+
const result = validateEnvelope(env);
|
|
88
|
+
assert.equal(result.valid, true, `Type "${type}" should be valid`);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// --- New security tests ---
|
|
93
|
+
|
|
94
|
+
it('rejects trajectory_log with > 500 steps', () => {
|
|
95
|
+
const env = validEnvelope();
|
|
96
|
+
env.trajectory_log = Array.from({ length: 501 }, (_, i) => ({
|
|
97
|
+
step: i, type: 'thought', timestamp: Date.now() / 1000,
|
|
98
|
+
}));
|
|
99
|
+
const result = validateEnvelope(env);
|
|
100
|
+
assert.equal(result.valid, false);
|
|
101
|
+
assert.ok(result.errors.some(e => e.includes('500')));
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('rejects step with content > 10KB', () => {
|
|
105
|
+
const env = validEnvelope();
|
|
106
|
+
env.trajectory_log[0].content = 'x'.repeat(10_001);
|
|
107
|
+
const result = validateEnvelope(env);
|
|
108
|
+
assert.equal(result.valid, false);
|
|
109
|
+
assert.ok(result.errors.some(e => e.includes('10000')));
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('rejects step with token_count > 100,000', () => {
|
|
113
|
+
const env = validEnvelope();
|
|
114
|
+
env.trajectory_log[0].token_count = 100_001;
|
|
115
|
+
const result = validateEnvelope(env);
|
|
116
|
+
assert.equal(result.valid, false);
|
|
117
|
+
assert.ok(result.errors.some(e => e.includes('token_count')));
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('rejects step with negative token_count', () => {
|
|
121
|
+
const env = validEnvelope();
|
|
122
|
+
env.trajectory_log[0].token_count = -1;
|
|
123
|
+
const result = validateEnvelope(env);
|
|
124
|
+
assert.equal(result.valid, false);
|
|
125
|
+
assert.ok(result.errors.some(e => e.includes('token_count')));
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('rejects step with step number > 50,000', () => {
|
|
129
|
+
const env = validEnvelope();
|
|
130
|
+
env.trajectory_log[0].step = 50_001;
|
|
131
|
+
const result = validateEnvelope(env);
|
|
132
|
+
assert.equal(result.valid, false);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('rejects step with negative step number', () => {
|
|
136
|
+
const env = validEnvelope();
|
|
137
|
+
env.trajectory_log[0].step = -1;
|
|
138
|
+
const result = validateEnvelope(env);
|
|
139
|
+
assert.equal(result.valid, false);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('rejects invalid provider', () => {
|
|
143
|
+
const env = validEnvelope();
|
|
144
|
+
env.metadata.provider = 'fake-provider';
|
|
145
|
+
const result = validateEnvelope(env);
|
|
146
|
+
assert.equal(result.valid, false);
|
|
147
|
+
assert.ok(result.errors.some(e => e.includes('provider')));
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('rejects invalid model_engine', () => {
|
|
151
|
+
const env = validEnvelope();
|
|
152
|
+
env.metadata.model_engine = 'gpt-5-turbo';
|
|
153
|
+
const result = validateEnvelope(env);
|
|
154
|
+
assert.equal(result.valid, false);
|
|
155
|
+
assert.ok(result.errors.some(e => e.includes('model_engine')));
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('rejects contributor_id that is not 32-char hex', () => {
|
|
159
|
+
const env = validEnvelope();
|
|
160
|
+
env.contributor_id = 'user_abc';
|
|
161
|
+
const result = validateEnvelope(env);
|
|
162
|
+
assert.equal(result.valid, false);
|
|
163
|
+
assert.ok(result.errors.some(e => e.includes('contributor_id')));
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('rejects contributor_id with uppercase hex', () => {
|
|
167
|
+
const env = validEnvelope();
|
|
168
|
+
env.contributor_id = 'A'.repeat(32);
|
|
169
|
+
const result = validateEnvelope(env);
|
|
170
|
+
assert.equal(result.valid, false);
|
|
171
|
+
assert.ok(result.errors.some(e => e.includes('contributor_id')));
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('rejects attestation.session_hmac that is not 64-char hex', () => {
|
|
175
|
+
const env = validEnvelope();
|
|
176
|
+
env.attestation.session_hmac = 'abc';
|
|
177
|
+
const result = validateEnvelope(env);
|
|
178
|
+
assert.equal(result.valid, false);
|
|
179
|
+
assert.ok(result.errors.some(e => e.includes('session_hmac')));
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('rejects attestation.app_version_hash that is not 64-char hex', () => {
|
|
183
|
+
const env = validEnvelope();
|
|
184
|
+
env.attestation.app_version_hash = 'def';
|
|
185
|
+
const result = validateEnvelope(env);
|
|
186
|
+
assert.equal(result.valid, false);
|
|
187
|
+
assert.ok(result.errors.some(e => e.includes('app_version_hash')));
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('rejects future timestamps beyond 1 hour', () => {
|
|
191
|
+
const env = validEnvelope();
|
|
192
|
+
env.trajectory_log[0].timestamp = (Date.now() + 2 * 60 * 60 * 1000) / 1000;
|
|
193
|
+
const result = validateEnvelope(env);
|
|
194
|
+
assert.equal(result.valid, false);
|
|
195
|
+
assert.ok(result.errors.some(e => e.includes('Timestamp out of range')));
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('rejects timestamps older than 7 days', () => {
|
|
199
|
+
const env = validEnvelope();
|
|
200
|
+
env.trajectory_log[0].timestamp = (Date.now() - 8 * 24 * 60 * 60 * 1000) / 1000;
|
|
201
|
+
const result = validateEnvelope(env);
|
|
202
|
+
assert.equal(result.valid, false);
|
|
203
|
+
assert.ok(result.errors.some(e => e.includes('Timestamp out of range')));
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('rejects team_size outside 1-50', () => {
|
|
207
|
+
const env = validEnvelope();
|
|
208
|
+
env.metadata.team_size = 0;
|
|
209
|
+
let result = validateEnvelope(env);
|
|
210
|
+
assert.equal(result.valid, false);
|
|
211
|
+
|
|
212
|
+
env.metadata.team_size = 51;
|
|
213
|
+
result = validateEnvelope(env);
|
|
214
|
+
assert.equal(result.valid, false);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('rejects invalid task_complexity', () => {
|
|
218
|
+
const env = validEnvelope();
|
|
219
|
+
env.metadata.task_complexity = 'extreme';
|
|
220
|
+
const result = validateEnvelope(env);
|
|
221
|
+
assert.equal(result.valid, false);
|
|
222
|
+
assert.ok(result.errors.some(e => e.includes('task_complexity')));
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('accepts absent optional metadata fields', () => {
|
|
226
|
+
const env = validEnvelope();
|
|
227
|
+
delete env.metadata.team_size;
|
|
228
|
+
delete env.metadata.task_complexity;
|
|
229
|
+
delete env.metadata.groove_version;
|
|
230
|
+
const result = validateEnvelope(env);
|
|
231
|
+
assert.equal(result.valid, true);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it('valid SESSION_CLOSE passes', () => {
|
|
235
|
+
const close = {
|
|
236
|
+
envelope_id: 'env_close-1',
|
|
237
|
+
session_id: 'sess_test-1',
|
|
238
|
+
type: 'SESSION_CLOSE',
|
|
239
|
+
attestation: { session_hmac: VALID_HMAC, sequence: 5, app_version_hash: VALID_APP_HASH },
|
|
240
|
+
outcome: {
|
|
241
|
+
status: 'SUCCESS',
|
|
242
|
+
user_interventions: 1,
|
|
243
|
+
total_steps: 100,
|
|
244
|
+
total_chunks: 2,
|
|
245
|
+
total_tokens: 5000,
|
|
246
|
+
duration_seconds: 300,
|
|
247
|
+
files_modified: 3,
|
|
248
|
+
errors_encountered: 1,
|
|
249
|
+
errors_recovered: 1,
|
|
250
|
+
coordination_events: 0,
|
|
251
|
+
},
|
|
252
|
+
};
|
|
253
|
+
const result = validateEnvelope(close);
|
|
254
|
+
assert.equal(result.valid, true);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it('SESSION_CLOSE missing outcome fails', () => {
|
|
258
|
+
const close = {
|
|
259
|
+
envelope_id: 'env_close-1',
|
|
260
|
+
session_id: 'sess_test-1',
|
|
261
|
+
type: 'SESSION_CLOSE',
|
|
262
|
+
attestation: { session_hmac: VALID_HMAC, sequence: 5, app_version_hash: VALID_APP_HASH },
|
|
263
|
+
};
|
|
264
|
+
const result = validateEnvelope(close);
|
|
265
|
+
assert.equal(result.valid, false);
|
|
266
|
+
assert.ok(result.errors.some((e) => e.includes('outcome')));
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it('SESSION_CLOSE missing status fails', () => {
|
|
270
|
+
const close = {
|
|
271
|
+
envelope_id: 'env_close-1',
|
|
272
|
+
session_id: 'sess_test-1',
|
|
273
|
+
type: 'SESSION_CLOSE',
|
|
274
|
+
attestation: { session_hmac: VALID_HMAC, sequence: 0, app_version_hash: VALID_APP_HASH },
|
|
275
|
+
outcome: { total_steps: 10, total_chunks: 1 },
|
|
276
|
+
};
|
|
277
|
+
const result = validateEnvelope(close);
|
|
278
|
+
assert.equal(result.valid, false);
|
|
279
|
+
assert.ok(result.errors.some((e) => e.includes('status')));
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it('SESSION_CLOSE rejects invalid outcome status', () => {
|
|
283
|
+
const close = {
|
|
284
|
+
envelope_id: 'env_close-1',
|
|
285
|
+
session_id: 'sess_test-1',
|
|
286
|
+
type: 'SESSION_CLOSE',
|
|
287
|
+
attestation: { session_hmac: VALID_HMAC, sequence: 0, app_version_hash: VALID_APP_HASH },
|
|
288
|
+
outcome: { status: 'TIMEOUT', total_steps: 10, total_chunks: 1 },
|
|
289
|
+
};
|
|
290
|
+
const result = validateEnvelope(close);
|
|
291
|
+
assert.equal(result.valid, false);
|
|
292
|
+
assert.ok(result.errors.some(e => e.includes('status')));
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it('SESSION_CLOSE rejects negative outcome numerics', () => {
|
|
296
|
+
const close = {
|
|
297
|
+
envelope_id: 'env_close-1',
|
|
298
|
+
session_id: 'sess_test-1',
|
|
299
|
+
type: 'SESSION_CLOSE',
|
|
300
|
+
attestation: { session_hmac: VALID_HMAC, sequence: 0, app_version_hash: VALID_APP_HASH },
|
|
301
|
+
outcome: { status: 'SUCCESS', total_steps: 10, total_chunks: 1, user_interventions: -5 },
|
|
302
|
+
};
|
|
303
|
+
const result = validateEnvelope(close);
|
|
304
|
+
assert.equal(result.valid, false);
|
|
305
|
+
assert.ok(result.errors.some(e => e.includes('user_interventions')));
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it('SESSION_CLOSE rejects outcome numerics > 50,000', () => {
|
|
309
|
+
const close = {
|
|
310
|
+
envelope_id: 'env_close-1',
|
|
311
|
+
session_id: 'sess_test-1',
|
|
312
|
+
type: 'SESSION_CLOSE',
|
|
313
|
+
attestation: { session_hmac: VALID_HMAC, sequence: 0, app_version_hash: VALID_APP_HASH },
|
|
314
|
+
outcome: { status: 'SUCCESS', total_steps: 50_001, total_chunks: 1 },
|
|
315
|
+
};
|
|
316
|
+
const result = validateEnvelope(close);
|
|
317
|
+
assert.equal(result.valid, false);
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it('rejects token_count = 999999', () => {
|
|
321
|
+
const env = validEnvelope();
|
|
322
|
+
env.trajectory_log[0].token_count = 999_999;
|
|
323
|
+
const result = validateEnvelope(env);
|
|
324
|
+
assert.equal(result.valid, false);
|
|
325
|
+
assert.ok(result.errors.some(e => e.includes('token_count')));
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it('rejects SQL injection in contributor_id', () => {
|
|
329
|
+
const env = validEnvelope();
|
|
330
|
+
env.contributor_id = "'; DROP TABLE balances; --";
|
|
331
|
+
const result = validateEnvelope(env);
|
|
332
|
+
assert.equal(result.valid, false);
|
|
333
|
+
assert.ok(result.errors.some(e => e.includes('contributor_id')));
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
it('rejects attestation.session_hmac = giant string', () => {
|
|
337
|
+
const env = validEnvelope();
|
|
338
|
+
env.attestation.session_hmac = 'x'.repeat(1_000_000);
|
|
339
|
+
const result = validateEnvelope(env);
|
|
340
|
+
assert.equal(result.valid, false);
|
|
341
|
+
assert.ok(result.errors.some(e => e.includes('session_hmac')));
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
it('rejects empty attestation.session_hmac', () => {
|
|
345
|
+
const env = validEnvelope();
|
|
346
|
+
env.attestation.session_hmac = '';
|
|
347
|
+
const result = validateEnvelope(env);
|
|
348
|
+
assert.equal(result.valid, false);
|
|
349
|
+
assert.ok(result.errors.some(e => e.includes('session_hmac')));
|
|
350
|
+
});
|
|
351
|
+
});
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
// existing GROOVE orchestration (rotation, journalist, token tracking, routing).
|
|
7
7
|
|
|
8
8
|
import { EventEmitter } from 'events';
|
|
9
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, unlinkSync } from 'fs';
|
|
10
|
+
import { resolve, dirname } from 'path';
|
|
9
11
|
import { TOOL_DEFINITIONS, ToolExecutor } from './tool-executor.js';
|
|
10
12
|
|
|
11
13
|
export class AgentLoop extends EventEmitter {
|
|
@@ -36,11 +38,19 @@ export class AgentLoop extends EventEmitter {
|
|
|
36
38
|
agent.id,
|
|
37
39
|
);
|
|
38
40
|
|
|
39
|
-
//
|
|
40
|
-
this.
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
// Session persistence
|
|
42
|
+
this.sessionPath = resolve(daemon.grooveDir, 'sessions', `${agent.id}.json`);
|
|
43
|
+
|
|
44
|
+
// Load existing session or initialize with system prompt
|
|
45
|
+
const savedMessages = AgentLoop.loadSession(this.sessionPath);
|
|
46
|
+
if (savedMessages && savedMessages.length > 0) {
|
|
47
|
+
this.messages = savedMessages;
|
|
48
|
+
} else {
|
|
49
|
+
this.messages.push({
|
|
50
|
+
role: 'system',
|
|
51
|
+
content: this._buildSystemPrompt(),
|
|
52
|
+
});
|
|
53
|
+
}
|
|
44
54
|
}
|
|
45
55
|
|
|
46
56
|
// --- Lifecycle ---
|
|
@@ -68,6 +78,7 @@ export class AgentLoop extends EventEmitter {
|
|
|
68
78
|
this.emit('error', { message: err.message });
|
|
69
79
|
}
|
|
70
80
|
|
|
81
|
+
this._saveSession();
|
|
71
82
|
this.idle = true;
|
|
72
83
|
}
|
|
73
84
|
|
|
@@ -444,4 +455,36 @@ export class AgentLoop extends EventEmitter {
|
|
|
444
455
|
uptime: Date.now() - this.startedAt,
|
|
445
456
|
};
|
|
446
457
|
}
|
|
458
|
+
|
|
459
|
+
// --- Session Persistence ---
|
|
460
|
+
|
|
461
|
+
_saveSession() {
|
|
462
|
+
try {
|
|
463
|
+
mkdirSync(dirname(this.sessionPath), { recursive: true });
|
|
464
|
+
let toSave = this.messages;
|
|
465
|
+
if (toSave.length > 201) {
|
|
466
|
+
const hasSystem = toSave[0]?.role === 'system';
|
|
467
|
+
toSave = hasSystem
|
|
468
|
+
? [toSave[0], ...toSave.slice(-200)]
|
|
469
|
+
: toSave.slice(-200);
|
|
470
|
+
}
|
|
471
|
+
writeFileSync(this.sessionPath, JSON.stringify(toSave), { mode: 0o600 });
|
|
472
|
+
} catch { /* non-fatal */ }
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
static loadSession(sessionPath) {
|
|
476
|
+
try {
|
|
477
|
+
if (!existsSync(sessionPath)) return null;
|
|
478
|
+
const data = readFileSync(sessionPath, 'utf8');
|
|
479
|
+
const messages = JSON.parse(data);
|
|
480
|
+
if (Array.isArray(messages) && messages.length > 0) return messages;
|
|
481
|
+
} catch { /* corrupted session */ }
|
|
482
|
+
return null;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
clearSession() {
|
|
486
|
+
try {
|
|
487
|
+
if (existsSync(this.sessionPath)) unlinkSync(this.sessionPath);
|
|
488
|
+
} catch { /* non-fatal */ }
|
|
489
|
+
}
|
|
447
490
|
}
|
|
@@ -15,6 +15,7 @@ import { listProviders, getProvider, clearInstallCache, getProviderMetadata, get
|
|
|
15
15
|
import { OllamaProvider } from './providers/ollama.js';
|
|
16
16
|
import { ClaudeCodeProvider } from './providers/claude-code.js';
|
|
17
17
|
import { supportsSignalFlag, compareSemver, parseSemver } from './providers/groove-network.js';
|
|
18
|
+
import { ConsentManager } from '../../../moe-training/client/index.js';
|
|
18
19
|
import { validateAgentConfig } from './validate.js';
|
|
19
20
|
import { ROLE_INTEGRATIONS, wrapWithRoleReminder } from './process.js';
|
|
20
21
|
|
|
@@ -4402,6 +4403,81 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
4402
4403
|
res.json({ ok: true });
|
|
4403
4404
|
});
|
|
4404
4405
|
|
|
4406
|
+
// --- Training Data ---
|
|
4407
|
+
|
|
4408
|
+
app.get('/api/training/status', (req, res) => {
|
|
4409
|
+
let userId = null;
|
|
4410
|
+
try { userId = ConsentManager.isCaptureEnabled() ? ConsentManager.getOrCreateUserId() : null; } catch (e) { /* no db yet */ }
|
|
4411
|
+
res.json({
|
|
4412
|
+
optedIn: !!daemon.config.training_opt_in,
|
|
4413
|
+
userId: userId ? userId.substring(0, 8) + '...' : null,
|
|
4414
|
+
captureActive: !!daemon.trajectoryCapture,
|
|
4415
|
+
sessionsCaptured: daemon.state.get('training_sessions_captured') || 0,
|
|
4416
|
+
envelopesSent: daemon.state.get('training_envelopes_sent') || 0,
|
|
4417
|
+
});
|
|
4418
|
+
});
|
|
4419
|
+
|
|
4420
|
+
app.post('/api/training/opt-in', async (req, res) => {
|
|
4421
|
+
const { enabled } = req.body;
|
|
4422
|
+
if (typeof enabled !== 'boolean') return res.status(400).json({ error: 'enabled must be boolean' });
|
|
4423
|
+
|
|
4424
|
+
daemon.config.training_opt_in = enabled;
|
|
4425
|
+
const { saveConfig } = await import('./firstrun.js');
|
|
4426
|
+
saveConfig(daemon.grooveDir, daemon.config);
|
|
4427
|
+
|
|
4428
|
+
if (enabled) {
|
|
4429
|
+
const userId = ConsentManager.getOrCreateUserId();
|
|
4430
|
+
const consent = new ConsentManager();
|
|
4431
|
+
try {
|
|
4432
|
+
consent.recordConsent(userId, true, '1.0');
|
|
4433
|
+
} finally {
|
|
4434
|
+
consent.close();
|
|
4435
|
+
}
|
|
4436
|
+
await daemon._initTrajectoryCapture();
|
|
4437
|
+
daemon.state.set('training_enrolled_at', new Date().toISOString());
|
|
4438
|
+
} else {
|
|
4439
|
+
if (daemon.trajectoryCapture) {
|
|
4440
|
+
try { await daemon.trajectoryCapture.shutdown(); } catch (e) { /* */ }
|
|
4441
|
+
daemon.trajectoryCapture = null;
|
|
4442
|
+
}
|
|
4443
|
+
try {
|
|
4444
|
+
const userId = ConsentManager.getOrCreateUserId();
|
|
4445
|
+
const consent = new ConsentManager();
|
|
4446
|
+
try {
|
|
4447
|
+
consent.revokeConsent(userId);
|
|
4448
|
+
} finally {
|
|
4449
|
+
consent.close();
|
|
4450
|
+
}
|
|
4451
|
+
} catch (e) { /* no user_id yet */ }
|
|
4452
|
+
}
|
|
4453
|
+
|
|
4454
|
+
daemon.broadcast({ type: 'training:status', data: { optedIn: enabled, captureActive: !!daemon.trajectoryCapture } });
|
|
4455
|
+
if (daemon.audit) daemon.audit.log('training.consent', { opt_in: enabled });
|
|
4456
|
+
res.json({ ok: true, optedIn: enabled });
|
|
4457
|
+
});
|
|
4458
|
+
|
|
4459
|
+
app.post('/api/training/opt-in/delete', async (req, res) => {
|
|
4460
|
+
try {
|
|
4461
|
+
daemon.config.training_opt_in = false;
|
|
4462
|
+
const { saveConfig } = await import('./firstrun.js');
|
|
4463
|
+
saveConfig(daemon.grooveDir, daemon.config);
|
|
4464
|
+
if (daemon.trajectoryCapture) {
|
|
4465
|
+
try { await daemon.trajectoryCapture.shutdown(); } catch (e) { /* */ }
|
|
4466
|
+
daemon.trajectoryCapture = null;
|
|
4467
|
+
}
|
|
4468
|
+
try {
|
|
4469
|
+
const userId = ConsentManager.getOrCreateUserId();
|
|
4470
|
+
const consent = new ConsentManager();
|
|
4471
|
+
try { consent.revokeConsent(userId); } finally { consent.close(); }
|
|
4472
|
+
} catch (e) { /* */ }
|
|
4473
|
+
daemon.broadcast({ type: 'training:status', data: { optedIn: false, captureActive: false } });
|
|
4474
|
+
if (daemon.audit) daemon.audit.log('training.delete', {});
|
|
4475
|
+
res.json({ ok: true, deleted: true });
|
|
4476
|
+
} catch (e) {
|
|
4477
|
+
res.status(500).json({ error: 'Failed to delete data' });
|
|
4478
|
+
}
|
|
4479
|
+
});
|
|
4480
|
+
|
|
4405
4481
|
// --- Config ---
|
|
4406
4482
|
|
|
4407
4483
|
app.get('/api/config', (req, res) => {
|
|
@@ -4419,6 +4495,7 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
4419
4495
|
'port', 'journalistInterval', 'rotationThreshold', 'autoRotation',
|
|
4420
4496
|
'qcThreshold', 'maxAgents', 'defaultProvider', 'defaultWorkingDir',
|
|
4421
4497
|
'onboardingDismissed', 'defaultModel', 'defaultChatProvider', 'defaultChatModel',
|
|
4498
|
+
'training_opt_in',
|
|
4422
4499
|
];
|
|
4423
4500
|
for (const key of Object.keys(req.body)) {
|
|
4424
4501
|
if (!ALLOWED_KEYS.includes(key)) {
|
|
@@ -43,6 +43,7 @@ import { LlamaServerManager } from './llama-server.js';
|
|
|
43
43
|
import { RepoImporter } from './repo-import.js';
|
|
44
44
|
import { ConversationManager } from './conversations.js';
|
|
45
45
|
import { Toys } from './toys.js';
|
|
46
|
+
import { TrajectoryCapture, ConsentManager } from '../../../moe-training/client/index.js';
|
|
46
47
|
import { isFirstRun, runFirstTimeSetup, loadConfig, saveConfig, printWelcome } from './firstrun.js';
|
|
47
48
|
import { bindDaemon as bindGrooveNetworkDaemon } from './providers/groove-network.js';
|
|
48
49
|
import { setProviderPaths } from './providers/index.js';
|
|
@@ -151,6 +152,17 @@ export class Daemon {
|
|
|
151
152
|
this.tunnelManager = new TunnelManager(this);
|
|
152
153
|
this.repoImporter = new RepoImporter(this);
|
|
153
154
|
this.toys = new Toys(this);
|
|
155
|
+
this.trajectoryCapture = null;
|
|
156
|
+
|
|
157
|
+
// Hook teams.delete to clean up agent-loop session files
|
|
158
|
+
const originalTeamDelete = this.teams.delete.bind(this.teams);
|
|
159
|
+
this.teams.delete = (id) => {
|
|
160
|
+
const agents = this.registry.getAll().filter(a => a.teamId === id);
|
|
161
|
+
const agentIds = agents.map(a => a.id);
|
|
162
|
+
const result = originalTeamDelete(id);
|
|
163
|
+
if (agentIds.length > 0) this.state.cleanupSessions(agentIds);
|
|
164
|
+
return result;
|
|
165
|
+
};
|
|
154
166
|
|
|
155
167
|
// Subscription state (populated by Electron IPC or direct auth)
|
|
156
168
|
this.authToken = null;
|
|
@@ -390,6 +402,20 @@ export class Daemon {
|
|
|
390
402
|
client.send(payload);
|
|
391
403
|
}
|
|
392
404
|
}
|
|
405
|
+
if (this.trajectoryCapture && message.type) {
|
|
406
|
+
try {
|
|
407
|
+
if (['approval:request', 'approval:resolved', 'conflict:detected', 'qc:activated'].includes(message.type)) {
|
|
408
|
+
const agentId = message.data?.agentId || message.agentId;
|
|
409
|
+
if (agentId) {
|
|
410
|
+
this.trajectoryCapture.onCoordinationEvent(agentId, {
|
|
411
|
+
type: message.type,
|
|
412
|
+
data: message.data,
|
|
413
|
+
timestamp: Date.now(),
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
} catch (e) { /* fail silent */ }
|
|
418
|
+
}
|
|
393
419
|
}
|
|
394
420
|
|
|
395
421
|
async setAuthToken(token) {
|
|
@@ -527,6 +553,17 @@ export class Daemon {
|
|
|
527
553
|
const purged = this.locks.purgeOrphans(runningIds);
|
|
528
554
|
if (purged > 0) console.log(` Purged ${purged} orphaned lock(s) from previous session`);
|
|
529
555
|
|
|
556
|
+
// Mark agents with saved agent-loop sessions as resumable
|
|
557
|
+
const resumableIds = new Set(this.state.getResumableSessions());
|
|
558
|
+
if (resumableIds.size > 0) {
|
|
559
|
+
for (const agent of this.registry.getAll()) {
|
|
560
|
+
if (resumableIds.has(agent.id) && (agent.status === 'running' || agent.status === 'idle' || agent.status === 'completed')) {
|
|
561
|
+
this.registry.update(agent.id, { status: 'completed', hasSession: true, pid: null });
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
console.log(` ${resumableIds.size} agent-loop session(s) marked as resumable`);
|
|
565
|
+
}
|
|
566
|
+
|
|
530
567
|
// Migrate old agents without teamId to default team
|
|
531
568
|
this.teams.migrateAgents();
|
|
532
569
|
|
|
@@ -542,6 +579,7 @@ export class Daemon {
|
|
|
542
579
|
printWelcome(this.port, this.host, this._firstRun);
|
|
543
580
|
|
|
544
581
|
// Start background services
|
|
582
|
+
this._initTrajectoryCapture().catch(() => {});
|
|
545
583
|
this.journalist.start();
|
|
546
584
|
this.rotator.start();
|
|
547
585
|
this.scheduler.start();
|
|
@@ -627,6 +665,23 @@ export class Daemon {
|
|
|
627
665
|
});
|
|
628
666
|
}
|
|
629
667
|
|
|
668
|
+
async _initTrajectoryCapture() {
|
|
669
|
+
if (!this.config.training_opt_in) return;
|
|
670
|
+
try {
|
|
671
|
+
if (ConsentManager.isCaptureEnabled()) {
|
|
672
|
+
const pkgPath = new URL('../package.json', import.meta.url);
|
|
673
|
+
const version = JSON.parse(readFileSync(pkgPath, 'utf8')).version;
|
|
674
|
+
this.trajectoryCapture = new TrajectoryCapture({
|
|
675
|
+
centralCommandUrl: process.env.GROOVE_CENTRAL_URL || 'https://api.groovedev.ai',
|
|
676
|
+
grooveVersion: version,
|
|
677
|
+
});
|
|
678
|
+
this.trajectoryCapture.init();
|
|
679
|
+
}
|
|
680
|
+
} catch (e) {
|
|
681
|
+
// Training capture is never critical
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
630
685
|
_startGarbageCollector() {
|
|
631
686
|
// Run once on startup, then every 24 hours
|
|
632
687
|
this._gc();
|
|
@@ -737,6 +792,12 @@ export class Daemon {
|
|
|
737
792
|
// Disconnect all SSH tunnels
|
|
738
793
|
this.tunnelManager.shutdown();
|
|
739
794
|
|
|
795
|
+
// Shut down training capture
|
|
796
|
+
if (this.trajectoryCapture) {
|
|
797
|
+
try { await this.trajectoryCapture.shutdown(); } catch (e) { /* fail silent */ }
|
|
798
|
+
this.trajectoryCapture = null;
|
|
799
|
+
}
|
|
800
|
+
|
|
740
801
|
// Kill all agent processes, stop MCP servers, and stop inference servers
|
|
741
802
|
await this.processes.killAll();
|
|
742
803
|
if (this.preview) await this.preview.killAll();
|