warpmetal 0.9.0 → 0.9.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/README.md +49 -2
- package/docs/agent-insights-cli.md +123 -0
- package/docs/agent-work-cli.md +72 -0
- package/docs/session-handoff-transport.md +68 -0
- package/package.json +5 -3
- package/skills/warpmetal/SKILL.md +47 -0
- package/skills/warpmetal/references/cli-reference.md +54 -2
- package/skills/warpmetal/references/runtime.md +19 -0
- package/src/agent-insights.js +638 -0
- package/src/agent-management.js +146 -0
- package/src/agent-work.js +800 -0
- package/src/api.js +6 -4
- package/src/cli.js +90 -6
- package/src/contracts/agent-manager-control-v1.schema.json +565 -0
- package/src/contracts/http.js +3 -0
- package/src/contracts/insights.js +133 -0
- package/src/contracts/manager.js +86 -0
- package/src/contracts/session-handoff.js +49 -0
- package/src/contracts/sources.json +12 -0
- package/src/contracts/work-handoff.js +73 -0
- package/src/contracts/work.js +310 -0
- package/src/installer.js +5 -5
- package/src/session-handoff.js +915 -0
|
@@ -0,0 +1,638 @@
|
|
|
1
|
+
import { CliError } from "./errors.js";
|
|
2
|
+
import { rejectUnknownOptions, stringOption } from "./args.js";
|
|
3
|
+
import {
|
|
4
|
+
FINDING_ID,
|
|
5
|
+
INSIGHTS_ID,
|
|
6
|
+
insightsMutation,
|
|
7
|
+
insightsQuery,
|
|
8
|
+
projectInsightDetail,
|
|
9
|
+
projectInsightsList,
|
|
10
|
+
projectInsightsSettings,
|
|
11
|
+
projectInsightsSummary,
|
|
12
|
+
} from "./contracts/insights.js";
|
|
13
|
+
import {
|
|
14
|
+
MANAGER_ID,
|
|
15
|
+
managerListQuery,
|
|
16
|
+
managerMutation,
|
|
17
|
+
projectManager,
|
|
18
|
+
} from "./contracts/manager.js";
|
|
19
|
+
import { projectSessionHandoff } from "./contracts/session-handoff.js";
|
|
20
|
+
|
|
21
|
+
const COMMON = ["base-url", "json", "state-dir", "help", "token-file"];
|
|
22
|
+
const MUTATION = [...COMMON, "file", "idempotency-key"];
|
|
23
|
+
const SESSION = [...COMMON, "connection-file", "identity"];
|
|
24
|
+
const SAFE_IDEMPOTENCY_KEY = /^[A-Za-z0-9][A-Za-z0-9._:-]{3,159}$/;
|
|
25
|
+
const BOX_ID = /^sbx_[A-Za-z0-9_-]{4,72}$/;
|
|
26
|
+
|
|
27
|
+
function fail(message, exitCode = 2, code = "invalid_insights_command") {
|
|
28
|
+
throw new CliError(message, { exitCode, code });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function invalidResponse() {
|
|
32
|
+
fail("WarpMetal returned an invalid Agent Insights response.", 3, "agent_management_invalid_response");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function exactPositionals(positionals, expected, usage) {
|
|
36
|
+
if (positionals.length !== expected) fail(`Usage: ${usage}`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function identifier(value, label, pattern = INSIGHTS_ID) {
|
|
40
|
+
if (typeof value !== "string" || !pattern.test(value)) fail(`Invalid ${label}.`);
|
|
41
|
+
return value;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function segment(value) {
|
|
45
|
+
return encodeURIComponent(value);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function ownerBase(serverId, sandboxId) {
|
|
49
|
+
return `/servers/${segment(serverId)}/sandboxes/${segment(sandboxId)}/insights`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function fileOption(options) {
|
|
53
|
+
return stringOption(options, "file", { required: true });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function idempotencyKey(options, requestId) {
|
|
57
|
+
const value = stringOption(options, "idempotency-key") || requestId;
|
|
58
|
+
if (!SAFE_IDEMPOTENCY_KEY.test(value)) fail("Invalid idempotency key.");
|
|
59
|
+
return value;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function pretty(value) {
|
|
63
|
+
return JSON.stringify(value, null, 2);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function canonical(value) {
|
|
67
|
+
return Array.isArray(value) ? `[${value.map(canonical).join(",")}]`
|
|
68
|
+
: value !== null && typeof value === "object"
|
|
69
|
+
? `{${Object.keys(value).sort().map(key => `${JSON.stringify(key)}:${canonical(value[key])}`).join(",")}}`
|
|
70
|
+
: JSON.stringify(value);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function output(services, value) {
|
|
74
|
+
services.emit(services.context.stdout, value, services.context.json, pretty(value));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function settingsExit(value) {
|
|
78
|
+
return value.settings.status === "pending" ? 8
|
|
79
|
+
: ["ready", "disabled"].includes(value.settings.status) ? 0 : 5;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function policyExit(value) {
|
|
83
|
+
return value.status === "awaiting_runtime" ? 8 : value.status === "applied" ? 0 : 5;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function runExit(value) {
|
|
87
|
+
return ["reserved", "reviewing"].includes(value.state) ? 8
|
|
88
|
+
: ["recommended", "no_action", "needs_owner"].includes(value.state) ? 0 : 5;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function takeoverExit(value) {
|
|
92
|
+
return ["awaiting_runtime", "unsettled"].includes(value.state) ? 8
|
|
93
|
+
: ["ready", "resumed"].includes(value.state) ? 0 : 5;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function projected(result, projector) {
|
|
97
|
+
const value = projector(result?.data);
|
|
98
|
+
if (!value || canonical(value) !== canonical(result?.data)) invalidResponse();
|
|
99
|
+
return value;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function same(left, right) {
|
|
103
|
+
return canonical(left) === canonical(right);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function verifySettingsReceipt(value, body) {
|
|
107
|
+
if (value.settings.revision !== body.expectedRevision + 1 || value.settings.enabled !== body.enabled) invalidResponse();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function verifyFindingReceipt(value, body, recovered) {
|
|
111
|
+
const attention = value.finding.attention;
|
|
112
|
+
const requestedState = {
|
|
113
|
+
acknowledge: "acknowledged",
|
|
114
|
+
snooze: "snoozed",
|
|
115
|
+
dismiss: "dismissed",
|
|
116
|
+
}[body.action];
|
|
117
|
+
if (attention.revision !== body.expectedRevision + 1 || attention.state !== requestedState) invalidResponse();
|
|
118
|
+
if (body.action === "snooze") {
|
|
119
|
+
// The current-resource GET has no immutable mutation timestamp. It cannot
|
|
120
|
+
// prove runtime.now()+snoozeSeconds, even when the state still says snoozed.
|
|
121
|
+
if (recovered || typeof attention.snoozedUntil !== "string" || !Number.isFinite(Date.parse(attention.snoozedUntil))) invalidResponse();
|
|
122
|
+
} else if (attention.snoozedUntil !== null) invalidResponse();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function verifyPolicyReceipt(value, body) {
|
|
126
|
+
const duration = Date.parse(value.authorizationExpiresAt) - Date.parse(value.updatedAt);
|
|
127
|
+
if (value.revision !== body.expectedRevision + 1 || value.mode !== body.mode ||
|
|
128
|
+
!same(value.allowedRules, body.allowedRules) || value.dailyRunLimit !== body.dailyRunLimit ||
|
|
129
|
+
value.dailyInputTokenLimit !== body.dailyInputTokenLimit ||
|
|
130
|
+
value.dailyOutputTokenLimit !== body.dailyOutputTokenLimit ||
|
|
131
|
+
!Number.isFinite(duration) || duration !== body.authorizationValidSeconds * 1000) invalidResponse();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function verifyRecheckReceipt(value, body) {
|
|
135
|
+
if (value.manual !== true || value.findingRevision !== body.expectedFindingRevision ||
|
|
136
|
+
value.policyRevision !== body.expectedPolicyRevision || !same(value.source, body.source) ||
|
|
137
|
+
!same(value.target, body.target) || !same(value.reservedBudget, body.budget)) invalidResponse();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function verifyTakeoverReceipt(value, body) {
|
|
141
|
+
if (value.revision !== 1 || value.action !== "pause_manager_and_hold_member" || value.predecessorOperationId !== null ||
|
|
142
|
+
!same(value.source, body.source) || !same(value.target, body.target) ||
|
|
143
|
+
value.monitorPolicy.desiredMode !== "off" ||
|
|
144
|
+
value.monitorPolicy.desiredRevision !== body.expectedPolicyRevision + 1 ||
|
|
145
|
+
value.memberHold.desiredState !== "active" || value.memberHold.desiredRevision !== 1 ||
|
|
146
|
+
value.memberHold.brokerApplied !== true) invalidResponse();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function verifyResumePredecessor(value, body) {
|
|
150
|
+
const prior = value.target, target = body.target;
|
|
151
|
+
const taskMatches = target.taskId === null && target.taskAttempt === null ||
|
|
152
|
+
target.taskId === prior.taskId && target.taskAttempt === prior.taskAttempt;
|
|
153
|
+
const workMatches = ["workId", "workRevision", "bindingId", "bindingRevision"]
|
|
154
|
+
.every(key => target[key] === prior[key]);
|
|
155
|
+
if (value.action !== "pause_manager_and_hold_member" || value.state !== "ready" ||
|
|
156
|
+
value.revision !== body.expectedTakeoverRevision ||
|
|
157
|
+
value.monitorPolicy.desiredRevision !== body.expectedPolicyRevision ||
|
|
158
|
+
value.memberHold.desiredState !== "active" || value.memberHold.desiredRevision !== body.expectedHoldRevision ||
|
|
159
|
+
!same(value.source, body.source) || target.teamId !== prior.teamId ||
|
|
160
|
+
target.memberId !== prior.memberId || !taskMatches || !workMatches) invalidResponse();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function verifyResumeReceipt(value, body, predecessorId, predecessor) {
|
|
164
|
+
if (value.revision !== 1 || value.action !== "resume_manager_and_release_member" ||
|
|
165
|
+
value.predecessorOperationId !== predecessorId || !same(value.source, body.source) ||
|
|
166
|
+
!same(value.target, body.target) || value.monitorPolicy.desiredMode !== body.restoreMode ||
|
|
167
|
+
value.monitorPolicy.desiredRevision !== body.expectedPolicyRevision + 1 ||
|
|
168
|
+
value.memberHold.holdId !== predecessor.memberHold.holdId ||
|
|
169
|
+
value.memberHold.desiredState !== "released" ||
|
|
170
|
+
value.memberHold.desiredRevision !== body.expectedHoldRevision + 1 ||
|
|
171
|
+
value.memberHold.brokerApplied !== true) invalidResponse();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async function authority(serverId, services, options) {
|
|
175
|
+
return services.requireServerToken(services.store, serverId, options, services.context.env);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async function request(services, method, path, token, options = {}) {
|
|
179
|
+
return services.client.request(method, path, { token, ...options });
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function insightParams(options) {
|
|
183
|
+
const params = new URLSearchParams();
|
|
184
|
+
for (const [option, query] of [
|
|
185
|
+
["limit", "limit"], ["cursor", "cursor"], ["state", "state"],
|
|
186
|
+
["session", "session"], ["severity", "severity"], ["attention", "attention"],
|
|
187
|
+
["rule", "rule"], ["recent-hours", "recentHours"],
|
|
188
|
+
]) {
|
|
189
|
+
const value = stringOption(options, option);
|
|
190
|
+
if (value !== undefined) params.set(query, value);
|
|
191
|
+
}
|
|
192
|
+
const query = insightsQuery(params);
|
|
193
|
+
if (query === null) fail("Invalid Insights list query.");
|
|
194
|
+
return query;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function summaryParams(options) {
|
|
198
|
+
const params = new URLSearchParams();
|
|
199
|
+
const limit = stringOption(options, "limit");
|
|
200
|
+
const cursor = stringOption(options, "cursor");
|
|
201
|
+
if (limit !== undefined && (!/^[1-9][0-9]{0,2}$/.test(limit) || Number(limit) > 100)) fail("--limit must be between 1 and 100.");
|
|
202
|
+
if (cursor !== undefined && !BOX_ID.test(cursor)) fail("Invalid summary cursor.");
|
|
203
|
+
if (limit !== undefined) params.set("limit", limit);
|
|
204
|
+
if (cursor !== undefined) params.set("cursor", cursor);
|
|
205
|
+
return params.size ? `?${params}` : "";
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function managerParams(options, allowFinding) {
|
|
209
|
+
const params = new URLSearchParams();
|
|
210
|
+
for (const [option, query] of [["limit", "limit"], ["cursor", "cursor"], ["finding", "findingId"]]) {
|
|
211
|
+
const value = stringOption(options, option);
|
|
212
|
+
if (value !== undefined) params.set(query, value);
|
|
213
|
+
}
|
|
214
|
+
const query = managerListQuery(params, allowFinding);
|
|
215
|
+
if (query === null) fail("Invalid manager list query.");
|
|
216
|
+
return query;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async function readSettings(serverId, sandboxId, services, options) {
|
|
220
|
+
const token = await authority(serverId, services, options);
|
|
221
|
+
const path = `${ownerBase(serverId, sandboxId)}/settings`;
|
|
222
|
+
return projected(await request(services, "GET", path, token), projectInsightsSettings);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async function mutateSettings(verb, serverId, sandboxId, services, options) {
|
|
226
|
+
const body = insightsMutation(await services.readInput(fileOption(options), 64 * 1024));
|
|
227
|
+
if (!body || body.enabled !== (verb === "enable")) fail(`The input is not a valid Insights ${verb} mutation.`);
|
|
228
|
+
const token = await authority(serverId, services, options);
|
|
229
|
+
const path = `${ownerBase(serverId, sandboxId)}/settings`;
|
|
230
|
+
const key = idempotencyKey(options, body.requestId);
|
|
231
|
+
const result = await services.runMutation({
|
|
232
|
+
path, serverId, sandboxId, kind: `insights_${verb}`, requestId: body.requestId, body,
|
|
233
|
+
submit: () => request(services, "PATCH", path, token, { body, idempotencyKey: key }),
|
|
234
|
+
reconcile: () => request(services, "GET", path, token),
|
|
235
|
+
});
|
|
236
|
+
const value = projected(result, projectInsightsSettings);
|
|
237
|
+
verifySettingsReceipt(value, body);
|
|
238
|
+
return value;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async function mutateFinding(verb, serverId, sandboxId, findingId, services, options) {
|
|
242
|
+
const body = insightsMutation(await services.readInput(fileOption(options), 64 * 1024), true);
|
|
243
|
+
const expected = verb === "acknowledge" ? "acknowledge" : verb;
|
|
244
|
+
if (!body || body.action !== expected) fail(`The input is not a valid Insights ${verb} mutation.`);
|
|
245
|
+
const token = await authority(serverId, services, options);
|
|
246
|
+
const base = ownerBase(serverId, sandboxId);
|
|
247
|
+
const path = `${base}/${segment(findingId)}/actions`;
|
|
248
|
+
const key = idempotencyKey(options, body.requestId);
|
|
249
|
+
let recovered = false;
|
|
250
|
+
const result = await services.runMutation({
|
|
251
|
+
path, serverId, sandboxId, kind: `insights_${verb}`, requestId: body.requestId, body,
|
|
252
|
+
submit: () => request(services, "POST", path, token, { body, idempotencyKey: key }),
|
|
253
|
+
reconcile: () => {
|
|
254
|
+
recovered = true;
|
|
255
|
+
return request(services, "GET", `${base}/${segment(findingId)}`, token);
|
|
256
|
+
},
|
|
257
|
+
});
|
|
258
|
+
const value = projected(result, resultValue => projectInsightDetail(resultValue, findingId));
|
|
259
|
+
verifyFindingReceipt(value, body, recovered);
|
|
260
|
+
return value;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
async function readPolicy(serverId, sandboxId, services, options, token) {
|
|
264
|
+
const credential = token || await authority(serverId, services, options);
|
|
265
|
+
const path = `${ownerBase(serverId, sandboxId)}/manager/settings`;
|
|
266
|
+
return projected(await request(services, "GET", path, credential), value =>
|
|
267
|
+
projectManager(value, "policy", { sandboxId }));
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async function mutatePolicy(serverId, sandboxId, services, options) {
|
|
271
|
+
const body = managerMutation(await services.readInput(fileOption(options), 64 * 1024), "policyPatch");
|
|
272
|
+
if (!body) fail("The input is not a valid manager settings mutation.");
|
|
273
|
+
const token = await authority(serverId, services, options);
|
|
274
|
+
const path = `${ownerBase(serverId, sandboxId)}/manager/settings`;
|
|
275
|
+
const key = idempotencyKey(options, body.requestId);
|
|
276
|
+
const result = await services.runMutation({
|
|
277
|
+
path, serverId, sandboxId, kind: "manager_settings", requestId: body.requestId, body,
|
|
278
|
+
submit: () => request(services, "PATCH", path, token, { body, idempotencyKey: key }),
|
|
279
|
+
reconcile: () => request(services, "GET", path, token),
|
|
280
|
+
});
|
|
281
|
+
const value = projected(result, resultValue => projectManager(resultValue, "policy", { sandboxId }));
|
|
282
|
+
verifyPolicyReceipt(value, body);
|
|
283
|
+
return value;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
async function recheck(serverId, sandboxId, findingId, services, options) {
|
|
287
|
+
const body = managerMutation(await services.readInput(fileOption(options), 64 * 1024), "recheckRequest");
|
|
288
|
+
if (!body) fail("The input is not a valid manager Recheck request.");
|
|
289
|
+
const token = await authority(serverId, services, options);
|
|
290
|
+
const path = `${ownerBase(serverId, sandboxId)}/${segment(findingId)}/manager-rechecks`;
|
|
291
|
+
const key = idempotencyKey(options, body.requestId);
|
|
292
|
+
const result = await services.runMutation({
|
|
293
|
+
path, serverId, sandboxId, kind: "manager_recheck", requestId: body.requestId, body,
|
|
294
|
+
submit: () => request(services, "POST", path, token, { body, idempotencyKey: key }),
|
|
295
|
+
reconcile: () => request(services, "GET", `${path}?requestId=${segment(body.requestId)}`, token),
|
|
296
|
+
});
|
|
297
|
+
const value = projected(result, resultValue => projectManager(resultValue, "reservation", { findingId, requestId: body.requestId }));
|
|
298
|
+
verifyRecheckReceipt(value, body);
|
|
299
|
+
return value;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
async function createTakeover(serverId, sandboxId, findingId, services, options) {
|
|
303
|
+
const body = managerMutation(await services.readInput(fileOption(options), 64 * 1024), "takeoverRequest");
|
|
304
|
+
if (!body) fail("The input is not a valid manager Takeover request.");
|
|
305
|
+
const token = await authority(serverId, services, options);
|
|
306
|
+
const base = `${ownerBase(serverId, sandboxId)}/${segment(findingId)}/takeovers`;
|
|
307
|
+
const key = idempotencyKey(options, body.requestId);
|
|
308
|
+
const result = await services.runMutation({
|
|
309
|
+
path: base, serverId, sandboxId, kind: "manager_takeover", requestId: body.requestId, body,
|
|
310
|
+
submit: () => request(services, "POST", base, token, { body, idempotencyKey: key }),
|
|
311
|
+
reconcile: () => request(services, "GET", `${base}/${segment(body.operationId)}`, token),
|
|
312
|
+
});
|
|
313
|
+
const value = projected(result, resultValue => projectManager(resultValue, "takeover", { findingId, operationId: body.operationId, requestId: body.requestId }));
|
|
314
|
+
verifyTakeoverReceipt(value, body);
|
|
315
|
+
return value;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
async function resumeTakeover(serverId, sandboxId, findingId, predecessorId, services, options) {
|
|
319
|
+
const body = managerMutation(await services.readInput(fileOption(options), 64 * 1024), "resumeRequest");
|
|
320
|
+
if (!body) fail("The input is not a valid manager Resume request.");
|
|
321
|
+
const token = await authority(serverId, services, options);
|
|
322
|
+
const base = `${ownerBase(serverId, sandboxId)}/${segment(findingId)}/takeovers`;
|
|
323
|
+
const path = `${base}/${segment(predecessorId)}/resume`;
|
|
324
|
+
const key = idempotencyKey(options, body.requestId);
|
|
325
|
+
const predecessor = projected(
|
|
326
|
+
await request(services, "GET", `${base}/${segment(predecessorId)}`, token),
|
|
327
|
+
value => projectManager(value, "takeover", { findingId, operationId: predecessorId }),
|
|
328
|
+
);
|
|
329
|
+
verifyResumePredecessor(predecessor, body);
|
|
330
|
+
const result = await services.runMutation({
|
|
331
|
+
path, serverId, sandboxId, kind: "manager_resume", requestId: body.requestId, body,
|
|
332
|
+
submit: () => request(services, "POST", path, token, { body, idempotencyKey: key }),
|
|
333
|
+
reconcile: () => request(services, "GET", `${base}/${segment(body.resumeOperationId)}`, token),
|
|
334
|
+
});
|
|
335
|
+
const value = projected(result, resultValue => projectManager(resultValue, "takeover", {
|
|
336
|
+
findingId, operationId: body.resumeOperationId, requestId: body.requestId,
|
|
337
|
+
}));
|
|
338
|
+
verifyResumeReceipt(value, body, predecessorId, predecessor);
|
|
339
|
+
return value;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function takeoverMatchesHandoff(takeover, handoff) {
|
|
343
|
+
const i = handoff.identity, s = handoff.source, source = takeover.source, target = takeover.target;
|
|
344
|
+
return i.teamId === target.teamId && i.memberId === target.memberId &&
|
|
345
|
+
i.sandboxGeneration === source.sandboxGeneration && i.serviceRegistrationId === source.serviceRegistrationId &&
|
|
346
|
+
i.serviceGeneration === source.serviceGeneration && i.workspaceEpoch === source.workspaceEpoch &&
|
|
347
|
+
i.profileRevision === source.profileRevision && i.instructionRevision === source.instructionRevision &&
|
|
348
|
+
s.registeredSourceId === source.registeredSourceId && s.nativeSessionId === source.nativeSessionId;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
async function verifiedTakeover(serverId, sandboxId, findingId, operationId, services, options, token) {
|
|
352
|
+
const base = `${ownerBase(serverId, sandboxId)}/${segment(findingId)}/takeovers`;
|
|
353
|
+
const takeover = projected(await request(services, "GET", `${base}/${segment(operationId)}`, token), value =>
|
|
354
|
+
projectManager(value, "takeover", { findingId, operationId }));
|
|
355
|
+
if (takeover.state !== "ready" || takeover.action !== "pause_manager_and_hold_member" ||
|
|
356
|
+
takeover.monitorPolicy.desiredMode !== "off" ||
|
|
357
|
+
takeover.monitorPolicy.appliedRevision !== takeover.monitorPolicy.desiredRevision ||
|
|
358
|
+
takeover.memberHold.desiredState !== "active" || !takeover.memberHold.brokerApplied ||
|
|
359
|
+
takeover.memberHold.nodeAppliedRevision !== takeover.memberHold.desiredRevision) {
|
|
360
|
+
fail("The requested Takeover is no longer an exact ready hold.", 5, "manager_takeover_stale");
|
|
361
|
+
}
|
|
362
|
+
const policy = await readPolicy(serverId, sandboxId, services, options, token);
|
|
363
|
+
if (policy.mode !== "off" || policy.status !== "applied" ||
|
|
364
|
+
policy.revision !== takeover.monitorPolicy.desiredRevision ||
|
|
365
|
+
policy.appliedRevision !== takeover.monitorPolicy.appliedRevision) {
|
|
366
|
+
fail("The requested Takeover no longer matches the current manager policy.", 5, "manager_takeover_stale");
|
|
367
|
+
}
|
|
368
|
+
const listing = projected(await request(services, "GET", `${base}?limit=100`, token), value =>
|
|
369
|
+
projectManager(value, "takeoverList", { findingId }));
|
|
370
|
+
if (listing.takeovers.some(row => row.predecessorOperationId === operationId)) {
|
|
371
|
+
fail("The requested Takeover has been superseded by Resume.", 5, "manager_takeover_stale");
|
|
372
|
+
}
|
|
373
|
+
const finding = projected(await request(services, "GET", `${ownerBase(serverId, sandboxId)}/${segment(findingId)}`, token), value =>
|
|
374
|
+
projectInsightDetail(value, findingId)).finding;
|
|
375
|
+
const current = projected(await request(services, "GET", `${ownerBase(serverId, sandboxId)}/${segment(findingId)}/manager-target`, token), value =>
|
|
376
|
+
projectManager(value, "managerTarget", { findingId }));
|
|
377
|
+
// The current manager target owns the complete Task and Work authority.
|
|
378
|
+
if (current.findingRevision !== finding.revision || !same(current.source, takeover.source) || !same(current.target, takeover.target)) {
|
|
379
|
+
fail("The requested Takeover no longer matches the current finding target.", 5, "manager_takeover_stale");
|
|
380
|
+
}
|
|
381
|
+
return takeover;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
async function handoff(kind, serverId, sandboxId, referenceId, services, options) {
|
|
385
|
+
const token = await authority(serverId, services, options);
|
|
386
|
+
let takeover = null;
|
|
387
|
+
if (kind === "finding") {
|
|
388
|
+
const operationId = stringOption(options, "takeover");
|
|
389
|
+
if (operationId !== undefined) {
|
|
390
|
+
identifier(operationId, "Takeover operation", MANAGER_ID);
|
|
391
|
+
takeover = await verifiedTakeover(serverId, sandboxId, referenceId, operationId, services, options, token);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
const path = kind === "finding"
|
|
395
|
+
? `${ownerBase(serverId, sandboxId)}/${segment(referenceId)}/handoff`
|
|
396
|
+
: `${ownerBase(serverId, sandboxId)}/manager/activity/${segment(referenceId)}/session-handoff`;
|
|
397
|
+
const envelope = projected(await request(services, "GET", path, token), value =>
|
|
398
|
+
projectSessionHandoff(value, serverId, sandboxId));
|
|
399
|
+
if (envelope.capability !== "exact_session" || !envelope.handoff) {
|
|
400
|
+
output(services, envelope);
|
|
401
|
+
return 5;
|
|
402
|
+
}
|
|
403
|
+
if (takeover && !takeoverMatchesHandoff(takeover, envelope.handoff)) {
|
|
404
|
+
fail("The Takeover does not match the current exact session.", 5, "manager_takeover_stale");
|
|
405
|
+
}
|
|
406
|
+
if (kind === "review" && envelope.handoff.identity.role !== "manager") {
|
|
407
|
+
fail("The manager run does not resolve to a manager session.", 5, "manager_session_unavailable");
|
|
408
|
+
}
|
|
409
|
+
if (services.context.json) {
|
|
410
|
+
output(services, envelope);
|
|
411
|
+
return 0;
|
|
412
|
+
}
|
|
413
|
+
const result = await services.openSessionHandoff(envelope, {
|
|
414
|
+
connectionFile: stringOption(options, "connection-file"),
|
|
415
|
+
identityPath: stringOption(options, "identity"),
|
|
416
|
+
context: { ...services.context, spawn: services.context.spawnImpl },
|
|
417
|
+
});
|
|
418
|
+
if (!Number.isInteger(result) || result < 0 || result > 255) {
|
|
419
|
+
fail("The session handoff transport returned an invalid exit status.", 3, "session_handoff_transport_invalid");
|
|
420
|
+
}
|
|
421
|
+
return result;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/** Positional Insights/manager owner API and exact-session handler. */
|
|
425
|
+
export async function handleInsights(positionals, options, services) {
|
|
426
|
+
const command = positionals.slice(0, 3).join(" ");
|
|
427
|
+
if (positionals[0] !== "insights") fail("Invalid Insights command.");
|
|
428
|
+
|
|
429
|
+
if (positionals[1] === "summary") {
|
|
430
|
+
exactPositionals(positionals, 3, "warpmetal insights summary SERVER");
|
|
431
|
+
rejectUnknownOptions(options, [...COMMON, "limit", "cursor"]);
|
|
432
|
+
const serverId = identifier(positionals[2], "server ID");
|
|
433
|
+
const token = await authority(serverId, services, options);
|
|
434
|
+
const result = await request(services, "GET", `/servers/${segment(serverId)}/insights/summary${summaryParams(options)}`, token);
|
|
435
|
+
const value = projected(result, data => projectInsightsSummary(data, serverId));
|
|
436
|
+
output(services, value);
|
|
437
|
+
return 0;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if (positionals[1] === "status") {
|
|
441
|
+
exactPositionals(positionals, 4, "warpmetal insights status SERVER BOX");
|
|
442
|
+
rejectUnknownOptions(options, COMMON);
|
|
443
|
+
const serverId = identifier(positionals[2], "server ID");
|
|
444
|
+
const sandboxId = identifier(positionals[3], "sandbox ID", BOX_ID);
|
|
445
|
+
const value = await readSettings(serverId, sandboxId, services, options);
|
|
446
|
+
output(services, value);
|
|
447
|
+
return settingsExit(value);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
if (["enable", "disable"].includes(positionals[1])) {
|
|
451
|
+
exactPositionals(positionals, 4, `warpmetal insights ${positionals[1]} SERVER BOX --file FILE`);
|
|
452
|
+
rejectUnknownOptions(options, MUTATION);
|
|
453
|
+
const serverId = identifier(positionals[2], "server ID");
|
|
454
|
+
const sandboxId = identifier(positionals[3], "sandbox ID", BOX_ID);
|
|
455
|
+
const value = await mutateSettings(positionals[1], serverId, sandboxId, services, options);
|
|
456
|
+
output(services, value);
|
|
457
|
+
return settingsExit(value);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
if (positionals[1] === "list") {
|
|
461
|
+
exactPositionals(positionals, 4, "warpmetal insights list SERVER BOX");
|
|
462
|
+
rejectUnknownOptions(options, [...COMMON, "limit", "cursor", "state", "session", "severity", "attention", "rule", "recent-hours"]);
|
|
463
|
+
const serverId = identifier(positionals[2], "server ID");
|
|
464
|
+
const sandboxId = identifier(positionals[3], "sandbox ID", BOX_ID);
|
|
465
|
+
const token = await authority(serverId, services, options);
|
|
466
|
+
const result = await request(services, "GET", `${ownerBase(serverId, sandboxId)}${insightParams(options)}`, token);
|
|
467
|
+
const value = projected(result, projectInsightsList);
|
|
468
|
+
output(services, value);
|
|
469
|
+
return 0;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
if (positionals[1] === "show") {
|
|
473
|
+
exactPositionals(positionals, 5, "warpmetal insights show SERVER BOX FINDING");
|
|
474
|
+
rejectUnknownOptions(options, COMMON);
|
|
475
|
+
const serverId = identifier(positionals[2], "server ID");
|
|
476
|
+
const sandboxId = identifier(positionals[3], "sandbox ID", BOX_ID);
|
|
477
|
+
const findingId = identifier(positionals[4], "finding ID", FINDING_ID);
|
|
478
|
+
const token = await authority(serverId, services, options);
|
|
479
|
+
const result = await request(services, "GET", `${ownerBase(serverId, sandboxId)}/${segment(findingId)}`, token);
|
|
480
|
+
const value = projected(result, data => projectInsightDetail(data, findingId));
|
|
481
|
+
output(services, value);
|
|
482
|
+
return 0;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
if (["acknowledge", "snooze", "dismiss"].includes(positionals[1])) {
|
|
486
|
+
exactPositionals(positionals, 5, `warpmetal insights ${positionals[1]} SERVER BOX FINDING --file FILE`);
|
|
487
|
+
rejectUnknownOptions(options, MUTATION);
|
|
488
|
+
const serverId = identifier(positionals[2], "server ID");
|
|
489
|
+
const sandboxId = identifier(positionals[3], "sandbox ID", BOX_ID);
|
|
490
|
+
const findingId = identifier(positionals[4], "finding ID", FINDING_ID);
|
|
491
|
+
const value = await mutateFinding(positionals[1], serverId, sandboxId, findingId, services, options);
|
|
492
|
+
output(services, value);
|
|
493
|
+
return 0;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
if (positionals[1] === "open") {
|
|
497
|
+
exactPositionals(positionals, 5, "warpmetal insights open SERVER BOX FINDING");
|
|
498
|
+
rejectUnknownOptions(options, [...SESSION, "takeover"]);
|
|
499
|
+
return handoff("finding", identifier(positionals[2], "server ID"),
|
|
500
|
+
identifier(positionals[3], "sandbox ID", BOX_ID), identifier(positionals[4], "finding ID", FINDING_ID), services, options);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
if (positionals[1] === "review") {
|
|
504
|
+
exactPositionals(positionals, 5, "warpmetal insights review SERVER BOX RUN");
|
|
505
|
+
rejectUnknownOptions(options, SESSION);
|
|
506
|
+
return handoff("review", identifier(positionals[2], "server ID"),
|
|
507
|
+
identifier(positionals[3], "sandbox ID", BOX_ID), identifier(positionals[4], "run ID", MANAGER_ID), services, options);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
if (command === "insights manager settings") {
|
|
511
|
+
exactPositionals(positionals, 5, "warpmetal insights manager settings SERVER BOX [--file FILE]");
|
|
512
|
+
rejectUnknownOptions(options, options.file === undefined ? COMMON : MUTATION);
|
|
513
|
+
const serverId = identifier(positionals[3], "server ID");
|
|
514
|
+
const sandboxId = identifier(positionals[4], "sandbox ID", BOX_ID);
|
|
515
|
+
const value = options.file === undefined
|
|
516
|
+
? await readPolicy(serverId, sandboxId, services, options)
|
|
517
|
+
: await mutatePolicy(serverId, sandboxId, services, options);
|
|
518
|
+
output(services, value);
|
|
519
|
+
return policyExit(value);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
if (command === "insights manager activity") {
|
|
523
|
+
exactPositionals(positionals, 5, "warpmetal insights manager activity SERVER BOX");
|
|
524
|
+
rejectUnknownOptions(options, [...COMMON, "limit", "cursor", "finding"]);
|
|
525
|
+
const serverId = identifier(positionals[3], "server ID");
|
|
526
|
+
const sandboxId = identifier(positionals[4], "sandbox ID", BOX_ID);
|
|
527
|
+
const token = await authority(serverId, services, options);
|
|
528
|
+
const result = await request(services, "GET", `${ownerBase(serverId, sandboxId)}/manager/activity${managerParams(options, true)}`, token);
|
|
529
|
+
const value = projected(result, data => projectManager(data, "activityList", {
|
|
530
|
+
findingId: stringOption(options, "finding"),
|
|
531
|
+
}));
|
|
532
|
+
output(services, value);
|
|
533
|
+
return 0;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
if (command === "insights manager run") {
|
|
537
|
+
exactPositionals(positionals, 6, "warpmetal insights manager run SERVER BOX RUN");
|
|
538
|
+
rejectUnknownOptions(options, COMMON);
|
|
539
|
+
const serverId = identifier(positionals[3], "server ID");
|
|
540
|
+
const sandboxId = identifier(positionals[4], "sandbox ID", BOX_ID);
|
|
541
|
+
const runId = identifier(positionals[5], "run ID", MANAGER_ID);
|
|
542
|
+
const token = await authority(serverId, services, options);
|
|
543
|
+
const result = await request(services, "GET", `${ownerBase(serverId, sandboxId)}/manager/activity/${segment(runId)}`, token);
|
|
544
|
+
const value = projected(result, data => projectManager(data, "activity", { runId }));
|
|
545
|
+
output(services, value);
|
|
546
|
+
return runExit(value);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
if (command === "insights manager target") {
|
|
550
|
+
exactPositionals(positionals, 6, "warpmetal insights manager target SERVER BOX FINDING");
|
|
551
|
+
rejectUnknownOptions(options, COMMON);
|
|
552
|
+
const serverId = identifier(positionals[3], "server ID");
|
|
553
|
+
const sandboxId = identifier(positionals[4], "sandbox ID", BOX_ID);
|
|
554
|
+
const findingId = identifier(positionals[5], "finding ID", FINDING_ID);
|
|
555
|
+
const token = await authority(serverId, services, options);
|
|
556
|
+
const result = await request(services, "GET", `${ownerBase(serverId, sandboxId)}/${segment(findingId)}/manager-target`, token);
|
|
557
|
+
const value = projected(result, data => projectManager(data, "managerTarget", { findingId }));
|
|
558
|
+
output(services, value);
|
|
559
|
+
return 0;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
if (command === "insights manager recheck") {
|
|
563
|
+
exactPositionals(positionals, 6, "warpmetal insights manager recheck SERVER BOX FINDING --file FILE");
|
|
564
|
+
rejectUnknownOptions(options, MUTATION);
|
|
565
|
+
const serverId = identifier(positionals[3], "server ID");
|
|
566
|
+
const sandboxId = identifier(positionals[4], "sandbox ID", BOX_ID);
|
|
567
|
+
const findingId = identifier(positionals[5], "finding ID", FINDING_ID);
|
|
568
|
+
const value = await recheck(serverId, sandboxId, findingId, services, options);
|
|
569
|
+
output(services, value);
|
|
570
|
+
return runExit(value);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
if (command === "insights manager status") {
|
|
574
|
+
exactPositionals(positionals, 6, "warpmetal insights manager status SERVER BOX FINDING --request REQUEST");
|
|
575
|
+
rejectUnknownOptions(options, [...COMMON, "request"]);
|
|
576
|
+
const serverId = identifier(positionals[3], "server ID");
|
|
577
|
+
const sandboxId = identifier(positionals[4], "sandbox ID", BOX_ID);
|
|
578
|
+
const findingId = identifier(positionals[5], "finding ID", FINDING_ID);
|
|
579
|
+
const requestId = identifier(stringOption(options, "request", { required: true }), "request ID", MANAGER_ID);
|
|
580
|
+
const token = await authority(serverId, services, options);
|
|
581
|
+
const result = await request(services, "GET", `${ownerBase(serverId, sandboxId)}/${segment(findingId)}/manager-rechecks?requestId=${segment(requestId)}`, token);
|
|
582
|
+
const value = projected(result, data => projectManager(data, "reservation", { findingId, requestId }));
|
|
583
|
+
output(services, value);
|
|
584
|
+
return runExit(value);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
if (command === "insights takeover list") {
|
|
588
|
+
exactPositionals(positionals, 6, "warpmetal insights takeover list SERVER BOX FINDING");
|
|
589
|
+
rejectUnknownOptions(options, [...COMMON, "limit", "cursor"]);
|
|
590
|
+
const serverId = identifier(positionals[3], "server ID");
|
|
591
|
+
const sandboxId = identifier(positionals[4], "sandbox ID", BOX_ID);
|
|
592
|
+
const findingId = identifier(positionals[5], "finding ID", FINDING_ID);
|
|
593
|
+
const token = await authority(serverId, services, options);
|
|
594
|
+
const result = await request(services, "GET", `${ownerBase(serverId, sandboxId)}/${segment(findingId)}/takeovers${managerParams(options, false)}`, token);
|
|
595
|
+
const value = projected(result, data => projectManager(data, "takeoverList", { findingId }));
|
|
596
|
+
output(services, value);
|
|
597
|
+
return 0;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
if (positionals[1] === "takeover" && positionals[2] !== "status" && positionals[2] !== "resume" && positionals[2] !== "list") {
|
|
601
|
+
exactPositionals(positionals, 5, "warpmetal insights takeover SERVER BOX FINDING --file FILE");
|
|
602
|
+
rejectUnknownOptions(options, MUTATION);
|
|
603
|
+
const serverId = identifier(positionals[2], "server ID");
|
|
604
|
+
const sandboxId = identifier(positionals[3], "sandbox ID", BOX_ID);
|
|
605
|
+
const findingId = identifier(positionals[4], "finding ID", FINDING_ID);
|
|
606
|
+
const value = await createTakeover(serverId, sandboxId, findingId, services, options);
|
|
607
|
+
output(services, value);
|
|
608
|
+
return takeoverExit(value);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
if (command === "insights takeover status") {
|
|
612
|
+
exactPositionals(positionals, 6, "warpmetal insights takeover status SERVER BOX FINDING --operation OPERATION");
|
|
613
|
+
rejectUnknownOptions(options, [...COMMON, "operation"]);
|
|
614
|
+
const serverId = identifier(positionals[3], "server ID");
|
|
615
|
+
const sandboxId = identifier(positionals[4], "sandbox ID", BOX_ID);
|
|
616
|
+
const findingId = identifier(positionals[5], "finding ID", FINDING_ID);
|
|
617
|
+
const operationId = identifier(stringOption(options, "operation", { required: true }), "operation ID", MANAGER_ID);
|
|
618
|
+
const token = await authority(serverId, services, options);
|
|
619
|
+
const result = await request(services, "GET", `${ownerBase(serverId, sandboxId)}/${segment(findingId)}/takeovers/${segment(operationId)}`, token);
|
|
620
|
+
const value = projected(result, data => projectManager(data, "takeover", { findingId, operationId }));
|
|
621
|
+
output(services, value);
|
|
622
|
+
return takeoverExit(value);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
if (command === "insights takeover resume") {
|
|
626
|
+
exactPositionals(positionals, 6, "warpmetal insights takeover resume SERVER BOX FINDING --operation OPERATION --file FILE");
|
|
627
|
+
rejectUnknownOptions(options, [...MUTATION, "operation"]);
|
|
628
|
+
const serverId = identifier(positionals[3], "server ID");
|
|
629
|
+
const sandboxId = identifier(positionals[4], "sandbox ID", BOX_ID);
|
|
630
|
+
const findingId = identifier(positionals[5], "finding ID", FINDING_ID);
|
|
631
|
+
const predecessor = identifier(stringOption(options, "operation", { required: true }), "operation ID", MANAGER_ID);
|
|
632
|
+
const value = await resumeTakeover(serverId, sandboxId, findingId, predecessor, services, options);
|
|
633
|
+
output(services, value);
|
|
634
|
+
return takeoverExit(value);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
fail(`Unknown Insights command: ${positionals.join(" ")}`);
|
|
638
|
+
}
|