@hasna/loops 0.4.27 → 0.4.29

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.
Files changed (79) hide show
  1. package/CHANGELOG.md +205 -1
  2. package/README.md +232 -49
  3. package/dist/api/index.d.ts +20 -19
  4. package/dist/api/index.js +7770 -1342
  5. package/dist/cli/index.js +2976 -966
  6. package/dist/cli/safe-error-context.d.ts +1 -0
  7. package/dist/daemon/daemon.d.ts +1 -0
  8. package/dist/daemon/index.js +1899 -499
  9. package/dist/generated/storage-kit/index.d.ts +1 -1
  10. package/dist/generated/storage-kit/mode.d.ts +6 -12
  11. package/dist/index.d.ts +3 -3
  12. package/dist/index.js +5319 -1096
  13. package/dist/lib/advancement.d.ts +52 -0
  14. package/dist/lib/agent-adapter.d.ts +20 -2
  15. package/dist/lib/auth/route-policy.d.ts +14 -0
  16. package/dist/lib/auth/tenant-auth.d.ts +38 -0
  17. package/dist/lib/cloud/mode.d.ts +2 -8
  18. package/dist/lib/cloud/storage.d.ts +1 -2
  19. package/dist/lib/cloud/transport.d.ts +4 -18
  20. package/dist/lib/errors.d.ts +43 -1
  21. package/dist/lib/executor.d.ts +9 -0
  22. package/dist/lib/format.d.ts +2 -2
  23. package/dist/lib/goal/runner.d.ts +36 -3
  24. package/dist/lib/health.d.ts +1 -1
  25. package/dist/lib/labels.d.ts +4 -0
  26. package/dist/lib/loop-status.d.ts +4 -0
  27. package/dist/lib/migration.d.ts +70 -17
  28. package/dist/lib/mode.d.ts +2 -5
  29. package/dist/lib/mode.js +28 -37
  30. package/dist/lib/route/fields.d.ts +8 -0
  31. package/dist/lib/route/index.d.ts +2 -2
  32. package/dist/lib/route/throttle.d.ts +7 -0
  33. package/dist/lib/route/types.d.ts +3 -0
  34. package/dist/lib/run-completion.d.ts +18 -0
  35. package/dist/lib/scheduler.d.ts +5 -11
  36. package/dist/lib/storage/contract.d.ts +15 -1
  37. package/dist/lib/storage/index.d.ts +1 -1
  38. package/dist/lib/storage/index.js +4083 -486
  39. package/dist/lib/storage/pg-executor.d.ts +10 -5
  40. package/dist/lib/storage/postgres-loop-storage.d.ts +52 -26
  41. package/dist/lib/storage/postgres-schema.d.ts +8 -0
  42. package/dist/lib/storage/postgres-schema.js +1326 -1
  43. package/dist/lib/storage/postgres.d.ts +3 -1
  44. package/dist/lib/storage/postgres.js +1356 -27
  45. package/dist/lib/storage/provider-credentials.d.ts +84 -0
  46. package/dist/lib/storage/shared-database-transfer.d.ts +136 -0
  47. package/dist/lib/storage/sqlite.d.ts +15 -2
  48. package/dist/lib/storage/sqlite.js +1379 -217
  49. package/dist/lib/storage/tenant-backfill-s3.d.ts +53 -0
  50. package/dist/lib/storage/tenant-backfill.d.ts +40 -0
  51. package/dist/lib/store/index.d.ts +14 -8
  52. package/dist/lib/store.d.ts +129 -7
  53. package/dist/lib/store.js +1352 -218
  54. package/dist/lib/templates.d.ts +11 -0
  55. package/dist/lib/workflow-events.d.ts +6 -0
  56. package/dist/lib/workflow-provenance.d.ts +8 -0
  57. package/dist/lib/workflow-runner.d.ts +52 -4
  58. package/dist/mcp/index.js +2074 -657
  59. package/dist/runner/index.d.ts +20 -2
  60. package/dist/runner/index.js +2146 -183
  61. package/dist/sdk/http.d.ts +364 -4
  62. package/dist/sdk/http.js +379 -1
  63. package/dist/sdk/index.d.ts +7 -2
  64. package/dist/sdk/index.js +2341 -742
  65. package/dist/serve/index.d.ts +14 -0
  66. package/dist/serve/index.js +13269 -1830
  67. package/dist/types.d.ts +75 -3
  68. package/docs/AUTOMATION_RUNTIME_DESIGN.md +32 -32
  69. package/docs/CUTOVER-RUNBOOK.md +158 -31
  70. package/docs/DEPLOYMENT_MODES.md +84 -56
  71. package/docs/RUNTIME_BOUNDARY.md +26 -26
  72. package/docs/SHARED-DATABASE-TRANSFER.md +95 -0
  73. package/docs/SHARED_KIT_EXTRACTION_INVENTORY.md +631 -0
  74. package/docs/TRANSCRIPT_LOOP_PATTERNS.md +3 -3
  75. package/docs/UNIFIED_PRODUCT_CONTRACT.md +365 -0
  76. package/docs/USAGE.md +150 -56
  77. package/docs/workflows/transcript-feedback-to-loops.json +2 -2
  78. package/package.json +8 -4
  79. package/dist/lib/storage/pg-runner-claim.d.ts +0 -40
package/README.md CHANGED
@@ -1,14 +1,14 @@
1
- # OpenLoops
1
+ # Loops
2
2
 
3
- OpenLoops is a local CLI and daemon for persistent loops and workflows: scheduled or recurring work that survives process restarts and records every run.
3
+ Loops is a local CLI and daemon for persistent loops and workflows: scheduled or recurring work that survives process restarts and records every run.
4
4
 
5
- Naming: the product is **OpenLoops**, published on npm as
5
+ Naming: the product is **Loops**, published on npm as
6
6
  [`@hasna/loops`](https://www.npmjs.com/package/@hasna/loops) and developed in the
7
7
  [`hasna/loops`](https://github.com/hasna/loops) repository. The installed
8
8
  binaries are `loops`, `loops-daemon`, `loops-api`, `loops-serve`,
9
9
  `loops-runner`, and `loops-mcp`.
10
10
 
11
- OpenLoops is the **runtime, scheduler, and workflow engine** for automations —
11
+ Loops is the **runtime, scheduler, and workflow engine** for automations —
12
12
  not the automation domain model. Specs, triggers, queue ownership, approvals,
13
13
  DLQ/replay, and audit live in `@hasna/automations` and `@hasna/actions`; see
14
14
  [Runtime Boundary](docs/RUNTIME_BOUNDARY.md) for ownership split and external
@@ -25,7 +25,7 @@ It supports deterministic command loops, JSON-defined workflows, and guarded CLI
25
25
 
26
26
  ## Deployment Modes
27
27
 
28
- OpenLoops has three deployment modes:
28
+ Loops has three deployment modes:
29
29
 
30
30
  - `local`: SQLite in `LOOPS_DATA_DIR` is authoritative and `loops-daemon` executes scheduled work.
31
31
  - `self_hosted`: the Hasna-owned AWS/RDS control-plane deployment, served by
@@ -33,23 +33,20 @@ OpenLoops has three deployment modes:
33
33
  shared by serve, SDK, and tests. This release exposes status, storage-backed
34
34
  `/v1` loop CRUD and run listing, runner claim/heartbeat/finalize protocol
35
35
  endpoints, a one-shot `loops-runner run-once` execution path for embedded
36
- control-plane hosts, id-preserving local export/import, and preview-only
37
- self-hosted migration/sync commands; full fleet rollout and id-preserving
38
- remote apply are follow-up work.
39
- - `cloud`: a hosted control-plane contract. This release exposes client/runner status only; hosted tenant auth and infrastructure live outside this package.
36
+ control-plane hosts, id-preserving local export/import, tenant-bound API-key
37
+ authentication, and forced Postgres row-level tenant isolation.
38
+ - `cloud`: a hosted control-plane contract using the same tenant-bound API.
40
39
 
41
40
  `local` is the default and requires no network, token, Postgres, or hosted
42
- service. Set `LOOPS_MODE` or `HASNA_LOOPS_MODE` to `local`, `self_hosted`, or
43
- `cloud` to choose explicitly. Without an explicit mode, `LOOPS_CLOUD_API_URL`
44
- selects `cloud`, while `LOOPS_API_URL` or `LOOPS_DATABASE_URL` selects
45
- `self_hosted`.
41
+ service. Set `HASNA_LOOPS_STORAGE_MODE` to `local`, `self_hosted`, or
42
+ `cloud` to choose explicitly. Without an explicit mode,
43
+ `HASNA_LOOPS_API_URL` or `HASNA_LOOPS_DATABASE_URL` selects `self_hosted`.
46
44
 
47
45
  The public `@hasna/loops` package owns the local runtime, mode resolver,
48
- self-hosted API contract, runner contract, SDK, MCP server, and CLI. Hosted
49
- tenant auth, account administration, and infrastructure live outside this
50
- package. Cloud mode is a public contract until a cloud-specific hosted URL and
51
- cloud token are configured through `LOOPS_CLOUD_API_URL` plus
52
- `LOOPS_CLOUD_TOKEN` or `HASNA_LOOPS_CLOUD_TOKEN`.
46
+ self-hosted API contract, tenant authentication and authorization, runner
47
+ contract, SDK, MCP server, and CLI. Account provisioning and infrastructure
48
+ deployment remain operator responsibilities. Cloud clients use
49
+ `HASNA_LOOPS_API_URL` plus `HASNA_LOOPS_API_KEY`.
53
50
 
54
51
  Scheduler state is explicit in status JSON. `schedulerState.localStore` is
55
52
  SQLite plus local run artifact files: authoritative in `local`, cache/spool in
@@ -73,7 +70,6 @@ loops self-hosted status
73
70
  loops self-hosted migrate --dry-run
74
71
  loops self-hosted push --dry-run
75
72
  loops self-hosted pull --dry-run
76
- loops self-hosted runner-register --runner-id <id> --machine-id <machine> --apply
77
73
  loops export --file ./loops-export.json --dry-run
78
74
  loops export --file ./loops-export.json
79
75
  loops import ./loops-export.json
@@ -81,16 +77,55 @@ loops import ./loops-export.json --apply
81
77
  loops cloud status
82
78
  loops-api status
83
79
  loops-serve version
84
- HASNA_LOOPS_DATABASE_URL=... loops-serve migrate --dry-run
80
+ HASNA_LOOPS_MIGRATOR_DATABASE_URL=... loops-serve migrate --dry-run
81
+ loops-serve db-credentials reconcile
82
+ HASNA_LOOPS_DATABASE_URL=... HASNA_LOOPS_AUTH_DATABASE_URL=... loops-serve serve
85
83
  loops-runner status
86
84
  ```
87
85
 
86
+ `loops self-hosted push` is safe by default: imported workflows are archived and
87
+ imported loops are paused with `nextRunAt`/`retryScheduledFor` cleared. Even
88
+ without `--replace`, the control-plane import boundary may rewrite existing
89
+ same-id workflows/loops into that disabled representation; use explicit
90
+ preserve flags only for an intentional activation.
91
+
88
92
  See [Deployment Modes](docs/DEPLOYMENT_MODES.md) for the full package boundary
89
93
  and machine-placement contract.
90
94
 
95
+ ### Provider database credentials
96
+
97
+ `loops-serve db-credentials reconcile` is the fixed in-cluster reconciler for
98
+ provider-managed RDS app credentials. It reads the RDS-managed master secret
99
+ with the AWS Secrets Manager SDK, builds `sslmode=verify-full` DSNs only in
100
+ memory, and writes each app DSN to that app's Secrets Manager secret through
101
+ `AWSPENDING` before promoting `AWSCURRENT`. It never accepts database URLs,
102
+ passwords, access keys, profiles, or secret material as command flags.
103
+
104
+ Required environment is intentionally strict: `AWS_REGION`,
105
+ `AWS_CONTAINER_CREDENTIALS_RELATIVE_URI`, `HASNA_LOOPS_RDS_MASTER_SECRET_ARN`,
106
+ `HASNA_LOOPS_MIGRATOR_DATABASE_URL_SECRET_ARN`,
107
+ `HASNA_LOOPS_RUNTIME_DATABASE_URL_SECRET_ARN`,
108
+ `HASNA_LOOPS_AUTH_DATABASE_URL_SECRET_ARN`, `HASNA_LOOPS_RDS_INSTANCE_ID`,
109
+ `HASNA_LOOPS_RDS_ENDPOINT`, `HASNA_LOOPS_RDS_PORT`,
110
+ `HASNA_LOOPS_RDS_DATABASE`, and `HASNA_LOOPS_RDS_MASTER_USERNAME`. Static AWS
111
+ credential environment variables, profile/web-identity inputs, full credential
112
+ URIs, non-RDS-style endpoints, duplicate secret ARNs, cross-region secret ARNs,
113
+ and malformed identifiers fail closed before any secret or database operation.
114
+
115
+ The command normalizes the fixed NOLOGIN database roles
116
+ `open_loops_owner`, `open_loops_migrator`, `open_loops_runtime`, and
117
+ `open_loops_authenticator`; manages exactly three LOGIN users
118
+ `open_loops_migrator_login`, `open_loops_runtime_login`, and
119
+ `open_loops_authenticator_login`; grants only the migrator login exact
120
+ owner/migrator membership plus `CREATEROLE`; and keeps runtime/authenticator
121
+ without memberships until migration `0010_tenant_enforce` has landed. After
122
+ `0010`, runtime and authenticator are attached only to their matching roles.
123
+ If PostgreSQL 16 implicit creator memberships cannot be normalized, the
124
+ reconciler fails before publishing app credentials.
125
+
91
126
  ## Install
92
127
 
93
- **OpenLoops requires the [Bun](https://bun.sh) runtime (`bun >= 1.0`).** The
128
+ **Loops requires the [Bun](https://bun.sh) runtime (`bun >= 1.0`).** The
94
129
  `loops`, `loops-daemon`, `loops-api`, `loops-serve`, `loops-runner`, and
95
130
  `loops-mcp` binaries run with a `#!/usr/bin/env bun` shebang, so Bun must be on
96
131
  `PATH` even when the package is installed with npm. Node.js is not a supported
@@ -126,7 +161,7 @@ The CLI stores state in `~/.hasna/loops` by default. Set `LOOPS_DATA_DIR` to iso
126
161
 
127
162
  ## MCP Server
128
163
 
129
- OpenLoops ships a stdio MCP server for safe loop and workflow inspection from
164
+ Loops ships a stdio MCP server for safe loop and workflow inspection from
130
165
  MCP-capable agents:
131
166
 
132
167
  ```bash
@@ -143,6 +178,7 @@ import { createLoopsMcpServer } from "@hasna/loops/mcp";
143
178
  Available read tools include `loops_list`, `loops_show`, `loops_runs`,
144
179
  `loops_doctor`, `loops_workflows_list`, `loops_workflow_read`, and
145
180
  `loops_workflow_validate`.
181
+ `loops_list` and `loops_runs` accept label arrays with AND semantics.
146
182
  Resources are available at `loops://runtime` and `loops://tools`.
147
183
  Those tools use the same `Store`, public redaction helpers, and workflow parser
148
184
  as the CLI and SDK, so read output and validation behavior stay aligned across
@@ -152,12 +188,16 @@ Mutation tools are disabled by default. Start the server with
152
188
  `LOOPS_MCP_ALLOW_MUTATIONS=true` only for a trusted local MCP host that should be
153
189
  allowed to change loop state. The guarded mutation tools use canonical names:
154
190
  `loops_pause`, `loops_resume`, `loops_stop`, `loops_run_now`, `loops_archive`,
155
- `loops_unarchive`, `loops_create_command`, and `loops_create_workflow`.
191
+ `loops_unarchive`, `loops_labels_update`, `loops_create_command`, and
192
+ `loops_create_workflow`.
156
193
  Deprecated `loop_*` aliases are still registered where compatibility needs them,
157
194
  but callers should use the `loops_*` names. Mutation tools do not require or
158
195
  accept confirmation-string parameters; the server-side environment opt-in is the
159
196
  gate. MCP `loops_run_now` schedules the loop for immediate daemon pickup; inline
160
197
  execution remains CLI-only.
198
+ When `showOutput` is enabled, `loops_runs` caps each stdout/stderr field at
199
+ 32,000 characters, caps output-bearing pages at 25 runs, and caps the aggregate
200
+ MCP JSON response at 128,000 characters.
161
201
 
162
202
  Keep host-affecting or long-running operations on the CLI: daemon
163
203
  start/stop/install/logs, inline `run-now`, `tick`, loop deletion, workflow
@@ -217,8 +257,41 @@ loops create agent supply-chain-watch \
217
257
  --prompt "Check for suspicious dependency or supply-chain changes. Report only concrete findings."
218
258
  ```
219
259
 
260
+ Codewith `--auth-profile` is provider-native, not an OpenAccounts selector.
261
+ Local and remote preflight first request `codewith profile list --json` and
262
+ match the requested name exactly against the root `data` or `profiles`
263
+ inventory. That inventory is authoritative: `usable: false` rejects a profile,
264
+ legacy entries without `usable` remain usable, and `currentProfile` does not add
265
+ inventory membership. Loops falls back to the human-readable table,
266
+ including active `*` rows, only when JSON mode is unsupported or its inventory
267
+ is structurally unavailable. Embedded NUL bytes, missing profiles, and
268
+ non-fallback profile-list failures fail closed.
269
+
270
+ ## Labels
271
+
272
+ Command, agent, and workflow loops accept repeatable labels:
273
+
274
+ ```bash
275
+ loops create command repo-status --every 1m --cmd "git status --short" \
276
+ --label BrowserPlan --label maintenance
277
+ loops list --label browserplan --label maintenance
278
+ loops runs --label browserplan
279
+ ```
280
+
281
+ Labels are normalized to lowercase, deduplicated, limited to 32 per loop, and
282
+ must match `[a-z0-9][a-z0-9._-]{0,63}` after normalization. Repeated filters use
283
+ AND semantics. Run filtering uses the loop's current labels rather than a
284
+ historical snapshot on each run.
285
+
286
+ ```bash
287
+ loops labels add repo-status urgent
288
+ loops labels remove repo-status maintenance
289
+ loops labels set repo-status browserplan nightly
290
+ loops labels clear repo-status
291
+ ```
292
+
220
293
  Run an OpenCode loop with an explicit provider/model. OpenCode reads
221
- `~/.config/opencode/config.json` when no model is supplied, so OpenLoops rejects
294
+ `~/.config/opencode/config.json` when no model is supplied, so Loops rejects
222
295
  OpenCode agent targets without `--model` instead of inheriting a stale or
223
296
  machine-specific default.
224
297
 
@@ -231,6 +304,32 @@ loops create agent opencode-smoke \
231
304
  --prompt "Reply with exactly OK."
232
305
  ```
233
306
 
307
+ Direct agent loops can persist an auditable session contract:
308
+
309
+ ```bash
310
+ loops create agent repo-check \
311
+ --provider codewith \
312
+ --every 15m \
313
+ --cwd /path/to/repo \
314
+ --sandbox workspace-write \
315
+ --prompt "Check the repo and report concrete failures." \
316
+ --allow-tool functions.exec_command \
317
+ --allow-command git,bun \
318
+ --safety-reason "isolated repository status inspection"
319
+ ```
320
+
321
+ Tool and command lists are advisory metadata, not provider-enforced policy.
322
+ Loops stores the reason with the direct loop target, adds an explicit
323
+ advisory contract to provider stdin, and exports `LOOPS_AGENT_SESSION_CONTRACT`.
324
+ A direct agent loop has no workflow run, so it does not create a workflow event.
325
+ Agent steps inside workflows that carry an audit contract additionally receive
326
+ one server-derived `agent_session_contract` event per step and workflow run.
327
+
328
+ Codewith/Codex `danger-full-access`, Cursor `sandbox=disabled`, and provider
329
+ bypass modes (Claude, Cursor, AI Copilot, and OpenCode) require both a non-empty
330
+ safety reason and `--manual-break-glass`. A reason cannot replace explicit
331
+ operator approval.
332
+
234
333
  For `codewith` and `aicopilot` account isolation, register matching OpenAccounts tools first if they are not built in on the machine:
235
334
 
236
335
  ```bash
@@ -274,7 +373,7 @@ resolve from the workflow JSON file's directory:
274
373
  }
275
374
  ```
276
375
 
277
- OpenLoops records `promptSource` metadata and redacts prompt bodies in public
376
+ Loops records `promptSource` metadata and redacts prompt bodies in public
278
377
  CLI output by default, including `templates render`. Use
279
378
  `~/.hasna/loops/prompts/<stable-name>.md` as the default prompt store for
280
379
  production loops.
@@ -285,7 +384,7 @@ non-secret routing/configuration data.
285
384
 
286
385
  ## Goals
287
386
 
288
- Add `--goal` to wrap a command, agent, or workflow loop in an AI-SDK orchestration layer. OpenLoops asks the configured model to create a flat DAG plan, then executes ready plan nodes by re-running the underlying target once per node. Each node run is parameterized with that node's objective: agent prompts get the goal and node objective appended, and every wrapped process receives `LOOPS_GOAL_NODE_KEY` and `LOOPS_GOAL_NODE_OBJECTIVE` in its environment. An adversarial achievement audit runs before the goal is marked complete.
387
+ Add `--goal` to wrap a command, agent, or workflow loop in an AI-SDK orchestration layer. Loops asks the configured model to create a flat DAG plan, then executes ready plan nodes by re-running the underlying target once per node. Each node run is parameterized with that node's objective: agent prompts get the goal and node objective appended, and every wrapped process receives `LOOPS_GOAL_NODE_KEY` and `LOOPS_GOAL_NODE_OBJECTIVE` in its environment. An adversarial achievement audit runs before the goal is marked complete.
289
388
 
290
389
  The goal `autoExecute` mode is honored: `off` plans and persists the DAG without executing any nodes (the run reports the persisted plan), while `readyOnly` (the default) and `aiDirected` run the dependency-driven execution loop over ready nodes.
291
390
 
@@ -336,7 +435,7 @@ Workflow JSON can also embed goals at the workflow or step level:
336
435
  For **workflow loops** (`loops create workflow ...`), prefer a single loop-level
337
436
  `--goal` wrapper around the scheduled workflow. Do not also define a top-level
338
437
  `"goal"` on the workflow JSON spec: nested loop-level and workflow-level goal
339
- wrappers deadlocked execution because each layer waited on the other. OpenLoops
438
+ wrappers deadlocked execution because each layer waited on the other. Loops
340
439
  now rejects new workflow loops that combine both wrappers at creation time, and
341
440
  blocks retargeting a loop-level goal loop onto a workflow that also carries a
342
441
  top-level goal.
@@ -415,7 +514,7 @@ Use `shell: true` only when you intentionally want shell parsing:
415
514
  ## Templates And Task Events
416
515
 
417
516
  Built-in templates turn common orchestration flows into reusable workflow JSON.
418
- Todos-task routes are OpenLoops-native runtime admission — see
517
+ Todos-task routes are Loops-native runtime admission — see
419
518
  [Runtime Boundary](docs/RUNTIME_BOUNDARY.md) for how this differs from
420
519
  OpenAutomations queue ownership.
421
520
  `todos-task-worker-verifier` performs one todos task and then verifies it.
@@ -471,7 +570,7 @@ loops templates render routing-remediation \
471
570
  --var idempotencyKey=routing-health:repo:shard0
472
571
  ```
473
572
 
474
- Custom reusable workflow templates live under the OpenLoops app data directory:
573
+ Custom reusable workflow templates live under the Loops app data directory:
475
574
  `~/.hasna/loops/templates` by default, or `$LOOPS_DATA_DIR/templates` when
476
575
  `LOOPS_DATA_DIR` is set. Store templates as declarative JSON files; listing,
477
576
  showing, and rendering templates never executes workflow steps or mutates the
@@ -575,6 +674,8 @@ arguments, and implicit Codewith/Codex full-access defaults. If a custom
575
674
  Codewith/Codex template uses `permissionMode: "bypass"`, it must also set
576
675
  `sandbox` to `workspace-write` or `read-only`. Use built-in templates with
577
676
  explicit break-glass handling for emergency workflows that need full access.
677
+ Claude, Cursor, AI Copilot, and OpenCode bypass modes always require explicit
678
+ break-glass plus a non-empty safety reason.
578
679
 
579
680
  For event-driven task automation, `loops routes create todos-task` reads a
580
681
  Hasna event envelope from stdin or `HASNA_EVENT_JSON`, records a
@@ -676,10 +777,10 @@ cat event.json | loops routes create generic \
676
777
  ```
677
778
 
678
779
  This is the intended deterministic-to-agentic path: a producer creates a todos
679
- task, `@hasna/events` delivers `task.created`, OpenLoops records the invocation
680
- and admission item, OpenLoops creates a worker/verifier workflow when admitted,
780
+ task, `@hasna/events` delivers `task.created`, Loops records the invocation
781
+ and admission item, Loops creates a worker/verifier workflow when admitted,
681
782
  and the workflow updates todos with evidence. Use account pools so worker and
682
- verifier steps do not burn the same profile. For live Codewith drains, OpenLoops
783
+ verifier steps do not burn the same profile. For live Codewith drains, Loops
683
784
  spreads each route to the **least-loaded** pool account (counting running steps
684
785
  per account) and still keeps the verifier on a different profile than the worker;
685
786
  the deterministic hash is the tie-break, so a cold store reproduces the previous
@@ -692,9 +793,9 @@ Use `--dry-run` to inspect the rendered invocation, work item, workflow, and
692
793
  loop input without storing anything.
693
794
 
694
795
  Generated worker/verifier workflows fail closed when `sandbox=danger-full-access`
695
- is requested without `manualBreakGlass=true`. Use `workspace-write` for
696
- unattended task/event routes. Full access is an explicit manual emergency path,
697
- not a default automation mode.
796
+ is requested without both `manualBreakGlass=true` and a non-empty
797
+ `safetyReason`. Use `workspace-write` for unattended task/event routes. Full
798
+ access is an explicit manual emergency path, not a default automation mode.
698
799
 
699
800
  When a sandboxed Codewith/Codex worker must update app stores outside the repo
700
801
  worktree, pass those stores explicitly with `--add-dir` or template `addDirs`.
@@ -786,7 +887,21 @@ anti-hog caps counted over all routes. Raise a router's `--max-active`
786
887
  deliberately once counting is per-route; keep `--max-per-profile` set so the
787
888
  extra concurrency spreads across subscription accounts.
788
889
 
789
- `--max-active` and related route throttles count OpenLoops routed workflow work
890
+ Route flags or an expanded named policy are authoritative ceilings. Positive
891
+ integer task/event fields (`max_active`, `max_active_per_project`, and
892
+ `max_active_per_project_group`, including camel-case aliases) may only tighten
893
+ an already configured ceiling for that admission attempt; metadata cannot
894
+ create a cap, raise it, or override an explicit `--project-group`. Generated
895
+ workflow prompts and invocation manifests include the resolved route scope,
896
+ project group, and throttle limits so the admission decision is auditable from
897
+ the same evidence the agent sees.
898
+
899
+ These caps are local-store admission controls, not a distributed semaphore. The
900
+ count and admission write are serialized in one transaction for routers sharing
901
+ the same local `loops.db`; different `LOOPS_DATA_DIR` databases or machines do
902
+ not share project-group counts.
903
+
904
+ `--max-active` and related route throttles count Loops routed workflow work
790
905
  items; they do not know whether Codewith is already at its live background-agent
791
906
  admission limit. Add `--provider-active-cap <n>` (or the Codewith-specific alias
792
907
  `--codewith-active-cap <n>`) to make the route run `codewith agent diagnostics
@@ -811,7 +926,7 @@ Workflow run manifests are written under
811
926
 
812
927
  ## OpenAutomations Runtime Binding
813
928
 
814
- OpenLoops executes automations that external compilers have materialized; it
929
+ Loops executes automations that external compilers have materialized; it
815
930
  does not own the automation product surface. See
816
931
  [Runtime Boundary](docs/RUNTIME_BOUNDARY.md) for the full ownership table,
817
932
  three handoff paths (claim-queue, planned `@hasna/actions` upsert-one-shot, and
@@ -833,7 +948,7 @@ automations queue complete <action-id> --runner open-loops:<worker-id>
833
948
  ```
834
949
 
835
950
  For explicit event workflow routing, OpenAutomations can export the normalized
836
- event envelope and OpenLoops can consume it through the existing generic event
951
+ event envelope and Loops can consume it through the existing generic event
837
952
  route:
838
953
 
839
954
  ```bash
@@ -841,13 +956,13 @@ automations --json webhooks event <route> --body-json '<json>' \
841
956
  | loops --json routes create generic
842
957
  ```
843
958
 
844
- This is not automation materialization in OpenLoops. It is an explicit
959
+ This is not automation materialization in Loops. It is an explicit
845
960
  event-envelope workflow handoff: OpenAutomations owns deterministic automation
846
- specs, webhook normalization, queue state, approvals, DLQ, and replay; OpenLoops
961
+ specs, webhook normalization, queue state, approvals, DLQ, and replay; Loops
847
962
  owns agent workflow invocation after the operator routes the envelope to
848
963
  `loops routes create generic`.
849
964
 
850
- Do not store automation specs in OpenLoops, infer automation triggers from event
965
+ Do not store automation specs in Loops, infer automation triggers from event
851
966
  transport alone, or replace the OpenAutomations queue with loop/workflow rows.
852
967
  When a loop or workflow is used for execution, keep `HASNA_AUTOMATIONS_DIR`
853
968
  pointing at the owning OpenAutomations data root and preserve the runner id in
@@ -856,7 +971,7 @@ completion/failure calls so OpenAutomations can enforce action leases.
856
971
  ### Planned Workflow Upsert SDK
857
972
 
858
973
  External compilers such as `@hasna/actions` and `@hasna/automations` should not
859
- write OpenLoops SQLite rows directly. The stable contract should be an
974
+ write Loops SQLite rows directly. The stable contract should be an
860
975
  idempotent CLI/SDK upsert that accepts a fully rendered one-shot workflow loop
861
976
  request and returns durable refs.
862
977
 
@@ -869,7 +984,7 @@ does not duplicate that spec.
869
984
  That design doc also defines the planned `@hasna/actions` target binding. The
870
985
  binding reuses `ActionManifest`, `ActionInvocation`, `ActionRunStatus`,
871
986
  `ActionQueueStatus`, action-owned idempotency keys, action audit events, and
872
- dead-letter/replay records. OpenLoops only admits and executes the handed-off
987
+ dead-letter/replay records. Loops only admits and executes the handed-off
873
988
  workflow, then returns workflow refs for action-owned evidence.
874
989
 
875
990
  The same design doc covers the planned DLQ/dead-letter lifecycle, including
@@ -883,7 +998,7 @@ work.
883
998
 
884
999
  ## Transcript-Driven Loops
885
1000
 
886
- OpenLoops can turn long-form media or meeting transcripts into recurring workflow work when paired with `iapp-transcriber`. The template at `docs/workflows/transcript-feedback-to-loops.json` transcribes an authorized media URL, asks an agent to extract recurring loop candidates, authors workflow specs, and validates generated workflows before scheduling. Copy it into the target repo, replace `/path/to/repo` with that repo's absolute path, and provide `TRANSCRIBER_SOURCE_URL` through the runner environment or a private, uncommitted workflow copy before storing or scheduling it. Do not commit private or signed media URLs.
1001
+ Loops can turn long-form media or meeting transcripts into recurring workflow work when paired with `iapp-transcriber`. The template at `docs/workflows/transcript-feedback-to-loops.json` transcribes an authorized media URL, asks an agent to extract recurring loop candidates, authors workflow specs, and validates generated workflows before scheduling. Copy it into the target repo, replace `/path/to/repo` with that repo's absolute path, and provide `TRANSCRIBER_SOURCE_URL` through the runner environment or a private, uncommitted workflow copy before storing or scheduling it. Do not commit private or signed media URLs.
887
1002
 
888
1003
  ```bash
889
1004
  loops workflows validate /path/to/repo/.openloops/transcript-feedback-to-loops.json --preflight
@@ -993,7 +1108,7 @@ Archived loops are hidden from the default `loops list`, excluded from daemon sc
993
1108
 
994
1109
  `loops run-now` reports the manual run source:
995
1110
 
996
- - `source=ad_hoc`: the loop was not due yet, so OpenLoops created a one-off manual slot. This is a single immediate attempt and does not schedule retries or consume the future scheduled slot.
1111
+ - `source=ad_hoc`: the loop was not due yet, so Loops created a one-off manual slot. This is a single immediate attempt and does not schedule retries or consume the future scheduled slot.
997
1112
  - `source=due_slot`: the persisted scheduled slot was already due, so the manual run claims that slot and advances or retries the loop using normal scheduler rules.
998
1113
  - `source=retry_slot`: the loop was waiting on a failed slot retry, so the manual run claims that retry slot and advances the loop using normal retry rules.
999
1114
 
@@ -1042,16 +1157,84 @@ On Linux this writes a user systemd service. On macOS it writes a LaunchAgent pl
1042
1157
  The adapters intentionally use provider command surfaces instead of pretending every agent has one SDK:
1043
1158
 
1044
1159
  - Claude uses `claude -p --output-format json` and safe-mode/local setting sources by default.
1045
- - Codewith runs non-interactive `codewith --ask-for-approval never exec --json` sessions by default. exec starts a fresh session per invocation, avoiding the multi-megabyte rollout history that `codewith agent start` reloaded every turn (which drove `context_length_exceeded` silent no-ops), and it keeps network egress for gh/git — the `workspace-write` sandbox opts back into `sandbox_workspace_write.network_access`. Codewith exec is remote-capable like codex. OpenLoops rejects Codewith `extraArgs` that try to force `exec`, `--ephemeral`, `--json`, or other exec launch flags that the adapter already manages.
1160
+ - Codewith runs non-interactive `codewith --ask-for-approval never exec --json` sessions by default. exec starts a fresh session per invocation, avoiding the multi-megabyte rollout history that `codewith agent start` reloaded every turn (which drove `context_length_exceeded` silent no-ops), and it keeps network egress for gh/git — the `workspace-write` sandbox opts back into `sandbox_workspace_write.network_access`. Codewith exec is remote-capable like codex.
1046
1161
  - AI Copilot and OpenCode use `run --format json --pure`. OpenCode requires an explicit provider/model id because ambient OpenCode config is machine-specific.
1047
- - Cursor is CLI-first for now via the standalone `agent -p` binary. OpenLoops no longer falls back to `cursor agent`; install the standalone Cursor Agent CLI so preflight and scheduled runs use the same executable.
1162
+ - Cursor is CLI-first for now via the standalone `agent -p` binary. Loops no longer falls back to `cursor agent`; install the standalone Cursor Agent CLI so preflight and scheduled runs use the same executable.
1048
1163
  - Codex uses `codex --ask-for-approval never exec --json --ephemeral --skip-git-repo-check`, with `--add-dir` for explicit extra writable directories where supported.
1049
1164
  - Agent prompts are sent through child stdin instead of argv where the provider supports stdin, including Codewith `exec` (which reads instructions from stdin when no positional prompt is given), so the prompt never lands on argv.
1050
- - When `--account` or a step `account` is set, OpenLoops resolves `accounts env <profile> --tool <tool>` before spawning the target, strips inherited tool home/API-key variables, and applies the selected profile only to that process. Missing account profiles fail before the provider binary receives the prompt.
1051
- - `--auth-profile` and step `authProfile` are provider-native auth selectors. They currently apply to Codewith and are passed to Codewith as `--auth-profile <name>` on the `exec` invocation; they do not call OpenAccounts.
1165
+ - When `--account` or a step `account` is set, Loops resolves `accounts env <profile> --tool <tool>` before spawning the target, strips inherited tool home/API-key variables, and applies the selected profile only to that process. Missing account profiles fail before the provider binary receives the prompt.
1166
+ - `--auth-profile` and step `authProfile` are provider-native Codewith selectors passed as `--auth-profile <name>` on the `exec` invocation; they do not call OpenAccounts. Local and remote preflight share the JSON-first, exact-name contract described above, use human-table parsing only as a compatibility fallback, and fail closed for unusable or missing profiles, NUL-containing names, and non-fallback list failures.
1052
1167
  - `--sandbox` maps to provider-native sandbox flags. Codewith/Codex accept `read-only`, `workspace-write`, or `danger-full-access`; Cursor accepts `enabled` or `disabled`.
1168
+ - `--allow-tool` and `--allow-command` declare advisory, metadata-only
1169
+ restrictions and require `--safety-reason`. The exact contract is persisted
1170
+ and emitted for audit, but Loops does not claim provider-side enforcement.
1171
+ Relaxed sandboxes and native provider bypass modes also require explicit
1172
+ `--manual-break-glass`.
1053
1173
  - `--permission-mode` maps `plan`, `auto`, and `bypass` where the provider supports it. Claude uses native permission modes, Cursor maps bypass to `--force`, and OpenCode/AICopilot map bypass to `--dangerously-skip-permissions`.
1174
+ - `extraArgs` is fail-closed: every provider currently rejects non-empty
1175
+ passthrough arguments, including unknown options, positional subcommands,
1176
+ split values, `--option=value`, and attached short-option forms. Configure
1177
+ execution, output, permissions, sandboxing, model, cwd, and other supported
1178
+ behavior through the modeled agent-target fields. A passthrough option must
1179
+ be explicitly reviewed and added to the provider allowlist before use.
1180
+ Legacy persisted targets are not silently accepted or rewritten: execution
1181
+ fails validation until `extraArgs` is removed and replaced with modeled
1182
+ fields. API and migration imports reject those targets instead of stripping
1183
+ the arguments; update the source record and retry the import.
1054
1184
  - `--variant` is provider-specific reasoning/model effort. Claude maps it to `--effort`, Codewith/Codex map it to `model_reasoning_effort`, and OpenCode/AICopilot pass `--variant`.
1055
1185
  - Daemon and scheduled runs prepend common user executable directories such as `~/.local/bin` and `~/.bun/bin` before resolving provider CLIs.
1056
1186
 
1187
+ For hosted workflow runs, the control plane derives any required agent session
1188
+ contract from the stored workflow step and persists it before execution. Reusing an
1189
+ older workflow run backfills a missing valid contract idempotently. Stored
1190
+ pre-contract workflows with unsafe/invalid agent options, mismatched stored
1191
+ contracts, duplicate contracts, command-step contracts, and client-fabricated
1192
+ contracts fail closed. Direct agent loops continue to expose their contract
1193
+ through prompt/environment metadata only.
1194
+
1057
1195
  For production loops that can mutate repos, use disposable worktrees (`--worktree-mode required` / `worktreeMode=required`) and explicit prompts that name allowed write scope. Worktrees are executor-enforced: when a target carries worktree metadata, the executor prepares and enters the git worktree before spawning the child process, records the worktree it entered, and with `mode=required` fails the run closed instead of falling back to the original checkout when preparation fails. Existing managed worktrees are reused only after top-level and git-common-dir checks; a clean detached or stale-branch checkout may be reattached to the expected generated branch, while dirty or unsafe mismatches fail with cleanup evidence. Remote machine runs with a required worktree apply the same checks on the remote side before the target executes.
1196
+
1197
+ ## Immutable ECR Candidate Images
1198
+
1199
+ `.github/workflows/ecr-candidate.yml` is the repository workflow for building
1200
+ an AWS ECR release candidate. It is manual and source-only: it does not update
1201
+ `latest`, ECS task definitions, services, or deployments. The job checks out
1202
+ an exact full commit SHA, proves that commit is reachable from `origin/main`,
1203
+ builds the `Dockerfile` `runner` target natively for `linux/arm64`, blocks on
1204
+ local Trivy critical/high findings, and only then assumes the ECR push role.
1205
+ After the immutable push it waits for ECR scanning and blocks on critical/high
1206
+ findings there as well. The retained artifact contains the local scan report,
1207
+ a CycloneDX SBOM, an unsigned in-toto/SLSA provenance statement, and bounded
1208
+ ECR scan counts. The statement is evidence, not a cryptographic attestation.
1209
+
1210
+ Before the first run, repository administrators must create a protected GitHub
1211
+ environment named `ecr-candidate` with required reviewers and no deployment
1212
+ branches other than `main`. Configure these repository variables (not secrets):
1213
+
1214
+ - `AWS_REGION`: AWS region containing the existing candidate repository.
1215
+ - `AWS_ROLE_ARN`: OIDC role dedicated to this workflow.
1216
+ - `ECR_REPOSITORY`: existing ECR repository name. It must use immutable tags
1217
+ and scan-on-push.
1218
+
1219
+ The OIDC role trust must constrain `token.actions.githubusercontent.com:aud`
1220
+ to `sts.amazonaws.com` and `token.actions.githubusercontent.com:sub` to exactly
1221
+ `repo:hasna/loops:environment:ecr-candidate`. Its permissions should allow
1222
+ `ecr:GetAuthorizationToken` on `*`, and only these actions on the configured
1223
+ repository ARN: `ecr:DescribeRepositories`, `ecr:BatchCheckLayerAvailability`,
1224
+ `ecr:GetDownloadUrlForLayer`, `ecr:BatchGetImage`, `ecr:InitiateLayerUpload`,
1225
+ `ecr:UploadLayerPart`, `ecr:CompleteLayerUpload`, `ecr:PutImage`,
1226
+ `ecr:DescribeImages`, and `ecr:DescribeImageScanFindings`. No static AWS access
1227
+ key is used.
1228
+
1229
+ Dispatch only after the source commit is merged to `main`:
1230
+
1231
+ ```bash
1232
+ sha="$(git rev-parse origin/main)"
1233
+ gh workflow run ecr-candidate.yml --repo hasna/loops --ref main \
1234
+ -f source_sha="${sha}" -f confirmation="push ${sha}"
1235
+ ```
1236
+
1237
+ Dispatch is an AWS mutation and requires the applicable operator approval and
1238
+ fresh release preflight. Do not run it merely because the source workflow has
1239
+ merged. Review the workflow summary and retained evidence; promote only by
1240
+ the reported immutable digest through the separate deployment process.
@@ -1,5 +1,8 @@
1
1
  #!/usr/bin/env bun
2
+ import { type CircuitBreakerThreshold } from "../lib/advancement.js";
2
3
  import type { LoopStorageContract } from "../lib/storage/contract.js";
4
+ import { type RoutePolicy } from "../lib/auth/route-policy.js";
5
+ import type { TenantAuthContext, TenantAuthDecision } from "../lib/auth/tenant-auth.js";
3
6
  /** The serve OpenAPI document (source of the generated SDK), version-synced. */
4
7
  export declare function openApiDocument(): Record<string, unknown>;
5
8
  export declare function apiStatus(): {
@@ -7,23 +10,12 @@ export declare function apiStatus(): {
7
10
  service: string;
8
11
  status: import("../index.js").LoopDeploymentStatus;
9
12
  };
10
- /**
11
- * Framework-agnostic API-key verifier shape (matches
12
- * `@hasna/contracts/auth` `ApiKeyVerifier`). Kept structural so the api module
13
- * has no hard dependency on the auth package: the serve entry injects the real
14
- * verifier built from the vendored kit's client + the HMAC signing secret.
15
- */
16
13
  export interface ApiAuthenticator {
17
- authenticate(headers: Headers, context?: {
18
- method?: string | null;
19
- path?: string | null;
20
- requiredScopes?: readonly string[];
21
- }): Promise<{
22
- ok: boolean;
23
- status: number;
24
- reason?: string;
25
- message?: string;
26
- }>;
14
+ authenticate(headers: Headers, context: {
15
+ method: string;
16
+ path: string;
17
+ policy: RoutePolicy;
18
+ }): Promise<TenantAuthDecision>;
27
19
  }
28
20
  export interface LoopsApiServerOptions {
29
21
  host?: string;
@@ -33,21 +25,30 @@ export interface LoopsApiServerOptions {
33
25
  evidenceLimitBytes?: number;
34
26
  importLimitBytes?: number;
35
27
  now?: () => Date;
28
+ random?: () => number;
29
+ circuitBreakerThreshold?: CircuitBreakerThreshold;
36
30
  /**
37
31
  * API-key verifier (from `@hasna/contracts/auth`). When present, every
38
32
  * request outside the open foundation probes (`/health`, `/ready`,
39
- * `/version`, `/status`) must present a valid `loops:*` scoped key. This is
33
+ * `/version`, `/openapi.json`) must present a valid `loops:*` scoped key. This is
40
34
  * the internet-facing auth path (no bearer token, no loopback bypass).
41
35
  */
42
36
  authenticator?: ApiAuthenticator;
37
+ withTenantStorage?: <T>(principal: TenantAuthContext, fn: (storage: LoopStorageContract) => Promise<T>) => Promise<T>;
43
38
  /**
44
39
  * Readiness probe. Should prove the storage backend is reachable AND fully
45
- * migrated. Returns `{ ready, detail? }`. Defaults to a storage list probe.
40
+ * migrated. Returns a stable public code only. Defaults to a storage list probe.
46
41
  */
47
42
  readyCheck?: () => Promise<{
48
43
  ready: boolean;
49
- detail?: string;
44
+ code?: "storage_unconfigured" | "storage_unreachable" | "auth_unreachable" | "unsafe_database_role" | "pending_migrations" | "unknown_migrations" | "migration_checksum_mismatch";
50
45
  }>;
51
46
  }
47
+ export declare function contractHealthResponse(env?: Record<string, string | undefined>): {
48
+ status: "ok";
49
+ version: string;
50
+ mode: "local" | "cloud";
51
+ };
52
52
  export declare function createLoopsApiServer(opts?: LoopsApiServerOptions): Bun.Server<undefined>;
53
+ export declare function logApiCommandFailure(error: unknown): void;
53
54
  export declare function main(argv?: string[]): Promise<void>;