@hasna/loops 0.4.12 → 0.4.14
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 +106 -3
- package/README.md +70 -17
- package/dist/api/index.d.ts +35 -0
- package/dist/api/index.js +750 -10
- package/dist/cli/index.js +1744 -361
- package/dist/cli/ui.d.ts +44 -0
- package/dist/daemon/index.js +948 -212
- 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 +3 -3
- package/dist/index.js +6513 -4840
- package/dist/lib/health.d.ts +83 -3
- package/dist/lib/mode.d.ts +27 -0
- package/dist/lib/mode.js +69 -2
- package/dist/lib/scheduler.d.ts +5 -2
- package/dist/lib/storage/index.d.ts +1 -0
- package/dist/lib/storage/index.js +1329 -211
- 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 +114 -0
- package/dist/lib/storage/sqlite.js +336 -8
- package/dist/lib/store.d.ts +234 -0
- package/dist/lib/store.js +350 -8
- package/dist/mcp/index.js +1447 -479
- package/dist/runner/index.js +179 -25
- package/dist/sdk/http.d.ts +115 -0
- package/dist/sdk/http.js +145 -0
- package/dist/sdk/index.d.ts +5 -1
- package/dist/sdk/index.js +1106 -296
- package/dist/serve/index.d.ts +4 -0
- package/dist/serve/index.js +7619 -0
- package/dist/types.d.ts +3 -0
- package/docs/CUTOVER-RUNBOOK.md +61 -0
- package/docs/DEPLOYMENT_MODES.md +23 -1
- package/docs/USAGE.md +66 -16
- package/package.json +14 -2
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.14",
|
|
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",
|
|
@@ -104,11 +113,13 @@ var package_default = {
|
|
|
104
113
|
bun: ">=1.0.0"
|
|
105
114
|
},
|
|
106
115
|
dependencies: {
|
|
116
|
+
"@hasna/contracts": "^0.4.1",
|
|
107
117
|
"@hasna/events": "^0.1.9",
|
|
108
118
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
109
119
|
"@openrouter/ai-sdk-provider": "2.9.1",
|
|
110
120
|
ai: "6.0.204",
|
|
111
121
|
commander: "^13.1.0",
|
|
122
|
+
pg: "^8.13.1",
|
|
112
123
|
zod: "4.4.3"
|
|
113
124
|
},
|
|
114
125
|
optionalDependencies: {
|
|
@@ -116,6 +127,7 @@ var package_default = {
|
|
|
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: {
|
|
@@ -194,6 +206,50 @@ function sourceOfTruthForMode(mode) {
|
|
|
194
206
|
return "self_hosted_control_plane";
|
|
195
207
|
return "cloud_control_plane";
|
|
196
208
|
}
|
|
209
|
+
var ROUTE_ADMISSION_GATES = [
|
|
210
|
+
"max_dispatch",
|
|
211
|
+
"max_active",
|
|
212
|
+
"max_active_per_project",
|
|
213
|
+
"max_active_per_project_group",
|
|
214
|
+
"max_active_scope",
|
|
215
|
+
"max_per_profile"
|
|
216
|
+
];
|
|
217
|
+
function remoteSchedulerBackendForMode(mode, config) {
|
|
218
|
+
if (mode === "local")
|
|
219
|
+
return "none";
|
|
220
|
+
if (mode === "cloud")
|
|
221
|
+
return config.cloudApiUrl ? "hosted_control_plane_contract" : "unconfigured";
|
|
222
|
+
if (config.databaseUrlPresent)
|
|
223
|
+
return "postgres_contract";
|
|
224
|
+
if (config.apiUrl)
|
|
225
|
+
return "api_control_plane_contract";
|
|
226
|
+
return "unconfigured";
|
|
227
|
+
}
|
|
228
|
+
function schedulerStateForMode(args) {
|
|
229
|
+
const nonLocal = args.deploymentMode !== "local";
|
|
230
|
+
return {
|
|
231
|
+
authority: args.sourceOfTruth,
|
|
232
|
+
localStore: {
|
|
233
|
+
backend: "sqlite",
|
|
234
|
+
role: args.localRole,
|
|
235
|
+
runArtifacts: "local_files",
|
|
236
|
+
routeAdmissionState: "workflow_work_items"
|
|
237
|
+
},
|
|
238
|
+
remoteStore: {
|
|
239
|
+
backend: remoteSchedulerBackendForMode(args.deploymentMode, args.config),
|
|
240
|
+
configured: nonLocal && args.controlPlaneConfigured,
|
|
241
|
+
applySupported: false,
|
|
242
|
+
objectArtifacts: nonLocal ? "object_store_contract" : "none",
|
|
243
|
+
mutatesAws: false
|
|
244
|
+
},
|
|
245
|
+
routeAdmission: {
|
|
246
|
+
stateStore: nonLocal ? "control_plane_contract" : "local_sqlite",
|
|
247
|
+
activeStatuses: ["admitted", "running"],
|
|
248
|
+
gates: ROUTE_ADMISSION_GATES,
|
|
249
|
+
dryRunEvaluatesLiveCounts: false
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
}
|
|
197
253
|
function displayControlPlaneUrl(value) {
|
|
198
254
|
if (!value)
|
|
199
255
|
return;
|
|
@@ -219,6 +275,9 @@ function buildDeploymentStatus(opts = {}) {
|
|
|
219
275
|
if (deploymentMode === "self_hosted" && !controlPlaneConfigured) {
|
|
220
276
|
warnings.push("self_hosted mode needs LOOPS_API_URL or LOOPS_DATABASE_URL before it can become authoritative");
|
|
221
277
|
}
|
|
278
|
+
if (deploymentMode === "self_hosted" && config.databaseUrlPresent && !config.apiUrl) {
|
|
279
|
+
warnings.push("LOOPS_DATABASE_URL selects the self_hosted storage contract; loops-runner still needs LOOPS_API_URL to claim remote work");
|
|
280
|
+
}
|
|
222
281
|
if (deploymentMode === "cloud" && config.apiUrl && !config.cloudApiUrl) {
|
|
223
282
|
warnings.push("LOOPS_API_URL selects self_hosted; cloud mode uses LOOPS_CLOUD_API_URL");
|
|
224
283
|
}
|
|
@@ -252,6 +311,13 @@ function buildDeploymentStatus(opts = {}) {
|
|
|
252
311
|
required: deploymentMode !== "local",
|
|
253
312
|
role: deploymentMode === "local" ? "daemon" : "control_plane_worker"
|
|
254
313
|
},
|
|
314
|
+
schedulerState: schedulerStateForMode({
|
|
315
|
+
deploymentMode,
|
|
316
|
+
sourceOfTruth: sourceOfTruthForMode(deploymentMode),
|
|
317
|
+
localRole: deploymentMode === "local" ? "authoritative" : "cache_and_spool",
|
|
318
|
+
controlPlaneConfigured,
|
|
319
|
+
config
|
|
320
|
+
}),
|
|
255
321
|
warnings
|
|
256
322
|
};
|
|
257
323
|
}
|
|
@@ -264,6 +330,7 @@ function deploymentStatusLine(status) {
|
|
|
264
330
|
`source=${status.deploymentModeSource}`,
|
|
265
331
|
`truth=${status.sourceOfTruth}`,
|
|
266
332
|
`local=${status.localStore.role}`,
|
|
333
|
+
`scheduler=${status.schedulerState.routeAdmission.stateStore}`,
|
|
267
334
|
`control_plane=${configured}`
|
|
268
335
|
].join(" ");
|
|
269
336
|
}
|
|
@@ -760,8 +827,639 @@ function scrubSecretsDeep(value) {
|
|
|
760
827
|
}
|
|
761
828
|
return value;
|
|
762
829
|
}
|
|
830
|
+
// openapi/loops.json
|
|
831
|
+
var loops_default = {
|
|
832
|
+
openapi: "3.1.0",
|
|
833
|
+
info: {
|
|
834
|
+
title: "OpenLoops",
|
|
835
|
+
version: "0.4.14",
|
|
836
|
+
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."
|
|
837
|
+
},
|
|
838
|
+
servers: [
|
|
839
|
+
{
|
|
840
|
+
url: "https://loops.hasna.xyz"
|
|
841
|
+
}
|
|
842
|
+
],
|
|
843
|
+
paths: {
|
|
844
|
+
"/health": {
|
|
845
|
+
get: {
|
|
846
|
+
operationId: "healthCheck",
|
|
847
|
+
summary: "Liveness probe",
|
|
848
|
+
responses: {
|
|
849
|
+
"200": {
|
|
850
|
+
description: "alive",
|
|
851
|
+
content: {
|
|
852
|
+
"application/json": {
|
|
853
|
+
schema: {
|
|
854
|
+
$ref: "#/components/schemas/Foundation"
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
},
|
|
862
|
+
"/ready": {
|
|
863
|
+
get: {
|
|
864
|
+
operationId: "readyCheck",
|
|
865
|
+
summary: "Readiness probe (storage reachable + migrated)",
|
|
866
|
+
responses: {
|
|
867
|
+
"200": {
|
|
868
|
+
description: "ready",
|
|
869
|
+
content: {
|
|
870
|
+
"application/json": {
|
|
871
|
+
schema: {
|
|
872
|
+
$ref: "#/components/schemas/Foundation"
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
},
|
|
877
|
+
"503": {
|
|
878
|
+
description: "not ready",
|
|
879
|
+
content: {
|
|
880
|
+
"application/json": {
|
|
881
|
+
schema: {
|
|
882
|
+
$ref: "#/components/schemas/Foundation"
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
},
|
|
890
|
+
"/version": {
|
|
891
|
+
get: {
|
|
892
|
+
operationId: "getVersion",
|
|
893
|
+
summary: "Service version",
|
|
894
|
+
responses: {
|
|
895
|
+
"200": {
|
|
896
|
+
description: "version",
|
|
897
|
+
content: {
|
|
898
|
+
"application/json": {
|
|
899
|
+
schema: {
|
|
900
|
+
$ref: "#/components/schemas/Foundation"
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
},
|
|
908
|
+
"/v1/loops": {
|
|
909
|
+
get: {
|
|
910
|
+
operationId: "listLoops",
|
|
911
|
+
summary: "List loops",
|
|
912
|
+
parameters: [
|
|
913
|
+
{
|
|
914
|
+
name: "status",
|
|
915
|
+
in: "query",
|
|
916
|
+
required: false,
|
|
917
|
+
schema: {
|
|
918
|
+
type: "string",
|
|
919
|
+
enum: [
|
|
920
|
+
"active",
|
|
921
|
+
"paused",
|
|
922
|
+
"stopped",
|
|
923
|
+
"expired"
|
|
924
|
+
]
|
|
925
|
+
}
|
|
926
|
+
},
|
|
927
|
+
{
|
|
928
|
+
name: "limit",
|
|
929
|
+
in: "query",
|
|
930
|
+
required: false,
|
|
931
|
+
schema: {
|
|
932
|
+
type: "integer"
|
|
933
|
+
}
|
|
934
|
+
},
|
|
935
|
+
{
|
|
936
|
+
name: "includeArchived",
|
|
937
|
+
in: "query",
|
|
938
|
+
required: false,
|
|
939
|
+
schema: {
|
|
940
|
+
type: "boolean"
|
|
941
|
+
}
|
|
942
|
+
},
|
|
943
|
+
{
|
|
944
|
+
name: "archived",
|
|
945
|
+
in: "query",
|
|
946
|
+
required: false,
|
|
947
|
+
schema: {
|
|
948
|
+
type: "boolean"
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
],
|
|
952
|
+
responses: {
|
|
953
|
+
"200": {
|
|
954
|
+
description: "loops",
|
|
955
|
+
content: {
|
|
956
|
+
"application/json": {
|
|
957
|
+
schema: {
|
|
958
|
+
$ref: "#/components/schemas/LoopListResponse"
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
},
|
|
965
|
+
post: {
|
|
966
|
+
operationId: "createLoop",
|
|
967
|
+
summary: "Create a loop",
|
|
968
|
+
requestBody: {
|
|
969
|
+
required: true,
|
|
970
|
+
content: {
|
|
971
|
+
"application/json": {
|
|
972
|
+
schema: {
|
|
973
|
+
$ref: "#/components/schemas/CreateLoopInput"
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
},
|
|
978
|
+
responses: {
|
|
979
|
+
"201": {
|
|
980
|
+
description: "created",
|
|
981
|
+
content: {
|
|
982
|
+
"application/json": {
|
|
983
|
+
schema: {
|
|
984
|
+
$ref: "#/components/schemas/LoopResponse"
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
},
|
|
992
|
+
"/v1/loops/{id}": {
|
|
993
|
+
get: {
|
|
994
|
+
operationId: "getLoop",
|
|
995
|
+
summary: "Get a loop by id",
|
|
996
|
+
parameters: [
|
|
997
|
+
{
|
|
998
|
+
name: "id",
|
|
999
|
+
in: "path",
|
|
1000
|
+
required: true,
|
|
1001
|
+
schema: {
|
|
1002
|
+
type: "string"
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
],
|
|
1006
|
+
responses: {
|
|
1007
|
+
"200": {
|
|
1008
|
+
description: "loop",
|
|
1009
|
+
content: {
|
|
1010
|
+
"application/json": {
|
|
1011
|
+
schema: {
|
|
1012
|
+
$ref: "#/components/schemas/LoopResponse"
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
},
|
|
1019
|
+
patch: {
|
|
1020
|
+
operationId: "updateLoop",
|
|
1021
|
+
summary: "Update a loop (status / schedule fields)",
|
|
1022
|
+
parameters: [
|
|
1023
|
+
{
|
|
1024
|
+
name: "id",
|
|
1025
|
+
in: "path",
|
|
1026
|
+
required: true,
|
|
1027
|
+
schema: {
|
|
1028
|
+
type: "string"
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
],
|
|
1032
|
+
requestBody: {
|
|
1033
|
+
required: true,
|
|
1034
|
+
content: {
|
|
1035
|
+
"application/json": {
|
|
1036
|
+
schema: {
|
|
1037
|
+
$ref: "#/components/schemas/UpdateLoopInput"
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
},
|
|
1042
|
+
responses: {
|
|
1043
|
+
"200": {
|
|
1044
|
+
description: "loop",
|
|
1045
|
+
content: {
|
|
1046
|
+
"application/json": {
|
|
1047
|
+
schema: {
|
|
1048
|
+
$ref: "#/components/schemas/LoopResponse"
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
},
|
|
1055
|
+
delete: {
|
|
1056
|
+
operationId: "deleteLoop",
|
|
1057
|
+
summary: "Delete a loop",
|
|
1058
|
+
parameters: [
|
|
1059
|
+
{
|
|
1060
|
+
name: "id",
|
|
1061
|
+
in: "path",
|
|
1062
|
+
required: true,
|
|
1063
|
+
schema: {
|
|
1064
|
+
type: "string"
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
],
|
|
1068
|
+
responses: {
|
|
1069
|
+
"200": {
|
|
1070
|
+
description: "deleted",
|
|
1071
|
+
content: {
|
|
1072
|
+
"application/json": {
|
|
1073
|
+
schema: {
|
|
1074
|
+
$ref: "#/components/schemas/DeleteResponse"
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
},
|
|
1082
|
+
"/v1/loops/{id}/archive": {
|
|
1083
|
+
post: {
|
|
1084
|
+
operationId: "archiveLoop",
|
|
1085
|
+
summary: "Archive a loop",
|
|
1086
|
+
parameters: [
|
|
1087
|
+
{
|
|
1088
|
+
name: "id",
|
|
1089
|
+
in: "path",
|
|
1090
|
+
required: true,
|
|
1091
|
+
schema: {
|
|
1092
|
+
type: "string"
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
],
|
|
1096
|
+
responses: {
|
|
1097
|
+
"200": {
|
|
1098
|
+
description: "loop",
|
|
1099
|
+
content: {
|
|
1100
|
+
"application/json": {
|
|
1101
|
+
schema: {
|
|
1102
|
+
$ref: "#/components/schemas/LoopResponse"
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
},
|
|
1110
|
+
"/v1/loops/{id}/unarchive": {
|
|
1111
|
+
post: {
|
|
1112
|
+
operationId: "unarchiveLoop",
|
|
1113
|
+
summary: "Unarchive a loop",
|
|
1114
|
+
parameters: [
|
|
1115
|
+
{
|
|
1116
|
+
name: "id",
|
|
1117
|
+
in: "path",
|
|
1118
|
+
required: true,
|
|
1119
|
+
schema: {
|
|
1120
|
+
type: "string"
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
],
|
|
1124
|
+
responses: {
|
|
1125
|
+
"200": {
|
|
1126
|
+
description: "loop",
|
|
1127
|
+
content: {
|
|
1128
|
+
"application/json": {
|
|
1129
|
+
schema: {
|
|
1130
|
+
$ref: "#/components/schemas/LoopResponse"
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
},
|
|
1138
|
+
"/v1/runs": {
|
|
1139
|
+
get: {
|
|
1140
|
+
operationId: "listRuns",
|
|
1141
|
+
summary: "List runs",
|
|
1142
|
+
parameters: [
|
|
1143
|
+
{
|
|
1144
|
+
name: "loopId",
|
|
1145
|
+
in: "query",
|
|
1146
|
+
required: false,
|
|
1147
|
+
schema: {
|
|
1148
|
+
type: "string"
|
|
1149
|
+
}
|
|
1150
|
+
},
|
|
1151
|
+
{
|
|
1152
|
+
name: "status",
|
|
1153
|
+
in: "query",
|
|
1154
|
+
required: false,
|
|
1155
|
+
schema: {
|
|
1156
|
+
type: "string"
|
|
1157
|
+
}
|
|
1158
|
+
},
|
|
1159
|
+
{
|
|
1160
|
+
name: "limit",
|
|
1161
|
+
in: "query",
|
|
1162
|
+
required: false,
|
|
1163
|
+
schema: {
|
|
1164
|
+
type: "integer"
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
],
|
|
1168
|
+
responses: {
|
|
1169
|
+
"200": {
|
|
1170
|
+
description: "runs",
|
|
1171
|
+
content: {
|
|
1172
|
+
"application/json": {
|
|
1173
|
+
schema: {
|
|
1174
|
+
$ref: "#/components/schemas/RunListResponse"
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
},
|
|
1182
|
+
"/v1/runs/{id}": {
|
|
1183
|
+
get: {
|
|
1184
|
+
operationId: "getRun",
|
|
1185
|
+
summary: "Get a run by id",
|
|
1186
|
+
parameters: [
|
|
1187
|
+
{
|
|
1188
|
+
name: "id",
|
|
1189
|
+
in: "path",
|
|
1190
|
+
required: true,
|
|
1191
|
+
schema: {
|
|
1192
|
+
type: "string"
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
],
|
|
1196
|
+
responses: {
|
|
1197
|
+
"200": {
|
|
1198
|
+
description: "run",
|
|
1199
|
+
content: {
|
|
1200
|
+
"application/json": {
|
|
1201
|
+
schema: {
|
|
1202
|
+
$ref: "#/components/schemas/RunResponse"
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
},
|
|
1211
|
+
components: {
|
|
1212
|
+
schemas: {
|
|
1213
|
+
Foundation: {
|
|
1214
|
+
type: "object",
|
|
1215
|
+
properties: {
|
|
1216
|
+
status: {
|
|
1217
|
+
type: "string"
|
|
1218
|
+
},
|
|
1219
|
+
version: {
|
|
1220
|
+
type: "string"
|
|
1221
|
+
},
|
|
1222
|
+
mode: {
|
|
1223
|
+
type: "string"
|
|
1224
|
+
},
|
|
1225
|
+
service: {
|
|
1226
|
+
type: "string"
|
|
1227
|
+
},
|
|
1228
|
+
detail: {
|
|
1229
|
+
type: "string"
|
|
1230
|
+
}
|
|
1231
|
+
},
|
|
1232
|
+
required: [
|
|
1233
|
+
"status",
|
|
1234
|
+
"version",
|
|
1235
|
+
"mode"
|
|
1236
|
+
]
|
|
1237
|
+
},
|
|
1238
|
+
Loop: {
|
|
1239
|
+
type: "object",
|
|
1240
|
+
properties: {
|
|
1241
|
+
id: {
|
|
1242
|
+
type: "string"
|
|
1243
|
+
},
|
|
1244
|
+
name: {
|
|
1245
|
+
type: "string"
|
|
1246
|
+
},
|
|
1247
|
+
description: {
|
|
1248
|
+
type: "string",
|
|
1249
|
+
nullable: true
|
|
1250
|
+
},
|
|
1251
|
+
status: {
|
|
1252
|
+
type: "string"
|
|
1253
|
+
},
|
|
1254
|
+
schedule: {
|
|
1255
|
+
type: "object",
|
|
1256
|
+
additionalProperties: true
|
|
1257
|
+
},
|
|
1258
|
+
target: {
|
|
1259
|
+
type: "object",
|
|
1260
|
+
additionalProperties: true
|
|
1261
|
+
},
|
|
1262
|
+
nextRunAt: {
|
|
1263
|
+
type: "string",
|
|
1264
|
+
nullable: true
|
|
1265
|
+
},
|
|
1266
|
+
createdAt: {
|
|
1267
|
+
type: "string"
|
|
1268
|
+
},
|
|
1269
|
+
updatedAt: {
|
|
1270
|
+
type: "string"
|
|
1271
|
+
}
|
|
1272
|
+
},
|
|
1273
|
+
required: [
|
|
1274
|
+
"id",
|
|
1275
|
+
"name",
|
|
1276
|
+
"status"
|
|
1277
|
+
]
|
|
1278
|
+
},
|
|
1279
|
+
CreateLoopInput: {
|
|
1280
|
+
type: "object",
|
|
1281
|
+
properties: {
|
|
1282
|
+
name: {
|
|
1283
|
+
type: "string"
|
|
1284
|
+
},
|
|
1285
|
+
description: {
|
|
1286
|
+
type: "string"
|
|
1287
|
+
},
|
|
1288
|
+
schedule: {
|
|
1289
|
+
type: "object",
|
|
1290
|
+
additionalProperties: true
|
|
1291
|
+
},
|
|
1292
|
+
target: {
|
|
1293
|
+
type: "object",
|
|
1294
|
+
additionalProperties: true
|
|
1295
|
+
}
|
|
1296
|
+
},
|
|
1297
|
+
required: [
|
|
1298
|
+
"name",
|
|
1299
|
+
"schedule",
|
|
1300
|
+
"target"
|
|
1301
|
+
]
|
|
1302
|
+
},
|
|
1303
|
+
UpdateLoopInput: {
|
|
1304
|
+
type: "object",
|
|
1305
|
+
properties: {
|
|
1306
|
+
status: {
|
|
1307
|
+
type: "string",
|
|
1308
|
+
enum: [
|
|
1309
|
+
"active",
|
|
1310
|
+
"paused",
|
|
1311
|
+
"stopped",
|
|
1312
|
+
"expired"
|
|
1313
|
+
]
|
|
1314
|
+
},
|
|
1315
|
+
nextRunAt: {
|
|
1316
|
+
type: "string",
|
|
1317
|
+
nullable: true
|
|
1318
|
+
},
|
|
1319
|
+
retryScheduledFor: {
|
|
1320
|
+
type: "string",
|
|
1321
|
+
nullable: true
|
|
1322
|
+
},
|
|
1323
|
+
expiresAt: {
|
|
1324
|
+
type: "string",
|
|
1325
|
+
nullable: true
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
},
|
|
1329
|
+
Run: {
|
|
1330
|
+
type: "object",
|
|
1331
|
+
properties: {
|
|
1332
|
+
id: {
|
|
1333
|
+
type: "string"
|
|
1334
|
+
},
|
|
1335
|
+
loopId: {
|
|
1336
|
+
type: "string"
|
|
1337
|
+
},
|
|
1338
|
+
status: {
|
|
1339
|
+
type: "string"
|
|
1340
|
+
},
|
|
1341
|
+
attempt: {
|
|
1342
|
+
type: "integer"
|
|
1343
|
+
},
|
|
1344
|
+
scheduledFor: {
|
|
1345
|
+
type: "string"
|
|
1346
|
+
},
|
|
1347
|
+
startedAt: {
|
|
1348
|
+
type: "string",
|
|
1349
|
+
nullable: true
|
|
1350
|
+
},
|
|
1351
|
+
finishedAt: {
|
|
1352
|
+
type: "string",
|
|
1353
|
+
nullable: true
|
|
1354
|
+
}
|
|
1355
|
+
},
|
|
1356
|
+
required: [
|
|
1357
|
+
"id",
|
|
1358
|
+
"loopId",
|
|
1359
|
+
"status"
|
|
1360
|
+
]
|
|
1361
|
+
},
|
|
1362
|
+
LoopResponse: {
|
|
1363
|
+
type: "object",
|
|
1364
|
+
properties: {
|
|
1365
|
+
ok: {
|
|
1366
|
+
type: "boolean"
|
|
1367
|
+
},
|
|
1368
|
+
loop: {
|
|
1369
|
+
$ref: "#/components/schemas/Loop"
|
|
1370
|
+
}
|
|
1371
|
+
},
|
|
1372
|
+
required: [
|
|
1373
|
+
"ok",
|
|
1374
|
+
"loop"
|
|
1375
|
+
]
|
|
1376
|
+
},
|
|
1377
|
+
LoopListResponse: {
|
|
1378
|
+
type: "object",
|
|
1379
|
+
properties: {
|
|
1380
|
+
ok: {
|
|
1381
|
+
type: "boolean"
|
|
1382
|
+
},
|
|
1383
|
+
loops: {
|
|
1384
|
+
type: "array",
|
|
1385
|
+
items: {
|
|
1386
|
+
$ref: "#/components/schemas/Loop"
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
},
|
|
1390
|
+
required: [
|
|
1391
|
+
"ok",
|
|
1392
|
+
"loops"
|
|
1393
|
+
]
|
|
1394
|
+
},
|
|
1395
|
+
RunResponse: {
|
|
1396
|
+
type: "object",
|
|
1397
|
+
properties: {
|
|
1398
|
+
ok: {
|
|
1399
|
+
type: "boolean"
|
|
1400
|
+
},
|
|
1401
|
+
run: {
|
|
1402
|
+
$ref: "#/components/schemas/Run"
|
|
1403
|
+
}
|
|
1404
|
+
},
|
|
1405
|
+
required: [
|
|
1406
|
+
"ok",
|
|
1407
|
+
"run"
|
|
1408
|
+
]
|
|
1409
|
+
},
|
|
1410
|
+
RunListResponse: {
|
|
1411
|
+
type: "object",
|
|
1412
|
+
properties: {
|
|
1413
|
+
ok: {
|
|
1414
|
+
type: "boolean"
|
|
1415
|
+
},
|
|
1416
|
+
runs: {
|
|
1417
|
+
type: "array",
|
|
1418
|
+
items: {
|
|
1419
|
+
$ref: "#/components/schemas/Run"
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
},
|
|
1423
|
+
required: [
|
|
1424
|
+
"ok",
|
|
1425
|
+
"runs"
|
|
1426
|
+
]
|
|
1427
|
+
},
|
|
1428
|
+
DeleteResponse: {
|
|
1429
|
+
type: "object",
|
|
1430
|
+
properties: {
|
|
1431
|
+
ok: {
|
|
1432
|
+
type: "boolean"
|
|
1433
|
+
},
|
|
1434
|
+
deleted: {
|
|
1435
|
+
type: "boolean"
|
|
1436
|
+
}
|
|
1437
|
+
},
|
|
1438
|
+
required: [
|
|
1439
|
+
"ok",
|
|
1440
|
+
"deleted"
|
|
1441
|
+
]
|
|
1442
|
+
}
|
|
1443
|
+
},
|
|
1444
|
+
securitySchemes: {
|
|
1445
|
+
apiKey: {
|
|
1446
|
+
type: "apiKey",
|
|
1447
|
+
in: "header",
|
|
1448
|
+
name: "x-api-key"
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
},
|
|
1452
|
+
security: [
|
|
1453
|
+
{
|
|
1454
|
+
apiKey: []
|
|
1455
|
+
}
|
|
1456
|
+
]
|
|
1457
|
+
};
|
|
763
1458
|
|
|
764
1459
|
// src/api/index.ts
|
|
1460
|
+
function openApiDocument() {
|
|
1461
|
+
return { ...loops_default, info: { ...loops_default.info, version: packageVersion() } };
|
|
1462
|
+
}
|
|
765
1463
|
var program = new Command;
|
|
766
1464
|
var DEFAULT_BODY_LIMIT_BYTES = 64 * 1024;
|
|
767
1465
|
var DEFAULT_EVIDENCE_LIMIT_BYTES = 256 * 1024;
|
|
@@ -811,21 +1509,62 @@ function apiStatus() {
|
|
|
811
1509
|
status: buildDeploymentStatus({ perspective: "self_hosted" })
|
|
812
1510
|
};
|
|
813
1511
|
}
|
|
1512
|
+
function foundationMode() {
|
|
1513
|
+
return buildDeploymentStatus({}).activeDeploymentMode;
|
|
1514
|
+
}
|
|
1515
|
+
function foundationEnvelope(status, extra = {}) {
|
|
1516
|
+
return { status, version: packageVersion(), mode: foundationMode(), service: "loops", ...extra };
|
|
1517
|
+
}
|
|
814
1518
|
function createLoopsApiServer(opts = {}) {
|
|
815
1519
|
const host = opts.host ?? "127.0.0.1";
|
|
816
1520
|
const port = opts.port ?? 8787;
|
|
817
|
-
if (!isLocalBind(host) && !configuredAuthToken()) {
|
|
818
|
-
throw new Error("non-local loops-
|
|
1521
|
+
if (!isLocalBind(host) && !opts.authenticator && !configuredAuthToken()) {
|
|
1522
|
+
throw new Error("non-local loops-serve binds require an API-key authenticator or LOOPS_API_TOKEN");
|
|
819
1523
|
}
|
|
1524
|
+
const defaultReady = async () => {
|
|
1525
|
+
if (!opts.storage)
|
|
1526
|
+
return { ready: false, detail: "storage_unconfigured" };
|
|
1527
|
+
try {
|
|
1528
|
+
await opts.storage.listLoops({ limit: 1 });
|
|
1529
|
+
return { ready: true };
|
|
1530
|
+
} catch (error) {
|
|
1531
|
+
return { ready: false, detail: error instanceof Error ? error.message : "storage_unreachable" };
|
|
1532
|
+
}
|
|
1533
|
+
};
|
|
1534
|
+
const readyCheck = opts.readyCheck ?? defaultReady;
|
|
820
1535
|
return Bun.serve({
|
|
821
1536
|
hostname: host,
|
|
822
1537
|
port,
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
if (unauthorized)
|
|
826
|
-
return unauthorized;
|
|
1538
|
+
idleTimeout: 60,
|
|
1539
|
+
async fetch(request) {
|
|
827
1540
|
const url = new URL(request.url);
|
|
828
|
-
if (url.pathname === "/health" || url.pathname === "/
|
|
1541
|
+
if (request.method === "GET" && (url.pathname === "/health" || url.pathname === "/healthz")) {
|
|
1542
|
+
return Response.json(foundationEnvelope("ok"));
|
|
1543
|
+
}
|
|
1544
|
+
if (request.method === "GET" && (url.pathname === "/version" || url.pathname === "/v1/version")) {
|
|
1545
|
+
return Response.json(foundationEnvelope("ok"));
|
|
1546
|
+
}
|
|
1547
|
+
if (request.method === "GET" && (url.pathname === "/ready" || url.pathname === "/readyz")) {
|
|
1548
|
+
const result = await readyCheck();
|
|
1549
|
+
return Response.json(foundationEnvelope(result.ready ? "ready" : "not_ready", result.detail ? { detail: result.detail } : {}), { status: result.ready ? 200 : 503 });
|
|
1550
|
+
}
|
|
1551
|
+
if (request.method === "GET" && url.pathname === "/openapi.json") {
|
|
1552
|
+
return Response.json(openApiDocument());
|
|
1553
|
+
}
|
|
1554
|
+
if (opts.authenticator) {
|
|
1555
|
+
const decision = await opts.authenticator.authenticate(request.headers, {
|
|
1556
|
+
method: request.method,
|
|
1557
|
+
path: url.pathname
|
|
1558
|
+
});
|
|
1559
|
+
if (!decision.ok) {
|
|
1560
|
+
return Response.json({ ok: false, error: decision.reason ?? "unauthorized", message: decision.message }, { status: decision.status || 401 });
|
|
1561
|
+
}
|
|
1562
|
+
} else {
|
|
1563
|
+
const unauthorized = authorizeRequest(request, host);
|
|
1564
|
+
if (unauthorized)
|
|
1565
|
+
return unauthorized;
|
|
1566
|
+
}
|
|
1567
|
+
if (request.method === "GET" && url.pathname === "/status") {
|
|
829
1568
|
return Response.json(apiStatus());
|
|
830
1569
|
}
|
|
831
1570
|
return handleV1Request({
|
|
@@ -1286,13 +2025,14 @@ program.command("serve").description("serve the foundation health/status endpoin
|
|
|
1286
2025
|
const server = createLoopsApiServer({ host, port });
|
|
1287
2026
|
console.log(`loops-api listening on http://${server.hostname}:${server.port}`);
|
|
1288
2027
|
});
|
|
1289
|
-
if (import.meta.main) {
|
|
2028
|
+
if (import.meta.main && (import.meta.url.endsWith("api/index.ts") || import.meta.url.endsWith("api/index.js"))) {
|
|
1290
2029
|
main().catch((error) => {
|
|
1291
2030
|
console.error(error instanceof Error ? error.message : String(error));
|
|
1292
2031
|
process.exit(1);
|
|
1293
2032
|
});
|
|
1294
2033
|
}
|
|
1295
2034
|
export {
|
|
2035
|
+
openApiDocument,
|
|
1296
2036
|
main,
|
|
1297
2037
|
createLoopsApiServer,
|
|
1298
2038
|
apiStatus
|