@hasna/loops 0.4.14 → 0.4.22
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/CHANGELOG.md +25 -0
- package/README.md +119 -29
- package/dist/api/index.d.ts +1 -0
- package/dist/api/index.js +825 -7
- package/dist/cli/index.js +5419 -2619
- package/dist/daemon/index.js +478 -84
- package/dist/generated/storage-kit/index.d.ts +1 -1
- package/dist/index.js +2521 -226
- package/dist/lib/cloud/mode.d.ts +17 -0
- package/dist/lib/cloud/resolve.d.ts +16 -0
- package/dist/lib/cloud/storage.d.ts +82 -0
- package/dist/lib/cloud/transport.d.ts +148 -0
- package/dist/lib/format.d.ts +2 -1
- package/dist/lib/migration.d.ts +40 -0
- package/dist/lib/mode.js +3 -3
- package/dist/lib/route/index.d.ts +2 -0
- package/dist/lib/route/policies.d.ts +51 -0
- package/dist/lib/route/provider-admission.d.ts +37 -0
- package/dist/lib/route/throttle.d.ts +4 -0
- package/dist/lib/route/types.d.ts +8 -0
- package/dist/lib/run-receipts.d.ts +14 -0
- package/dist/lib/storage/contract.d.ts +7 -1
- package/dist/lib/storage/index.js +710 -31
- package/dist/lib/storage/postgres-loop-storage.d.ts +6 -0
- package/dist/lib/storage/postgres-schema.js +29 -0
- package/dist/lib/storage/postgres.js +29 -0
- package/dist/lib/storage/sqlite.d.ts +6 -0
- package/dist/lib/storage/sqlite.js +412 -18
- package/dist/lib/store/index.d.ts +268 -0
- package/dist/lib/store.d.ts +48 -1
- package/dist/lib/store.js +396 -18
- package/dist/lib/template-kit.d.ts +25 -0
- package/dist/lib/templates.d.ts +16 -1
- package/dist/mcp/http.d.ts +16 -0
- package/dist/mcp/index.js +1658 -168
- package/dist/runner/index.js +76 -9
- package/dist/sdk/http.d.ts +67 -0
- package/dist/sdk/http.js +21 -0
- package/dist/sdk/index.d.ts +50 -17
- package/dist/sdk/index.js +1509 -132
- package/dist/serve/index.js +1598 -122
- package/dist/types.d.ts +49 -0
- package/docs/AUTOMATION_RUNTIME_DESIGN.md +442 -1
- package/docs/CUTOVER-RUNBOOK.md +73 -56
- package/docs/DEPLOYMENT_MODES.md +35 -18
- package/docs/RUNTIME_BOUNDARY.md +203 -0
- package/docs/USAGE.md +145 -27
- package/package.json +3 -3
package/dist/api/index.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// package.json
|
|
4
4
|
var package_default = {
|
|
5
5
|
name: "@hasna/loops",
|
|
6
|
-
version: "0.4.
|
|
6
|
+
version: "0.4.22",
|
|
7
7
|
description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
|
|
8
8
|
type: "module",
|
|
9
9
|
main: "dist/index.js",
|
|
@@ -113,7 +113,6 @@ var package_default = {
|
|
|
113
113
|
bun: ">=1.0.0"
|
|
114
114
|
},
|
|
115
115
|
dependencies: {
|
|
116
|
-
"@hasna/contracts": "^0.4.1",
|
|
117
116
|
"@hasna/events": "^0.1.9",
|
|
118
117
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
119
118
|
"@openrouter/ai-sdk-provider": "2.9.1",
|
|
@@ -123,7 +122,8 @@ var package_default = {
|
|
|
123
122
|
zod: "4.4.3"
|
|
124
123
|
},
|
|
125
124
|
optionalDependencies: {
|
|
126
|
-
"@hasna/machines": "0.0.49"
|
|
125
|
+
"@hasna/machines": "0.0.49",
|
|
126
|
+
"@hasna/contracts": "^0.4.2"
|
|
127
127
|
},
|
|
128
128
|
devDependencies: {
|
|
129
129
|
"@types/bun": "latest",
|
|
@@ -438,6 +438,9 @@ function publicRun(run, showOutput = false, opts = {}) {
|
|
|
438
438
|
error: opts.redactError ? redact(run.error) : run.error
|
|
439
439
|
};
|
|
440
440
|
}
|
|
441
|
+
function publicRunReceipt(receipt) {
|
|
442
|
+
return { ...receipt };
|
|
443
|
+
}
|
|
441
444
|
function publicExecutorResult(result, showOutput = false) {
|
|
442
445
|
return {
|
|
443
446
|
...result,
|
|
@@ -989,6 +992,50 @@ var loops_default = {
|
|
|
989
992
|
}
|
|
990
993
|
}
|
|
991
994
|
},
|
|
995
|
+
"/v1/loops/count": {
|
|
996
|
+
get: {
|
|
997
|
+
operationId: "countLoops",
|
|
998
|
+
summary: "Count loops (total-row verification)",
|
|
999
|
+
parameters: [
|
|
1000
|
+
{
|
|
1001
|
+
name: "status",
|
|
1002
|
+
in: "query",
|
|
1003
|
+
required: false,
|
|
1004
|
+
schema: {
|
|
1005
|
+
type: "string"
|
|
1006
|
+
}
|
|
1007
|
+
},
|
|
1008
|
+
{
|
|
1009
|
+
name: "includeArchived",
|
|
1010
|
+
in: "query",
|
|
1011
|
+
required: false,
|
|
1012
|
+
schema: {
|
|
1013
|
+
type: "boolean"
|
|
1014
|
+
}
|
|
1015
|
+
},
|
|
1016
|
+
{
|
|
1017
|
+
name: "archived",
|
|
1018
|
+
in: "query",
|
|
1019
|
+
required: false,
|
|
1020
|
+
schema: {
|
|
1021
|
+
type: "boolean"
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
],
|
|
1025
|
+
responses: {
|
|
1026
|
+
"200": {
|
|
1027
|
+
description: "count",
|
|
1028
|
+
content: {
|
|
1029
|
+
"application/json": {
|
|
1030
|
+
schema: {
|
|
1031
|
+
$ref: "#/components/schemas/CountResponse"
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
},
|
|
992
1039
|
"/v1/loops/{id}": {
|
|
993
1040
|
get: {
|
|
994
1041
|
operationId: "getLoop",
|
|
@@ -1179,6 +1226,34 @@ var loops_default = {
|
|
|
1179
1226
|
}
|
|
1180
1227
|
}
|
|
1181
1228
|
},
|
|
1229
|
+
"/v1/runs/count": {
|
|
1230
|
+
get: {
|
|
1231
|
+
operationId: "countRuns",
|
|
1232
|
+
summary: "Count runs (total-row verification)",
|
|
1233
|
+
parameters: [
|
|
1234
|
+
{
|
|
1235
|
+
name: "status",
|
|
1236
|
+
in: "query",
|
|
1237
|
+
required: false,
|
|
1238
|
+
schema: {
|
|
1239
|
+
type: "string"
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
],
|
|
1243
|
+
responses: {
|
|
1244
|
+
"200": {
|
|
1245
|
+
description: "count",
|
|
1246
|
+
content: {
|
|
1247
|
+
"application/json": {
|
|
1248
|
+
schema: {
|
|
1249
|
+
$ref: "#/components/schemas/CountResponse"
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
},
|
|
1182
1257
|
"/v1/runs/{id}": {
|
|
1183
1258
|
get: {
|
|
1184
1259
|
operationId: "getRun",
|
|
@@ -1206,6 +1281,157 @@ var loops_default = {
|
|
|
1206
1281
|
}
|
|
1207
1282
|
}
|
|
1208
1283
|
}
|
|
1284
|
+
},
|
|
1285
|
+
"/v1/import": {
|
|
1286
|
+
post: {
|
|
1287
|
+
operationId: "importRows",
|
|
1288
|
+
summary: "Bulk id-preserving import (self-hosted backfill)",
|
|
1289
|
+
description: "Upsert full workflow/loop/run rows by id (idempotent; ON CONFLICT(id) DO UPDATE). Preserves id, status, archived state, and timestamps. Applied FK-safe: workflows, then loops, then runs. Volatile running runs are skipped and reported.",
|
|
1290
|
+
requestBody: {
|
|
1291
|
+
required: true,
|
|
1292
|
+
content: {
|
|
1293
|
+
"application/json": {
|
|
1294
|
+
schema: {
|
|
1295
|
+
$ref: "#/components/schemas/ImportInput"
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
},
|
|
1300
|
+
responses: {
|
|
1301
|
+
"200": {
|
|
1302
|
+
description: "import counts",
|
|
1303
|
+
content: {
|
|
1304
|
+
"application/json": {
|
|
1305
|
+
schema: {
|
|
1306
|
+
$ref: "#/components/schemas/ImportResponse"
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
},
|
|
1314
|
+
"/v1/receipts": {
|
|
1315
|
+
get: {
|
|
1316
|
+
operationId: "listRunReceipts",
|
|
1317
|
+
summary: "List run receipts",
|
|
1318
|
+
parameters: [
|
|
1319
|
+
{
|
|
1320
|
+
name: "loopId",
|
|
1321
|
+
in: "query",
|
|
1322
|
+
required: false,
|
|
1323
|
+
schema: {
|
|
1324
|
+
type: "string"
|
|
1325
|
+
}
|
|
1326
|
+
},
|
|
1327
|
+
{
|
|
1328
|
+
name: "repo",
|
|
1329
|
+
in: "query",
|
|
1330
|
+
required: false,
|
|
1331
|
+
schema: {
|
|
1332
|
+
type: "string"
|
|
1333
|
+
}
|
|
1334
|
+
},
|
|
1335
|
+
{
|
|
1336
|
+
name: "taskId",
|
|
1337
|
+
in: "query",
|
|
1338
|
+
required: false,
|
|
1339
|
+
schema: {
|
|
1340
|
+
type: "string"
|
|
1341
|
+
}
|
|
1342
|
+
},
|
|
1343
|
+
{
|
|
1344
|
+
name: "knowledgeId",
|
|
1345
|
+
in: "query",
|
|
1346
|
+
required: false,
|
|
1347
|
+
schema: {
|
|
1348
|
+
type: "string"
|
|
1349
|
+
}
|
|
1350
|
+
},
|
|
1351
|
+
{
|
|
1352
|
+
name: "status",
|
|
1353
|
+
in: "query",
|
|
1354
|
+
required: false,
|
|
1355
|
+
schema: {
|
|
1356
|
+
type: "string"
|
|
1357
|
+
}
|
|
1358
|
+
},
|
|
1359
|
+
{
|
|
1360
|
+
name: "limit",
|
|
1361
|
+
in: "query",
|
|
1362
|
+
required: false,
|
|
1363
|
+
schema: {
|
|
1364
|
+
type: "integer"
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
],
|
|
1368
|
+
responses: {
|
|
1369
|
+
"200": {
|
|
1370
|
+
description: "run receipts",
|
|
1371
|
+
content: {
|
|
1372
|
+
"application/json": {
|
|
1373
|
+
schema: {
|
|
1374
|
+
$ref: "#/components/schemas/RunReceiptListResponse"
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
},
|
|
1381
|
+
post: {
|
|
1382
|
+
operationId: "writeRunReceipt",
|
|
1383
|
+
summary: "Write a run receipt",
|
|
1384
|
+
requestBody: {
|
|
1385
|
+
required: true,
|
|
1386
|
+
content: {
|
|
1387
|
+
"application/json": {
|
|
1388
|
+
schema: {
|
|
1389
|
+
$ref: "#/components/schemas/WriteRunReceiptInput"
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
},
|
|
1394
|
+
responses: {
|
|
1395
|
+
"201": {
|
|
1396
|
+
description: "run receipt",
|
|
1397
|
+
content: {
|
|
1398
|
+
"application/json": {
|
|
1399
|
+
schema: {
|
|
1400
|
+
$ref: "#/components/schemas/RunReceiptResponse"
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
},
|
|
1408
|
+
"/v1/receipts/{runId}": {
|
|
1409
|
+
get: {
|
|
1410
|
+
operationId: "getRunReceipt",
|
|
1411
|
+
summary: "Get a run receipt by run id",
|
|
1412
|
+
parameters: [
|
|
1413
|
+
{
|
|
1414
|
+
name: "runId",
|
|
1415
|
+
in: "path",
|
|
1416
|
+
required: true,
|
|
1417
|
+
schema: {
|
|
1418
|
+
type: "string"
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
],
|
|
1422
|
+
responses: {
|
|
1423
|
+
"200": {
|
|
1424
|
+
description: "run receipt",
|
|
1425
|
+
content: {
|
|
1426
|
+
"application/json": {
|
|
1427
|
+
schema: {
|
|
1428
|
+
$ref: "#/components/schemas/RunReceiptResponse"
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1209
1435
|
}
|
|
1210
1436
|
},
|
|
1211
1437
|
components: {
|
|
@@ -1439,6 +1665,334 @@ var loops_default = {
|
|
|
1439
1665
|
"ok",
|
|
1440
1666
|
"deleted"
|
|
1441
1667
|
]
|
|
1668
|
+
},
|
|
1669
|
+
RunReceiptMachine: {
|
|
1670
|
+
oneOf: [
|
|
1671
|
+
{
|
|
1672
|
+
type: "string"
|
|
1673
|
+
},
|
|
1674
|
+
{
|
|
1675
|
+
type: "object",
|
|
1676
|
+
additionalProperties: true
|
|
1677
|
+
}
|
|
1678
|
+
]
|
|
1679
|
+
},
|
|
1680
|
+
RunReceiptSummary: {
|
|
1681
|
+
type: "object",
|
|
1682
|
+
required: [
|
|
1683
|
+
"stdout_bytes",
|
|
1684
|
+
"stderr_bytes"
|
|
1685
|
+
],
|
|
1686
|
+
properties: {
|
|
1687
|
+
text: {
|
|
1688
|
+
type: "string"
|
|
1689
|
+
},
|
|
1690
|
+
stdout_bytes: {
|
|
1691
|
+
type: "integer",
|
|
1692
|
+
minimum: 0
|
|
1693
|
+
},
|
|
1694
|
+
stderr_bytes: {
|
|
1695
|
+
type: "integer",
|
|
1696
|
+
minimum: 0
|
|
1697
|
+
},
|
|
1698
|
+
stdout_excerpt: {
|
|
1699
|
+
type: "string"
|
|
1700
|
+
},
|
|
1701
|
+
stderr_excerpt: {
|
|
1702
|
+
type: "string"
|
|
1703
|
+
},
|
|
1704
|
+
error: {
|
|
1705
|
+
type: "string"
|
|
1706
|
+
},
|
|
1707
|
+
duration_ms: {
|
|
1708
|
+
type: "integer",
|
|
1709
|
+
minimum: 0
|
|
1710
|
+
}
|
|
1711
|
+
},
|
|
1712
|
+
additionalProperties: false
|
|
1713
|
+
},
|
|
1714
|
+
RunReceipt: {
|
|
1715
|
+
type: "object",
|
|
1716
|
+
required: [
|
|
1717
|
+
"loop_id",
|
|
1718
|
+
"run_id",
|
|
1719
|
+
"machine",
|
|
1720
|
+
"repo",
|
|
1721
|
+
"task_ids",
|
|
1722
|
+
"knowledge_ids",
|
|
1723
|
+
"digest_id",
|
|
1724
|
+
"started_at",
|
|
1725
|
+
"finished_at",
|
|
1726
|
+
"status",
|
|
1727
|
+
"exit_code",
|
|
1728
|
+
"summary",
|
|
1729
|
+
"evidence_paths",
|
|
1730
|
+
"created_at",
|
|
1731
|
+
"updated_at"
|
|
1732
|
+
],
|
|
1733
|
+
properties: {
|
|
1734
|
+
loop_id: {
|
|
1735
|
+
type: "string"
|
|
1736
|
+
},
|
|
1737
|
+
run_id: {
|
|
1738
|
+
type: "string"
|
|
1739
|
+
},
|
|
1740
|
+
machine: {
|
|
1741
|
+
$ref: "#/components/schemas/RunReceiptMachine"
|
|
1742
|
+
},
|
|
1743
|
+
repo: {
|
|
1744
|
+
type: "string"
|
|
1745
|
+
},
|
|
1746
|
+
task_ids: {
|
|
1747
|
+
type: "array",
|
|
1748
|
+
items: {
|
|
1749
|
+
type: "string"
|
|
1750
|
+
}
|
|
1751
|
+
},
|
|
1752
|
+
knowledge_ids: {
|
|
1753
|
+
type: "array",
|
|
1754
|
+
items: {
|
|
1755
|
+
type: "string"
|
|
1756
|
+
}
|
|
1757
|
+
},
|
|
1758
|
+
digest_id: {
|
|
1759
|
+
type: "string"
|
|
1760
|
+
},
|
|
1761
|
+
started_at: {
|
|
1762
|
+
type: "string",
|
|
1763
|
+
format: "date-time",
|
|
1764
|
+
nullable: true
|
|
1765
|
+
},
|
|
1766
|
+
finished_at: {
|
|
1767
|
+
type: "string",
|
|
1768
|
+
format: "date-time",
|
|
1769
|
+
nullable: true
|
|
1770
|
+
},
|
|
1771
|
+
status: {
|
|
1772
|
+
type: "string"
|
|
1773
|
+
},
|
|
1774
|
+
exit_code: {
|
|
1775
|
+
type: "integer",
|
|
1776
|
+
nullable: true
|
|
1777
|
+
},
|
|
1778
|
+
summary: {
|
|
1779
|
+
$ref: "#/components/schemas/RunReceiptSummary"
|
|
1780
|
+
},
|
|
1781
|
+
evidence_paths: {
|
|
1782
|
+
type: "array",
|
|
1783
|
+
items: {
|
|
1784
|
+
type: "string"
|
|
1785
|
+
}
|
|
1786
|
+
},
|
|
1787
|
+
created_at: {
|
|
1788
|
+
type: "string",
|
|
1789
|
+
format: "date-time"
|
|
1790
|
+
},
|
|
1791
|
+
updated_at: {
|
|
1792
|
+
type: "string",
|
|
1793
|
+
format: "date-time"
|
|
1794
|
+
}
|
|
1795
|
+
},
|
|
1796
|
+
additionalProperties: false
|
|
1797
|
+
},
|
|
1798
|
+
WriteRunReceiptInput: {
|
|
1799
|
+
type: "object",
|
|
1800
|
+
required: [
|
|
1801
|
+
"run_id"
|
|
1802
|
+
],
|
|
1803
|
+
properties: {
|
|
1804
|
+
loop_id: {
|
|
1805
|
+
type: "string"
|
|
1806
|
+
},
|
|
1807
|
+
run_id: {
|
|
1808
|
+
type: "string"
|
|
1809
|
+
},
|
|
1810
|
+
machine: {
|
|
1811
|
+
$ref: "#/components/schemas/RunReceiptMachine"
|
|
1812
|
+
},
|
|
1813
|
+
repo: {
|
|
1814
|
+
type: "string"
|
|
1815
|
+
},
|
|
1816
|
+
task_ids: {
|
|
1817
|
+
type: "array",
|
|
1818
|
+
items: {
|
|
1819
|
+
type: "string"
|
|
1820
|
+
}
|
|
1821
|
+
},
|
|
1822
|
+
knowledge_ids: {
|
|
1823
|
+
type: "array",
|
|
1824
|
+
items: {
|
|
1825
|
+
type: "string"
|
|
1826
|
+
}
|
|
1827
|
+
},
|
|
1828
|
+
digest_id: {
|
|
1829
|
+
type: "string"
|
|
1830
|
+
},
|
|
1831
|
+
started_at: {
|
|
1832
|
+
type: "string",
|
|
1833
|
+
format: "date-time",
|
|
1834
|
+
nullable: true
|
|
1835
|
+
},
|
|
1836
|
+
finished_at: {
|
|
1837
|
+
type: "string",
|
|
1838
|
+
format: "date-time",
|
|
1839
|
+
nullable: true
|
|
1840
|
+
},
|
|
1841
|
+
status: {
|
|
1842
|
+
type: "string"
|
|
1843
|
+
},
|
|
1844
|
+
exit_code: {
|
|
1845
|
+
type: "integer",
|
|
1846
|
+
nullable: true
|
|
1847
|
+
},
|
|
1848
|
+
summary: {
|
|
1849
|
+
oneOf: [
|
|
1850
|
+
{
|
|
1851
|
+
type: "string"
|
|
1852
|
+
},
|
|
1853
|
+
{
|
|
1854
|
+
$ref: "#/components/schemas/RunReceiptSummary"
|
|
1855
|
+
},
|
|
1856
|
+
{
|
|
1857
|
+
type: "null"
|
|
1858
|
+
}
|
|
1859
|
+
]
|
|
1860
|
+
},
|
|
1861
|
+
evidence_paths: {
|
|
1862
|
+
type: "array",
|
|
1863
|
+
items: {
|
|
1864
|
+
type: "string"
|
|
1865
|
+
}
|
|
1866
|
+
},
|
|
1867
|
+
stdout: {
|
|
1868
|
+
type: "string"
|
|
1869
|
+
},
|
|
1870
|
+
stderr: {
|
|
1871
|
+
type: "string"
|
|
1872
|
+
},
|
|
1873
|
+
error: {
|
|
1874
|
+
type: "string"
|
|
1875
|
+
},
|
|
1876
|
+
duration_ms: {
|
|
1877
|
+
type: "integer",
|
|
1878
|
+
minimum: 0
|
|
1879
|
+
}
|
|
1880
|
+
},
|
|
1881
|
+
additionalProperties: false
|
|
1882
|
+
},
|
|
1883
|
+
RunReceiptResponse: {
|
|
1884
|
+
type: "object",
|
|
1885
|
+
properties: {
|
|
1886
|
+
ok: {
|
|
1887
|
+
type: "boolean"
|
|
1888
|
+
},
|
|
1889
|
+
receipt: {
|
|
1890
|
+
$ref: "#/components/schemas/RunReceipt"
|
|
1891
|
+
}
|
|
1892
|
+
},
|
|
1893
|
+
required: [
|
|
1894
|
+
"ok",
|
|
1895
|
+
"receipt"
|
|
1896
|
+
]
|
|
1897
|
+
},
|
|
1898
|
+
RunReceiptListResponse: {
|
|
1899
|
+
type: "object",
|
|
1900
|
+
properties: {
|
|
1901
|
+
ok: {
|
|
1902
|
+
type: "boolean"
|
|
1903
|
+
},
|
|
1904
|
+
receipts: {
|
|
1905
|
+
type: "array",
|
|
1906
|
+
items: {
|
|
1907
|
+
$ref: "#/components/schemas/RunReceipt"
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1910
|
+
},
|
|
1911
|
+
required: [
|
|
1912
|
+
"ok",
|
|
1913
|
+
"receipts"
|
|
1914
|
+
]
|
|
1915
|
+
},
|
|
1916
|
+
ImportInput: {
|
|
1917
|
+
type: "object",
|
|
1918
|
+
description: "Batches of full rows to upsert by id. Any array may be omitted.",
|
|
1919
|
+
properties: {
|
|
1920
|
+
workflows: {
|
|
1921
|
+
type: "array",
|
|
1922
|
+
items: {
|
|
1923
|
+
type: "object",
|
|
1924
|
+
additionalProperties: true
|
|
1925
|
+
}
|
|
1926
|
+
},
|
|
1927
|
+
loops: {
|
|
1928
|
+
type: "array",
|
|
1929
|
+
items: {
|
|
1930
|
+
type: "object",
|
|
1931
|
+
additionalProperties: true
|
|
1932
|
+
}
|
|
1933
|
+
},
|
|
1934
|
+
runs: {
|
|
1935
|
+
type: "array",
|
|
1936
|
+
items: {
|
|
1937
|
+
type: "object",
|
|
1938
|
+
additionalProperties: true
|
|
1939
|
+
}
|
|
1940
|
+
},
|
|
1941
|
+
replace: {
|
|
1942
|
+
type: "boolean",
|
|
1943
|
+
description: "Update existing rows whose id matches (default false: existing rows are left unchanged)."
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1946
|
+
},
|
|
1947
|
+
ImportResponse: {
|
|
1948
|
+
type: "object",
|
|
1949
|
+
properties: {
|
|
1950
|
+
ok: {
|
|
1951
|
+
type: "boolean"
|
|
1952
|
+
},
|
|
1953
|
+
imported: {
|
|
1954
|
+
type: "object",
|
|
1955
|
+
properties: {
|
|
1956
|
+
workflows: {
|
|
1957
|
+
type: "integer"
|
|
1958
|
+
},
|
|
1959
|
+
loops: {
|
|
1960
|
+
type: "integer"
|
|
1961
|
+
},
|
|
1962
|
+
runs: {
|
|
1963
|
+
type: "integer"
|
|
1964
|
+
}
|
|
1965
|
+
},
|
|
1966
|
+
required: [
|
|
1967
|
+
"workflows",
|
|
1968
|
+
"loops",
|
|
1969
|
+
"runs"
|
|
1970
|
+
]
|
|
1971
|
+
},
|
|
1972
|
+
skippedRunning: {
|
|
1973
|
+
type: "integer"
|
|
1974
|
+
}
|
|
1975
|
+
},
|
|
1976
|
+
required: [
|
|
1977
|
+
"ok",
|
|
1978
|
+
"imported",
|
|
1979
|
+
"skippedRunning"
|
|
1980
|
+
]
|
|
1981
|
+
},
|
|
1982
|
+
CountResponse: {
|
|
1983
|
+
type: "object",
|
|
1984
|
+
properties: {
|
|
1985
|
+
ok: {
|
|
1986
|
+
type: "boolean"
|
|
1987
|
+
},
|
|
1988
|
+
count: {
|
|
1989
|
+
type: "integer"
|
|
1990
|
+
}
|
|
1991
|
+
},
|
|
1992
|
+
required: [
|
|
1993
|
+
"ok",
|
|
1994
|
+
"count"
|
|
1995
|
+
]
|
|
1442
1996
|
}
|
|
1443
1997
|
},
|
|
1444
1998
|
securitySchemes: {
|
|
@@ -1463,6 +2017,7 @@ function openApiDocument() {
|
|
|
1463
2017
|
var program = new Command;
|
|
1464
2018
|
var DEFAULT_BODY_LIMIT_BYTES = 64 * 1024;
|
|
1465
2019
|
var DEFAULT_EVIDENCE_LIMIT_BYTES = 256 * 1024;
|
|
2020
|
+
var DEFAULT_IMPORT_LIMIT_BYTES = 32 * 1024 * 1024;
|
|
1466
2021
|
var MIN_RUNNER_LEASE_MS = 1000;
|
|
1467
2022
|
program.name("loops-api").description("OpenLoops self-hosted control-plane API foundation").version(packageVersion()).option("-j, --json", "print JSON");
|
|
1468
2023
|
function wantsJson(opts) {
|
|
@@ -1573,6 +2128,7 @@ function createLoopsApiServer(opts = {}) {
|
|
|
1573
2128
|
storage: opts.storage,
|
|
1574
2129
|
bodyLimitBytes: opts.bodyLimitBytes ?? DEFAULT_BODY_LIMIT_BYTES,
|
|
1575
2130
|
evidenceLimitBytes: opts.evidenceLimitBytes ?? DEFAULT_EVIDENCE_LIMIT_BYTES,
|
|
2131
|
+
importLimitBytes: opts.importLimitBytes ?? DEFAULT_IMPORT_LIMIT_BYTES,
|
|
1576
2132
|
now: opts.now ?? (() => new Date)
|
|
1577
2133
|
});
|
|
1578
2134
|
}
|
|
@@ -1587,10 +2143,28 @@ async function handleV1Request(ctx) {
|
|
|
1587
2143
|
if (ctx.request.method === "GET" && segments[1] === "status")
|
|
1588
2144
|
return Response.json(apiStatus());
|
|
1589
2145
|
try {
|
|
2146
|
+
if (segments[1] === "import")
|
|
2147
|
+
return await handleImportRequest(ctx, segments.slice(2));
|
|
1590
2148
|
if (segments[1] === "loops")
|
|
1591
2149
|
return await handleLoopsRequest(ctx, segments.slice(2));
|
|
1592
2150
|
if (segments[1] === "runs")
|
|
1593
2151
|
return await handleRunsRequest(ctx, segments.slice(2));
|
|
2152
|
+
if (segments[1] === "receipts")
|
|
2153
|
+
return await handleReceiptsRequest(ctx, segments.slice(2));
|
|
2154
|
+
if (segments[1] === "workflows")
|
|
2155
|
+
return await handleWorkflowsRequest(ctx, segments.slice(2));
|
|
2156
|
+
if (segments[1] === "workflow-runs")
|
|
2157
|
+
return await handleWorkflowRunsRequest(ctx, segments.slice(2));
|
|
2158
|
+
if (segments[1] === "work-items")
|
|
2159
|
+
return await handleWorkItemsRequest(ctx, segments.slice(2));
|
|
2160
|
+
if (segments[1] === "invocations")
|
|
2161
|
+
return await handleInvocationsRequest(ctx, segments.slice(2));
|
|
2162
|
+
if (segments[1] === "goals")
|
|
2163
|
+
return await handleGoalsRequest(ctx, segments.slice(2));
|
|
2164
|
+
if (segments[1] === "goal-runs")
|
|
2165
|
+
return await handleGoalRunsRequest(ctx, segments.slice(2));
|
|
2166
|
+
if (segments[1] === "history")
|
|
2167
|
+
return await handleHistoryRequest(ctx, segments.slice(2));
|
|
1594
2168
|
if (segments[1] === "runners")
|
|
1595
2169
|
return await handleRunnerRequest(ctx, segments.slice(2));
|
|
1596
2170
|
if (segments[1] === "leases" && segments[2] === "recover" && ctx.request.method === "POST") {
|
|
@@ -1601,14 +2175,45 @@ async function handleV1Request(ctx) {
|
|
|
1601
2175
|
return errorResponse(error);
|
|
1602
2176
|
}
|
|
1603
2177
|
}
|
|
2178
|
+
async function handleImportRequest(ctx, segments) {
|
|
2179
|
+
if (segments.length !== 0 || ctx.request.method !== "POST")
|
|
2180
|
+
return fail("not_found", 404);
|
|
2181
|
+
const storage = requireStorage(ctx.storage);
|
|
2182
|
+
const body = await readJsonBody(ctx.request, ctx.importLimitBytes);
|
|
2183
|
+
const replace = body.replace === true;
|
|
2184
|
+
const workflows = Array.isArray(body.workflows) ? body.workflows : [];
|
|
2185
|
+
const loops = Array.isArray(body.loops) ? body.loops : [];
|
|
2186
|
+
const runs = Array.isArray(body.runs) ? body.runs : [];
|
|
2187
|
+
const imported = { workflows: 0, loops: 0, runs: 0 };
|
|
2188
|
+
let skippedRunning = 0;
|
|
2189
|
+
for (const workflow of workflows) {
|
|
2190
|
+
await storage.upsertMigrationWorkflow(workflow, { replace });
|
|
2191
|
+
imported.workflows += 1;
|
|
2192
|
+
}
|
|
2193
|
+
for (const loop of loops) {
|
|
2194
|
+
await storage.upsertMigrationLoop(loop, { replace });
|
|
2195
|
+
imported.loops += 1;
|
|
2196
|
+
}
|
|
2197
|
+
for (const run of runs) {
|
|
2198
|
+
if (run.status === "running") {
|
|
2199
|
+
skippedRunning += 1;
|
|
2200
|
+
continue;
|
|
2201
|
+
}
|
|
2202
|
+
await storage.upsertMigrationRun(run, { replace });
|
|
2203
|
+
imported.runs += 1;
|
|
2204
|
+
}
|
|
2205
|
+
return ok({ imported, skippedRunning });
|
|
2206
|
+
}
|
|
1604
2207
|
async function handleLoopsRequest(ctx, segments) {
|
|
1605
2208
|
const storage = requireStorage(ctx.storage);
|
|
1606
2209
|
if (segments.length === 0 && ctx.request.method === "GET") {
|
|
1607
2210
|
const loops = await storage.listLoops({
|
|
1608
2211
|
status: optionalEnum(ctx.url.searchParams.get("status"), ["active", "paused", "stopped", "expired"]),
|
|
1609
2212
|
limit: optionalLimit(ctx.url.searchParams.get("limit")),
|
|
2213
|
+
offset: optionalOffset(ctx.url.searchParams.get("offset")),
|
|
1610
2214
|
includeArchived: optionalBoolean(ctx.url.searchParams.get("includeArchived")),
|
|
1611
|
-
archived: optionalBoolean(ctx.url.searchParams.get("archived"))
|
|
2215
|
+
archived: optionalBoolean(ctx.url.searchParams.get("archived")),
|
|
2216
|
+
name: optionalString(ctx.url.searchParams.get("name"))
|
|
1612
2217
|
});
|
|
1613
2218
|
return ok({ loops: loops.map(publicLoop) });
|
|
1614
2219
|
}
|
|
@@ -1617,6 +2222,13 @@ async function handleLoopsRequest(ctx, segments) {
|
|
|
1617
2222
|
const loop = await storage.createLoop(body);
|
|
1618
2223
|
return ok({ loop: publicLoop(loop) }, { status: 201 });
|
|
1619
2224
|
}
|
|
2225
|
+
if (segments.length === 1 && segments[0] === "count" && ctx.request.method === "GET") {
|
|
2226
|
+
const count = await storage.countLoops(optionalEnum(ctx.url.searchParams.get("status"), ["active", "paused", "stopped", "expired"]), {
|
|
2227
|
+
includeArchived: optionalBoolean(ctx.url.searchParams.get("includeArchived")),
|
|
2228
|
+
archived: optionalBoolean(ctx.url.searchParams.get("archived"))
|
|
2229
|
+
});
|
|
2230
|
+
return ok({ count });
|
|
2231
|
+
}
|
|
1620
2232
|
const id = segments[0];
|
|
1621
2233
|
if (!id)
|
|
1622
2234
|
return fail("not_found", 404);
|
|
@@ -1649,6 +2261,170 @@ async function handleLoopsRequest(ctx, segments) {
|
|
|
1649
2261
|
if (segments.length === 2 && segments[1] === "unarchive" && ctx.request.method === "POST") {
|
|
1650
2262
|
return ok({ loop: publicLoop(await storage.unarchiveLoop(id)) });
|
|
1651
2263
|
}
|
|
2264
|
+
if (segments.length === 2 && segments[1] === "rename" && ctx.request.method === "POST") {
|
|
2265
|
+
const body = await readJsonBody(ctx.request, ctx.bodyLimitBytes);
|
|
2266
|
+
const name = requiredString(body.name, "name");
|
|
2267
|
+
return ok({ loop: publicLoop(await storage.renameLoop(id, name)) });
|
|
2268
|
+
}
|
|
2269
|
+
return fail("not_found", 404);
|
|
2270
|
+
}
|
|
2271
|
+
async function handleWorkflowsRequest(ctx, segments) {
|
|
2272
|
+
const storage = requireStorage(ctx.storage);
|
|
2273
|
+
if (segments.length === 0 && ctx.request.method === "GET") {
|
|
2274
|
+
const workflows = await storage.listWorkflows({
|
|
2275
|
+
status: optionalEnum(ctx.url.searchParams.get("status"), ["active", "archived"]),
|
|
2276
|
+
limit: optionalLimit(ctx.url.searchParams.get("limit")),
|
|
2277
|
+
offset: optionalOffset(ctx.url.searchParams.get("offset"))
|
|
2278
|
+
});
|
|
2279
|
+
return ok({ workflows: workflows.map(publicWorkflow) });
|
|
2280
|
+
}
|
|
2281
|
+
if (segments.length === 0 && ctx.request.method === "POST") {
|
|
2282
|
+
const body = await readJsonBody(ctx.request, ctx.bodyLimitBytes);
|
|
2283
|
+
return ok({ workflow: publicWorkflow(await storage.createWorkflow(body)) }, { status: 201 });
|
|
2284
|
+
}
|
|
2285
|
+
if (segments.length === 1 && segments[0] === "count" && ctx.request.method === "GET") {
|
|
2286
|
+
const count = await storage.countWorkflows({
|
|
2287
|
+
status: optionalEnum(ctx.url.searchParams.get("status"), ["active", "archived"])
|
|
2288
|
+
});
|
|
2289
|
+
return ok({ count });
|
|
2290
|
+
}
|
|
2291
|
+
const id = segments[0];
|
|
2292
|
+
if (!id)
|
|
2293
|
+
return fail("not_found", 404);
|
|
2294
|
+
if (segments.length === 1 && ctx.request.method === "GET") {
|
|
2295
|
+
const workflow = await storage.getWorkflow(id);
|
|
2296
|
+
if (!workflow)
|
|
2297
|
+
return fail("workflow_not_found", 404);
|
|
2298
|
+
return ok({ workflow: publicWorkflow(workflow) });
|
|
2299
|
+
}
|
|
2300
|
+
if (segments.length === 2 && segments[1] === "archive" && ctx.request.method === "POST") {
|
|
2301
|
+
return ok({ workflow: publicWorkflow(await storage.archiveWorkflow(id)) });
|
|
2302
|
+
}
|
|
2303
|
+
return fail("not_found", 404);
|
|
2304
|
+
}
|
|
2305
|
+
async function handleWorkflowRunsRequest(ctx, segments) {
|
|
2306
|
+
const storage = requireStorage(ctx.storage);
|
|
2307
|
+
if (segments.length === 0 && ctx.request.method === "GET") {
|
|
2308
|
+
const runs = await storage.listWorkflowRuns({
|
|
2309
|
+
workflowId: ctx.url.searchParams.get("workflowId") ?? undefined,
|
|
2310
|
+
loopRunId: ctx.url.searchParams.get("loopRunId") ?? undefined,
|
|
2311
|
+
limit: optionalLimit(ctx.url.searchParams.get("limit"))
|
|
2312
|
+
});
|
|
2313
|
+
return ok({ workflowRuns: runs.map(publicWorkflowRun) });
|
|
2314
|
+
}
|
|
2315
|
+
const id = segments[0];
|
|
2316
|
+
if (!id)
|
|
2317
|
+
return fail("not_found", 404);
|
|
2318
|
+
if (segments.length === 1 && ctx.request.method === "GET") {
|
|
2319
|
+
const run = await storage.getWorkflowRun(id);
|
|
2320
|
+
if (!run)
|
|
2321
|
+
return fail("workflow_run_not_found", 404);
|
|
2322
|
+
return ok({ workflowRun: publicWorkflowRun(run) });
|
|
2323
|
+
}
|
|
2324
|
+
if (segments.length === 2 && segments[1] === "steps" && ctx.request.method === "GET") {
|
|
2325
|
+
const steps = await storage.listWorkflowStepRuns(id);
|
|
2326
|
+
return ok({ steps: steps.map((step) => publicWorkflowStepRun(step)) });
|
|
2327
|
+
}
|
|
2328
|
+
if (segments.length === 2 && segments[1] === "events" && ctx.request.method === "GET") {
|
|
2329
|
+
const events = await storage.listWorkflowEvents(id, optionalLimit(ctx.url.searchParams.get("limit")) ?? 200);
|
|
2330
|
+
return ok({ events: events.map(publicWorkflowEvent) });
|
|
2331
|
+
}
|
|
2332
|
+
if (segments.length === 2 && segments[1] === "recover" && ctx.request.method === "POST") {
|
|
2333
|
+
const body = await readJsonBody(ctx.request, ctx.bodyLimitBytes);
|
|
2334
|
+
const reason = optionalText(body.reason);
|
|
2335
|
+
const result = reason === undefined ? await storage.recoverWorkflowRun(id) : await storage.recoverWorkflowRun(id, reason);
|
|
2336
|
+
return ok({
|
|
2337
|
+
workflowRun: publicWorkflowRun(result.run),
|
|
2338
|
+
recoveredSteps: result.recoveredSteps.map((step) => publicWorkflowStepRun(step))
|
|
2339
|
+
});
|
|
2340
|
+
}
|
|
2341
|
+
return fail("not_found", 404);
|
|
2342
|
+
}
|
|
2343
|
+
async function handleWorkItemsRequest(ctx, segments) {
|
|
2344
|
+
const storage = requireStorage(ctx.storage);
|
|
2345
|
+
if (segments.length === 0 && ctx.request.method === "GET") {
|
|
2346
|
+
const items = await storage.listWorkflowWorkItems({
|
|
2347
|
+
status: optionalString(ctx.url.searchParams.get("status")),
|
|
2348
|
+
routeKey: ctx.url.searchParams.get("routeKey") ?? undefined,
|
|
2349
|
+
limit: optionalLimit(ctx.url.searchParams.get("limit"))
|
|
2350
|
+
});
|
|
2351
|
+
return ok({ workItems: items.map(publicWorkflowWorkItem) });
|
|
2352
|
+
}
|
|
2353
|
+
const id = segments[0];
|
|
2354
|
+
if (!id)
|
|
2355
|
+
return fail("not_found", 404);
|
|
2356
|
+
if (segments.length === 1 && ctx.request.method === "GET") {
|
|
2357
|
+
const item = await storage.getWorkflowWorkItem(id);
|
|
2358
|
+
if (!item)
|
|
2359
|
+
return fail("work_item_not_found", 404);
|
|
2360
|
+
return ok({ workItem: publicWorkflowWorkItem(item) });
|
|
2361
|
+
}
|
|
2362
|
+
return fail("not_found", 404);
|
|
2363
|
+
}
|
|
2364
|
+
async function handleInvocationsRequest(ctx, segments) {
|
|
2365
|
+
const storage = requireStorage(ctx.storage);
|
|
2366
|
+
if (segments.length === 0 && ctx.request.method === "GET") {
|
|
2367
|
+
const invocations = await storage.listWorkflowInvocations({ limit: optionalLimit(ctx.url.searchParams.get("limit")) });
|
|
2368
|
+
return ok({ invocations: invocations.map(publicWorkflowInvocation) });
|
|
2369
|
+
}
|
|
2370
|
+
const id = segments[0];
|
|
2371
|
+
if (!id)
|
|
2372
|
+
return fail("not_found", 404);
|
|
2373
|
+
if (segments.length === 1 && ctx.request.method === "GET") {
|
|
2374
|
+
const invocation = await storage.getWorkflowInvocation(id);
|
|
2375
|
+
if (!invocation)
|
|
2376
|
+
return fail("invocation_not_found", 404);
|
|
2377
|
+
return ok({ invocation: publicWorkflowInvocation(invocation) });
|
|
2378
|
+
}
|
|
2379
|
+
return fail("not_found", 404);
|
|
2380
|
+
}
|
|
2381
|
+
async function handleGoalsRequest(ctx, segments) {
|
|
2382
|
+
const storage = requireStorage(ctx.storage);
|
|
2383
|
+
if (segments.length === 0 && ctx.request.method === "GET") {
|
|
2384
|
+
const goals = await storage.listGoals({
|
|
2385
|
+
status: optionalString(ctx.url.searchParams.get("status")),
|
|
2386
|
+
limit: optionalLimit(ctx.url.searchParams.get("limit"))
|
|
2387
|
+
});
|
|
2388
|
+
return ok({ goals: goals.map(publicGoal) });
|
|
2389
|
+
}
|
|
2390
|
+
const id = segments[0];
|
|
2391
|
+
if (!id)
|
|
2392
|
+
return fail("not_found", 404);
|
|
2393
|
+
if (segments.length === 1 && ctx.request.method === "GET") {
|
|
2394
|
+
const goal = await storage.getGoal(id);
|
|
2395
|
+
if (!goal)
|
|
2396
|
+
return fail("goal_not_found", 404);
|
|
2397
|
+
return ok({ goal: publicGoal(goal) });
|
|
2398
|
+
}
|
|
2399
|
+
if (segments.length === 2 && segments[1] === "plan-nodes" && ctx.request.method === "GET") {
|
|
2400
|
+
const nodes = await storage.listGoalPlanNodes(id);
|
|
2401
|
+
return ok({ nodes });
|
|
2402
|
+
}
|
|
2403
|
+
return fail("not_found", 404);
|
|
2404
|
+
}
|
|
2405
|
+
async function handleGoalRunsRequest(ctx, segments) {
|
|
2406
|
+
const storage = requireStorage(ctx.storage);
|
|
2407
|
+
if (segments.length === 0 && ctx.request.method === "GET") {
|
|
2408
|
+
const runs = await storage.listGoalRuns({
|
|
2409
|
+
goalId: ctx.url.searchParams.get("goalId") ?? undefined,
|
|
2410
|
+
runId: ctx.url.searchParams.get("runId") ?? undefined,
|
|
2411
|
+
limit: optionalLimit(ctx.url.searchParams.get("limit"))
|
|
2412
|
+
});
|
|
2413
|
+
return ok({ goalRuns: runs.map(publicGoalRun) });
|
|
2414
|
+
}
|
|
2415
|
+
return fail("not_found", 404);
|
|
2416
|
+
}
|
|
2417
|
+
async function handleHistoryRequest(ctx, segments) {
|
|
2418
|
+
const storage = requireStorage(ctx.storage);
|
|
2419
|
+
if (segments.length === 1 && segments[0] === "prune" && ctx.request.method === "POST") {
|
|
2420
|
+
const body = await readJsonBody(ctx.request, ctx.bodyLimitBytes);
|
|
2421
|
+
const history = await storage.pruneHistory({
|
|
2422
|
+
maxAgeDays: optionalInteger(body.maxAgeDays),
|
|
2423
|
+
keepPerLoop: optionalInteger(body.keepPerLoop),
|
|
2424
|
+
dryRun: body.dryRun === undefined ? undefined : Boolean(body.dryRun)
|
|
2425
|
+
});
|
|
2426
|
+
return ok({ history });
|
|
2427
|
+
}
|
|
1652
2428
|
return fail("not_found", 404);
|
|
1653
2429
|
}
|
|
1654
2430
|
async function handleRunsRequest(ctx, segments) {
|
|
@@ -1684,11 +2460,16 @@ async function handleRunsRequest(ctx, segments) {
|
|
|
1684
2460
|
}
|
|
1685
2461
|
const storage = requireStorage(ctx.storage);
|
|
1686
2462
|
const showOutput = optionalBoolean(ctx.url.searchParams.get("showOutput")) ?? false;
|
|
2463
|
+
if (segments.length === 1 && id === "count" && ctx.request.method === "GET") {
|
|
2464
|
+
const count = await storage.countRuns(optionalEnum(ctx.url.searchParams.get("status"), ["running", "succeeded", "failed", "timed_out", "abandoned", "skipped"]));
|
|
2465
|
+
return ok({ count });
|
|
2466
|
+
}
|
|
1687
2467
|
if (segments.length === 0 && ctx.request.method === "GET") {
|
|
1688
2468
|
const runs = await storage.listRuns({
|
|
1689
2469
|
loopId: ctx.url.searchParams.get("loopId") ?? undefined,
|
|
1690
2470
|
status: optionalEnum(ctx.url.searchParams.get("status"), ["running", "succeeded", "failed", "timed_out", "abandoned", "skipped"]),
|
|
1691
|
-
limit: optionalLimit(ctx.url.searchParams.get("limit"))
|
|
2471
|
+
limit: optionalLimit(ctx.url.searchParams.get("limit")),
|
|
2472
|
+
offset: optionalOffset(ctx.url.searchParams.get("offset"))
|
|
1692
2473
|
});
|
|
1693
2474
|
return ok({ runs: runs.map((run) => publicRun(run, showOutput, { redactError: true })) });
|
|
1694
2475
|
}
|
|
@@ -1702,6 +2483,34 @@ async function handleRunsRequest(ctx, segments) {
|
|
|
1702
2483
|
}
|
|
1703
2484
|
return fail("not_found", 404);
|
|
1704
2485
|
}
|
|
2486
|
+
async function handleReceiptsRequest(ctx, segments) {
|
|
2487
|
+
const storage = requireStorage(ctx.storage);
|
|
2488
|
+
const id = segments[0];
|
|
2489
|
+
if (segments.length === 0 && ctx.request.method === "GET") {
|
|
2490
|
+
const receipts = await storage.listRunReceipts({
|
|
2491
|
+
loopId: ctx.url.searchParams.get("loopId") ?? undefined,
|
|
2492
|
+
repo: ctx.url.searchParams.get("repo") ?? undefined,
|
|
2493
|
+
taskId: ctx.url.searchParams.get("taskId") ?? undefined,
|
|
2494
|
+
knowledgeId: ctx.url.searchParams.get("knowledgeId") ?? undefined,
|
|
2495
|
+
status: ctx.url.searchParams.get("status") ?? undefined,
|
|
2496
|
+
limit: optionalLimit(ctx.url.searchParams.get("limit"))
|
|
2497
|
+
});
|
|
2498
|
+
return ok({ receipts: receipts.map(publicRunReceipt) });
|
|
2499
|
+
}
|
|
2500
|
+
if (segments.length === 0 && ctx.request.method === "POST") {
|
|
2501
|
+
const body = await readJsonBody(ctx.request, ctx.evidenceLimitBytes);
|
|
2502
|
+
return ok({ receipt: publicRunReceipt(await storage.writeRunReceipt(body)) }, { status: 201 });
|
|
2503
|
+
}
|
|
2504
|
+
if (!id)
|
|
2505
|
+
return fail("not_found", 404);
|
|
2506
|
+
if (segments.length === 1 && ctx.request.method === "GET") {
|
|
2507
|
+
const receipt = await storage.getRunReceipt(id);
|
|
2508
|
+
if (!receipt)
|
|
2509
|
+
return fail("run_receipt_not_found", 404);
|
|
2510
|
+
return ok({ receipt: publicRunReceipt(receipt) });
|
|
2511
|
+
}
|
|
2512
|
+
return fail("not_found", 404);
|
|
2513
|
+
}
|
|
1705
2514
|
async function handleRunnerRequest(ctx, segments) {
|
|
1706
2515
|
if (ctx.request.method !== "POST")
|
|
1707
2516
|
return fail("not_found", 404);
|
|
@@ -1913,13 +2722,22 @@ async function readBodyText(request, limitBytes) {
|
|
|
1913
2722
|
function runnerProtocolPending(message) {
|
|
1914
2723
|
return fail("runner_protocol_pending", 501, { message });
|
|
1915
2724
|
}
|
|
2725
|
+
var MAX_PAGE_LIMIT = 1000;
|
|
1916
2726
|
function optionalLimit(value) {
|
|
1917
2727
|
if (value == null || value === "")
|
|
1918
2728
|
return;
|
|
1919
2729
|
const limit = Number(value);
|
|
1920
|
-
if (!Number.isInteger(limit) || limit < 1
|
|
2730
|
+
if (!Number.isInteger(limit) || limit < 1)
|
|
1921
2731
|
throw Object.assign(new Error("invalid_limit"), { status: 422 });
|
|
1922
|
-
return limit;
|
|
2732
|
+
return Math.min(limit, MAX_PAGE_LIMIT);
|
|
2733
|
+
}
|
|
2734
|
+
function optionalOffset(value) {
|
|
2735
|
+
if (value == null || value === "")
|
|
2736
|
+
return;
|
|
2737
|
+
const offset = Number(value);
|
|
2738
|
+
if (!Number.isInteger(offset) || offset < 0)
|
|
2739
|
+
throw Object.assign(new Error("invalid_offset"), { status: 422 });
|
|
2740
|
+
return offset;
|
|
1923
2741
|
}
|
|
1924
2742
|
function optionalString(value) {
|
|
1925
2743
|
if (value === undefined || value === null)
|