@kbediako/codex-orchestrator 0.1.3 → 0.1.4

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 (28) hide show
  1. package/README.md +6 -1
  2. package/dist/bin/codex-orchestrator.js +38 -0
  3. package/dist/orchestrator/src/cli/config/delegationConfig.js +485 -0
  4. package/dist/orchestrator/src/cli/control/confirmations.js +262 -0
  5. package/dist/orchestrator/src/cli/control/controlServer.js +1476 -0
  6. package/dist/orchestrator/src/cli/control/controlState.js +46 -0
  7. package/dist/orchestrator/src/cli/control/controlWatcher.js +222 -0
  8. package/dist/orchestrator/src/cli/control/delegationTokens.js +62 -0
  9. package/dist/orchestrator/src/cli/control/questions.js +106 -0
  10. package/dist/orchestrator/src/cli/delegationServer.js +1368 -0
  11. package/dist/orchestrator/src/cli/events/runEventStream.js +246 -0
  12. package/dist/orchestrator/src/cli/exec/context.js +4 -1
  13. package/dist/orchestrator/src/cli/exec/stageRunner.js +30 -5
  14. package/dist/orchestrator/src/cli/metrics/metricsAggregator.js +377 -147
  15. package/dist/orchestrator/src/cli/metrics/metricsRecorder.js +3 -5
  16. package/dist/orchestrator/src/cli/orchestrator.js +217 -40
  17. package/dist/orchestrator/src/cli/rlmRunner.js +26 -3
  18. package/dist/orchestrator/src/cli/run/manifestPersister.js +33 -3
  19. package/dist/orchestrator/src/cli/run/runPaths.js +14 -0
  20. package/dist/orchestrator/src/cli/services/commandRunner.js +1 -1
  21. package/dist/orchestrator/src/cli/utils/devtools.js +33 -2
  22. package/dist/orchestrator/src/persistence/ExperienceStore.js +113 -46
  23. package/dist/orchestrator/src/persistence/PersistenceCoordinator.js +8 -8
  24. package/dist/orchestrator/src/persistence/TaskStateStore.js +2 -1
  25. package/dist/orchestrator/src/persistence/lockFile.js +26 -1
  26. package/dist/orchestrator/src/sync/CloudSyncWorker.js +17 -4
  27. package/dist/packages/orchestrator/src/telemetry/otel-exporter.js +21 -0
  28. package/package.json +3 -1
@@ -0,0 +1,262 @@
1
+ import { createHash, createHmac, randomBytes, timingSafeEqual } from 'node:crypto';
2
+ import canonicalize from 'canonicalize';
3
+ const NONCE_VERSION = 1;
4
+ export class ConfirmationStore {
5
+ runId;
6
+ now;
7
+ expiresInMs;
8
+ maxPending;
9
+ secret;
10
+ pending = new Map();
11
+ digestIndex = new Map();
12
+ issued = new Map();
13
+ issuedByRequest = new Map();
14
+ consumed = new Set();
15
+ constructor(options) {
16
+ this.runId = options.runId;
17
+ this.now = options.now ?? (() => new Date());
18
+ this.expiresInMs = options.expiresInMs;
19
+ this.maxPending = options.maxPending;
20
+ this.secret = options.secret ?? randomBytes(32);
21
+ const seed = options.seed ?? {};
22
+ const seededPending = Array.isArray(seed.pending) ? seed.pending : [];
23
+ for (const entry of seededPending) {
24
+ this.pending.set(entry.request_id, { ...entry });
25
+ this.digestIndex.set(entry.action_params_digest, entry.request_id);
26
+ }
27
+ const seededIssued = Array.isArray(seed.issued) ? seed.issued : [];
28
+ for (const entry of seededIssued) {
29
+ this.issued.set(entry.nonce_id, { ...entry });
30
+ this.issuedByRequest.set(entry.request_id, entry.nonce_id);
31
+ }
32
+ const seededConsumed = Array.isArray(seed.consumed_nonce_ids) ? seed.consumed_nonce_ids : [];
33
+ for (const nonceId of seededConsumed) {
34
+ this.consumed.add(nonceId);
35
+ }
36
+ }
37
+ create(input) {
38
+ const actionParamsDigest = buildActionParamsDigest({ tool: input.tool, params: input.params });
39
+ const existingId = this.digestIndex.get(actionParamsDigest);
40
+ if (existingId) {
41
+ const existing = this.pending.get(existingId);
42
+ if (existing) {
43
+ return { confirmation: { ...existing }, wasCreated: false };
44
+ }
45
+ this.digestIndex.delete(actionParamsDigest);
46
+ }
47
+ if (this.pending.size >= this.maxPending) {
48
+ throw new Error('confirmation_pending_limit_reached');
49
+ }
50
+ const requestId = `req-${randomBytes(8).toString('hex')}`;
51
+ const now = this.now();
52
+ const expiresAt = new Date(now.getTime() + this.expiresInMs);
53
+ const request = {
54
+ request_id: requestId,
55
+ action: input.action,
56
+ tool: input.tool,
57
+ params: { ...input.params },
58
+ action_params_digest: actionParamsDigest,
59
+ digest_alg: 'sha256',
60
+ requested_at: now.toISOString(),
61
+ expires_at: expiresAt.toISOString(),
62
+ approved_by: null,
63
+ approved_at: null
64
+ };
65
+ this.pending.set(requestId, request);
66
+ this.digestIndex.set(actionParamsDigest, requestId);
67
+ return { confirmation: { ...request }, wasCreated: true };
68
+ }
69
+ approve(requestId, actor) {
70
+ const entry = this.pending.get(requestId);
71
+ if (!entry) {
72
+ throw new Error('confirmation_request_not_found');
73
+ }
74
+ entry.approved_by = actor;
75
+ entry.approved_at = this.now().toISOString();
76
+ }
77
+ issue(requestId) {
78
+ const entry = this.pending.get(requestId);
79
+ if (!entry) {
80
+ throw new Error('confirmation_request_not_found');
81
+ }
82
+ if (!entry.approved_at) {
83
+ throw new Error('confirmation_not_approved');
84
+ }
85
+ const existingNonceId = this.issuedByRequest.get(requestId);
86
+ if (existingNonceId) {
87
+ return this.buildNonce(entry, this.issued.get(existingNonceId));
88
+ }
89
+ const issuedAt = this.now().toISOString();
90
+ const nonceId = `nonce-${randomBytes(8).toString('hex')}`;
91
+ const record = {
92
+ request_id: requestId,
93
+ nonce_id: nonceId,
94
+ issued_at: issuedAt,
95
+ expires_at: entry.expires_at
96
+ };
97
+ this.issued.set(nonceId, record);
98
+ this.issuedByRequest.set(requestId, nonceId);
99
+ return this.buildNonce(entry, record);
100
+ }
101
+ validateNonce(input) {
102
+ const parsed = parseConfirmNonce(input.confirmNonce, this.secret);
103
+ if (!parsed) {
104
+ throw new Error('confirmation_invalid');
105
+ }
106
+ const payload = parsed.payload;
107
+ if (payload.v !== NONCE_VERSION) {
108
+ throw new Error('confirmation_invalid');
109
+ }
110
+ if (payload.run_id !== this.runId) {
111
+ throw new Error('confirmation_scope_mismatch');
112
+ }
113
+ if (this.consumed.has(payload.nonce_id)) {
114
+ throw new Error('nonce_already_consumed');
115
+ }
116
+ const record = this.issued.get(payload.nonce_id);
117
+ if (!record || record.request_id !== payload.request_id) {
118
+ throw new Error('confirmation_invalid');
119
+ }
120
+ const request = this.pending.get(payload.request_id);
121
+ if (!request) {
122
+ throw new Error('confirmation_request_not_found');
123
+ }
124
+ if (!request.approved_at) {
125
+ throw new Error('confirmation_not_approved');
126
+ }
127
+ const expectedDigest = buildActionParamsDigest({ tool: input.tool, params: input.params });
128
+ if (payload.action_params_digest !== expectedDigest) {
129
+ throw new Error('confirmation_scope_mismatch');
130
+ }
131
+ if (payload.action !== request.action) {
132
+ throw new Error('confirmation_scope_mismatch');
133
+ }
134
+ const expiresAt = Date.parse(payload.expires_at);
135
+ if (Number.isFinite(expiresAt) && expiresAt <= this.now().getTime()) {
136
+ throw new Error('confirmation_expired');
137
+ }
138
+ this.pending.delete(payload.request_id);
139
+ this.digestIndex.delete(request.action_params_digest);
140
+ this.issued.delete(payload.nonce_id);
141
+ this.issuedByRequest.delete(payload.request_id);
142
+ this.consumed.add(payload.nonce_id);
143
+ return { request: { ...request }, nonce_id: payload.nonce_id };
144
+ }
145
+ get(requestId) {
146
+ const entry = this.pending.get(requestId);
147
+ return entry ? { ...entry } : undefined;
148
+ }
149
+ expire() {
150
+ const now = this.now().getTime();
151
+ const expired = [];
152
+ for (const [requestId, entry] of this.pending.entries()) {
153
+ const expiry = Date.parse(entry.expires_at);
154
+ if (Number.isFinite(expiry) && expiry <= now) {
155
+ this.pending.delete(requestId);
156
+ this.digestIndex.delete(entry.action_params_digest);
157
+ const nonceId = this.issuedByRequest.get(requestId) ?? null;
158
+ if (nonceId) {
159
+ this.issued.delete(nonceId);
160
+ this.issuedByRequest.delete(requestId);
161
+ }
162
+ expired.push({ request: { ...entry }, nonce_id: nonceId });
163
+ }
164
+ }
165
+ return expired;
166
+ }
167
+ listPending() {
168
+ return Array.from(this.pending.values()).map((entry) => ({ ...entry }));
169
+ }
170
+ snapshot() {
171
+ return {
172
+ pending: this.listPending(),
173
+ issued: Array.from(this.issued.values()).map((entry) => ({ ...entry })),
174
+ consumed_nonce_ids: Array.from(this.consumed.values())
175
+ };
176
+ }
177
+ buildNonce(entry, record) {
178
+ if (!record) {
179
+ throw new Error('confirmation_nonce_missing');
180
+ }
181
+ const payload = {
182
+ v: NONCE_VERSION,
183
+ run_id: this.runId,
184
+ request_id: record.request_id,
185
+ nonce_id: record.nonce_id,
186
+ action: entry.action,
187
+ action_params_digest: entry.action_params_digest,
188
+ issued_at: record.issued_at,
189
+ expires_at: record.expires_at
190
+ };
191
+ const confirmNonce = encodeConfirmNonce(payload, this.secret);
192
+ return {
193
+ request_id: record.request_id,
194
+ nonce_id: record.nonce_id,
195
+ confirm_nonce: confirmNonce,
196
+ action_params_digest: entry.action_params_digest,
197
+ digest_alg: 'sha256',
198
+ issued_at: record.issued_at,
199
+ expires_at: record.expires_at
200
+ };
201
+ }
202
+ }
203
+ export function buildActionParamsDigest(input) {
204
+ const sanitized = stripConfirmNonce({ tool: input.tool, params: input.params });
205
+ const canonicalizeFn = canonicalize;
206
+ const canonical = canonicalizeFn(sanitized);
207
+ if (typeof canonical !== 'string') {
208
+ throw new Error('Unable to canonicalize confirmation params.');
209
+ }
210
+ return createHash('sha256').update(canonical).digest('hex');
211
+ }
212
+ function stripConfirmNonce(value) {
213
+ if (Array.isArray(value)) {
214
+ return value.map(stripConfirmNonce);
215
+ }
216
+ if (value && typeof value === 'object') {
217
+ const record = value;
218
+ const result = {};
219
+ for (const key of Object.keys(record)) {
220
+ if (key === 'confirm_nonce') {
221
+ continue;
222
+ }
223
+ const cleaned = stripConfirmNonce(record[key]);
224
+ if (typeof cleaned !== 'undefined') {
225
+ result[key] = cleaned;
226
+ }
227
+ }
228
+ return result;
229
+ }
230
+ return value;
231
+ }
232
+ function encodeConfirmNonce(payload, secret) {
233
+ const serialized = JSON.stringify(payload);
234
+ const signature = createHmac('sha256', secret).update(serialized).digest('hex');
235
+ const encoded = Buffer.from(serialized, 'utf8').toString('base64url');
236
+ return `${encoded}.${signature}`;
237
+ }
238
+ function parseConfirmNonce(token, secret) {
239
+ const [encoded, signature] = token.split('.');
240
+ if (!encoded || !signature) {
241
+ return null;
242
+ }
243
+ let payload;
244
+ try {
245
+ const decoded = Buffer.from(encoded, 'base64url').toString('utf8');
246
+ payload = JSON.parse(decoded);
247
+ }
248
+ catch {
249
+ return null;
250
+ }
251
+ const expected = createHmac('sha256', secret).update(JSON.stringify(payload)).digest('hex');
252
+ if (!timingSafeEqualStrings(signature, expected)) {
253
+ return null;
254
+ }
255
+ return { payload };
256
+ }
257
+ function timingSafeEqualStrings(left, right) {
258
+ if (left.length !== right.length) {
259
+ return false;
260
+ }
261
+ return timingSafeEqual(Buffer.from(left, 'utf8'), Buffer.from(right, 'utf8'));
262
+ }