@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.
- package/README.md +6 -1
- package/dist/bin/codex-orchestrator.js +38 -0
- package/dist/orchestrator/src/cli/config/delegationConfig.js +485 -0
- package/dist/orchestrator/src/cli/control/confirmations.js +262 -0
- package/dist/orchestrator/src/cli/control/controlServer.js +1476 -0
- package/dist/orchestrator/src/cli/control/controlState.js +46 -0
- package/dist/orchestrator/src/cli/control/controlWatcher.js +222 -0
- package/dist/orchestrator/src/cli/control/delegationTokens.js +62 -0
- package/dist/orchestrator/src/cli/control/questions.js +106 -0
- package/dist/orchestrator/src/cli/delegationServer.js +1368 -0
- package/dist/orchestrator/src/cli/events/runEventStream.js +246 -0
- package/dist/orchestrator/src/cli/exec/context.js +4 -1
- package/dist/orchestrator/src/cli/exec/stageRunner.js +30 -5
- package/dist/orchestrator/src/cli/metrics/metricsAggregator.js +377 -147
- package/dist/orchestrator/src/cli/metrics/metricsRecorder.js +3 -5
- package/dist/orchestrator/src/cli/orchestrator.js +217 -40
- package/dist/orchestrator/src/cli/rlmRunner.js +26 -3
- package/dist/orchestrator/src/cli/run/manifestPersister.js +33 -3
- package/dist/orchestrator/src/cli/run/runPaths.js +14 -0
- package/dist/orchestrator/src/cli/services/commandRunner.js +1 -1
- package/dist/orchestrator/src/cli/utils/devtools.js +33 -2
- package/dist/orchestrator/src/persistence/ExperienceStore.js +113 -46
- package/dist/orchestrator/src/persistence/PersistenceCoordinator.js +8 -8
- package/dist/orchestrator/src/persistence/TaskStateStore.js +2 -1
- package/dist/orchestrator/src/persistence/lockFile.js +26 -1
- package/dist/orchestrator/src/sync/CloudSyncWorker.js +17 -4
- package/dist/packages/orchestrator/src/telemetry/otel-exporter.js +21 -0
- package/package.json +3 -1
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export class ControlStateStore {
|
|
2
|
+
now;
|
|
3
|
+
state;
|
|
4
|
+
constructor(options) {
|
|
5
|
+
this.now = options.now ?? (() => new Date().toISOString());
|
|
6
|
+
this.state = {
|
|
7
|
+
run_id: options.runId,
|
|
8
|
+
control_seq: options.controlSeq ?? 0,
|
|
9
|
+
latest_action: options.latestAction ?? null,
|
|
10
|
+
feature_toggles: options.featureToggles ?? null
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
updateAction(input) {
|
|
14
|
+
this.state.control_seq += 1;
|
|
15
|
+
this.state.latest_action = {
|
|
16
|
+
request_id: input.requestId ?? null,
|
|
17
|
+
requested_by: input.requestedBy,
|
|
18
|
+
requested_at: this.now(),
|
|
19
|
+
action: input.action,
|
|
20
|
+
reason: input.reason ?? null
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
updateFeatureToggles(toggles) {
|
|
24
|
+
const current = this.state.feature_toggles ?? {};
|
|
25
|
+
this.state.feature_toggles = mergeObjects(current, toggles);
|
|
26
|
+
}
|
|
27
|
+
snapshot() {
|
|
28
|
+
return structuredClone(this.state);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function mergeObjects(base, update) {
|
|
32
|
+
const merged = { ...base };
|
|
33
|
+
for (const [key, value] of Object.entries(update)) {
|
|
34
|
+
if (Array.isArray(value)) {
|
|
35
|
+
merged[key] = [...value];
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
39
|
+
const current = merged[key] ?? {};
|
|
40
|
+
merged[key] = mergeObjects(current, value);
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
merged[key] = value;
|
|
44
|
+
}
|
|
45
|
+
return merged;
|
|
46
|
+
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { isoTimestamp } from '../utils/time.js';
|
|
3
|
+
import { appendSummary } from '../run/manifest.js';
|
|
4
|
+
import { logger } from '../../logger.js';
|
|
5
|
+
export class ControlWatcher {
|
|
6
|
+
paths;
|
|
7
|
+
manifest;
|
|
8
|
+
persist;
|
|
9
|
+
eventStream;
|
|
10
|
+
onEntry;
|
|
11
|
+
now;
|
|
12
|
+
pollIntervalMs;
|
|
13
|
+
lastControlSeq = 0;
|
|
14
|
+
paused = false;
|
|
15
|
+
lastPauseRequestId = null;
|
|
16
|
+
lastPauseReason = null;
|
|
17
|
+
cancelRequested = false;
|
|
18
|
+
failureRequested = false;
|
|
19
|
+
constructor(options) {
|
|
20
|
+
this.paths = options.paths;
|
|
21
|
+
this.manifest = options.manifest;
|
|
22
|
+
this.persist = options.persist;
|
|
23
|
+
this.eventStream = options.eventStream;
|
|
24
|
+
this.onEntry = options.onEntry;
|
|
25
|
+
this.now = options.now ?? isoTimestamp;
|
|
26
|
+
this.pollIntervalMs = options.pollIntervalMs ?? 1000;
|
|
27
|
+
}
|
|
28
|
+
isCanceled() {
|
|
29
|
+
return this.cancelRequested;
|
|
30
|
+
}
|
|
31
|
+
isFailed() {
|
|
32
|
+
return this.failureRequested;
|
|
33
|
+
}
|
|
34
|
+
async sync() {
|
|
35
|
+
const snapshot = await readControlFile(this.paths.controlPath);
|
|
36
|
+
if (!snapshot) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const controlSeq = snapshot.control_seq ?? 0;
|
|
40
|
+
if (controlSeq <= this.lastControlSeq) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
this.lastControlSeq = controlSeq;
|
|
44
|
+
const latest = snapshot.latest_action;
|
|
45
|
+
if (latest &&
|
|
46
|
+
typeof latest === 'object' &&
|
|
47
|
+
(Object.prototype.hasOwnProperty.call(latest, 'confirm_nonce') ||
|
|
48
|
+
Object.prototype.hasOwnProperty.call(latest, 'confirmNonce'))) {
|
|
49
|
+
await this.safeAppend({
|
|
50
|
+
event: 'security_violation',
|
|
51
|
+
actor: 'runner',
|
|
52
|
+
payload: {
|
|
53
|
+
kind: 'confirm_nonce_present',
|
|
54
|
+
summary: 'confirm_nonce present in control action',
|
|
55
|
+
severity: 'high',
|
|
56
|
+
related_request_id: latest.request_id ?? null,
|
|
57
|
+
details_redacted: true
|
|
58
|
+
},
|
|
59
|
+
timestamp: this.now()
|
|
60
|
+
});
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const action = latest?.action;
|
|
64
|
+
if (!action) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (action === 'pause') {
|
|
68
|
+
await this.handlePause(snapshot);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (action === 'resume') {
|
|
72
|
+
await this.handleResume(snapshot);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (action === 'cancel') {
|
|
76
|
+
await this.handleCancel(snapshot);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (action === 'fail') {
|
|
80
|
+
await this.handleFail(snapshot);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
async waitForResume() {
|
|
84
|
+
if (!this.paused) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
while (this.paused && !this.cancelRequested && !this.failureRequested) {
|
|
88
|
+
await delay(this.pollIntervalMs);
|
|
89
|
+
await this.sync();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
async handlePause(snapshot) {
|
|
93
|
+
const nextRequestId = snapshot.latest_action?.request_id ?? null;
|
|
94
|
+
const nextReason = snapshot.latest_action?.reason ?? null;
|
|
95
|
+
if (this.paused && nextRequestId === this.lastPauseRequestId && nextReason === this.lastPauseReason) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const wasPaused = this.paused;
|
|
99
|
+
this.paused = true;
|
|
100
|
+
this.lastPauseRequestId = nextRequestId;
|
|
101
|
+
this.lastPauseReason = nextReason;
|
|
102
|
+
const nextDetail = nextReason ?? 'paused';
|
|
103
|
+
if (!wasPaused || this.manifest.status_detail !== nextDetail) {
|
|
104
|
+
this.manifest.status_detail = nextDetail;
|
|
105
|
+
if (!wasPaused) {
|
|
106
|
+
appendSummary(this.manifest, 'Run paused by control request.');
|
|
107
|
+
}
|
|
108
|
+
await this.persist();
|
|
109
|
+
}
|
|
110
|
+
await this.safeAppend({
|
|
111
|
+
event: 'pause_requested',
|
|
112
|
+
actor: snapshot.latest_action?.requested_by ?? 'user',
|
|
113
|
+
payload: {
|
|
114
|
+
request_id: snapshot.latest_action?.request_id ?? null,
|
|
115
|
+
control_seq: snapshot.control_seq ?? null,
|
|
116
|
+
requested_by: snapshot.latest_action?.requested_by ?? null,
|
|
117
|
+
reason: snapshot.latest_action?.reason ?? null
|
|
118
|
+
},
|
|
119
|
+
timestamp: this.now()
|
|
120
|
+
});
|
|
121
|
+
await this.safeAppend({
|
|
122
|
+
event: 'run_paused',
|
|
123
|
+
actor: 'runner',
|
|
124
|
+
payload: {
|
|
125
|
+
reason: 'control_request',
|
|
126
|
+
request_id: snapshot.latest_action?.request_id ?? null,
|
|
127
|
+
control_seq: snapshot.control_seq ?? null,
|
|
128
|
+
requested_reason: snapshot.latest_action?.reason ?? null
|
|
129
|
+
},
|
|
130
|
+
timestamp: this.now()
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
async handleResume(snapshot) {
|
|
134
|
+
if (!this.paused) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
this.paused = false;
|
|
138
|
+
this.lastPauseRequestId = null;
|
|
139
|
+
this.lastPauseReason = null;
|
|
140
|
+
this.manifest.status_detail = null;
|
|
141
|
+
appendSummary(this.manifest, 'Run resumed by control request.');
|
|
142
|
+
await this.persist();
|
|
143
|
+
await this.safeAppend({
|
|
144
|
+
event: 'run_resumed',
|
|
145
|
+
actor: 'runner',
|
|
146
|
+
payload: {
|
|
147
|
+
request_id: snapshot.latest_action?.request_id ?? null,
|
|
148
|
+
control_seq: snapshot.control_seq ?? null,
|
|
149
|
+
requested_by: snapshot.latest_action?.requested_by ?? null,
|
|
150
|
+
requested_reason: snapshot.latest_action?.reason ?? null
|
|
151
|
+
},
|
|
152
|
+
timestamp: this.now()
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
async handleCancel(snapshot) {
|
|
156
|
+
if (this.cancelRequested) {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
this.cancelRequested = true;
|
|
160
|
+
appendSummary(this.manifest, 'Run cancellation requested.');
|
|
161
|
+
await this.persist();
|
|
162
|
+
await this.safeAppend({
|
|
163
|
+
event: 'run_canceled',
|
|
164
|
+
actor: 'runner',
|
|
165
|
+
payload: {
|
|
166
|
+
request_id: snapshot.latest_action?.request_id ?? null,
|
|
167
|
+
control_seq: snapshot.control_seq ?? null,
|
|
168
|
+
requested_by: snapshot.latest_action?.requested_by ?? null,
|
|
169
|
+
requested_reason: snapshot.latest_action?.reason ?? null
|
|
170
|
+
},
|
|
171
|
+
timestamp: this.now()
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
async handleFail(snapshot) {
|
|
175
|
+
if (this.failureRequested) {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
this.failureRequested = true;
|
|
179
|
+
this.manifest.status_detail = snapshot.latest_action?.reason ?? 'control_failed';
|
|
180
|
+
appendSummary(this.manifest, 'Run failed by control request.');
|
|
181
|
+
await this.persist();
|
|
182
|
+
await this.safeAppend({
|
|
183
|
+
event: 'run_failed',
|
|
184
|
+
actor: 'runner',
|
|
185
|
+
payload: {
|
|
186
|
+
reason: 'control_request',
|
|
187
|
+
request_id: snapshot.latest_action?.request_id ?? null,
|
|
188
|
+
control_seq: snapshot.control_seq ?? null,
|
|
189
|
+
requested_by: snapshot.latest_action?.requested_by ?? null,
|
|
190
|
+
requested_reason: snapshot.latest_action?.reason ?? null
|
|
191
|
+
},
|
|
192
|
+
timestamp: this.now()
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
async safeAppend(entry) {
|
|
196
|
+
try {
|
|
197
|
+
const appended = await this.eventStream?.append(entry);
|
|
198
|
+
if (appended) {
|
|
199
|
+
this.onEntry?.(appended);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
catch (error) {
|
|
203
|
+
logger.warn(`[ControlWatcher] Failed to append event: ${error?.message ?? String(error)}`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
async function readControlFile(pathname) {
|
|
208
|
+
try {
|
|
209
|
+
const raw = await readFile(pathname, 'utf8');
|
|
210
|
+
return JSON.parse(raw);
|
|
211
|
+
}
|
|
212
|
+
catch (error) {
|
|
213
|
+
if (error.code === 'ENOENT') {
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
logger.warn(`[ControlWatcher] Failed to read control file: ${error?.message ?? String(error)}`);
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
function delay(ms) {
|
|
221
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
222
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { createHash, randomBytes, timingSafeEqual } from 'node:crypto';
|
|
2
|
+
export class DelegationTokenStore {
|
|
3
|
+
now;
|
|
4
|
+
records = new Map();
|
|
5
|
+
constructor(options = {}) {
|
|
6
|
+
this.now = options.now ?? (() => new Date().toISOString());
|
|
7
|
+
if (options.seed) {
|
|
8
|
+
for (const record of options.seed) {
|
|
9
|
+
this.records.set(record.token_id, { ...record });
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
issue(parentRunId, childRunId) {
|
|
14
|
+
const token = randomBytes(32).toString('hex');
|
|
15
|
+
const record = {
|
|
16
|
+
token_id: `dlt-${randomBytes(8).toString('hex')}`,
|
|
17
|
+
token_hash: hashToken(token),
|
|
18
|
+
parent_run_id: parentRunId,
|
|
19
|
+
child_run_id: childRunId,
|
|
20
|
+
created_at: this.now(),
|
|
21
|
+
expires_at: null
|
|
22
|
+
};
|
|
23
|
+
this.records.set(record.token_id, record);
|
|
24
|
+
return { token, record };
|
|
25
|
+
}
|
|
26
|
+
register(token, parentRunId, childRunId) {
|
|
27
|
+
const record = {
|
|
28
|
+
token_id: `dlt-${randomBytes(8).toString('hex')}`,
|
|
29
|
+
token_hash: hashToken(token),
|
|
30
|
+
parent_run_id: parentRunId,
|
|
31
|
+
child_run_id: childRunId,
|
|
32
|
+
created_at: this.now(),
|
|
33
|
+
expires_at: null
|
|
34
|
+
};
|
|
35
|
+
this.records.set(record.token_id, record);
|
|
36
|
+
return { ...record };
|
|
37
|
+
}
|
|
38
|
+
validate(token, parentRunId, childRunId) {
|
|
39
|
+
const tokenHash = hashToken(token);
|
|
40
|
+
for (const record of this.records.values()) {
|
|
41
|
+
if (record.parent_run_id !== parentRunId || record.child_run_id !== childRunId) {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (timingSafeEqualStrings(record.token_hash, tokenHash)) {
|
|
45
|
+
return { ...record };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
list() {
|
|
51
|
+
return Array.from(this.records.values()).map((record) => ({ ...record }));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function hashToken(token) {
|
|
55
|
+
return createHash('sha256').update(token).digest('hex');
|
|
56
|
+
}
|
|
57
|
+
function timingSafeEqualStrings(left, right) {
|
|
58
|
+
if (left.length !== right.length) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
return timingSafeEqual(Buffer.from(left, 'utf8'), Buffer.from(right, 'utf8'));
|
|
62
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
export class QuestionQueue {
|
|
2
|
+
now;
|
|
3
|
+
records = new Map();
|
|
4
|
+
counter = 0;
|
|
5
|
+
constructor(options = {}) {
|
|
6
|
+
this.now = options.now ?? (() => new Date().toISOString());
|
|
7
|
+
if (options.seed) {
|
|
8
|
+
this.hydrate(options.seed);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
enqueue(input) {
|
|
12
|
+
const queuedAt = this.now();
|
|
13
|
+
const expiresAt = typeof input.expiresInMs === 'number' && input.expiresInMs > 0
|
|
14
|
+
? new Date(Date.parse(queuedAt) + input.expiresInMs).toISOString()
|
|
15
|
+
: null;
|
|
16
|
+
const question = {
|
|
17
|
+
question_id: this.nextId(),
|
|
18
|
+
parent_run_id: input.parentRunId,
|
|
19
|
+
from_run_id: input.fromRunId,
|
|
20
|
+
from_manifest_path: input.fromManifestPath ?? null,
|
|
21
|
+
prompt: input.prompt,
|
|
22
|
+
urgency: input.urgency,
|
|
23
|
+
status: 'queued',
|
|
24
|
+
queued_at: queuedAt,
|
|
25
|
+
expires_at: expiresAt,
|
|
26
|
+
expires_in_ms: typeof input.expiresInMs === 'number' ? input.expiresInMs : null,
|
|
27
|
+
auto_pause: input.autoPause ?? true,
|
|
28
|
+
expiry_fallback: input.expiryFallback ?? null
|
|
29
|
+
};
|
|
30
|
+
this.records.set(question.question_id, question);
|
|
31
|
+
return question;
|
|
32
|
+
}
|
|
33
|
+
get(questionId) {
|
|
34
|
+
return this.records.get(questionId);
|
|
35
|
+
}
|
|
36
|
+
list() {
|
|
37
|
+
return Array.from(this.records.values());
|
|
38
|
+
}
|
|
39
|
+
answer(questionId, answer, answeredBy) {
|
|
40
|
+
const record = this.records.get(questionId);
|
|
41
|
+
if (!record) {
|
|
42
|
+
throw new Error('question_not_found');
|
|
43
|
+
}
|
|
44
|
+
if (record.status !== 'queued') {
|
|
45
|
+
throw new Error('question_closed');
|
|
46
|
+
}
|
|
47
|
+
const now = this.now();
|
|
48
|
+
record.status = 'answered';
|
|
49
|
+
record.answer = answer;
|
|
50
|
+
record.answered_by = answeredBy;
|
|
51
|
+
record.answered_at = now;
|
|
52
|
+
record.closed_at = now;
|
|
53
|
+
}
|
|
54
|
+
dismiss(questionId, dismissedBy) {
|
|
55
|
+
const record = this.records.get(questionId);
|
|
56
|
+
if (!record) {
|
|
57
|
+
throw new Error('question_not_found');
|
|
58
|
+
}
|
|
59
|
+
if (record.status !== 'queued') {
|
|
60
|
+
throw new Error('question_closed');
|
|
61
|
+
}
|
|
62
|
+
const now = this.now();
|
|
63
|
+
record.status = 'dismissed';
|
|
64
|
+
record.dismissed_by = dismissedBy;
|
|
65
|
+
record.closed_at = now;
|
|
66
|
+
}
|
|
67
|
+
expire() {
|
|
68
|
+
const nowIso = this.now();
|
|
69
|
+
const now = Date.parse(nowIso);
|
|
70
|
+
const expired = [];
|
|
71
|
+
for (const record of this.records.values()) {
|
|
72
|
+
if (record.status !== 'queued' || !record.expires_at) {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (Date.parse(record.expires_at) <= now) {
|
|
76
|
+
record.status = 'expired';
|
|
77
|
+
record.closed_at = nowIso;
|
|
78
|
+
expired.push({ ...record });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return expired;
|
|
82
|
+
}
|
|
83
|
+
hydrate(records) {
|
|
84
|
+
for (const record of records) {
|
|
85
|
+
this.records.set(record.question_id, { ...record });
|
|
86
|
+
}
|
|
87
|
+
this.counter = Math.max(this.counter, resolveCounter(records));
|
|
88
|
+
}
|
|
89
|
+
nextId() {
|
|
90
|
+
this.counter += 1;
|
|
91
|
+
return `q-${this.counter.toString().padStart(4, '0')}`;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function resolveCounter(records) {
|
|
95
|
+
let max = 0;
|
|
96
|
+
for (const record of records) {
|
|
97
|
+
const match = /^q-(\d+)/.exec(record.question_id);
|
|
98
|
+
if (match) {
|
|
99
|
+
const value = Number.parseInt(match[1] ?? '0', 10);
|
|
100
|
+
if (Number.isFinite(value)) {
|
|
101
|
+
max = Math.max(max, value);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return max;
|
|
106
|
+
}
|