@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/types.d.ts
CHANGED
|
@@ -212,6 +212,8 @@ export interface WorkflowWorkItem {
|
|
|
212
212
|
subjectRef: string;
|
|
213
213
|
projectKey?: string;
|
|
214
214
|
projectGroup?: string;
|
|
215
|
+
/** Machine that reserved/admitted this route work item, when known. */
|
|
216
|
+
machineId?: string;
|
|
215
217
|
/**
|
|
216
218
|
* The drain/route identity (loop name) that admitted this item. Used to scope
|
|
217
219
|
* the `--max-active` global admission count to a single route instead of the
|
|
@@ -239,6 +241,7 @@ export interface UpsertWorkflowWorkItemInput {
|
|
|
239
241
|
subjectRef: string;
|
|
240
242
|
projectKey?: string;
|
|
241
243
|
projectGroup?: string;
|
|
244
|
+
machineId?: string;
|
|
242
245
|
routeScope?: string;
|
|
243
246
|
priority?: number;
|
|
244
247
|
status?: Extract<WorkflowWorkItemStatus, "queued" | "deferred">;
|
|
@@ -394,6 +397,52 @@ export interface LoopRun {
|
|
|
394
397
|
createdAt: string;
|
|
395
398
|
updatedAt: string;
|
|
396
399
|
}
|
|
400
|
+
export type RunReceiptMachine = string | Record<string, unknown>;
|
|
401
|
+
export interface RunReceiptSummary {
|
|
402
|
+
text?: string;
|
|
403
|
+
stdout_bytes: number;
|
|
404
|
+
stderr_bytes: number;
|
|
405
|
+
stdout_excerpt?: string;
|
|
406
|
+
stderr_excerpt?: string;
|
|
407
|
+
error?: string;
|
|
408
|
+
duration_ms?: number;
|
|
409
|
+
}
|
|
410
|
+
export interface RunReceipt {
|
|
411
|
+
loop_id: string;
|
|
412
|
+
run_id: string;
|
|
413
|
+
machine: RunReceiptMachine;
|
|
414
|
+
repo: string;
|
|
415
|
+
task_ids: string[];
|
|
416
|
+
knowledge_ids: string[];
|
|
417
|
+
digest_id: string;
|
|
418
|
+
started_at: string | null;
|
|
419
|
+
finished_at: string | null;
|
|
420
|
+
status: string;
|
|
421
|
+
exit_code: number | null;
|
|
422
|
+
summary: RunReceiptSummary;
|
|
423
|
+
evidence_paths: string[];
|
|
424
|
+
created_at: string;
|
|
425
|
+
updated_at: string;
|
|
426
|
+
}
|
|
427
|
+
export interface WriteRunReceiptInput {
|
|
428
|
+
loop_id?: string;
|
|
429
|
+
run_id: string;
|
|
430
|
+
machine?: RunReceiptMachine;
|
|
431
|
+
repo?: string;
|
|
432
|
+
task_ids?: string[];
|
|
433
|
+
knowledge_ids?: string[];
|
|
434
|
+
digest_id?: string;
|
|
435
|
+
started_at?: string | null;
|
|
436
|
+
finished_at?: string | null;
|
|
437
|
+
status?: string;
|
|
438
|
+
exit_code?: number | null;
|
|
439
|
+
summary?: string | Partial<RunReceiptSummary> | null;
|
|
440
|
+
evidence_paths?: string[];
|
|
441
|
+
stdout?: string;
|
|
442
|
+
stderr?: string;
|
|
443
|
+
error?: string;
|
|
444
|
+
duration_ms?: number;
|
|
445
|
+
}
|
|
397
446
|
export interface CreateLoopInput {
|
|
398
447
|
name: string;
|
|
399
448
|
description?: string;
|
|
@@ -7,6 +7,9 @@ materialization, queue state, approvals, DLQ/replay, idempotency, and audit
|
|
|
7
7
|
evidence. OpenLoops owns workflow invocation, admission, execution, run
|
|
8
8
|
manifests, and provider routing once work is explicitly handed off.
|
|
9
9
|
|
|
10
|
+
For operator-facing ownership tables, handoff path examples, and anti-patterns,
|
|
11
|
+
see [Runtime Boundary](./RUNTIME_BOUNDARY.md).
|
|
12
|
+
|
|
10
13
|
## Planned Workflow Upsert SDK
|
|
11
14
|
|
|
12
15
|
External compilers should not write OpenLoops SQLite rows directly. The stable
|
|
@@ -52,7 +55,8 @@ type WorkflowUpsertResult = {
|
|
|
52
55
|
manifestPath?: string;
|
|
53
56
|
};
|
|
54
57
|
action: "created" | "updated" | "reused" | "rejected";
|
|
55
|
-
preflight?:
|
|
58
|
+
preflight?: AutomationPreflightResult;
|
|
59
|
+
evidence?: AutomationEvidenceSummary;
|
|
56
60
|
};
|
|
57
61
|
```
|
|
58
62
|
|
|
@@ -73,6 +77,222 @@ Required semantics:
|
|
|
73
77
|
for the caller to inspect, cancel, replay, or resolve the run without querying
|
|
74
78
|
SQLite directly.
|
|
75
79
|
|
|
80
|
+
## Planned Actions Target Binding
|
|
81
|
+
|
|
82
|
+
`@hasna/actions` now exposes typed action contracts: `ActionManifest`,
|
|
83
|
+
`ActionInvocation`, `ActionRun`, `ActionRunStatus`, `ActionQueueStatus`,
|
|
84
|
+
`ActionAuditEvent`, `EvidenceRef`, and `ActionDeadLetter`. A future OpenLoops
|
|
85
|
+
action binding must reuse those contracts instead of adding an OpenLoops-owned
|
|
86
|
+
action status, action queue, or idempotency dialect.
|
|
87
|
+
|
|
88
|
+
The binding should be a workflow-admission descriptor, not a direct executor in
|
|
89
|
+
the first implementation:
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
type ActionsRuntimeBinding = {
|
|
93
|
+
integration: "hasna-actions";
|
|
94
|
+
role: "workflow-runtime";
|
|
95
|
+
targetType: "action";
|
|
96
|
+
actionOwner: "@hasna/actions";
|
|
97
|
+
runtimeOwner: "@hasna/loops";
|
|
98
|
+
handoff: "workflow-upsert";
|
|
99
|
+
statusModel: "action-owned";
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
type ActionTargetBindingRequest = {
|
|
103
|
+
type: "action";
|
|
104
|
+
action: {
|
|
105
|
+
id: string;
|
|
106
|
+
version: string;
|
|
107
|
+
invocationId?: string;
|
|
108
|
+
runId?: string;
|
|
109
|
+
idempotencyKey?: string;
|
|
110
|
+
dedupeKey?: string;
|
|
111
|
+
};
|
|
112
|
+
subject?: WorkflowUpsertRequest["subject"];
|
|
113
|
+
workflow: WorkflowUpsertRequest["workflow"];
|
|
114
|
+
route?: WorkflowUpsertRequest["route"];
|
|
115
|
+
execution?: WorkflowUpsertRequest["execution"];
|
|
116
|
+
mode?: WorkflowUpsertRequest["mode"];
|
|
117
|
+
dispatch?: WorkflowUpsertRequest["dispatch"];
|
|
118
|
+
};
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
The planned implementation path is:
|
|
122
|
+
|
|
123
|
+
1. The action owner validates the manifest, input, actor, approvals, dry-run
|
|
124
|
+
support, idempotency requirement, guardrails, audit fields, evidence fields,
|
|
125
|
+
and rollback policy using `@hasna/actions`.
|
|
126
|
+
2. The action owner passes only a normalized `ActionTargetBindingRequest` or
|
|
127
|
+
`WorkflowUpsertRequest` to OpenLoops. Large inputs, private prompts, secret
|
|
128
|
+
values, and mutable action state stay in the action store or a referenced
|
|
129
|
+
artifact.
|
|
130
|
+
3. OpenLoops admits a one-shot workflow loop through the planned upsert
|
|
131
|
+
contract, records workflow/run/manifests refs, and reports those refs back to
|
|
132
|
+
the action owner.
|
|
133
|
+
4. The action owner records `ActionAuditEvent` and `EvidenceRef` entries that
|
|
134
|
+
include the returned OpenLoops refs. OpenLoops may store source refs for
|
|
135
|
+
lookup, but `@hasna/actions` remains the source of truth for action runs and
|
|
136
|
+
queue state.
|
|
137
|
+
|
|
138
|
+
### Request And Result Mapping
|
|
139
|
+
|
|
140
|
+
`ActionManifest.id` and `ActionManifest.version` map to
|
|
141
|
+
`WorkflowUpsertRequest.source.id` and the canonical workflow hash input:
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
const request: WorkflowUpsertRequest = {
|
|
145
|
+
idempotencyKey:
|
|
146
|
+
invocation.idempotencyKey ??
|
|
147
|
+
`actions:${manifest.id}:${manifest.version}:${invocation.id}`,
|
|
148
|
+
source: {
|
|
149
|
+
kind: "action",
|
|
150
|
+
id: manifest.id,
|
|
151
|
+
dedupeKey: invocation.idempotencyKey ?? invocation.id,
|
|
152
|
+
},
|
|
153
|
+
subject: subjectFromAction(manifest, invocation),
|
|
154
|
+
workflow: renderedWorkflow,
|
|
155
|
+
loop: { name: `action-${manifest.id}`, schedule: { type: "once", at } },
|
|
156
|
+
route,
|
|
157
|
+
execution,
|
|
158
|
+
mode,
|
|
159
|
+
dispatch,
|
|
160
|
+
};
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
`specHash` must be computed from the canonical OpenLoops workflow request plus
|
|
164
|
+
the stable action contract identity: action id, manifest version, idempotency
|
|
165
|
+
key or invocation id, selected executor binding kind/ref, route policy, and
|
|
166
|
+
execution policy. It must not include raw secret values, raw credentials, or
|
|
167
|
+
unredacted prompt bodies. If `@hasna/actions` later publishes its own manifest
|
|
168
|
+
hash, OpenLoops should include that hash instead of re-hashing action internals.
|
|
169
|
+
|
|
170
|
+
`WorkflowUpsertResult` keeps its existing semantics:
|
|
171
|
+
|
|
172
|
+
- `idempotencyKey` is the action-owned idempotency key selected above.
|
|
173
|
+
- `specHash` identifies the exact OpenLoops workflow admission request and
|
|
174
|
+
action contract identity that produced it.
|
|
175
|
+
- `refs.workflowId`, `refs.loopId`, `refs.invocationId`, `refs.workItemId`,
|
|
176
|
+
`refs.runId`, and `refs.manifestPath` are OpenLoops refs that the action
|
|
177
|
+
owner can store as `EvidenceRef` or audit event data.
|
|
178
|
+
- `action="created" | "updated" | "reused" | "rejected"` describes OpenLoops
|
|
179
|
+
materialization only. It is not an action run status.
|
|
180
|
+
- Action run fields such as `status`, `dedupedFromRunId`, `evidence`, `events`,
|
|
181
|
+
and `error` remain on `ActionRun`.
|
|
182
|
+
|
|
183
|
+
### Status Translation
|
|
184
|
+
|
|
185
|
+
OpenLoops should translate its workflow and work-item state into action-owned
|
|
186
|
+
status updates without persisting a new action status enum.
|
|
187
|
+
|
|
188
|
+
Recommended initial translation:
|
|
189
|
+
|
|
190
|
+
- An accepted dry-run/preflight maps to an `ActionRun` that remains
|
|
191
|
+
`planned`, `previewed`, or `awaiting_approval` according to
|
|
192
|
+
`@hasna/actions`; OpenLoops only returns preflight details.
|
|
193
|
+
- A scheduled workflow maps to action queue status `queued` or
|
|
194
|
+
`waiting_approval`, depending on action-owned approval gates.
|
|
195
|
+
- A claimed/running workflow maps to action queue status `claimed` and action
|
|
196
|
+
run status `executing`.
|
|
197
|
+
- A successful workflow maps to action queue status `succeeded` and action run
|
|
198
|
+
status `succeeded`.
|
|
199
|
+
- A terminal workflow failure maps to action queue status `failed` or `dead`
|
|
200
|
+
and action run status `failed`; the action owner decides whether the
|
|
201
|
+
`ActionDeadLetter.replayable` flag is true.
|
|
202
|
+
- Operator cancellation maps to action queue status `cancelled` and action run
|
|
203
|
+
status `cancelled`.
|
|
204
|
+
|
|
205
|
+
These mappings are adapter behavior. OpenLoops should expose its own workflow
|
|
206
|
+
statuses for OpenLoops APIs and should call or return enough refs for
|
|
207
|
+
`@hasna/actions` to update `ActionRun`, `ActionQueueStatus`, audit events, and
|
|
208
|
+
dead-letter records.
|
|
209
|
+
|
|
210
|
+
### Failure And Replay
|
|
211
|
+
|
|
212
|
+
Failure handling must flow through action-owned APIs:
|
|
213
|
+
|
|
214
|
+
- Provider, worktree, account, policy, and command failures are classified by
|
|
215
|
+
OpenLoops and returned as redacted workflow error evidence.
|
|
216
|
+
- The action owner turns those failures into `ActionError`,
|
|
217
|
+
`ActionDeadLetter`, `ActionAuditEvent`, and `EvidenceRef` records.
|
|
218
|
+
- Replay starts from an action-owned replay decision and a new action-owned
|
|
219
|
+
idempotency key. OpenLoops then receives a new upsert request whose
|
|
220
|
+
`source.dedupeKey` and `idempotencyKey` are the replay keys.
|
|
221
|
+
- OpenLoops DLQ commands may mirror or inspect linked workflow failures, but
|
|
222
|
+
they must not fabricate action queue entries or mark an action replayable on
|
|
223
|
+
their own.
|
|
224
|
+
|
|
225
|
+
### Mode And Dispatch Examples
|
|
226
|
+
|
|
227
|
+
Dry-run without OpenLoops mutation:
|
|
228
|
+
|
|
229
|
+
```bash
|
|
230
|
+
actions run deploy.manifest.json \
|
|
231
|
+
--input-file deploy-input.json \
|
|
232
|
+
--idempotency-key deploy:preview:42 \
|
|
233
|
+
--dry-run \
|
|
234
|
+
--json
|
|
235
|
+
|
|
236
|
+
loops workflows upsert-one-shot deploy-workflow.json \
|
|
237
|
+
--idempotency-key deploy:preview:42 \
|
|
238
|
+
--source-kind action \
|
|
239
|
+
--source-id deploy.service \
|
|
240
|
+
--mode dry-run \
|
|
241
|
+
--dispatch none \
|
|
242
|
+
--json
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
Preflight before scheduling:
|
|
246
|
+
|
|
247
|
+
```bash
|
|
248
|
+
loops workflows upsert-one-shot deploy-workflow.json \
|
|
249
|
+
--idempotency-key deploy:release:42 \
|
|
250
|
+
--source-kind action \
|
|
251
|
+
--source-id deploy.service \
|
|
252
|
+
--mode preflight \
|
|
253
|
+
--dispatch schedule \
|
|
254
|
+
--json
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
Commit and run immediately after the action owner approves:
|
|
258
|
+
|
|
259
|
+
```bash
|
|
260
|
+
actions approve <action-run-id> --reason "preview accepted" --json
|
|
261
|
+
|
|
262
|
+
loops workflows upsert-one-shot deploy-workflow.json \
|
|
263
|
+
--idempotency-key deploy:release:42 \
|
|
264
|
+
--source-kind action \
|
|
265
|
+
--source-id deploy.service \
|
|
266
|
+
--mode commit \
|
|
267
|
+
--dispatch run-now \
|
|
268
|
+
--json
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
Commit without dispatch, for another owner to trigger later:
|
|
272
|
+
|
|
273
|
+
```bash
|
|
274
|
+
loops workflows upsert-one-shot deploy-workflow.json \
|
|
275
|
+
--idempotency-key deploy:release:42 \
|
|
276
|
+
--source-kind action \
|
|
277
|
+
--source-id deploy.service \
|
|
278
|
+
--mode commit \
|
|
279
|
+
--dispatch none \
|
|
280
|
+
--json
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
### Non-Goals
|
|
284
|
+
|
|
285
|
+
- Do not let `@hasna/actions` write OpenLoops SQLite/Postgres rows directly.
|
|
286
|
+
- Do not duplicate the action queue in OpenLoops.
|
|
287
|
+
- Do not materialize triggers in OpenLoops; action and automation owners decide
|
|
288
|
+
when a concrete invocation exists.
|
|
289
|
+
- Do not store secret values, unredacted prompts, credentials, or private
|
|
290
|
+
action input payloads in workflow specs, prompts, manifests, task comments,
|
|
291
|
+
or run output.
|
|
292
|
+
- Do not add `target.type = "action"` as an executable target until the action
|
|
293
|
+
package has a stable runtime handoff API that can complete, fail, audit, and
|
|
294
|
+
replay action runs by action-owned refs.
|
|
295
|
+
|
|
76
296
|
## CLI Surface
|
|
77
297
|
|
|
78
298
|
The planned CLI should mirror the SDK:
|
|
@@ -96,6 +316,227 @@ loops workflows upsert-one-shot ./workflow.json \
|
|
|
96
316
|
would fail. `--mode commit` must return the durable refs required for later
|
|
97
317
|
inspection and replay.
|
|
98
318
|
|
|
319
|
+
## Planned Automation Preflight API
|
|
320
|
+
|
|
321
|
+
Automation-generated workflows need an aggregate preflight result that is richer
|
|
322
|
+
than the current `PreflightResult` command/account tuple, but still compact
|
|
323
|
+
enough to return from CLI, SDK, MCP, route drains, and run manifests. The API is
|
|
324
|
+
a reporting contract first; implementation should reuse existing validators in
|
|
325
|
+
`workflow-spec.ts`, provider capability metadata in `agent-adapter.ts`, account
|
|
326
|
+
resolution in `accounts.ts`, machine/worktree checks in `executor.ts`, and
|
|
327
|
+
redaction helpers in `redact.ts`.
|
|
328
|
+
|
|
329
|
+
Proposed SDK shape:
|
|
330
|
+
|
|
331
|
+
```ts
|
|
332
|
+
type AutomationPreflightMode = "dry-run" | "preflight" | "commit";
|
|
333
|
+
|
|
334
|
+
type AutomationPreflightDomain =
|
|
335
|
+
| "spec"
|
|
336
|
+
| "provider"
|
|
337
|
+
| "account"
|
|
338
|
+
| "command"
|
|
339
|
+
| "secret"
|
|
340
|
+
| "connector"
|
|
341
|
+
| "permissions"
|
|
342
|
+
| "sandbox"
|
|
343
|
+
| "worktree"
|
|
344
|
+
| "machine"
|
|
345
|
+
| "allowlist"
|
|
346
|
+
| "redaction"
|
|
347
|
+
| "evidence";
|
|
348
|
+
|
|
349
|
+
type AutomationPreflightStatus = "pass" | "warn" | "fail" | "skipped";
|
|
350
|
+
type AutomationPreflightSeverity = "info" | "warning" | "error" | "fatal";
|
|
351
|
+
|
|
352
|
+
type AutomationPreflightClassification =
|
|
353
|
+
| "validation"
|
|
354
|
+
| "provider"
|
|
355
|
+
| "account"
|
|
356
|
+
| "auth"
|
|
357
|
+
| "command"
|
|
358
|
+
| "secret"
|
|
359
|
+
| "connector"
|
|
360
|
+
| "policy"
|
|
361
|
+
| "permission"
|
|
362
|
+
| "sandbox"
|
|
363
|
+
| "worktree"
|
|
364
|
+
| "machine"
|
|
365
|
+
| "redaction"
|
|
366
|
+
| "unknown";
|
|
367
|
+
|
|
368
|
+
type AutomationPreflightCheck = {
|
|
369
|
+
id: string;
|
|
370
|
+
domain: AutomationPreflightDomain;
|
|
371
|
+
status: AutomationPreflightStatus;
|
|
372
|
+
severity: AutomationPreflightSeverity;
|
|
373
|
+
classification: AutomationPreflightClassification;
|
|
374
|
+
stepId?: string;
|
|
375
|
+
targetType?: "command" | "agent" | "workflow";
|
|
376
|
+
provider?: AgentProvider;
|
|
377
|
+
account?: { profile: string; tool?: string };
|
|
378
|
+
command?: string;
|
|
379
|
+
subject?: { kind: string; ref?: string };
|
|
380
|
+
summary: string;
|
|
381
|
+
remediation?: string;
|
|
382
|
+
evidenceRefs?: Array<{ kind: "manifest" | "run" | "workflow" | "loop" | "workItem" | "external"; ref: string }>;
|
|
383
|
+
redacted: true;
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
type AutomationPreflightResult = {
|
|
387
|
+
ok: boolean;
|
|
388
|
+
mode: AutomationPreflightMode;
|
|
389
|
+
executionMode: "standard" | "strict";
|
|
390
|
+
idempotencyKey: string;
|
|
391
|
+
specHash: string;
|
|
392
|
+
policyHash?: string;
|
|
393
|
+
generatedAt: string;
|
|
394
|
+
checks: AutomationPreflightCheck[];
|
|
395
|
+
summary: {
|
|
396
|
+
pass: number;
|
|
397
|
+
warn: number;
|
|
398
|
+
fail: number;
|
|
399
|
+
skipped: number;
|
|
400
|
+
highestSeverity: AutomationPreflightSeverity;
|
|
401
|
+
firstFailure?: Pick<AutomationPreflightCheck, "id" | "domain" | "classification" | "summary" | "remediation">;
|
|
402
|
+
};
|
|
403
|
+
};
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
Required check domains:
|
|
407
|
+
|
|
408
|
+
- `spec`: parse workflow JSON, canonicalize prompt file references, validate
|
|
409
|
+
command/action binding, enforce no embedded secrets in target env/prompt
|
|
410
|
+
metadata, and return the stable `specHash`.
|
|
411
|
+
- `provider`: verify provider name, executable binding, provider capability
|
|
412
|
+
support, prompt delivery mode, and bounded subprocess preflight for binaries
|
|
413
|
+
only.
|
|
414
|
+
- `account`: resolve Codewith `authProfile` or OpenAccounts `{profile, tool}`
|
|
415
|
+
refs, record profile/tool names only, and fail when the profile is missing or
|
|
416
|
+
the profile directory cannot be resolved.
|
|
417
|
+
- `command`: validate command targets with `shell=false` executable rules,
|
|
418
|
+
`preflightAnyOf`, and planned allow-command enforcement before execution.
|
|
419
|
+
- `secret`: check required secret refs by ref/name and owner response only;
|
|
420
|
+
never place secret values, exported env, or raw secret-manager payloads in the
|
|
421
|
+
result.
|
|
422
|
+
- `connector`: ask the owning action/connector system whether the connector
|
|
423
|
+
credential handle exists, is scoped for the source action, and is usable; do
|
|
424
|
+
not make OpenLoops the connector credential store.
|
|
425
|
+
- `permissions`: reject unsafe `permissionMode="bypass"`, unsafe
|
|
426
|
+
`configIsolation="none"`, mutable command targets without an explicit policy,
|
|
427
|
+
or missing approval evidence when strict automation requires it.
|
|
428
|
+
- `sandbox`: verify provider-native sandbox support and reject strict mode when
|
|
429
|
+
requested sandboxing cannot be proven.
|
|
430
|
+
- `worktree`: require `worktree.mode="required"` for repo mutation in strict
|
|
431
|
+
mode and verify worktree metadata contains repo root, path, branch, original
|
|
432
|
+
cwd, and target cwd without preparing the worktree during pure preflight.
|
|
433
|
+
- `machine`: check requested machine route, local/remote confidence, and remote
|
|
434
|
+
bootstrap availability; fail a strict request when the route does not match
|
|
435
|
+
the workflow scope.
|
|
436
|
+
- `allowlist`: in standard mode, preserve current `metadata_only` behavior as a
|
|
437
|
+
warning; in strict mode, fail unless command/tool allowlist enforcement can be
|
|
438
|
+
proven for every step that requests it.
|
|
439
|
+
- `redaction`: prove the result was built through the redaction policy that will
|
|
440
|
+
be used for persistence and manifests.
|
|
441
|
+
- `evidence`: include durable refs when available and enough context for the
|
|
442
|
+
caller to inspect the follow-on run without querying SQLite directly.
|
|
443
|
+
|
|
444
|
+
Preflight must be side-effect bounded:
|
|
445
|
+
|
|
446
|
+
- `dry-run` performs schema validation, canonicalization, policy normalization,
|
|
447
|
+
and hashing only. It must not create a database file in an empty data dir and
|
|
448
|
+
must not spawn account/provider probes.
|
|
449
|
+
- `preflight` may run bounded local or remote readiness probes such as
|
|
450
|
+
`command -v`, `accounts env <profile> --tool <tool>`, provider profile-list
|
|
451
|
+
checks, and connector readiness calls owned by the automation/action system.
|
|
452
|
+
It must not run target commands, dispatch agents, create workflows or loops,
|
|
453
|
+
write task comments, prepare worktrees, or mutate queue state.
|
|
454
|
+
- `commit` is idempotent. In strict mode it must run or reuse a fresh compatible
|
|
455
|
+
preflight result before storing or dispatching. In standard mode it may attach
|
|
456
|
+
warnings without changing current execution defaults.
|
|
457
|
+
|
|
458
|
+
Strict-mode failures are fail-closed for missing account profiles, missing
|
|
459
|
+
connector auth, missing required secret refs, unsupported sandbox posture,
|
|
460
|
+
unsupported provider allowlist enforcement, bypass permissions, non-required
|
|
461
|
+
worktrees for repo mutation, missing prompt files, and machine route mismatch.
|
|
462
|
+
|
|
463
|
+
## Planned Compact Evidence API
|
|
464
|
+
|
|
465
|
+
Every public surface should expose the same compact evidence shape so callers
|
|
466
|
+
can hand results across `@hasna/actions`, `@hasna/automations`, OpenLoops route
|
|
467
|
+
drains, MCP tools, and CLI scripts without scraping SQLite or leaking secrets.
|
|
468
|
+
|
|
469
|
+
```ts
|
|
470
|
+
type AutomationEvidenceRef = {
|
|
471
|
+
workflowId?: string;
|
|
472
|
+
loopId?: string;
|
|
473
|
+
invocationId?: string;
|
|
474
|
+
workItemId?: string;
|
|
475
|
+
runId?: string;
|
|
476
|
+
manifestPath?: string;
|
|
477
|
+
external?: Array<{ system: "actions" | "automations" | "connector" | string; ref: string }>;
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
type AutomationEvidenceSummary = {
|
|
481
|
+
version: 1;
|
|
482
|
+
kind: "workflow-upsert" | "preflight" | "route-admission" | "workflow-run";
|
|
483
|
+
generatedAt: string;
|
|
484
|
+
source: { kind: string; id: string; dedupeKey?: string };
|
|
485
|
+
subject: { kind: string; id?: string; path?: string; url?: string };
|
|
486
|
+
refs: AutomationEvidenceRef;
|
|
487
|
+
specHash: string;
|
|
488
|
+
policyHash?: string;
|
|
489
|
+
executionMode: "standard" | "strict";
|
|
490
|
+
dispatch: "schedule" | "run-now" | "none";
|
|
491
|
+
status: "accepted" | "reused" | "rejected" | "running" | "succeeded" | "failed" | "dead_letter";
|
|
492
|
+
preflight?: {
|
|
493
|
+
ok: boolean;
|
|
494
|
+
counts: AutomationPreflightResult["summary"];
|
|
495
|
+
failedCheckIds: string[];
|
|
496
|
+
warningCheckIds: string[];
|
|
497
|
+
};
|
|
498
|
+
redactionProfile: "default" | "strict";
|
|
499
|
+
redacted: true;
|
|
500
|
+
};
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
Persistence and output rules:
|
|
504
|
+
|
|
505
|
+
- CLI `--json`, SDK results, MCP tool results, route dry-run output, route work
|
|
506
|
+
item evidence, and workflow run manifests all return `AutomationEvidenceSummary`
|
|
507
|
+
plus optional full `AutomationPreflightResult` when the caller explicitly asks
|
|
508
|
+
for preflight detail.
|
|
509
|
+
- Evidence contains names, refs, hashes, status, classifications, counts, and
|
|
510
|
+
remediation hints only. It never contains secret values, full env maps, raw
|
|
511
|
+
connector/provider payloads, private prompt text, stdout/stderr dumps, or
|
|
512
|
+
unsanitized errors.
|
|
513
|
+
- `manifestPath` is the filesystem ref for durable evidence, but the compact
|
|
514
|
+
summary must be enough for a normal caller to find the workflow, loop, run,
|
|
515
|
+
work item, and owning external action/automation refs through public APIs.
|
|
516
|
+
- Failed preflight evidence should be persistable when a commit attempt is
|
|
517
|
+
rejected, but pure `dry-run`/`preflight` calls should return it directly
|
|
518
|
+
without creating OpenLoops database rows.
|
|
519
|
+
- Redaction happens before persistence and before API return. Tests should
|
|
520
|
+
exercise both flat secret-looking text and nested JSON evidence values.
|
|
521
|
+
|
|
522
|
+
Implementation should expose the contract incrementally:
|
|
523
|
+
|
|
524
|
+
1. Add shared types and a pure result builder that converts validator outcomes
|
|
525
|
+
into `AutomationPreflightCheck` entries.
|
|
526
|
+
2. Wrap existing `preflightTarget`/`preflightWorkflow` with aggregate result
|
|
527
|
+
builders while keeping the current throwing APIs for compatibility.
|
|
528
|
+
3. Add `loops workflows upsert-one-shot --mode dry-run|preflight|commit --json`
|
|
529
|
+
and SDK equivalents that return `WorkflowUpsertResult`.
|
|
530
|
+
4. Thread compact evidence through route dry-runs, admitted work items, workflow
|
|
531
|
+
run manifests, MCP workflow validation, and the HTTP API.
|
|
532
|
+
5. Add strict-mode enforcement only after provider allowlist and secret/connector
|
|
533
|
+
readiness adapters can prove enforcement without leaking payloads.
|
|
534
|
+
|
|
535
|
+
Expected validation for that future implementation: focused unit tests for
|
|
536
|
+
workflow parsing, executor preflight, route dry-run output, run manifests,
|
|
537
|
+
redaction, SDK/CLI/MCP/API JSON shape, then `bun run typecheck`, `bun test`,
|
|
538
|
+
`bun run build`, and `bun run test:boundary`.
|
|
539
|
+
|
|
99
540
|
## Planned Strict Automation Execution
|
|
100
541
|
|
|
101
542
|
Automation-generated targets need a stricter execution mode than ordinary local
|
package/docs/CUTOVER-RUNBOOK.md
CHANGED
|
@@ -1,61 +1,78 @@
|
|
|
1
1
|
# Loops Postgres Cutover Runbook
|
|
2
2
|
|
|
3
|
-
Status: **backend
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
3
|
+
Status: **self-hosted control-plane backend landed; local daemon cutover is not
|
|
4
|
+
complete.** Do not flip scheduled production execution away from local SQLite
|
|
5
|
+
until the runner and migration follow-ups below are green.
|
|
6
|
+
|
|
7
|
+
Deployment vocabulary: `self_hosted` is the Hasna-owned AWS/RDS control-plane
|
|
8
|
+
deployment. `cloud` is the future hosted SaaS contract for outside users. The
|
|
9
|
+
vendored `@hasna/contracts` storage kit still calls direct Postgres storage
|
|
10
|
+
mode `cloud`; treat that as a storage-kit implementation term, not the
|
|
11
|
+
OpenLoops deployment mode.
|
|
12
|
+
|
|
13
|
+
## What Shipped
|
|
14
|
+
|
|
15
|
+
- `loops-serve` HTTP control plane: RDS-direct Postgres, `GET /health`,
|
|
16
|
+
`/ready`, `/version`, `/openapi.json`, storage-backed `/v1` loop CRUD and run
|
|
17
|
+
listing, and runner registration/claim/heartbeat/finalize protocol routes.
|
|
18
|
+
- `@hasna/contracts` API-key auth on non-local `loops-serve` binds, backed by
|
|
19
|
+
the shared `api_keys` table and a signing secret.
|
|
20
|
+
- Full `PostgresLoopStorage` behind `LoopStorageContract`, plus
|
|
21
|
+
`PgPoolExecutor`, runner claim/lease helpers, and Postgres concurrency tests.
|
|
22
|
+
- `loops-serve migrate`, which applies the ledger-tracked Postgres migrations
|
|
23
|
+
and ensures the `api_keys` table.
|
|
24
|
+
- `@hasna/loops/sdk/http`, generated from `openapi/loops.json`.
|
|
25
|
+
- ARM64/Bun `Dockerfile`, local `docker-compose.yml`, `hasna.contract.json`,
|
|
26
|
+
and a generated `migrations/` mirror for review.
|
|
27
|
+
|
|
28
|
+
## Local Postgres Smoke For loops-serve
|
|
29
|
+
|
|
30
|
+
Run the local development stack for the self-hosted service:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
docker compose run --rm loops-migrate
|
|
34
|
+
docker compose up --build loops-serve
|
|
35
|
+
curl -fsS http://127.0.0.1:8787/health
|
|
36
|
+
curl -fsS http://127.0.0.1:8787/ready
|
|
37
|
+
curl -fsS http://127.0.0.1:8787/openapi.json
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
`loops-serve` may bind to loopback without an API signing secret for local
|
|
41
|
+
development. A non-local bind must set `HASNA_LOOPS_API_SIGNING_KEY`,
|
|
42
|
+
`HASNA_API_SIGNING_KEY`, or `API_KEY_SIGNING_SECRET`; otherwise the service
|
|
43
|
+
fails closed before exposing `/v1`.
|
|
44
|
+
|
|
45
|
+
## AWS Self-Hosted Gates
|
|
46
|
+
|
|
47
|
+
1. Apply migrations with the ECS one-shot task or an operator command:
|
|
48
|
+
`HASNA_LOOPS_DATABASE_URL=... loops-serve migrate --dry-run`, then
|
|
49
|
+
`HASNA_LOOPS_DATABASE_URL=... loops-serve migrate`.
|
|
50
|
+
2. Start `loops-serve` with `HASNA_LOOPS_MODE=self_hosted`,
|
|
51
|
+
`HASNA_LOOPS_DATABASE_URL`, and the API signing secret from the approved
|
|
52
|
+
vault item. Do not log or copy the secret value into task evidence.
|
|
53
|
+
3. Verify `/health`, `/ready`, `/version`, and `/openapi.json`.
|
|
54
|
+
4. Verify an authenticated `/v1` read/write smoke against a throwaway loop and
|
|
55
|
+
a runner registration/claim/finalize smoke if a runner API URL is configured.
|
|
56
|
+
5. Record package version, git SHA, image tag, database migration plan/result,
|
|
57
|
+
redacted API URL, health/readiness responses, and rollback handle.
|
|
58
|
+
|
|
59
|
+
## Still Pending Before Daemon Cutover
|
|
60
|
+
|
|
61
|
+
- Long-running `loops-runner` daemon mode with backoff, fleet observability, and
|
|
62
|
+
durable machine registration records.
|
|
63
|
+
- Workflow target execution over the runner protocol.
|
|
64
|
+
- Id-preserving self-hosted import endpoints for workflow specs, loop
|
|
65
|
+
definitions, run history, workflow history, work items, goals, and audit rows.
|
|
66
|
+
- A no-loss migration path from local SQLite into the self-hosted control plane.
|
|
67
|
+
Current `loops export`/`loops import` are local-store tools, and
|
|
68
|
+
`loops self-hosted migrate|push|pull` remain previews.
|
|
69
|
+
- Hosted SaaS integration outside this public package.
|
|
56
70
|
|
|
57
71
|
## Rollback
|
|
58
72
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
73
|
+
For the self-hosted service, roll back by moving traffic to the previous image
|
|
74
|
+
or stopping the new `loops-serve` task. Local scheduled execution remains on
|
|
75
|
+
SQLite unless operators explicitly configure a runner/control-plane cutover, so
|
|
76
|
+
removing `LOOPS_API_URL`, `HASNA_LOOPS_API_URL`, `LOOPS_DATABASE_URL`, or
|
|
77
|
+
`HASNA_LOOPS_DATABASE_URL` returns the standalone CLI/daemon perspective to
|
|
78
|
+
`local`.
|