@delexec/ops 0.1.3 → 0.1.5
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.
|
@@ -47,7 +47,9 @@ function sendError(res, statusCode, code, message, { retryable, ...extra } = {})
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
async function requestJson(baseUrl, pathname, { method = "GET", headers = {}, body } = {}) {
|
|
50
|
-
const
|
|
50
|
+
const base = String(baseUrl || "").endsWith("/") ? String(baseUrl) : `${baseUrl}/`;
|
|
51
|
+
const relativePath = String(pathname || "").replace(/^\/+/, "");
|
|
52
|
+
const response = await fetch(new URL(relativePath, base), {
|
|
51
53
|
method,
|
|
52
54
|
headers: {
|
|
53
55
|
...headers,
|
|
@@ -49,7 +49,9 @@ function sendError(res, statusCode, code, message, { retryable, ...extra } = {})
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
async function postJson(baseUrl, pathname, { method = "POST", headers = {}, body } = {}) {
|
|
52
|
-
const
|
|
52
|
+
const base = String(baseUrl || "").endsWith("/") ? String(baseUrl) : `${baseUrl}/`;
|
|
53
|
+
const relativePath = String(pathname || "").replace(/^\/+/, "");
|
|
54
|
+
const response = await fetch(new URL(relativePath, base), {
|
|
53
55
|
method,
|
|
54
56
|
headers: {
|
|
55
57
|
"content-type": "application/json; charset=utf-8",
|
|
@@ -29,7 +29,9 @@ function resolveReceiver(target) {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
async function requestJson(baseUrl, pathname, { method = "GET", body } = {}) {
|
|
32
|
-
const
|
|
32
|
+
const base = String(baseUrl || "").endsWith("/") ? String(baseUrl) : `${baseUrl}/`;
|
|
33
|
+
const relativePath = String(pathname || "").replace(/^\/+/, "");
|
|
34
|
+
const response = await fetch(new URL(relativePath, base), {
|
|
33
35
|
method,
|
|
34
36
|
headers: body === undefined ? undefined : { "content-type": "application/json; charset=utf-8" },
|
|
35
37
|
body: body === undefined ? undefined : JSON.stringify(body)
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import crypto from "node:crypto";
|
|
2
3
|
import { execFile, spawn } from "node:child_process";
|
|
3
4
|
import fs from "node:fs";
|
|
4
5
|
import path from "node:path";
|
|
@@ -14,6 +15,7 @@ import {
|
|
|
14
15
|
ensureOpsState,
|
|
15
16
|
ensureResponderIdentity,
|
|
16
17
|
loadHotlineRegistrationDraft,
|
|
18
|
+
readResolvedOpsSecrets,
|
|
17
19
|
removeHotline,
|
|
18
20
|
saveOpsState,
|
|
19
21
|
setHotlineEnabled,
|
|
@@ -33,6 +35,8 @@ const FIXED_PRICE_MODEL = "fixed_price";
|
|
|
33
35
|
const DEFAULT_PRICING_CURRENCY = "PTS";
|
|
34
36
|
const DEFAULT_TRUST_TIER = "untrusted";
|
|
35
37
|
const TRUST_TIERS = Object.freeze(["untrusted", "trusted", "verified"]);
|
|
38
|
+
const DEFAULT_CALL_HOTLINE_TIMEOUT_MS = 60000;
|
|
39
|
+
const DEFAULT_CALL_HOTLINE_POLL_INTERVAL_MS = 1000;
|
|
36
40
|
|
|
37
41
|
function getOpsSessionFile() {
|
|
38
42
|
return path.join(ensureOpsDirectories(), "run", "session.json");
|
|
@@ -47,6 +51,7 @@ function usage() {
|
|
|
47
51
|
delexec-ops ui start [--host <host>] [--port <port>] [--open] [--no-browser]
|
|
48
52
|
delexec-ops mcp spec
|
|
49
53
|
delexec-ops auth register --email <email> [--local] [--platform <url>]
|
|
54
|
+
delexec-ops call-hotline --platform <url> --hotline-id <id> --responder-id <id> [--text <text> | --payload-json <json>] [--request-id <id>] [--max-charge-cents <amount>]
|
|
50
55
|
delexec-ops enable-responder [--responder-id <id>] [--display-name <name>]
|
|
51
56
|
delexec-ops add-hotline --type <process|http> --hotline-id <id> [--cmd <command> | --url <url>] [--cwd <path>] [--env KEY=VALUE] [--fixed-price-cents <amount>] [--currency <code>] [--billing-disclosure-url <url>]
|
|
52
57
|
delexec-ops attach-project --project-path <path> [--project-name <name>] [--project-description <text>] [--hotline-id <id>] [--cmd <command> | --url <url>] [--cwd <path>] [--env KEY=VALUE] [--task-type <type>] [--capability <capability>]
|
|
@@ -266,6 +271,118 @@ function parseOptionalNonNegativeInteger(value, fieldName) {
|
|
|
266
271
|
return parsed;
|
|
267
272
|
}
|
|
268
273
|
|
|
274
|
+
function parsePositiveInteger(value, fieldName, fallback) {
|
|
275
|
+
if (value === undefined || value === null || value === false || value === "") {
|
|
276
|
+
return fallback;
|
|
277
|
+
}
|
|
278
|
+
const parsed = Number(value);
|
|
279
|
+
if (!Number.isSafeInteger(parsed) || parsed <= 0) {
|
|
280
|
+
throw new Error(`${fieldName}_must_be_positive_integer`);
|
|
281
|
+
}
|
|
282
|
+
return parsed;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function requireStringArg(args, key) {
|
|
286
|
+
const value = String(args[key] || "").trim();
|
|
287
|
+
if (!value) {
|
|
288
|
+
throw new Error(`${key.replace(/-/g, "_")}_required`);
|
|
289
|
+
}
|
|
290
|
+
return value;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function parsePayloadArg(args = {}) {
|
|
294
|
+
if (args["payload-json"] !== undefined && args["payload-json"] !== false) {
|
|
295
|
+
try {
|
|
296
|
+
const parsed = JSON.parse(String(args["payload-json"]));
|
|
297
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
298
|
+
throw new Error("payload_json_must_be_object");
|
|
299
|
+
}
|
|
300
|
+
return parsed;
|
|
301
|
+
} catch (error) {
|
|
302
|
+
if (error instanceof Error && error.message === "payload_json_must_be_object") {
|
|
303
|
+
throw error;
|
|
304
|
+
}
|
|
305
|
+
throw new Error("payload_json_invalid");
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return {
|
|
309
|
+
text: String(args.text || "Run this paid hotline request.").trim()
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function resolveCallerApiKey(state) {
|
|
314
|
+
const secrets = readResolvedOpsSecrets(state);
|
|
315
|
+
const apiKey =
|
|
316
|
+
secrets.caller_api_key ||
|
|
317
|
+
state.config.caller?.api_key ||
|
|
318
|
+
state.env.CALLER_PLATFORM_API_KEY ||
|
|
319
|
+
state.env.PLATFORM_API_KEY ||
|
|
320
|
+
process.env.CALLER_PLATFORM_API_KEY ||
|
|
321
|
+
process.env.PLATFORM_API_KEY ||
|
|
322
|
+
null;
|
|
323
|
+
if (!apiKey) {
|
|
324
|
+
throw new Error("caller_platform_api_key_required");
|
|
325
|
+
}
|
|
326
|
+
return apiKey;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function buildBearerHeaders(apiKey) {
|
|
330
|
+
return { Authorization: `Bearer ${apiKey}` };
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function normalizeBaseUrl(value, fieldName) {
|
|
334
|
+
const raw = String(value || "").trim();
|
|
335
|
+
if (!raw) {
|
|
336
|
+
throw new Error(`${fieldName}_required`);
|
|
337
|
+
}
|
|
338
|
+
try {
|
|
339
|
+
return new URL(raw).toString().replace(/\/+$/, "");
|
|
340
|
+
} catch {
|
|
341
|
+
throw new Error(`${fieldName}_invalid`);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function deriveRelayBaseUrl(platformUrl, args = {}, state = null) {
|
|
346
|
+
const explicit = String(args.relay || "").trim();
|
|
347
|
+
if (explicit) {
|
|
348
|
+
return normalizeBaseUrl(explicit, "relay");
|
|
349
|
+
}
|
|
350
|
+
const envRelay =
|
|
351
|
+
process.env.TRANSPORT_BASE_URL ||
|
|
352
|
+
state?.env?.TRANSPORT_BASE_URL ||
|
|
353
|
+
state?.config?.runtime?.transport?.relay_http?.base_url ||
|
|
354
|
+
"";
|
|
355
|
+
if (envRelay) {
|
|
356
|
+
return normalizeBaseUrl(envRelay, "relay");
|
|
357
|
+
}
|
|
358
|
+
try {
|
|
359
|
+
const url = new URL(platformUrl);
|
|
360
|
+
if (url.hostname === "callanything.xyz" && url.pathname.replace(/\/+$/, "") === "/platform") {
|
|
361
|
+
url.pathname = "/relay";
|
|
362
|
+
url.search = "";
|
|
363
|
+
url.hash = "";
|
|
364
|
+
return url.toString().replace(/\/+$/, "");
|
|
365
|
+
}
|
|
366
|
+
} catch {}
|
|
367
|
+
return null;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function buildResultDelivery(requestId) {
|
|
371
|
+
return {
|
|
372
|
+
kind: "relay_http",
|
|
373
|
+
address: `local://relay/caller-controller/${requestId}`
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function ensureSuccess(response, expectedStatus, code) {
|
|
378
|
+
const expected = Array.isArray(expectedStatus) ? expectedStatus : [expectedStatus];
|
|
379
|
+
if (!expected.includes(response.status)) {
|
|
380
|
+
const upstreamCode = response.body?.error?.code || code;
|
|
381
|
+
throw new Error(`${code}:${response.status}:${upstreamCode}`);
|
|
382
|
+
}
|
|
383
|
+
return response.body;
|
|
384
|
+
}
|
|
385
|
+
|
|
269
386
|
function parsePricingHint(args = {}) {
|
|
270
387
|
const fixedPriceCents = parseOptionalNonNegativeInteger(args["fixed-price-cents"], "fixed_price_cents");
|
|
271
388
|
if (fixedPriceCents === null) {
|
|
@@ -1181,6 +1298,139 @@ async function commandRunExample(args) {
|
|
|
1181
1298
|
});
|
|
1182
1299
|
}
|
|
1183
1300
|
|
|
1301
|
+
async function commandCallHotline(args) {
|
|
1302
|
+
const state = ensureOpsState();
|
|
1303
|
+
const platformUrl = normalizeBaseUrl(requireStringArg(args, "platform"), "platform");
|
|
1304
|
+
const hotlineId = requireStringArg(args, "hotline-id");
|
|
1305
|
+
const responderId = requireStringArg(args, "responder-id");
|
|
1306
|
+
const requestId = String(args["request-id"] || `req_${crypto.randomUUID()}`).trim();
|
|
1307
|
+
const maxChargeCents = parseOptionalNonNegativeInteger(args["max-charge-cents"], "max_charge_cents") ?? 500;
|
|
1308
|
+
const currency = String(args.currency || DEFAULT_PRICING_CURRENCY).trim() || DEFAULT_PRICING_CURRENCY;
|
|
1309
|
+
const trustTier = String(args["trust-tier"] || DEFAULT_TRUST_TIER).trim() || DEFAULT_TRUST_TIER;
|
|
1310
|
+
if (!TRUST_TIERS.includes(trustTier)) {
|
|
1311
|
+
throw new Error("trust_tier_unsupported");
|
|
1312
|
+
}
|
|
1313
|
+
const callerBaseUrl = normalizeBaseUrl(
|
|
1314
|
+
args["caller-base-url"] || `http://127.0.0.1:${process.env.OPS_PORT_CALLER || state.config.runtime.ports.caller || 8081}`,
|
|
1315
|
+
"caller_base_url"
|
|
1316
|
+
);
|
|
1317
|
+
const relayBaseUrl = deriveRelayBaseUrl(platformUrl, args, state);
|
|
1318
|
+
const pollIntervalMs = parsePositiveInteger(args["poll-interval-ms"], "poll_interval_ms", DEFAULT_CALL_HOTLINE_POLL_INTERVAL_MS);
|
|
1319
|
+
const timeoutMs = parsePositiveInteger(args["timeout-ms"], "timeout_ms", DEFAULT_CALL_HOTLINE_TIMEOUT_MS);
|
|
1320
|
+
const payload = parsePayloadArg(args);
|
|
1321
|
+
const apiKey = resolveCallerApiKey(state);
|
|
1322
|
+
const resultDelivery = buildResultDelivery(requestId);
|
|
1323
|
+
const billing = {
|
|
1324
|
+
acknowledged: true,
|
|
1325
|
+
pricing_model: FIXED_PRICE_MODEL,
|
|
1326
|
+
currency,
|
|
1327
|
+
max_charge_cents: maxChargeCents,
|
|
1328
|
+
trust_tier: trustTier
|
|
1329
|
+
};
|
|
1330
|
+
|
|
1331
|
+
const tokenResponse = await requestJson(platformUrl, "/v1/tokens/task", {
|
|
1332
|
+
method: "POST",
|
|
1333
|
+
headers: buildBearerHeaders(apiKey),
|
|
1334
|
+
body: {
|
|
1335
|
+
request_id: requestId,
|
|
1336
|
+
responder_id: responderId,
|
|
1337
|
+
hotline_id: hotlineId,
|
|
1338
|
+
billing
|
|
1339
|
+
}
|
|
1340
|
+
});
|
|
1341
|
+
const tokenBody = ensureSuccess(tokenResponse, 201, "CALL_HOTLINE_TOKEN_FAILED");
|
|
1342
|
+
|
|
1343
|
+
const deliveryMetaResponse = await requestJson(platformUrl, `/v1/requests/${encodeURIComponent(requestId)}/delivery-meta`, {
|
|
1344
|
+
method: "POST",
|
|
1345
|
+
headers: buildBearerHeaders(apiKey),
|
|
1346
|
+
body: {
|
|
1347
|
+
responder_id: responderId,
|
|
1348
|
+
hotline_id: hotlineId,
|
|
1349
|
+
task_token: tokenBody.task_token,
|
|
1350
|
+
result_delivery: resultDelivery
|
|
1351
|
+
}
|
|
1352
|
+
});
|
|
1353
|
+
const deliveryMeta = ensureSuccess(deliveryMetaResponse, 200, "CALL_HOTLINE_DELIVERY_META_FAILED");
|
|
1354
|
+
|
|
1355
|
+
const createdResponse = await requestJson(callerBaseUrl, "/controller/requests", {
|
|
1356
|
+
method: "POST",
|
|
1357
|
+
body: {
|
|
1358
|
+
request_id: requestId,
|
|
1359
|
+
responder_id: responderId,
|
|
1360
|
+
hotline_id: hotlineId,
|
|
1361
|
+
task_token: tokenBody.task_token,
|
|
1362
|
+
delivery_meta: deliveryMeta,
|
|
1363
|
+
result_delivery: deliveryMeta.result_delivery || resultDelivery,
|
|
1364
|
+
expected_signer_public_key_pem: deliveryMeta.responder_public_key_pem || null,
|
|
1365
|
+
task_input: payload,
|
|
1366
|
+
payload
|
|
1367
|
+
}
|
|
1368
|
+
});
|
|
1369
|
+
const request = ensureSuccess(createdResponse, 201, "CALL_HOTLINE_CREATE_FAILED");
|
|
1370
|
+
|
|
1371
|
+
const dispatchResponse = await requestJson(callerBaseUrl, `/controller/requests/${encodeURIComponent(requestId)}/dispatch`, {
|
|
1372
|
+
method: "POST",
|
|
1373
|
+
body: {
|
|
1374
|
+
payload,
|
|
1375
|
+
task_input: payload,
|
|
1376
|
+
task_token: tokenBody.task_token,
|
|
1377
|
+
result_delivery: deliveryMeta.result_delivery || resultDelivery
|
|
1378
|
+
}
|
|
1379
|
+
});
|
|
1380
|
+
const dispatch = ensureSuccess(dispatchResponse, 202, "CALL_HOTLINE_DISPATCH_FAILED");
|
|
1381
|
+
|
|
1382
|
+
const inbox = await waitFor(async () => {
|
|
1383
|
+
const current = await requestJson(callerBaseUrl, "/controller/inbox/pull", {
|
|
1384
|
+
method: "POST",
|
|
1385
|
+
body: {
|
|
1386
|
+
receiver: "caller-controller"
|
|
1387
|
+
}
|
|
1388
|
+
});
|
|
1389
|
+
if (current.status !== 200 || !Array.isArray(current.body?.accepted) || current.body.accepted.length === 0) {
|
|
1390
|
+
throw new Error("call_hotline_inbox_not_ready");
|
|
1391
|
+
}
|
|
1392
|
+
return current.body;
|
|
1393
|
+
}, { timeoutMs, intervalMs: pollIntervalMs });
|
|
1394
|
+
|
|
1395
|
+
const result = await waitFor(async () => {
|
|
1396
|
+
const current = await requestJson(callerBaseUrl, `/controller/requests/${encodeURIComponent(requestId)}/result`);
|
|
1397
|
+
if (current.status !== 200 || current.body?.available !== true || !current.body?.result_package) {
|
|
1398
|
+
throw new Error("call_hotline_result_not_ready");
|
|
1399
|
+
}
|
|
1400
|
+
return current.body;
|
|
1401
|
+
}, { timeoutMs, intervalMs: pollIntervalMs });
|
|
1402
|
+
|
|
1403
|
+
const eventsResponse = await requestJson(platformUrl, `/v1/requests/${encodeURIComponent(requestId)}/events`, {
|
|
1404
|
+
headers: buildBearerHeaders(apiKey)
|
|
1405
|
+
});
|
|
1406
|
+
const events = ensureSuccess(eventsResponse, 200, "CALL_HOTLINE_EVENTS_FAILED");
|
|
1407
|
+
const balanceResponse = await requestJson(platformUrl, "/v1/tenants/me/balance", {
|
|
1408
|
+
headers: buildBearerHeaders(apiKey)
|
|
1409
|
+
});
|
|
1410
|
+
const balance = ensureSuccess(balanceResponse, 200, "CALL_HOTLINE_BALANCE_FAILED");
|
|
1411
|
+
const ledgerResponse = await requestJson(platformUrl, "/v1/tenants/me/ledger?limit=20", {
|
|
1412
|
+
headers: buildBearerHeaders(apiKey)
|
|
1413
|
+
});
|
|
1414
|
+
const ledger = ensureSuccess(ledgerResponse, 200, "CALL_HOTLINE_LEDGER_FAILED");
|
|
1415
|
+
|
|
1416
|
+
emit({
|
|
1417
|
+
ok: true,
|
|
1418
|
+
request_id: request.request_id,
|
|
1419
|
+
hotline_id: hotlineId,
|
|
1420
|
+
responder_id: responderId,
|
|
1421
|
+
relay_base_url: relayBaseUrl,
|
|
1422
|
+
task_token_present: Boolean(tokenBody.task_token),
|
|
1423
|
+
task_token_claims: tokenBody.claims || null,
|
|
1424
|
+
delivery_meta: deliveryMeta,
|
|
1425
|
+
dispatch,
|
|
1426
|
+
inbox,
|
|
1427
|
+
result,
|
|
1428
|
+
events,
|
|
1429
|
+
balance,
|
|
1430
|
+
ledger
|
|
1431
|
+
});
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1184
1434
|
async function commandBootstrap(args) {
|
|
1185
1435
|
const steps = [];
|
|
1186
1436
|
const initialState = ensureOpsState();
|
|
@@ -1548,6 +1798,10 @@ async function main() {
|
|
|
1548
1798
|
await commandRunExample(args);
|
|
1549
1799
|
return;
|
|
1550
1800
|
}
|
|
1801
|
+
if (group === "call-hotline") {
|
|
1802
|
+
await commandCallHotline(args);
|
|
1803
|
+
return;
|
|
1804
|
+
}
|
|
1551
1805
|
if (group === "auth" && command === "register") {
|
|
1552
1806
|
await commandAuthRegister(args);
|
|
1553
1807
|
return;
|
package/src/supervisor.js
CHANGED
|
@@ -122,7 +122,9 @@ function parseJsonBody(req) {
|
|
|
122
122
|
}
|
|
123
123
|
|
|
124
124
|
async function requestJson(baseUrl, pathname, { method = "GET", headers = {}, body } = {}) {
|
|
125
|
-
const
|
|
125
|
+
const base = String(baseUrl || "").endsWith("/") ? String(baseUrl) : `${baseUrl}/`;
|
|
126
|
+
const relativePath = String(pathname || "").replace(/^\/+/, "");
|
|
127
|
+
const response = await fetch(new URL(relativePath, base), {
|
|
126
128
|
method,
|
|
127
129
|
headers: {
|
|
128
130
|
...headers,
|
|
@@ -142,7 +144,9 @@ function processBaseUrl(port) {
|
|
|
142
144
|
}
|
|
143
145
|
|
|
144
146
|
function appendPath(baseUrl, pathname) {
|
|
145
|
-
|
|
147
|
+
const base = String(baseUrl || "").endsWith("/") ? String(baseUrl) : `${baseUrl}/`;
|
|
148
|
+
const relativePath = String(pathname || "").replace(/^\/+/, "");
|
|
149
|
+
return new URL(relativePath, base).toString();
|
|
146
150
|
}
|
|
147
151
|
|
|
148
152
|
function parseJsonArrayEnv(value) {
|