@hasna/loops 0.4.28 → 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 (77) hide show
  1. package/CHANGELOG.md +84 -1
  2. package/README.md +226 -49
  3. package/dist/api/index.d.ts +20 -19
  4. package/dist/api/index.js +6846 -1087
  5. package/dist/cli/index.js +2471 -885
  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 +1786 -493
  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 +4778 -1016
  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/format.d.ts +2 -2
  22. package/dist/lib/goal/runner.d.ts +36 -3
  23. package/dist/lib/health.d.ts +1 -1
  24. package/dist/lib/labels.d.ts +4 -0
  25. package/dist/lib/loop-status.d.ts +4 -0
  26. package/dist/lib/migration.d.ts +4 -17
  27. package/dist/lib/mode.d.ts +2 -5
  28. package/dist/lib/mode.js +27 -36
  29. package/dist/lib/route/index.d.ts +2 -2
  30. package/dist/lib/route/throttle.d.ts +7 -0
  31. package/dist/lib/route/types.d.ts +3 -0
  32. package/dist/lib/run-completion.d.ts +18 -0
  33. package/dist/lib/scheduler.d.ts +5 -11
  34. package/dist/lib/storage/contract.d.ts +15 -1
  35. package/dist/lib/storage/index.d.ts +1 -1
  36. package/dist/lib/storage/index.js +3864 -457
  37. package/dist/lib/storage/pg-executor.d.ts +10 -5
  38. package/dist/lib/storage/postgres-loop-storage.d.ts +50 -24
  39. package/dist/lib/storage/postgres-schema.d.ts +8 -0
  40. package/dist/lib/storage/postgres-schema.js +1323 -1
  41. package/dist/lib/storage/postgres.d.ts +3 -1
  42. package/dist/lib/storage/postgres.js +1353 -27
  43. package/dist/lib/storage/provider-credentials.d.ts +84 -0
  44. package/dist/lib/storage/shared-database-transfer.d.ts +136 -0
  45. package/dist/lib/storage/sqlite.d.ts +15 -2
  46. package/dist/lib/storage/sqlite.js +1299 -217
  47. package/dist/lib/storage/tenant-backfill-s3.d.ts +53 -0
  48. package/dist/lib/storage/tenant-backfill.d.ts +40 -0
  49. package/dist/lib/store/index.d.ts +11 -8
  50. package/dist/lib/store.d.ts +52 -7
  51. package/dist/lib/store.js +1266 -217
  52. package/dist/lib/templates.d.ts +11 -0
  53. package/dist/lib/workflow-events.d.ts +6 -0
  54. package/dist/lib/workflow-provenance.d.ts +8 -0
  55. package/dist/lib/workflow-runner.d.ts +52 -4
  56. package/dist/mcp/index.js +1961 -651
  57. package/dist/runner/index.d.ts +20 -2
  58. package/dist/runner/index.js +2113 -177
  59. package/dist/sdk/http.d.ts +211 -3
  60. package/dist/sdk/http.js +301 -0
  61. package/dist/sdk/index.d.ts +7 -2
  62. package/dist/sdk/index.js +1959 -711
  63. package/dist/serve/index.d.ts +14 -0
  64. package/dist/serve/index.js +12323 -1743
  65. package/dist/types.d.ts +64 -3
  66. package/docs/AUTOMATION_RUNTIME_DESIGN.md +32 -32
  67. package/docs/CUTOVER-RUNBOOK.md +158 -31
  68. package/docs/DEPLOYMENT_MODES.md +78 -56
  69. package/docs/RUNTIME_BOUNDARY.md +26 -26
  70. package/docs/SHARED-DATABASE-TRANSFER.md +95 -0
  71. package/docs/SHARED_KIT_EXTRACTION_INVENTORY.md +631 -0
  72. package/docs/TRANSCRIPT_LOOP_PATTERNS.md +3 -3
  73. package/docs/UNIFIED_PRODUCT_CONTRACT.md +365 -0
  74. package/docs/USAGE.md +143 -52
  75. package/docs/workflows/transcript-feedback-to-loops.json +2 -2
  76. package/package.json +7 -3
  77. package/dist/lib/storage/pg-runner-claim.d.ts +0 -40
package/CHANGELOG.md CHANGED
@@ -1,10 +1,83 @@
1
1
  # Changelog
2
2
 
3
- All notable changes to OpenLoops (npm `@hasna/loops`, repo `hasna/loops`) are
3
+ All notable changes to Loops (npm `@hasna/loops`, repo `hasna/loops`) are
4
4
  documented in this file. Version entries are generated from the
5
5
  conventional-commit git history; one commit maps to one released patch version
6
6
  unless noted.
7
7
 
8
+ ## 0.4.29 (2026-07-21)
9
+
10
+ This source release gives the post-`npm/loops/v0.4.28` code line a new,
11
+ unambiguous package, OpenAPI, and generated HTTP SDK identity.
12
+
13
+ ### Added
14
+
15
+ - **Tenant-bound authentication and PostgreSQL row isolation:** migrations
16
+ `0008_tenant_prepare`, `0009_tenant_backfill`, and
17
+ `0010_tenant_enforce` add tenant/principal/membership/API-key state, require
18
+ an explicit complete tenant assignment before enforcement, and install the
19
+ closed-world owner/migrator/runtime/authenticator role model with
20
+ tenant-scoped privileges and RLS. The API now validates tenant-bound keys,
21
+ roles, scopes, and token kinds and emits structured denial evidence.
22
+ - **Tenant migration operations:** encrypted tenant-backfill delivery,
23
+ provider credential reconciliation, PostgreSQL 16 bootstrap membership
24
+ gates, and protected shared-database transfer tooling now support rehearsed,
25
+ auditable cutovers without command overrides or unverified TLS targets.
26
+ - **Runner and workflow execution:** long-running runner readiness and workflow
27
+ execution are implemented, with operator recovery, replayable advancement,
28
+ workflow-definition provenance in `0011_workflow_run_provenance`, and loop
29
+ labels in `0012_loop_labels`.
30
+
31
+ ### Changed
32
+
33
+ - **Product identity:** user-facing product and documentation naming is now
34
+ **Loops**, while published compatibility identifiers and paths such as
35
+ the legacy uppercase environment-variable namespace, `open_loops`,
36
+ `.openloops`, `openloops`, `runtimeOwner`, MCP identifiers, project slugs,
37
+ and the `loops-api` compatibility binary/waiver remain stable.
38
+ - **Runtime and supply chain:** the runtime image now uses pinned Bun Alpine
39
+ packages, runs the runner as non-root, pins CI actions, adds a protected ECR
40
+ candidate workflow, and fails closed on high-severity or malformed scan
41
+ results. Packed artifacts are checked for private-host leakage, branding
42
+ regressions, storage-kit drift, and contract conformance.
43
+ - **Public contracts:** the OpenAPI document and generated HTTP SDK cover the
44
+ expanded tenant, runner, workflow, recovery, labels, count, archive, and
45
+ operational surfaces. MCP output is bounded, and public or persisted
46
+ workflow output is scrubbed before truncation.
47
+
48
+ ### Fixed
49
+
50
+ - **Agent boundaries:** Codewith auth profiles are resolved from exact
51
+ JSON-first inventory, unusable or malformed profiles and extra arguments
52
+ fail closed, session creation is atomic, root agent directories are rejected,
53
+ route agent allowlists propagate correctly, and Cursor provider/retry
54
+ handling no longer weakens trust boundaries.
55
+ - **API and claim safety:** tenant error compatibility is preserved while
56
+ runner endpoints, route admission, loop updates, runner clocks, local claims,
57
+ recovery snapshots, and persisted workflow/session contracts reject
58
+ ambiguous, stale, or cross-boundary state.
59
+ - **Archive and finalization:** ambiguous archive names fail closed and resolve
60
+ against target state; PostgreSQL finalization conflicts are enforced
61
+ directly; SQLite generated-route finalization matches PostgreSQL behavior;
62
+ and public CLI errors and pagination avoid leaking unsafe context.
63
+ - **Recovery and advancement:** workflow recovery ownership is fenced,
64
+ advancement is unified across schedulers, replay is idempotent, successor and
65
+ completion parity gaps are closed, and recovery cannot double-claim local
66
+ work or regenerate already-finalized routes.
67
+ - **Security boundaries:** private and encoded infrastructure hostnames are
68
+ removed from publishable output, encoded separator bypasses are rejected,
69
+ trusted contracts and legacy events are validated, and the shared transfer
70
+ gate accepts only verified TLS DSNs.
71
+
72
+ ### Upgrade warning
73
+
74
+ - **`0010_tenant_enforce` is a rollback boundary.** After this migration is
75
+ applied, the immutable published `0.4.28` image cannot pass readiness because
76
+ it does not contain the `0008`-`0010` tenant migration lineage. Do not roll
77
+ the binary back in place. Recovery requires rolling forward to `0.4.29` or a
78
+ later compatible image, or restoring the rehearsed pre-cutover database
79
+ target before starting the older image.
80
+
8
81
  ## 0.4.26
9
82
 
10
83
  ### Fixed
@@ -19,6 +92,16 @@ unless noted.
19
92
 
20
93
  ## Unreleased
21
94
 
95
+ ### Fixed
96
+
97
+ - **Codewith auth-profile preflight:** use JSON-first exact profile matching for
98
+ local and remote runs. Root `data`/`profiles` entries are authoritative:
99
+ `usable: false` is rejected, legacy entries without `usable` remain accepted,
100
+ and `currentProfile` does not add inventory membership. Human-table parsing,
101
+ including active `*` rows, is used only when JSON mode is unsupported or its
102
+ inventory is structurally unavailable. NUL-containing names, missing
103
+ profiles, and non-fallback profile-list failures fail closed.
104
+
22
105
  ### Added
23
106
 
24
107
  - `loops-mcp` now serves a shared Streamable HTTP transport (default
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,7 +77,9 @@ 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
 
@@ -94,9 +92,40 @@ preserve flags only for an intentional activation.
94
92
  See [Deployment Modes](docs/DEPLOYMENT_MODES.md) for the full package boundary
95
93
  and machine-placement contract.
96
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
+
97
126
  ## Install
98
127
 
99
- **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
100
129
  `loops`, `loops-daemon`, `loops-api`, `loops-serve`, `loops-runner`, and
101
130
  `loops-mcp` binaries run with a `#!/usr/bin/env bun` shebang, so Bun must be on
102
131
  `PATH` even when the package is installed with npm. Node.js is not a supported
@@ -132,7 +161,7 @@ The CLI stores state in `~/.hasna/loops` by default. Set `LOOPS_DATA_DIR` to iso
132
161
 
133
162
  ## MCP Server
134
163
 
135
- 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
136
165
  MCP-capable agents:
137
166
 
138
167
  ```bash
@@ -149,6 +178,7 @@ import { createLoopsMcpServer } from "@hasna/loops/mcp";
149
178
  Available read tools include `loops_list`, `loops_show`, `loops_runs`,
150
179
  `loops_doctor`, `loops_workflows_list`, `loops_workflow_read`, and
151
180
  `loops_workflow_validate`.
181
+ `loops_list` and `loops_runs` accept label arrays with AND semantics.
152
182
  Resources are available at `loops://runtime` and `loops://tools`.
153
183
  Those tools use the same `Store`, public redaction helpers, and workflow parser
154
184
  as the CLI and SDK, so read output and validation behavior stay aligned across
@@ -158,12 +188,16 @@ Mutation tools are disabled by default. Start the server with
158
188
  `LOOPS_MCP_ALLOW_MUTATIONS=true` only for a trusted local MCP host that should be
159
189
  allowed to change loop state. The guarded mutation tools use canonical names:
160
190
  `loops_pause`, `loops_resume`, `loops_stop`, `loops_run_now`, `loops_archive`,
161
- `loops_unarchive`, `loops_create_command`, and `loops_create_workflow`.
191
+ `loops_unarchive`, `loops_labels_update`, `loops_create_command`, and
192
+ `loops_create_workflow`.
162
193
  Deprecated `loop_*` aliases are still registered where compatibility needs them,
163
194
  but callers should use the `loops_*` names. Mutation tools do not require or
164
195
  accept confirmation-string parameters; the server-side environment opt-in is the
165
196
  gate. MCP `loops_run_now` schedules the loop for immediate daemon pickup; inline
166
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.
167
201
 
168
202
  Keep host-affecting or long-running operations on the CLI: daemon
169
203
  start/stop/install/logs, inline `run-now`, `tick`, loop deletion, workflow
@@ -223,8 +257,41 @@ loops create agent supply-chain-watch \
223
257
  --prompt "Check for suspicious dependency or supply-chain changes. Report only concrete findings."
224
258
  ```
225
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
+
226
293
  Run an OpenCode loop with an explicit provider/model. OpenCode reads
227
- `~/.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
228
295
  OpenCode agent targets without `--model` instead of inheriting a stale or
229
296
  machine-specific default.
230
297
 
@@ -237,6 +304,32 @@ loops create agent opencode-smoke \
237
304
  --prompt "Reply with exactly OK."
238
305
  ```
239
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
+
240
333
  For `codewith` and `aicopilot` account isolation, register matching OpenAccounts tools first if they are not built in on the machine:
241
334
 
242
335
  ```bash
@@ -280,7 +373,7 @@ resolve from the workflow JSON file's directory:
280
373
  }
281
374
  ```
282
375
 
283
- OpenLoops records `promptSource` metadata and redacts prompt bodies in public
376
+ Loops records `promptSource` metadata and redacts prompt bodies in public
284
377
  CLI output by default, including `templates render`. Use
285
378
  `~/.hasna/loops/prompts/<stable-name>.md` as the default prompt store for
286
379
  production loops.
@@ -291,7 +384,7 @@ non-secret routing/configuration data.
291
384
 
292
385
  ## Goals
293
386
 
294
- 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.
295
388
 
296
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.
297
390
 
@@ -342,7 +435,7 @@ Workflow JSON can also embed goals at the workflow or step level:
342
435
  For **workflow loops** (`loops create workflow ...`), prefer a single loop-level
343
436
  `--goal` wrapper around the scheduled workflow. Do not also define a top-level
344
437
  `"goal"` on the workflow JSON spec: nested loop-level and workflow-level goal
345
- wrappers deadlocked execution because each layer waited on the other. OpenLoops
438
+ wrappers deadlocked execution because each layer waited on the other. Loops
346
439
  now rejects new workflow loops that combine both wrappers at creation time, and
347
440
  blocks retargeting a loop-level goal loop onto a workflow that also carries a
348
441
  top-level goal.
@@ -421,7 +514,7 @@ Use `shell: true` only when you intentionally want shell parsing:
421
514
  ## Templates And Task Events
422
515
 
423
516
  Built-in templates turn common orchestration flows into reusable workflow JSON.
424
- Todos-task routes are OpenLoops-native runtime admission — see
517
+ Todos-task routes are Loops-native runtime admission — see
425
518
  [Runtime Boundary](docs/RUNTIME_BOUNDARY.md) for how this differs from
426
519
  OpenAutomations queue ownership.
427
520
  `todos-task-worker-verifier` performs one todos task and then verifies it.
@@ -477,7 +570,7 @@ loops templates render routing-remediation \
477
570
  --var idempotencyKey=routing-health:repo:shard0
478
571
  ```
479
572
 
480
- Custom reusable workflow templates live under the OpenLoops app data directory:
573
+ Custom reusable workflow templates live under the Loops app data directory:
481
574
  `~/.hasna/loops/templates` by default, or `$LOOPS_DATA_DIR/templates` when
482
575
  `LOOPS_DATA_DIR` is set. Store templates as declarative JSON files; listing,
483
576
  showing, and rendering templates never executes workflow steps or mutates the
@@ -581,6 +674,8 @@ arguments, and implicit Codewith/Codex full-access defaults. If a custom
581
674
  Codewith/Codex template uses `permissionMode: "bypass"`, it must also set
582
675
  `sandbox` to `workspace-write` or `read-only`. Use built-in templates with
583
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.
584
679
 
585
680
  For event-driven task automation, `loops routes create todos-task` reads a
586
681
  Hasna event envelope from stdin or `HASNA_EVENT_JSON`, records a
@@ -682,10 +777,10 @@ cat event.json | loops routes create generic \
682
777
  ```
683
778
 
684
779
  This is the intended deterministic-to-agentic path: a producer creates a todos
685
- task, `@hasna/events` delivers `task.created`, OpenLoops records the invocation
686
- 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,
687
782
  and the workflow updates todos with evidence. Use account pools so worker and
688
- 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
689
784
  spreads each route to the **least-loaded** pool account (counting running steps
690
785
  per account) and still keeps the verifier on a different profile than the worker;
691
786
  the deterministic hash is the tie-break, so a cold store reproduces the previous
@@ -698,9 +793,9 @@ Use `--dry-run` to inspect the rendered invocation, work item, workflow, and
698
793
  loop input without storing anything.
699
794
 
700
795
  Generated worker/verifier workflows fail closed when `sandbox=danger-full-access`
701
- is requested without `manualBreakGlass=true`. Use `workspace-write` for
702
- unattended task/event routes. Full access is an explicit manual emergency path,
703
- 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.
704
799
 
705
800
  When a sandboxed Codewith/Codex worker must update app stores outside the repo
706
801
  worktree, pass those stores explicitly with `--add-dir` or template `addDirs`.
@@ -792,7 +887,21 @@ anti-hog caps counted over all routes. Raise a router's `--max-active`
792
887
  deliberately once counting is per-route; keep `--max-per-profile` set so the
793
888
  extra concurrency spreads across subscription accounts.
794
889
 
795
- `--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
796
905
  items; they do not know whether Codewith is already at its live background-agent
797
906
  admission limit. Add `--provider-active-cap <n>` (or the Codewith-specific alias
798
907
  `--codewith-active-cap <n>`) to make the route run `codewith agent diagnostics
@@ -817,7 +926,7 @@ Workflow run manifests are written under
817
926
 
818
927
  ## OpenAutomations Runtime Binding
819
928
 
820
- OpenLoops executes automations that external compilers have materialized; it
929
+ Loops executes automations that external compilers have materialized; it
821
930
  does not own the automation product surface. See
822
931
  [Runtime Boundary](docs/RUNTIME_BOUNDARY.md) for the full ownership table,
823
932
  three handoff paths (claim-queue, planned `@hasna/actions` upsert-one-shot, and
@@ -839,7 +948,7 @@ automations queue complete <action-id> --runner open-loops:<worker-id>
839
948
  ```
840
949
 
841
950
  For explicit event workflow routing, OpenAutomations can export the normalized
842
- 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
843
952
  route:
844
953
 
845
954
  ```bash
@@ -847,13 +956,13 @@ automations --json webhooks event <route> --body-json '<json>' \
847
956
  | loops --json routes create generic
848
957
  ```
849
958
 
850
- This is not automation materialization in OpenLoops. It is an explicit
959
+ This is not automation materialization in Loops. It is an explicit
851
960
  event-envelope workflow handoff: OpenAutomations owns deterministic automation
852
- specs, webhook normalization, queue state, approvals, DLQ, and replay; OpenLoops
961
+ specs, webhook normalization, queue state, approvals, DLQ, and replay; Loops
853
962
  owns agent workflow invocation after the operator routes the envelope to
854
963
  `loops routes create generic`.
855
964
 
856
- 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
857
966
  transport alone, or replace the OpenAutomations queue with loop/workflow rows.
858
967
  When a loop or workflow is used for execution, keep `HASNA_AUTOMATIONS_DIR`
859
968
  pointing at the owning OpenAutomations data root and preserve the runner id in
@@ -862,7 +971,7 @@ completion/failure calls so OpenAutomations can enforce action leases.
862
971
  ### Planned Workflow Upsert SDK
863
972
 
864
973
  External compilers such as `@hasna/actions` and `@hasna/automations` should not
865
- write OpenLoops SQLite rows directly. The stable contract should be an
974
+ write Loops SQLite rows directly. The stable contract should be an
866
975
  idempotent CLI/SDK upsert that accepts a fully rendered one-shot workflow loop
867
976
  request and returns durable refs.
868
977
 
@@ -875,7 +984,7 @@ does not duplicate that spec.
875
984
  That design doc also defines the planned `@hasna/actions` target binding. The
876
985
  binding reuses `ActionManifest`, `ActionInvocation`, `ActionRunStatus`,
877
986
  `ActionQueueStatus`, action-owned idempotency keys, action audit events, and
878
- 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
879
988
  workflow, then returns workflow refs for action-owned evidence.
880
989
 
881
990
  The same design doc covers the planned DLQ/dead-letter lifecycle, including
@@ -889,7 +998,7 @@ work.
889
998
 
890
999
  ## Transcript-Driven Loops
891
1000
 
892
- 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.
893
1002
 
894
1003
  ```bash
895
1004
  loops workflows validate /path/to/repo/.openloops/transcript-feedback-to-loops.json --preflight
@@ -999,7 +1108,7 @@ Archived loops are hidden from the default `loops list`, excluded from daemon sc
999
1108
 
1000
1109
  `loops run-now` reports the manual run source:
1001
1110
 
1002
- - `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.
1003
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.
1004
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.
1005
1114
 
@@ -1048,16 +1157,84 @@ On Linux this writes a user systemd service. On macOS it writes a LaunchAgent pl
1048
1157
  The adapters intentionally use provider command surfaces instead of pretending every agent has one SDK:
1049
1158
 
1050
1159
  - Claude uses `claude -p --output-format json` and safe-mode/local setting sources by default.
1051
- - 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.
1052
1161
  - AI Copilot and OpenCode use `run --format json --pure`. OpenCode requires an explicit provider/model id because ambient OpenCode config is machine-specific.
1053
- - 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.
1054
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.
1055
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.
1056
- - 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.
1057
- - `--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.
1058
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`.
1059
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.
1060
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`.
1061
1185
  - Daemon and scheduled runs prepend common user executable directories such as `~/.local/bin` and `~/.bun/bin` before resolving provider CLIs.
1062
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
+
1063
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.