@hasna/loops 0.4.13 → 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 +99 -3
- package/README.md +171 -39
- package/dist/api/index.d.ts +36 -0
- package/dist/api/index.js +1518 -15
- package/dist/cli/index.js +7280 -3213
- package/dist/cli/ui.d.ts +44 -0
- package/dist/daemon/index.js +1433 -303
- package/dist/generated/storage-kit/health.d.ts +19 -0
- package/dist/generated/storage-kit/index.d.ts +7 -0
- package/dist/generated/storage-kit/migrations.d.ts +47 -0
- package/dist/generated/storage-kit/mode.d.ts +47 -0
- package/dist/generated/storage-kit/pool.d.ts +33 -0
- package/dist/generated/storage-kit/query.d.ts +35 -0
- package/dist/generated/storage-kit/tls.d.ts +25 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +8689 -4837
- 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/health.d.ts +83 -3
- package/dist/lib/migration.d.ts +40 -0
- package/dist/lib/mode.js +15 -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/scheduler.d.ts +5 -2
- package/dist/lib/storage/contract.d.ts +7 -1
- package/dist/lib/storage/index.d.ts +1 -0
- package/dist/lib/storage/index.js +2028 -231
- package/dist/lib/storage/pg-executor.d.ts +27 -0
- package/dist/lib/storage/pg-runner-claim.d.ts +40 -0
- package/dist/lib/storage/postgres-loop-storage.d.ts +120 -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 +748 -26
- package/dist/lib/store/index.d.ts +268 -0
- package/dist/lib/store.d.ts +282 -1
- package/dist/lib/store.js +746 -26
- 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 +2885 -634
- package/dist/runner/index.js +198 -32
- package/dist/sdk/http.d.ts +182 -0
- package/dist/sdk/http.js +166 -0
- package/dist/sdk/index.d.ts +55 -18
- package/dist/sdk/index.js +2734 -617
- package/dist/serve/index.d.ts +4 -0
- package/dist/serve/index.js +9095 -0
- package/dist/types.d.ts +52 -0
- package/docs/AUTOMATION_RUNTIME_DESIGN.md +442 -1
- package/docs/CUTOVER-RUNBOOK.md +78 -0
- package/docs/DEPLOYMENT_MODES.md +35 -18
- package/docs/RUNTIME_BOUNDARY.md +203 -0
- package/docs/USAGE.md +195 -38
- package/package.json +15 -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",
|
|
@@ -12,6 +12,7 @@ var package_default = {
|
|
|
12
12
|
loops: "dist/cli/index.js",
|
|
13
13
|
"loops-daemon": "dist/daemon/index.js",
|
|
14
14
|
"loops-api": "dist/api/index.js",
|
|
15
|
+
"loops-serve": "dist/serve/index.js",
|
|
15
16
|
"loops-runner": "dist/runner/index.js",
|
|
16
17
|
"loops-mcp": "dist/mcp/index.js"
|
|
17
18
|
},
|
|
@@ -24,6 +25,14 @@ var package_default = {
|
|
|
24
25
|
types: "./dist/sdk/index.d.ts",
|
|
25
26
|
import: "./dist/sdk/index.js"
|
|
26
27
|
},
|
|
28
|
+
"./sdk/http": {
|
|
29
|
+
types: "./dist/sdk/http.d.ts",
|
|
30
|
+
import: "./dist/sdk/http.js"
|
|
31
|
+
},
|
|
32
|
+
"./serve": {
|
|
33
|
+
types: "./dist/serve/index.d.ts",
|
|
34
|
+
import: "./dist/serve/index.js"
|
|
35
|
+
},
|
|
27
36
|
"./mcp": {
|
|
28
37
|
types: "./dist/mcp/index.d.ts",
|
|
29
38
|
import: "./dist/mcp/index.js"
|
|
@@ -69,7 +78,7 @@ var package_default = {
|
|
|
69
78
|
"LICENSE"
|
|
70
79
|
],
|
|
71
80
|
scripts: {
|
|
72
|
-
build: "rm -rf dist && bun build src/cli/index.ts src/daemon/index.ts src/api/index.ts src/runner/index.ts src/mcp/index.ts src/index.ts src/sdk/index.ts src/lib/store.ts src/lib/mode.ts src/lib/storage/index.ts src/lib/storage/contract.ts src/lib/storage/sqlite.ts src/lib/storage/postgres.ts src/lib/storage/postgres-schema.ts --root src --outdir dist --target bun --packages external && chmod +x dist/cli/index.js dist/daemon/index.js dist/api/index.js dist/runner/index.js dist/mcp/index.js && tsc -p tsconfig.build.json --emitDeclarationOnly --outDir dist",
|
|
81
|
+
build: "rm -rf dist && bun build src/cli/index.ts src/daemon/index.ts src/api/index.ts src/serve/index.ts src/runner/index.ts src/mcp/index.ts src/index.ts src/sdk/index.ts src/sdk/http.ts src/lib/store.ts src/lib/mode.ts src/lib/storage/index.ts src/lib/storage/contract.ts src/lib/storage/sqlite.ts src/lib/storage/postgres.ts src/lib/storage/postgres-schema.ts --root src --outdir dist --target bun --packages external && chmod +x dist/cli/index.js dist/daemon/index.js dist/api/index.js dist/serve/index.js dist/runner/index.js dist/mcp/index.js && tsc -p tsconfig.build.json --emitDeclarationOnly --outDir dist",
|
|
73
82
|
"build:bin": "bun build src/cli/index.ts --compile --outfile dist/loops",
|
|
74
83
|
typecheck: "tsc --noEmit",
|
|
75
84
|
test: "bun test",
|
|
@@ -109,13 +118,16 @@ var package_default = {
|
|
|
109
118
|
"@openrouter/ai-sdk-provider": "2.9.1",
|
|
110
119
|
ai: "6.0.204",
|
|
111
120
|
commander: "^13.1.0",
|
|
121
|
+
pg: "^8.13.1",
|
|
112
122
|
zod: "4.4.3"
|
|
113
123
|
},
|
|
114
124
|
optionalDependencies: {
|
|
115
|
-
"@hasna/machines": "0.0.49"
|
|
125
|
+
"@hasna/machines": "0.0.49",
|
|
126
|
+
"@hasna/contracts": "^0.4.2"
|
|
116
127
|
},
|
|
117
128
|
devDependencies: {
|
|
118
129
|
"@types/bun": "latest",
|
|
130
|
+
"@types/pg": "^8.11.10",
|
|
119
131
|
typescript: "^5.7.3"
|
|
120
132
|
},
|
|
121
133
|
publishConfig: {
|
|
@@ -426,6 +438,9 @@ function publicRun(run, showOutput = false, opts = {}) {
|
|
|
426
438
|
error: opts.redactError ? redact(run.error) : run.error
|
|
427
439
|
};
|
|
428
440
|
}
|
|
441
|
+
function publicRunReceipt(receipt) {
|
|
442
|
+
return { ...receipt };
|
|
443
|
+
}
|
|
429
444
|
function publicExecutorResult(result, showOutput = false) {
|
|
430
445
|
return {
|
|
431
446
|
...result,
|
|
@@ -815,11 +830,1194 @@ function scrubSecretsDeep(value) {
|
|
|
815
830
|
}
|
|
816
831
|
return value;
|
|
817
832
|
}
|
|
833
|
+
// openapi/loops.json
|
|
834
|
+
var loops_default = {
|
|
835
|
+
openapi: "3.1.0",
|
|
836
|
+
info: {
|
|
837
|
+
title: "OpenLoops",
|
|
838
|
+
version: "0.4.14",
|
|
839
|
+
description: "OpenLoops self-hosted control-plane HTTP API (loops-serve). RDS-direct (Amendment A1). All /v1 routes require an API key (x-api-key or Authorization: Bearer) with a loops:* scope."
|
|
840
|
+
},
|
|
841
|
+
servers: [
|
|
842
|
+
{
|
|
843
|
+
url: "https://loops.hasna.xyz"
|
|
844
|
+
}
|
|
845
|
+
],
|
|
846
|
+
paths: {
|
|
847
|
+
"/health": {
|
|
848
|
+
get: {
|
|
849
|
+
operationId: "healthCheck",
|
|
850
|
+
summary: "Liveness probe",
|
|
851
|
+
responses: {
|
|
852
|
+
"200": {
|
|
853
|
+
description: "alive",
|
|
854
|
+
content: {
|
|
855
|
+
"application/json": {
|
|
856
|
+
schema: {
|
|
857
|
+
$ref: "#/components/schemas/Foundation"
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
},
|
|
865
|
+
"/ready": {
|
|
866
|
+
get: {
|
|
867
|
+
operationId: "readyCheck",
|
|
868
|
+
summary: "Readiness probe (storage reachable + migrated)",
|
|
869
|
+
responses: {
|
|
870
|
+
"200": {
|
|
871
|
+
description: "ready",
|
|
872
|
+
content: {
|
|
873
|
+
"application/json": {
|
|
874
|
+
schema: {
|
|
875
|
+
$ref: "#/components/schemas/Foundation"
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
},
|
|
880
|
+
"503": {
|
|
881
|
+
description: "not ready",
|
|
882
|
+
content: {
|
|
883
|
+
"application/json": {
|
|
884
|
+
schema: {
|
|
885
|
+
$ref: "#/components/schemas/Foundation"
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
},
|
|
893
|
+
"/version": {
|
|
894
|
+
get: {
|
|
895
|
+
operationId: "getVersion",
|
|
896
|
+
summary: "Service version",
|
|
897
|
+
responses: {
|
|
898
|
+
"200": {
|
|
899
|
+
description: "version",
|
|
900
|
+
content: {
|
|
901
|
+
"application/json": {
|
|
902
|
+
schema: {
|
|
903
|
+
$ref: "#/components/schemas/Foundation"
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
},
|
|
911
|
+
"/v1/loops": {
|
|
912
|
+
get: {
|
|
913
|
+
operationId: "listLoops",
|
|
914
|
+
summary: "List loops",
|
|
915
|
+
parameters: [
|
|
916
|
+
{
|
|
917
|
+
name: "status",
|
|
918
|
+
in: "query",
|
|
919
|
+
required: false,
|
|
920
|
+
schema: {
|
|
921
|
+
type: "string",
|
|
922
|
+
enum: [
|
|
923
|
+
"active",
|
|
924
|
+
"paused",
|
|
925
|
+
"stopped",
|
|
926
|
+
"expired"
|
|
927
|
+
]
|
|
928
|
+
}
|
|
929
|
+
},
|
|
930
|
+
{
|
|
931
|
+
name: "limit",
|
|
932
|
+
in: "query",
|
|
933
|
+
required: false,
|
|
934
|
+
schema: {
|
|
935
|
+
type: "integer"
|
|
936
|
+
}
|
|
937
|
+
},
|
|
938
|
+
{
|
|
939
|
+
name: "includeArchived",
|
|
940
|
+
in: "query",
|
|
941
|
+
required: false,
|
|
942
|
+
schema: {
|
|
943
|
+
type: "boolean"
|
|
944
|
+
}
|
|
945
|
+
},
|
|
946
|
+
{
|
|
947
|
+
name: "archived",
|
|
948
|
+
in: "query",
|
|
949
|
+
required: false,
|
|
950
|
+
schema: {
|
|
951
|
+
type: "boolean"
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
],
|
|
955
|
+
responses: {
|
|
956
|
+
"200": {
|
|
957
|
+
description: "loops",
|
|
958
|
+
content: {
|
|
959
|
+
"application/json": {
|
|
960
|
+
schema: {
|
|
961
|
+
$ref: "#/components/schemas/LoopListResponse"
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
},
|
|
968
|
+
post: {
|
|
969
|
+
operationId: "createLoop",
|
|
970
|
+
summary: "Create a loop",
|
|
971
|
+
requestBody: {
|
|
972
|
+
required: true,
|
|
973
|
+
content: {
|
|
974
|
+
"application/json": {
|
|
975
|
+
schema: {
|
|
976
|
+
$ref: "#/components/schemas/CreateLoopInput"
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
},
|
|
981
|
+
responses: {
|
|
982
|
+
"201": {
|
|
983
|
+
description: "created",
|
|
984
|
+
content: {
|
|
985
|
+
"application/json": {
|
|
986
|
+
schema: {
|
|
987
|
+
$ref: "#/components/schemas/LoopResponse"
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
}
|
|
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
|
+
},
|
|
1039
|
+
"/v1/loops/{id}": {
|
|
1040
|
+
get: {
|
|
1041
|
+
operationId: "getLoop",
|
|
1042
|
+
summary: "Get a loop by id",
|
|
1043
|
+
parameters: [
|
|
1044
|
+
{
|
|
1045
|
+
name: "id",
|
|
1046
|
+
in: "path",
|
|
1047
|
+
required: true,
|
|
1048
|
+
schema: {
|
|
1049
|
+
type: "string"
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
],
|
|
1053
|
+
responses: {
|
|
1054
|
+
"200": {
|
|
1055
|
+
description: "loop",
|
|
1056
|
+
content: {
|
|
1057
|
+
"application/json": {
|
|
1058
|
+
schema: {
|
|
1059
|
+
$ref: "#/components/schemas/LoopResponse"
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
},
|
|
1066
|
+
patch: {
|
|
1067
|
+
operationId: "updateLoop",
|
|
1068
|
+
summary: "Update a loop (status / schedule fields)",
|
|
1069
|
+
parameters: [
|
|
1070
|
+
{
|
|
1071
|
+
name: "id",
|
|
1072
|
+
in: "path",
|
|
1073
|
+
required: true,
|
|
1074
|
+
schema: {
|
|
1075
|
+
type: "string"
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
],
|
|
1079
|
+
requestBody: {
|
|
1080
|
+
required: true,
|
|
1081
|
+
content: {
|
|
1082
|
+
"application/json": {
|
|
1083
|
+
schema: {
|
|
1084
|
+
$ref: "#/components/schemas/UpdateLoopInput"
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
},
|
|
1089
|
+
responses: {
|
|
1090
|
+
"200": {
|
|
1091
|
+
description: "loop",
|
|
1092
|
+
content: {
|
|
1093
|
+
"application/json": {
|
|
1094
|
+
schema: {
|
|
1095
|
+
$ref: "#/components/schemas/LoopResponse"
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
},
|
|
1102
|
+
delete: {
|
|
1103
|
+
operationId: "deleteLoop",
|
|
1104
|
+
summary: "Delete a loop",
|
|
1105
|
+
parameters: [
|
|
1106
|
+
{
|
|
1107
|
+
name: "id",
|
|
1108
|
+
in: "path",
|
|
1109
|
+
required: true,
|
|
1110
|
+
schema: {
|
|
1111
|
+
type: "string"
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
],
|
|
1115
|
+
responses: {
|
|
1116
|
+
"200": {
|
|
1117
|
+
description: "deleted",
|
|
1118
|
+
content: {
|
|
1119
|
+
"application/json": {
|
|
1120
|
+
schema: {
|
|
1121
|
+
$ref: "#/components/schemas/DeleteResponse"
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
},
|
|
1129
|
+
"/v1/loops/{id}/archive": {
|
|
1130
|
+
post: {
|
|
1131
|
+
operationId: "archiveLoop",
|
|
1132
|
+
summary: "Archive a loop",
|
|
1133
|
+
parameters: [
|
|
1134
|
+
{
|
|
1135
|
+
name: "id",
|
|
1136
|
+
in: "path",
|
|
1137
|
+
required: true,
|
|
1138
|
+
schema: {
|
|
1139
|
+
type: "string"
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
],
|
|
1143
|
+
responses: {
|
|
1144
|
+
"200": {
|
|
1145
|
+
description: "loop",
|
|
1146
|
+
content: {
|
|
1147
|
+
"application/json": {
|
|
1148
|
+
schema: {
|
|
1149
|
+
$ref: "#/components/schemas/LoopResponse"
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
},
|
|
1157
|
+
"/v1/loops/{id}/unarchive": {
|
|
1158
|
+
post: {
|
|
1159
|
+
operationId: "unarchiveLoop",
|
|
1160
|
+
summary: "Unarchive a loop",
|
|
1161
|
+
parameters: [
|
|
1162
|
+
{
|
|
1163
|
+
name: "id",
|
|
1164
|
+
in: "path",
|
|
1165
|
+
required: true,
|
|
1166
|
+
schema: {
|
|
1167
|
+
type: "string"
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
],
|
|
1171
|
+
responses: {
|
|
1172
|
+
"200": {
|
|
1173
|
+
description: "loop",
|
|
1174
|
+
content: {
|
|
1175
|
+
"application/json": {
|
|
1176
|
+
schema: {
|
|
1177
|
+
$ref: "#/components/schemas/LoopResponse"
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
},
|
|
1185
|
+
"/v1/runs": {
|
|
1186
|
+
get: {
|
|
1187
|
+
operationId: "listRuns",
|
|
1188
|
+
summary: "List runs",
|
|
1189
|
+
parameters: [
|
|
1190
|
+
{
|
|
1191
|
+
name: "loopId",
|
|
1192
|
+
in: "query",
|
|
1193
|
+
required: false,
|
|
1194
|
+
schema: {
|
|
1195
|
+
type: "string"
|
|
1196
|
+
}
|
|
1197
|
+
},
|
|
1198
|
+
{
|
|
1199
|
+
name: "status",
|
|
1200
|
+
in: "query",
|
|
1201
|
+
required: false,
|
|
1202
|
+
schema: {
|
|
1203
|
+
type: "string"
|
|
1204
|
+
}
|
|
1205
|
+
},
|
|
1206
|
+
{
|
|
1207
|
+
name: "limit",
|
|
1208
|
+
in: "query",
|
|
1209
|
+
required: false,
|
|
1210
|
+
schema: {
|
|
1211
|
+
type: "integer"
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
],
|
|
1215
|
+
responses: {
|
|
1216
|
+
"200": {
|
|
1217
|
+
description: "runs",
|
|
1218
|
+
content: {
|
|
1219
|
+
"application/json": {
|
|
1220
|
+
schema: {
|
|
1221
|
+
$ref: "#/components/schemas/RunListResponse"
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
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
|
+
},
|
|
1257
|
+
"/v1/runs/{id}": {
|
|
1258
|
+
get: {
|
|
1259
|
+
operationId: "getRun",
|
|
1260
|
+
summary: "Get a run by id",
|
|
1261
|
+
parameters: [
|
|
1262
|
+
{
|
|
1263
|
+
name: "id",
|
|
1264
|
+
in: "path",
|
|
1265
|
+
required: true,
|
|
1266
|
+
schema: {
|
|
1267
|
+
type: "string"
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
],
|
|
1271
|
+
responses: {
|
|
1272
|
+
"200": {
|
|
1273
|
+
description: "run",
|
|
1274
|
+
content: {
|
|
1275
|
+
"application/json": {
|
|
1276
|
+
schema: {
|
|
1277
|
+
$ref: "#/components/schemas/RunResponse"
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
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
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
},
|
|
1437
|
+
components: {
|
|
1438
|
+
schemas: {
|
|
1439
|
+
Foundation: {
|
|
1440
|
+
type: "object",
|
|
1441
|
+
properties: {
|
|
1442
|
+
status: {
|
|
1443
|
+
type: "string"
|
|
1444
|
+
},
|
|
1445
|
+
version: {
|
|
1446
|
+
type: "string"
|
|
1447
|
+
},
|
|
1448
|
+
mode: {
|
|
1449
|
+
type: "string"
|
|
1450
|
+
},
|
|
1451
|
+
service: {
|
|
1452
|
+
type: "string"
|
|
1453
|
+
},
|
|
1454
|
+
detail: {
|
|
1455
|
+
type: "string"
|
|
1456
|
+
}
|
|
1457
|
+
},
|
|
1458
|
+
required: [
|
|
1459
|
+
"status",
|
|
1460
|
+
"version",
|
|
1461
|
+
"mode"
|
|
1462
|
+
]
|
|
1463
|
+
},
|
|
1464
|
+
Loop: {
|
|
1465
|
+
type: "object",
|
|
1466
|
+
properties: {
|
|
1467
|
+
id: {
|
|
1468
|
+
type: "string"
|
|
1469
|
+
},
|
|
1470
|
+
name: {
|
|
1471
|
+
type: "string"
|
|
1472
|
+
},
|
|
1473
|
+
description: {
|
|
1474
|
+
type: "string",
|
|
1475
|
+
nullable: true
|
|
1476
|
+
},
|
|
1477
|
+
status: {
|
|
1478
|
+
type: "string"
|
|
1479
|
+
},
|
|
1480
|
+
schedule: {
|
|
1481
|
+
type: "object",
|
|
1482
|
+
additionalProperties: true
|
|
1483
|
+
},
|
|
1484
|
+
target: {
|
|
1485
|
+
type: "object",
|
|
1486
|
+
additionalProperties: true
|
|
1487
|
+
},
|
|
1488
|
+
nextRunAt: {
|
|
1489
|
+
type: "string",
|
|
1490
|
+
nullable: true
|
|
1491
|
+
},
|
|
1492
|
+
createdAt: {
|
|
1493
|
+
type: "string"
|
|
1494
|
+
},
|
|
1495
|
+
updatedAt: {
|
|
1496
|
+
type: "string"
|
|
1497
|
+
}
|
|
1498
|
+
},
|
|
1499
|
+
required: [
|
|
1500
|
+
"id",
|
|
1501
|
+
"name",
|
|
1502
|
+
"status"
|
|
1503
|
+
]
|
|
1504
|
+
},
|
|
1505
|
+
CreateLoopInput: {
|
|
1506
|
+
type: "object",
|
|
1507
|
+
properties: {
|
|
1508
|
+
name: {
|
|
1509
|
+
type: "string"
|
|
1510
|
+
},
|
|
1511
|
+
description: {
|
|
1512
|
+
type: "string"
|
|
1513
|
+
},
|
|
1514
|
+
schedule: {
|
|
1515
|
+
type: "object",
|
|
1516
|
+
additionalProperties: true
|
|
1517
|
+
},
|
|
1518
|
+
target: {
|
|
1519
|
+
type: "object",
|
|
1520
|
+
additionalProperties: true
|
|
1521
|
+
}
|
|
1522
|
+
},
|
|
1523
|
+
required: [
|
|
1524
|
+
"name",
|
|
1525
|
+
"schedule",
|
|
1526
|
+
"target"
|
|
1527
|
+
]
|
|
1528
|
+
},
|
|
1529
|
+
UpdateLoopInput: {
|
|
1530
|
+
type: "object",
|
|
1531
|
+
properties: {
|
|
1532
|
+
status: {
|
|
1533
|
+
type: "string",
|
|
1534
|
+
enum: [
|
|
1535
|
+
"active",
|
|
1536
|
+
"paused",
|
|
1537
|
+
"stopped",
|
|
1538
|
+
"expired"
|
|
1539
|
+
]
|
|
1540
|
+
},
|
|
1541
|
+
nextRunAt: {
|
|
1542
|
+
type: "string",
|
|
1543
|
+
nullable: true
|
|
1544
|
+
},
|
|
1545
|
+
retryScheduledFor: {
|
|
1546
|
+
type: "string",
|
|
1547
|
+
nullable: true
|
|
1548
|
+
},
|
|
1549
|
+
expiresAt: {
|
|
1550
|
+
type: "string",
|
|
1551
|
+
nullable: true
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
},
|
|
1555
|
+
Run: {
|
|
1556
|
+
type: "object",
|
|
1557
|
+
properties: {
|
|
1558
|
+
id: {
|
|
1559
|
+
type: "string"
|
|
1560
|
+
},
|
|
1561
|
+
loopId: {
|
|
1562
|
+
type: "string"
|
|
1563
|
+
},
|
|
1564
|
+
status: {
|
|
1565
|
+
type: "string"
|
|
1566
|
+
},
|
|
1567
|
+
attempt: {
|
|
1568
|
+
type: "integer"
|
|
1569
|
+
},
|
|
1570
|
+
scheduledFor: {
|
|
1571
|
+
type: "string"
|
|
1572
|
+
},
|
|
1573
|
+
startedAt: {
|
|
1574
|
+
type: "string",
|
|
1575
|
+
nullable: true
|
|
1576
|
+
},
|
|
1577
|
+
finishedAt: {
|
|
1578
|
+
type: "string",
|
|
1579
|
+
nullable: true
|
|
1580
|
+
}
|
|
1581
|
+
},
|
|
1582
|
+
required: [
|
|
1583
|
+
"id",
|
|
1584
|
+
"loopId",
|
|
1585
|
+
"status"
|
|
1586
|
+
]
|
|
1587
|
+
},
|
|
1588
|
+
LoopResponse: {
|
|
1589
|
+
type: "object",
|
|
1590
|
+
properties: {
|
|
1591
|
+
ok: {
|
|
1592
|
+
type: "boolean"
|
|
1593
|
+
},
|
|
1594
|
+
loop: {
|
|
1595
|
+
$ref: "#/components/schemas/Loop"
|
|
1596
|
+
}
|
|
1597
|
+
},
|
|
1598
|
+
required: [
|
|
1599
|
+
"ok",
|
|
1600
|
+
"loop"
|
|
1601
|
+
]
|
|
1602
|
+
},
|
|
1603
|
+
LoopListResponse: {
|
|
1604
|
+
type: "object",
|
|
1605
|
+
properties: {
|
|
1606
|
+
ok: {
|
|
1607
|
+
type: "boolean"
|
|
1608
|
+
},
|
|
1609
|
+
loops: {
|
|
1610
|
+
type: "array",
|
|
1611
|
+
items: {
|
|
1612
|
+
$ref: "#/components/schemas/Loop"
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
},
|
|
1616
|
+
required: [
|
|
1617
|
+
"ok",
|
|
1618
|
+
"loops"
|
|
1619
|
+
]
|
|
1620
|
+
},
|
|
1621
|
+
RunResponse: {
|
|
1622
|
+
type: "object",
|
|
1623
|
+
properties: {
|
|
1624
|
+
ok: {
|
|
1625
|
+
type: "boolean"
|
|
1626
|
+
},
|
|
1627
|
+
run: {
|
|
1628
|
+
$ref: "#/components/schemas/Run"
|
|
1629
|
+
}
|
|
1630
|
+
},
|
|
1631
|
+
required: [
|
|
1632
|
+
"ok",
|
|
1633
|
+
"run"
|
|
1634
|
+
]
|
|
1635
|
+
},
|
|
1636
|
+
RunListResponse: {
|
|
1637
|
+
type: "object",
|
|
1638
|
+
properties: {
|
|
1639
|
+
ok: {
|
|
1640
|
+
type: "boolean"
|
|
1641
|
+
},
|
|
1642
|
+
runs: {
|
|
1643
|
+
type: "array",
|
|
1644
|
+
items: {
|
|
1645
|
+
$ref: "#/components/schemas/Run"
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
},
|
|
1649
|
+
required: [
|
|
1650
|
+
"ok",
|
|
1651
|
+
"runs"
|
|
1652
|
+
]
|
|
1653
|
+
},
|
|
1654
|
+
DeleteResponse: {
|
|
1655
|
+
type: "object",
|
|
1656
|
+
properties: {
|
|
1657
|
+
ok: {
|
|
1658
|
+
type: "boolean"
|
|
1659
|
+
},
|
|
1660
|
+
deleted: {
|
|
1661
|
+
type: "boolean"
|
|
1662
|
+
}
|
|
1663
|
+
},
|
|
1664
|
+
required: [
|
|
1665
|
+
"ok",
|
|
1666
|
+
"deleted"
|
|
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
|
+
]
|
|
1996
|
+
}
|
|
1997
|
+
},
|
|
1998
|
+
securitySchemes: {
|
|
1999
|
+
apiKey: {
|
|
2000
|
+
type: "apiKey",
|
|
2001
|
+
in: "header",
|
|
2002
|
+
name: "x-api-key"
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
},
|
|
2006
|
+
security: [
|
|
2007
|
+
{
|
|
2008
|
+
apiKey: []
|
|
2009
|
+
}
|
|
2010
|
+
]
|
|
2011
|
+
};
|
|
818
2012
|
|
|
819
2013
|
// src/api/index.ts
|
|
2014
|
+
function openApiDocument() {
|
|
2015
|
+
return { ...loops_default, info: { ...loops_default.info, version: packageVersion() } };
|
|
2016
|
+
}
|
|
820
2017
|
var program = new Command;
|
|
821
2018
|
var DEFAULT_BODY_LIMIT_BYTES = 64 * 1024;
|
|
822
2019
|
var DEFAULT_EVIDENCE_LIMIT_BYTES = 256 * 1024;
|
|
2020
|
+
var DEFAULT_IMPORT_LIMIT_BYTES = 32 * 1024 * 1024;
|
|
823
2021
|
var MIN_RUNNER_LEASE_MS = 1000;
|
|
824
2022
|
program.name("loops-api").description("OpenLoops self-hosted control-plane API foundation").version(packageVersion()).option("-j, --json", "print JSON");
|
|
825
2023
|
function wantsJson(opts) {
|
|
@@ -866,21 +2064,62 @@ function apiStatus() {
|
|
|
866
2064
|
status: buildDeploymentStatus({ perspective: "self_hosted" })
|
|
867
2065
|
};
|
|
868
2066
|
}
|
|
2067
|
+
function foundationMode() {
|
|
2068
|
+
return buildDeploymentStatus({}).activeDeploymentMode;
|
|
2069
|
+
}
|
|
2070
|
+
function foundationEnvelope(status, extra = {}) {
|
|
2071
|
+
return { status, version: packageVersion(), mode: foundationMode(), service: "loops", ...extra };
|
|
2072
|
+
}
|
|
869
2073
|
function createLoopsApiServer(opts = {}) {
|
|
870
2074
|
const host = opts.host ?? "127.0.0.1";
|
|
871
2075
|
const port = opts.port ?? 8787;
|
|
872
|
-
if (!isLocalBind(host) && !configuredAuthToken()) {
|
|
873
|
-
throw new Error("non-local loops-
|
|
2076
|
+
if (!isLocalBind(host) && !opts.authenticator && !configuredAuthToken()) {
|
|
2077
|
+
throw new Error("non-local loops-serve binds require an API-key authenticator or LOOPS_API_TOKEN");
|
|
874
2078
|
}
|
|
2079
|
+
const defaultReady = async () => {
|
|
2080
|
+
if (!opts.storage)
|
|
2081
|
+
return { ready: false, detail: "storage_unconfigured" };
|
|
2082
|
+
try {
|
|
2083
|
+
await opts.storage.listLoops({ limit: 1 });
|
|
2084
|
+
return { ready: true };
|
|
2085
|
+
} catch (error) {
|
|
2086
|
+
return { ready: false, detail: error instanceof Error ? error.message : "storage_unreachable" };
|
|
2087
|
+
}
|
|
2088
|
+
};
|
|
2089
|
+
const readyCheck = opts.readyCheck ?? defaultReady;
|
|
875
2090
|
return Bun.serve({
|
|
876
2091
|
hostname: host,
|
|
877
2092
|
port,
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
if (unauthorized)
|
|
881
|
-
return unauthorized;
|
|
2093
|
+
idleTimeout: 60,
|
|
2094
|
+
async fetch(request) {
|
|
882
2095
|
const url = new URL(request.url);
|
|
883
|
-
if (url.pathname === "/health" || url.pathname === "/
|
|
2096
|
+
if (request.method === "GET" && (url.pathname === "/health" || url.pathname === "/healthz")) {
|
|
2097
|
+
return Response.json(foundationEnvelope("ok"));
|
|
2098
|
+
}
|
|
2099
|
+
if (request.method === "GET" && (url.pathname === "/version" || url.pathname === "/v1/version")) {
|
|
2100
|
+
return Response.json(foundationEnvelope("ok"));
|
|
2101
|
+
}
|
|
2102
|
+
if (request.method === "GET" && (url.pathname === "/ready" || url.pathname === "/readyz")) {
|
|
2103
|
+
const result = await readyCheck();
|
|
2104
|
+
return Response.json(foundationEnvelope(result.ready ? "ready" : "not_ready", result.detail ? { detail: result.detail } : {}), { status: result.ready ? 200 : 503 });
|
|
2105
|
+
}
|
|
2106
|
+
if (request.method === "GET" && url.pathname === "/openapi.json") {
|
|
2107
|
+
return Response.json(openApiDocument());
|
|
2108
|
+
}
|
|
2109
|
+
if (opts.authenticator) {
|
|
2110
|
+
const decision = await opts.authenticator.authenticate(request.headers, {
|
|
2111
|
+
method: request.method,
|
|
2112
|
+
path: url.pathname
|
|
2113
|
+
});
|
|
2114
|
+
if (!decision.ok) {
|
|
2115
|
+
return Response.json({ ok: false, error: decision.reason ?? "unauthorized", message: decision.message }, { status: decision.status || 401 });
|
|
2116
|
+
}
|
|
2117
|
+
} else {
|
|
2118
|
+
const unauthorized = authorizeRequest(request, host);
|
|
2119
|
+
if (unauthorized)
|
|
2120
|
+
return unauthorized;
|
|
2121
|
+
}
|
|
2122
|
+
if (request.method === "GET" && url.pathname === "/status") {
|
|
884
2123
|
return Response.json(apiStatus());
|
|
885
2124
|
}
|
|
886
2125
|
return handleV1Request({
|
|
@@ -889,6 +2128,7 @@ function createLoopsApiServer(opts = {}) {
|
|
|
889
2128
|
storage: opts.storage,
|
|
890
2129
|
bodyLimitBytes: opts.bodyLimitBytes ?? DEFAULT_BODY_LIMIT_BYTES,
|
|
891
2130
|
evidenceLimitBytes: opts.evidenceLimitBytes ?? DEFAULT_EVIDENCE_LIMIT_BYTES,
|
|
2131
|
+
importLimitBytes: opts.importLimitBytes ?? DEFAULT_IMPORT_LIMIT_BYTES,
|
|
892
2132
|
now: opts.now ?? (() => new Date)
|
|
893
2133
|
});
|
|
894
2134
|
}
|
|
@@ -903,10 +2143,28 @@ async function handleV1Request(ctx) {
|
|
|
903
2143
|
if (ctx.request.method === "GET" && segments[1] === "status")
|
|
904
2144
|
return Response.json(apiStatus());
|
|
905
2145
|
try {
|
|
2146
|
+
if (segments[1] === "import")
|
|
2147
|
+
return await handleImportRequest(ctx, segments.slice(2));
|
|
906
2148
|
if (segments[1] === "loops")
|
|
907
2149
|
return await handleLoopsRequest(ctx, segments.slice(2));
|
|
908
2150
|
if (segments[1] === "runs")
|
|
909
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));
|
|
910
2168
|
if (segments[1] === "runners")
|
|
911
2169
|
return await handleRunnerRequest(ctx, segments.slice(2));
|
|
912
2170
|
if (segments[1] === "leases" && segments[2] === "recover" && ctx.request.method === "POST") {
|
|
@@ -917,14 +2175,45 @@ async function handleV1Request(ctx) {
|
|
|
917
2175
|
return errorResponse(error);
|
|
918
2176
|
}
|
|
919
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
|
+
}
|
|
920
2207
|
async function handleLoopsRequest(ctx, segments) {
|
|
921
2208
|
const storage = requireStorage(ctx.storage);
|
|
922
2209
|
if (segments.length === 0 && ctx.request.method === "GET") {
|
|
923
2210
|
const loops = await storage.listLoops({
|
|
924
2211
|
status: optionalEnum(ctx.url.searchParams.get("status"), ["active", "paused", "stopped", "expired"]),
|
|
925
2212
|
limit: optionalLimit(ctx.url.searchParams.get("limit")),
|
|
2213
|
+
offset: optionalOffset(ctx.url.searchParams.get("offset")),
|
|
926
2214
|
includeArchived: optionalBoolean(ctx.url.searchParams.get("includeArchived")),
|
|
927
|
-
archived: optionalBoolean(ctx.url.searchParams.get("archived"))
|
|
2215
|
+
archived: optionalBoolean(ctx.url.searchParams.get("archived")),
|
|
2216
|
+
name: optionalString(ctx.url.searchParams.get("name"))
|
|
928
2217
|
});
|
|
929
2218
|
return ok({ loops: loops.map(publicLoop) });
|
|
930
2219
|
}
|
|
@@ -933,6 +2222,13 @@ async function handleLoopsRequest(ctx, segments) {
|
|
|
933
2222
|
const loop = await storage.createLoop(body);
|
|
934
2223
|
return ok({ loop: publicLoop(loop) }, { status: 201 });
|
|
935
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
|
+
}
|
|
936
2232
|
const id = segments[0];
|
|
937
2233
|
if (!id)
|
|
938
2234
|
return fail("not_found", 404);
|
|
@@ -965,6 +2261,170 @@ async function handleLoopsRequest(ctx, segments) {
|
|
|
965
2261
|
if (segments.length === 2 && segments[1] === "unarchive" && ctx.request.method === "POST") {
|
|
966
2262
|
return ok({ loop: publicLoop(await storage.unarchiveLoop(id)) });
|
|
967
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
|
+
}
|
|
968
2428
|
return fail("not_found", 404);
|
|
969
2429
|
}
|
|
970
2430
|
async function handleRunsRequest(ctx, segments) {
|
|
@@ -1000,11 +2460,16 @@ async function handleRunsRequest(ctx, segments) {
|
|
|
1000
2460
|
}
|
|
1001
2461
|
const storage = requireStorage(ctx.storage);
|
|
1002
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
|
+
}
|
|
1003
2467
|
if (segments.length === 0 && ctx.request.method === "GET") {
|
|
1004
2468
|
const runs = await storage.listRuns({
|
|
1005
2469
|
loopId: ctx.url.searchParams.get("loopId") ?? undefined,
|
|
1006
2470
|
status: optionalEnum(ctx.url.searchParams.get("status"), ["running", "succeeded", "failed", "timed_out", "abandoned", "skipped"]),
|
|
1007
|
-
limit: optionalLimit(ctx.url.searchParams.get("limit"))
|
|
2471
|
+
limit: optionalLimit(ctx.url.searchParams.get("limit")),
|
|
2472
|
+
offset: optionalOffset(ctx.url.searchParams.get("offset"))
|
|
1008
2473
|
});
|
|
1009
2474
|
return ok({ runs: runs.map((run) => publicRun(run, showOutput, { redactError: true })) });
|
|
1010
2475
|
}
|
|
@@ -1018,6 +2483,34 @@ async function handleRunsRequest(ctx, segments) {
|
|
|
1018
2483
|
}
|
|
1019
2484
|
return fail("not_found", 404);
|
|
1020
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
|
+
}
|
|
1021
2514
|
async function handleRunnerRequest(ctx, segments) {
|
|
1022
2515
|
if (ctx.request.method !== "POST")
|
|
1023
2516
|
return fail("not_found", 404);
|
|
@@ -1229,13 +2722,22 @@ async function readBodyText(request, limitBytes) {
|
|
|
1229
2722
|
function runnerProtocolPending(message) {
|
|
1230
2723
|
return fail("runner_protocol_pending", 501, { message });
|
|
1231
2724
|
}
|
|
2725
|
+
var MAX_PAGE_LIMIT = 1000;
|
|
1232
2726
|
function optionalLimit(value) {
|
|
1233
2727
|
if (value == null || value === "")
|
|
1234
2728
|
return;
|
|
1235
2729
|
const limit = Number(value);
|
|
1236
|
-
if (!Number.isInteger(limit) || limit < 1
|
|
2730
|
+
if (!Number.isInteger(limit) || limit < 1)
|
|
1237
2731
|
throw Object.assign(new Error("invalid_limit"), { status: 422 });
|
|
1238
|
-
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;
|
|
1239
2741
|
}
|
|
1240
2742
|
function optionalString(value) {
|
|
1241
2743
|
if (value === undefined || value === null)
|
|
@@ -1341,13 +2843,14 @@ program.command("serve").description("serve the foundation health/status endpoin
|
|
|
1341
2843
|
const server = createLoopsApiServer({ host, port });
|
|
1342
2844
|
console.log(`loops-api listening on http://${server.hostname}:${server.port}`);
|
|
1343
2845
|
});
|
|
1344
|
-
if (import.meta.main) {
|
|
2846
|
+
if (import.meta.main && (import.meta.url.endsWith("api/index.ts") || import.meta.url.endsWith("api/index.js"))) {
|
|
1345
2847
|
main().catch((error) => {
|
|
1346
2848
|
console.error(error instanceof Error ? error.message : String(error));
|
|
1347
2849
|
process.exit(1);
|
|
1348
2850
|
});
|
|
1349
2851
|
}
|
|
1350
2852
|
export {
|
|
2853
|
+
openApiDocument,
|
|
1351
2854
|
main,
|
|
1352
2855
|
createLoopsApiServer,
|
|
1353
2856
|
apiStatus
|