@dotdrelle/wiki-manager 0.7.3 → 0.8.2
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/.env.example +20 -0
- package/README.md +35 -1
- package/docker-compose.yml +1 -23
- package/mcp.endpoints.example.json +13 -0
- package/package.json +2 -2
- package/src/agent/graph.js +101 -15
- package/src/agent/graph.test.js +145 -0
- package/src/cli/wiki-manager.js +274 -53
- package/src/commands/slash.js +1 -1
- package/src/core/agentEvents.js +124 -4
- package/src/core/agentEvents.test.js +145 -4
- package/src/core/agentLoop.js +3 -0
- package/src/core/compose.js +1 -2
- package/src/core/dockerCompose.test.js +5 -5
- package/src/core/jobQueue.js +29 -12
- package/src/core/mcp.js +120 -10
- package/src/core/mcp.test.js +121 -1
- package/src/core/plan.js +33 -0
- package/src/core/queueStore.test.js +1 -0
- package/src/core/wikiWorkspace.test.js +24 -0
- package/src/runtime/approvals.js +113 -0
- package/src/runtime/auth.test.js +8 -0
- package/src/runtime/client.js +52 -6
- package/src/runtime/lifecycle.js +27 -3
- package/src/runtime/queueStore.js +3 -3
- package/src/runtime/runner.js +340 -0
- package/src/runtime/runner.test.js +270 -0
- package/src/runtime/server.js +85 -29
- package/src/runtime/server.test.js +255 -0
- package/src/runtime/store.js +178 -39
- package/src/runtime/store.test.js +338 -4
- package/src/runtime/supervisor.js +6 -0
- package/src/runtime/supervisor.test.js +141 -0
- package/src/shell/RightPane.tsx +1 -1
- package/src/shell/repl.js +22 -6
- package/src/shell/useAgent.ts +1 -1
- package/src/shell/useSession.ts +10 -5
- package/wiki-workspace +198 -4
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import { createAgentEvent, dispatchAgentEvent } from '../core/agentEvents.js';
|
|
4
|
+
import { finishRuntimeRun, runRuntimeAgenticWorkflow } from './runner.js';
|
|
5
|
+
|
|
6
|
+
test('finishRuntimeRun emits evaluation before run_done', async () => {
|
|
7
|
+
const events = [];
|
|
8
|
+
const session = {
|
|
9
|
+
activities: {},
|
|
10
|
+
headlessPlan: [
|
|
11
|
+
{ step: 1, description: 'Analyze', status: 'done' },
|
|
12
|
+
{ step: 2, description: 'Execute', status: 'done' },
|
|
13
|
+
],
|
|
14
|
+
agentProjection: {
|
|
15
|
+
conversation: [{ role: 'assistant', content: 'Done.' }],
|
|
16
|
+
},
|
|
17
|
+
llm: {
|
|
18
|
+
async completeWithTools({ system, tools, messages }) {
|
|
19
|
+
assert.match(system, /strict evaluator/);
|
|
20
|
+
assert.deepEqual(tools, []);
|
|
21
|
+
assert.match(messages[0].content, /Original task:/);
|
|
22
|
+
return { content: '{"ok":true,"reason":"Task complete.","suggestedAction":null}' };
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
_onAgentEvent: (event) => events.push(event),
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const result = await finishRuntimeRun(session, 'Build workspace', { runId: 'run-1' });
|
|
29
|
+
|
|
30
|
+
assert.equal(result.ok, true);
|
|
31
|
+
assert.deepEqual(events.map((event) => event.type), ['runtime_log', 'run_evaluated', 'run_done']);
|
|
32
|
+
assert.equal(session.agentProjection.evaluation.ok, true);
|
|
33
|
+
assert.equal(session.agentProjection.status, 'done');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('finishRuntimeRun turns negative evaluation into run_error', async () => {
|
|
37
|
+
const events = [];
|
|
38
|
+
const session = {
|
|
39
|
+
activities: {},
|
|
40
|
+
headlessPlan: [{ step: 1, description: 'Export', status: 'done' }],
|
|
41
|
+
agentProjection: {
|
|
42
|
+
conversation: [{ role: 'assistant', content: 'Done.' }],
|
|
43
|
+
},
|
|
44
|
+
llm: {
|
|
45
|
+
async completeWithTools() {
|
|
46
|
+
return { content: '{"ok":false,"reason":"Export file missing.","suggestedAction":"Run export again."}' };
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
_onAgentEvent: (event) => events.push(event),
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const result = await finishRuntimeRun(session, 'Export deliverable', { runId: 'run-2' });
|
|
53
|
+
|
|
54
|
+
assert.equal(result.ok, false);
|
|
55
|
+
assert.equal(result.evaluationRejected, true);
|
|
56
|
+
assert.deepEqual(events.map((event) => event.type), ['runtime_log', 'run_evaluated', 'run_error']);
|
|
57
|
+
assert.equal(session.agentProjection.evaluation.ok, false);
|
|
58
|
+
assert.equal(session.agentProjection.status, 'error');
|
|
59
|
+
assert.match(session.agentProjection.logs.at(-1), /Export file missing/);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('finishRuntimeRun falls back open when evaluator response is invalid', async () => {
|
|
63
|
+
const session = {
|
|
64
|
+
activities: {},
|
|
65
|
+
headlessPlan: null,
|
|
66
|
+
agentProjection: { conversation: [] },
|
|
67
|
+
llm: {
|
|
68
|
+
async completeWithTools() {
|
|
69
|
+
return { content: 'not json' };
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const result = await finishRuntimeRun(session, 'Do work', { runId: 'run-3' });
|
|
75
|
+
|
|
76
|
+
assert.equal(result.ok, true);
|
|
77
|
+
assert.equal(session.agentProjection.evaluation.ok, true);
|
|
78
|
+
assert.match(session.agentProjection.evaluation.reason, /Evaluator unavailable/);
|
|
79
|
+
assert.equal(session.agentProjection.status, 'done');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('finishRuntimeRun can skip evaluation', async () => {
|
|
83
|
+
let called = false;
|
|
84
|
+
const session = {
|
|
85
|
+
activities: {},
|
|
86
|
+
headlessPlan: null,
|
|
87
|
+
agentProjection: { conversation: [] },
|
|
88
|
+
llm: {
|
|
89
|
+
async completeWithTools() {
|
|
90
|
+
called = true;
|
|
91
|
+
return { content: '{"ok":true,"reason":"ok"}' };
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const result = await finishRuntimeRun(session, 'Do work', { runId: 'run-4', evaluate: false });
|
|
97
|
+
|
|
98
|
+
assert.equal(result.ok, true);
|
|
99
|
+
assert.equal(result.evaluation, null);
|
|
100
|
+
assert.equal(called, false);
|
|
101
|
+
assert.equal(session.agentProjection.evaluation, null);
|
|
102
|
+
assert.equal(session.agentProjection.status, 'done');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test('runRuntimeAgenticWorkflow replans after negative evaluation', async () => {
|
|
106
|
+
const events = [];
|
|
107
|
+
const llmCalls = [];
|
|
108
|
+
const session = {
|
|
109
|
+
activities: {},
|
|
110
|
+
headlessPlan: null,
|
|
111
|
+
llm: {
|
|
112
|
+
async completeWithTools({ system }) {
|
|
113
|
+
llmCalls.push(system);
|
|
114
|
+
if (/strict evaluator/.test(system) && llmCalls.length === 1) {
|
|
115
|
+
return { content: '{"ok":false,"reason":"Export missing.","suggestedAction":"Run export."}' };
|
|
116
|
+
}
|
|
117
|
+
if (/replanner/.test(system)) {
|
|
118
|
+
return { content: '{"steps":["Run export"]}' };
|
|
119
|
+
}
|
|
120
|
+
return { content: '{"ok":true,"reason":"Export complete.","suggestedAction":null}' };
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
_onAgentEvent: (event) => events.push(event),
|
|
124
|
+
};
|
|
125
|
+
let turns = 0;
|
|
126
|
+
const agent = {
|
|
127
|
+
async invoke({ session: turnSession }) {
|
|
128
|
+
turns += 1;
|
|
129
|
+
if (turnSession.headlessPlan?.[0]?.status === 'pending') {
|
|
130
|
+
dispatchAgentEvent(turnSession, createAgentEvent('plan_step_updated', {
|
|
131
|
+
origin: 'tool',
|
|
132
|
+
payload: { step: 1, status: 'done' },
|
|
133
|
+
}));
|
|
134
|
+
}
|
|
135
|
+
return { response: turns === 1 ? 'Initial done.' : 'Export done.' };
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const result = await runRuntimeAgenticWorkflow(agent, session, 'Export deliverable', {
|
|
140
|
+
runId: 'run-replan-eval',
|
|
141
|
+
timeoutMs: 1000,
|
|
142
|
+
maxTurns: 2,
|
|
143
|
+
maxReplans: 1,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
assert.equal(result.ok, true);
|
|
147
|
+
assert.equal(turns, 2);
|
|
148
|
+
assert.ok(events.some((event) => event.type === 'run_replanned'));
|
|
149
|
+
assert.deepEqual(session.agentProjection.replans[0].plan, ['Run export']);
|
|
150
|
+
assert.equal(session.agentProjection.status, 'done');
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test('runRuntimeAgenticWorkflow replans after terminal activity error', async () => {
|
|
154
|
+
const originalFetch = globalThis.fetch;
|
|
155
|
+
let pollAttempts = 0;
|
|
156
|
+
globalThis.fetch = async () => {
|
|
157
|
+
pollAttempts += 1;
|
|
158
|
+
return {
|
|
159
|
+
ok: true,
|
|
160
|
+
status: 200,
|
|
161
|
+
headers: { get: () => null },
|
|
162
|
+
text: async () => JSON.stringify({
|
|
163
|
+
result: {
|
|
164
|
+
content: [{ type: 'text', text: JSON.stringify({
|
|
165
|
+
_activity: {
|
|
166
|
+
id: 'job-failed',
|
|
167
|
+
source: 'production',
|
|
168
|
+
label: 'Production build',
|
|
169
|
+
status: 'error',
|
|
170
|
+
terminal: true,
|
|
171
|
+
error: 'build failed',
|
|
172
|
+
},
|
|
173
|
+
}) }],
|
|
174
|
+
},
|
|
175
|
+
}),
|
|
176
|
+
};
|
|
177
|
+
};
|
|
178
|
+
const session = {
|
|
179
|
+
mcp: {
|
|
180
|
+
production: {
|
|
181
|
+
status: 'connected',
|
|
182
|
+
url: 'http://127.0.0.1:3000/mcp/',
|
|
183
|
+
retry: { maxAttempts: 1, backoffMs: 0 },
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
activities: {},
|
|
187
|
+
headlessPlan: null,
|
|
188
|
+
llm: {
|
|
189
|
+
async completeWithTools({ system }) {
|
|
190
|
+
if (/replanner/.test(system)) return { content: '{"steps":["Retry build"]}' };
|
|
191
|
+
return { content: '{"ok":true,"reason":"Build complete.","suggestedAction":null}' };
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
let turns = 0;
|
|
196
|
+
const agent = {
|
|
197
|
+
async invoke({ session: turnSession }) {
|
|
198
|
+
turns += 1;
|
|
199
|
+
if (turns === 1) {
|
|
200
|
+
dispatchAgentEvent(turnSession, createAgentEvent('activity_upserted', {
|
|
201
|
+
payload: {
|
|
202
|
+
activity: {
|
|
203
|
+
id: 'job-failed',
|
|
204
|
+
source: 'production',
|
|
205
|
+
label: 'Production build',
|
|
206
|
+
status: 'running',
|
|
207
|
+
terminal: false,
|
|
208
|
+
poll: { server: 'production', tool: 'production_job_status', args: { jobId: 'job-failed' }, intervalMs: 0 },
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
}));
|
|
212
|
+
return { response: 'Started build.' };
|
|
213
|
+
}
|
|
214
|
+
if (turnSession.headlessPlan?.[0]?.status === 'pending') {
|
|
215
|
+
dispatchAgentEvent(turnSession, createAgentEvent('plan_step_updated', {
|
|
216
|
+
origin: 'tool',
|
|
217
|
+
payload: { step: 1, status: 'done' },
|
|
218
|
+
}));
|
|
219
|
+
}
|
|
220
|
+
return { response: 'Retry done.' };
|
|
221
|
+
},
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
try {
|
|
225
|
+
const result = await runRuntimeAgenticWorkflow(agent, session, 'Build workspace', {
|
|
226
|
+
runId: 'run-replan-activity',
|
|
227
|
+
timeoutMs: 1000,
|
|
228
|
+
maxTurns: 3,
|
|
229
|
+
maxReplans: 1,
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
assert.equal(result.ok, true);
|
|
233
|
+
assert.equal(pollAttempts, 1);
|
|
234
|
+
assert.equal(turns, 2);
|
|
235
|
+
assert.equal(session.agentProjection.replans[0].reason, 'Production build ended with error: build failed');
|
|
236
|
+
assert.deepEqual(session.agentProjection.replans[0].plan, ['Retry build']);
|
|
237
|
+
assert.equal(session.agentProjection.status, 'done');
|
|
238
|
+
} finally {
|
|
239
|
+
globalThis.fetch = originalFetch;
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test('runRuntimeAgenticWorkflow stops after replan budget is exhausted', async () => {
|
|
244
|
+
const session = {
|
|
245
|
+
activities: {},
|
|
246
|
+
headlessPlan: null,
|
|
247
|
+
llm: {
|
|
248
|
+
async completeWithTools() {
|
|
249
|
+
return { content: '{"ok":false,"reason":"Still missing.","suggestedAction":"Try again."}' };
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
};
|
|
253
|
+
const agent = {
|
|
254
|
+
async invoke() {
|
|
255
|
+
return { response: 'Done.' };
|
|
256
|
+
},
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
const result = await runRuntimeAgenticWorkflow(agent, session, 'Do task', {
|
|
260
|
+
runId: 'run-replan-limit',
|
|
261
|
+
timeoutMs: 1000,
|
|
262
|
+
maxTurns: 1,
|
|
263
|
+
maxReplans: 0,
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
assert.equal(result.ok, false);
|
|
267
|
+
assert.equal(result.evaluationRejected, true);
|
|
268
|
+
assert.equal(session.agentProjection.status, 'error');
|
|
269
|
+
assert.equal(session.agentProjection.replans.length, 0);
|
|
270
|
+
});
|
package/src/runtime/server.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createServer } from 'node:http';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
2
3
|
import { runtimeTokenFromEnv } from './auth.js';
|
|
3
4
|
|
|
4
5
|
export function startRuntimeServer({
|
|
@@ -6,18 +7,22 @@ export function startRuntimeServer({
|
|
|
6
7
|
port = 7788,
|
|
7
8
|
token = runtimeTokenFromEnv(),
|
|
8
9
|
store,
|
|
9
|
-
session,
|
|
10
|
+
session = null,
|
|
11
|
+
getContext,
|
|
10
12
|
run,
|
|
11
13
|
cancel,
|
|
14
|
+
resume,
|
|
15
|
+
approve,
|
|
12
16
|
} = {}) {
|
|
13
17
|
const clients = new Set();
|
|
14
|
-
|
|
15
|
-
|
|
18
|
+
const defaultContext = { workspace: null, session, running: false, currentAbortController: null };
|
|
19
|
+
const resolvedGetContext = getContext ?? (() => defaultContext);
|
|
16
20
|
|
|
17
21
|
function publish(event) {
|
|
18
22
|
const payload = `event: agent_event\ndata: ${JSON.stringify(event)}\n\n`;
|
|
19
|
-
for (const
|
|
20
|
-
|
|
23
|
+
for (const client of clients) {
|
|
24
|
+
if (client.workspace && event.workspace !== client.workspace) continue;
|
|
25
|
+
client.response.write(payload);
|
|
21
26
|
}
|
|
22
27
|
}
|
|
23
28
|
|
|
@@ -30,69 +35,106 @@ export function startRuntimeServer({
|
|
|
30
35
|
|
|
31
36
|
const url = new URL(request.url ?? '/', `http://${request.headers.host ?? 'localhost'}`);
|
|
32
37
|
if (request.method === 'GET' && url.pathname === '/health') {
|
|
33
|
-
|
|
38
|
+
const workspace = workspaceFromUrl(url);
|
|
39
|
+
const context = workspace ? await resolveContext({ workspace }) : null;
|
|
40
|
+
sendJson(response, 200, {
|
|
41
|
+
ok: true,
|
|
42
|
+
status: context?.running ? 'running' : 'idle',
|
|
43
|
+
workspace: context?.workspace ?? workspace ?? null,
|
|
44
|
+
dbPath: store.dbPath,
|
|
45
|
+
});
|
|
34
46
|
return;
|
|
35
47
|
}
|
|
36
48
|
if (request.method === 'GET' && url.pathname === '/state') {
|
|
37
|
-
|
|
49
|
+
const workspace = workspaceFromUrl(url);
|
|
50
|
+
const context = workspace ? await resolveContext({ workspace }) : null;
|
|
51
|
+
sendJson(response, 200, store.getState(context?.session ?? session, { workspace }));
|
|
38
52
|
return;
|
|
39
53
|
}
|
|
40
54
|
if (request.method === 'GET' && url.pathname === '/events') {
|
|
41
|
-
|
|
55
|
+
const workspace = workspaceFromUrl(url);
|
|
56
|
+
sendJson(response, 200, { events: store.listEvents({ workspace }) });
|
|
42
57
|
return;
|
|
43
58
|
}
|
|
44
59
|
if (request.method === 'GET' && url.pathname === '/events/stream') {
|
|
60
|
+
const workspace = workspaceFromUrl(url);
|
|
61
|
+
const context = workspace ? await resolveContext({ workspace }) : null;
|
|
45
62
|
response.writeHead(200, {
|
|
46
63
|
'Content-Type': 'text/event-stream',
|
|
47
64
|
'Cache-Control': 'no-cache',
|
|
48
65
|
Connection: 'keep-alive',
|
|
49
66
|
'X-Accel-Buffering': 'no',
|
|
50
67
|
});
|
|
51
|
-
response.write(`event: state\ndata: ${JSON.stringify(store.getState(session))}\n\n`);
|
|
52
|
-
|
|
53
|
-
|
|
68
|
+
response.write(`event: state\ndata: ${JSON.stringify(store.getState(context?.session ?? session, { workspace }))}\n\n`);
|
|
69
|
+
const client = { response, workspace };
|
|
70
|
+
clients.add(client);
|
|
71
|
+
request.on('close', () => clients.delete(client));
|
|
54
72
|
return;
|
|
55
73
|
}
|
|
56
74
|
if (request.method === 'POST' && url.pathname === '/run') {
|
|
57
|
-
|
|
75
|
+
const body = await readJson(request);
|
|
76
|
+
const workspace = workspaceFromBody(body) ?? workspaceFromUrl(url);
|
|
77
|
+
const context = await resolveContext({ workspace });
|
|
78
|
+
if (context.running) {
|
|
58
79
|
sendJson(response, 409, { error: 'A runtime run is already active.' });
|
|
59
80
|
return;
|
|
60
81
|
}
|
|
61
|
-
running = true;
|
|
62
|
-
currentAbortController = new AbortController();
|
|
63
82
|
try {
|
|
64
|
-
const body = await readJson(request);
|
|
65
83
|
const input = String(body.input ?? body.prompt ?? '').trim();
|
|
66
84
|
if (!input) {
|
|
67
|
-
running = false;
|
|
68
|
-
currentAbortController = null;
|
|
69
85
|
sendJson(response, 400, { error: 'Missing input.' });
|
|
70
86
|
return;
|
|
71
87
|
}
|
|
72
|
-
|
|
88
|
+
const runId = randomUUID();
|
|
89
|
+
const runWorkspace = context.workspace ?? workspace ?? null;
|
|
90
|
+
context.running = true;
|
|
91
|
+
context.currentAbortController = new AbortController();
|
|
92
|
+
const runBody = { ...body, workspace: runWorkspace, runId };
|
|
93
|
+
const runPromise = run(context, runBody, { signal: context.currentAbortController.signal, runId });
|
|
94
|
+
runPromise
|
|
73
95
|
.catch((err) => {
|
|
74
|
-
session
|
|
96
|
+
context.session?._onRuntimeError?.(err);
|
|
75
97
|
})
|
|
76
98
|
.finally(() => {
|
|
77
|
-
running = false;
|
|
78
|
-
currentAbortController = null;
|
|
99
|
+
context.running = false;
|
|
100
|
+
context.currentAbortController = null;
|
|
79
101
|
});
|
|
80
|
-
sendJson(response, 202, { accepted: true });
|
|
102
|
+
sendJson(response, 202, { accepted: true, runId, workspace: runWorkspace });
|
|
81
103
|
} catch (err) {
|
|
82
|
-
running = false;
|
|
83
|
-
currentAbortController = null;
|
|
104
|
+
context.running = false;
|
|
105
|
+
context.currentAbortController = null;
|
|
84
106
|
throw err;
|
|
85
107
|
}
|
|
86
108
|
return;
|
|
87
109
|
}
|
|
88
110
|
if (request.method === 'POST' && url.pathname === '/cancel') {
|
|
89
|
-
|
|
111
|
+
const workspace = workspaceFromUrl(url);
|
|
112
|
+
const context = await resolveContext({ workspace });
|
|
113
|
+
if (!context.running || !context.currentAbortController) {
|
|
90
114
|
sendJson(response, 200, { cancelled: false, reason: 'no active run' });
|
|
91
115
|
return;
|
|
92
116
|
}
|
|
93
|
-
currentAbortController.abort();
|
|
94
|
-
await cancel?.();
|
|
95
|
-
sendJson(response, 202, { cancelled: true });
|
|
117
|
+
context.currentAbortController.abort();
|
|
118
|
+
await cancel?.(context);
|
|
119
|
+
sendJson(response, 202, { cancelled: true, workspace: context.workspace ?? workspace ?? null });
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (request.method === 'POST' && url.pathname === '/resume') {
|
|
123
|
+
const workspace = workspaceFromUrl(url);
|
|
124
|
+
const result = await resume?.({ workspace });
|
|
125
|
+
sendJson(response, 202, result ?? { resumed: false, workspace: workspace ?? null });
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if (request.method === 'POST' && url.pathname === '/approve') {
|
|
129
|
+
const body = await readJson(request);
|
|
130
|
+
const workspace = workspaceFromBody(body) ?? workspaceFromUrl(url);
|
|
131
|
+
const result = await approve?.({
|
|
132
|
+
workspace,
|
|
133
|
+
runId: url.searchParams.get('runId') ?? body.runId ?? null,
|
|
134
|
+
itemId: url.searchParams.get('itemId') ?? body.itemId ?? null,
|
|
135
|
+
approvalId: url.searchParams.get('approvalId') ?? body.approvalId ?? null,
|
|
136
|
+
});
|
|
137
|
+
sendJson(response, result?.approved ? 202 : 404, result ?? { approved: false });
|
|
96
138
|
return;
|
|
97
139
|
}
|
|
98
140
|
|
|
@@ -112,13 +154,27 @@ export function startRuntimeServer({
|
|
|
112
154
|
port: typeof address === 'object' && address ? address.port : port,
|
|
113
155
|
publish,
|
|
114
156
|
close: () => new Promise((closeResolve, closeReject) => {
|
|
115
|
-
for (const
|
|
157
|
+
for (const client of clients) client.response.end();
|
|
116
158
|
clients.clear();
|
|
117
159
|
server.close((err) => (err ? closeReject(err) : closeResolve()));
|
|
118
160
|
}),
|
|
119
161
|
});
|
|
120
162
|
});
|
|
121
163
|
});
|
|
164
|
+
|
|
165
|
+
async function resolveContext({ workspace = null } = {}) {
|
|
166
|
+
return resolvedGetContext(workspace);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function workspaceFromUrl(url) {
|
|
171
|
+
const workspace = url.searchParams.get('workspace');
|
|
172
|
+
return workspace ? workspace.trim() || null : null;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function workspaceFromBody(body) {
|
|
176
|
+
const workspace = body?.workspace;
|
|
177
|
+
return workspace == null ? null : String(workspace).trim() || null;
|
|
122
178
|
}
|
|
123
179
|
|
|
124
180
|
function isAuthorized(request, token) {
|