ace-lab 0.9.0 → 0.13.1

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 (80) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +399 -102
  3. data/README.md +66 -100
  4. data/docs/usage.md +563 -592
  5. data/lib/ace/lab/atoms/install_schema.rb +444 -0
  6. data/lib/ace/lab/cli/commands/install.rb +161 -0
  7. data/lib/ace/lab/cli/commands/prepare.rb +120 -0
  8. data/lib/ace/lab/cli/commands/project_add.rb +101 -0
  9. data/lib/ace/lab/cli/commands/status.rb +109 -0
  10. data/lib/ace/lab/cli/commands/verify.rb +115 -0
  11. data/lib/ace/lab/cli.rb +21 -23
  12. data/lib/ace/lab/install_manifest_path.rb +23 -0
  13. data/lib/ace/lab/models/status_report.rb +38 -0
  14. data/lib/ace/lab/molecules/account_provisioner.rb +196 -0
  15. data/lib/ace/lab/molecules/config_state_classifier.rb +145 -0
  16. data/lib/ace/lab/molecules/directory_provisioner.rb +208 -0
  17. data/lib/ace/lab/molecules/gemset_provisioner.rb +222 -0
  18. data/lib/ace/lab/molecules/grant_provisioner.rb +250 -0
  19. data/lib/ace/lab/molecules/grant_resolver.rb +34 -23
  20. data/lib/ace/lab/molecules/hitl_routes.rb +263 -0
  21. data/lib/ace/lab/molecules/install_manifest_loader.rb +64 -0
  22. data/lib/ace/lab/molecules/key_provisioner.rb +736 -0
  23. data/lib/ace/lab/molecules/lab_state_scanner.rb +55 -0
  24. data/lib/ace/lab/molecules/overseer_provisioner.rb +188 -0
  25. data/lib/ace/lab/molecules/project_registry.rb +251 -0
  26. data/lib/ace/lab/molecules/repository_provisioner.rb +141 -0
  27. data/lib/ace/lab/molecules/role_verifier.rb +223 -0
  28. data/lib/ace/lab/organisms/install.rb +218 -0
  29. data/lib/ace/lab/organisms/prepare.rb +170 -0
  30. data/lib/ace/lab/organisms/project_add.rb +170 -0
  31. data/lib/ace/lab/organisms/status_service.rb +70 -0
  32. data/lib/ace/lab/organisms/verify.rb +836 -0
  33. data/lib/ace/lab/version.rb +1 -1
  34. data/lib/ace/lab.rb +34 -6
  35. metadata +35 -108
  36. data/lib/ace/lab/atoms/protected_publication_input.rb +0 -50
  37. data/lib/ace/lab/atoms/protected_workspace_prune_input.rb +0 -114
  38. data/lib/ace/lab/atoms/protected_workspace_prune_preview.rb +0 -86
  39. data/lib/ace/lab/atoms/service_input.rb +0 -92
  40. data/lib/ace/lab/cli/commands/assignment.rb +0 -92
  41. data/lib/ace/lab/cli/commands/command_execute.rb +0 -28
  42. data/lib/ace/lab/cli/commands/run.rb +0 -65
  43. data/lib/ace/lab/cli/commands/service.rb +0 -240
  44. data/lib/ace/lab/models/service_receipt.rb +0 -18
  45. data/lib/ace/lab/molecules/command_owner_admission.rb +0 -183
  46. data/lib/ace/lab/molecules/command_policy.rb +0 -158
  47. data/lib/ace/lab/molecules/hitl_authorizer.rb +0 -63
  48. data/lib/ace/lab/molecules/ordinary_admission_resolver.rb +0 -131
  49. data/lib/ace/lab/molecules/ordinary_listener.rb +0 -262
  50. data/lib/ace/lab/molecules/ordinary_merge_read.rb +0 -45
  51. data/lib/ace/lab/molecules/ordinary_native_read.rb +0 -106
  52. data/lib/ace/lab/molecules/ordinary_owner_admission.rb +0 -152
  53. data/lib/ace/lab/molecules/ordinary_owner_config.rb +0 -232
  54. data/lib/ace/lab/molecules/ordinary_receiver_config.rb +0 -94
  55. data/lib/ace/lab/molecules/ordinary_socket.rb +0 -126
  56. data/lib/ace/lab/molecules/ordinary_worktree_read.rb +0 -216
  57. data/lib/ace/lab/molecules/protected_cleanup_owner_admission.rb +0 -163
  58. data/lib/ace/lab/molecules/protected_cleanup_owner_client.rb +0 -165
  59. data/lib/ace/lab/molecules/protected_cleanup_owner_identity.rb +0 -239
  60. data/lib/ace/lab/molecules/protected_publication_publisher.rb +0 -110
  61. data/lib/ace/lab/molecules/protected_service_handler.rb +0 -43
  62. data/lib/ace/lab/molecules/protected_service_policy.rb +0 -156
  63. data/lib/ace/lab/molecules/service_executor.rb +0 -99
  64. data/lib/ace/lab/molecules/service_policy.rb +0 -289
  65. data/lib/ace/lab/organisms/authority_composition.rb +0 -87
  66. data/lib/ace/lab/organisms/command_executor.rb +0 -220
  67. data/lib/ace/lab/organisms/command_run_client.rb +0 -131
  68. data/lib/ace/lab/organisms/ordinary_assignment_client.rb +0 -120
  69. data/lib/ace/lab/organisms/ordinary_authority_composition.rb +0 -153
  70. data/lib/ace/lab/organisms/ordinary_merge_receiver.rb +0 -679
  71. data/lib/ace/lab/organisms/ordinary_merge_request.rb +0 -456
  72. data/lib/ace/lab/organisms/protected_cleanup_owner.rb +0 -361
  73. data/lib/ace/lab/organisms/protected_service_client.rb +0 -129
  74. data/lib/ace/lab/organisms/protected_service_listener.rb +0 -181
  75. data/lib/ace/lab/organisms/protected_service_publication.rb +0 -268
  76. data/lib/ace/lab/organisms/protected_service_receiver.rb +0 -267
  77. data/lib/ace/lab/organisms/protected_service_recovery.rb +0 -185
  78. data/lib/ace/lab/organisms/protected_service_request.rb +0 -128
  79. data/lib/ace/lab/organisms/protected_service_worker.rb +0 -89
  80. data/lib/ace/lab/organisms/service_request_service.rb +0 -249
data/docs/usage.md CHANGED
@@ -1,637 +1,608 @@
1
1
  # ace-lab usage
2
2
 
3
- Topology, routing, and scoped service requests for the Lab. Resolves stable
4
- project, agent, and service IDs without transient pane IDs or shared service
5
- credentials.
3
+ `ace-lab` exposes read-only topology queries, the lab status snapshot, the
4
+ `prepare` phase of the lab installer (L8.1), `project add` project
5
+ onboarding (L8.2), the full installer `install` (L8.3, which also resumes
6
+ its own interrupted installation, L8.4), and the end-to-end readiness
7
+ verification `verify` (L8.5).
6
8
 
7
- ## Commands
9
+ The file-based ownership model is documented once in
10
+ [../../docs/lab-flow.md](../../docs/lab-flow.md).
8
11
 
9
- Topology commands accept `--format json` (the only supported format) and
10
- `-q/--quiet`. Service status accepts `--format json`; service request always
11
- emits JSON.
12
+ ## Topology
12
13
 
13
- ### `ace-lab projects`
14
-
15
- List projects visible to the verified caller.
14
+ Topology configuration is read from `~/.ace/lab/config.yml` or
15
+ `.ace/lab/config.yml` through the normal ACE configuration cascade. All
16
+ topology commands accept `--format json` (the only supported format) and
17
+ `-q/--quiet`.
16
18
 
17
- $ ace-lab projects --format json
18
- {"status":"ok","data":{"projects":[{"id":"atlas","label":"Atlas platform"},{"id":"borealis"}]}}
19
+ ```yaml
20
+ # .ace/lab/config.yml -- topology only
21
+ schema_version: 1
22
+ topology:
23
+ projects:
24
+ - { id: atlas, label: Atlas platform }
25
+ agents:
26
+ - id: atlas-planner
27
+ project: atlas
28
+ role: planner
29
+ capabilities: [planning]
30
+ services:
31
+ - id: atlas-search
32
+ project: atlas
33
+ capabilities: [search]
34
+ default_for: [search]
35
+ endpoint: { kind: http, url: "https://search.example.internal/query" }
36
+ ```
19
37
 
20
- A caller with no configured principal receives:
38
+ ### `ace-lab projects`
21
39
 
22
- {"status":"error","error":{"code":"unauthorized","message":"caller has no configured authorization for any lab project"}}
40
+ ```bash
41
+ ace-lab projects --format json
42
+ # {"status":"ok","data":{"projects":[{"id":"atlas","label":"Atlas platform"}]}}
43
+ ```
23
44
 
24
45
  ### `ace-lab agents --project PROJECT`
25
46
 
26
- List agents in one project.
27
-
28
- $ ace-lab agents --project atlas --format json
29
- {"status":"ok","data":{"agents":[{"id":"atlas-planner","project":"atlas","role":"planner","capabilities":["planning"],"binding":{"kind":"runtime","state":"available"}}]}}
47
+ ```bash
48
+ ace-lab agents --project atlas --format json
49
+ ```
30
50
 
31
- `binding.state` is `available` when the configured runtime binding is active
32
- and its instance identity matches the attestation; otherwise `stale`.
51
+ `binding.state` is `available` when the configured runtime binding is active;
52
+ otherwise `stale`.
33
53
 
34
54
  ### `ace-lab services --project PROJECT`
35
55
 
36
- List services in one project.
37
-
38
- $ ace-lab services --project atlas --format json
39
- {"status":"ok","data":{"services":[{"id":"atlas-search","project":"atlas","capabilities":["search"],"default_for":["search"],"binding":{"kind":"service","state":"available","endpoint":{"kind":"http","url":"https://search.example.internal:8443"}}}]}}
56
+ ```bash
57
+ ace-lab services --project atlas --format json
58
+ ```
40
59
 
41
- The endpoint is a sanitized identity: scheme, host, and explicit port only.
60
+ The endpoint in public output is a sanitized identity: scheme, host and an
61
+ explicit port only. Query parameters, userinfo, fragments and credentials are
62
+ never printed.
42
63
 
43
64
  ### `ace-lab resolve --id ID`
44
65
 
45
- Resolve one entry by exact stable ID.
46
-
47
- $ ace-lab resolve --id atlas-planner --format json
48
- {"status":"ok","data":{"entry":{"id":"atlas-planner","project":"atlas","role":"planner","capabilities":["planning"],"binding":{"kind":"runtime","state":"available"}}}}
66
+ ```bash
67
+ ace-lab resolve --id atlas-planner --format json
68
+ ```
49
69
 
50
- Labels are display values and never resolve: `resolve "Atlas platform"` is a
70
+ Labels are display values and never resolve: resolving `"Atlas platform"` is a
51
71
  `missing` error.
52
72
 
53
73
  ### `ace-lab route --project PROJECT --capability CAPABILITY`
54
74
 
55
- Select a configured capable service in the requested project.
56
-
57
- $ ace-lab route --project atlas --capability search --format json
58
- {"status":"ok","data":{"entry":{"id":"atlas-search","project":"atlas","capabilities":["search"],"default_for":["search"],"binding":{"kind":"service","state":"available","endpoint":{"kind":"http","url":"https://search.example.internal:8443"}}}}}
59
-
60
- Routing never invokes the service and never grants credentials.
61
-
62
- ### `ace-lab service request`
63
-
64
- **Goal:** Ask a configured service to perform one named operation for an
65
- active, task-attached assignment attempt. First create a JSON input file:
66
-
67
- ```json
68
- {"target":{"resource":"release/1.2.3","artifact_digest":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},"arguments":{"version":"1.2.3"}}
75
+ ```bash
76
+ ace-lab route --project atlas --capability search --format json
69
77
  ```
70
78
 
71
- Run a preview, then submit the exact request:
79
+ Routing selects a capable service in the requested project. It never invokes
80
+ the service and never grants credentials.
72
81
 
73
- ```sh
74
- ace-lab service request --project atlas --assignment A --attempt ATT \
75
- --operation publish --input release.json --authorization DECISION \
76
- --request-id REQUEST --dry-run
77
- ace-lab service request --project atlas --assignment A --attempt ATT \
78
- --operation publish --input release.json --authorization DECISION \
79
- --request-id REQUEST
80
- ```
82
+ ## Status
81
83
 
82
- The preview reports `outcome: accepted` with `dry_run: true` and the selected
83
- service ID, target, and candidate head. It creates no request claim or effect.
84
- The second command returns `succeeded`, `failed`, or `uncertain` with a
85
- non-secret receipt when the executor confirms an outcome. A missing response
86
- leaves `uncertain` in the assignment evidence ref; do not retry with a new ID
87
- until the external outcome is reconciled. Repeating the same ID and input
88
- returns its stored state without dispatching again. A changed binding under
89
- the same ID is a `conflict` error.
90
-
91
- Input must be a JSON object of at most 64 KiB. `target.resource` is a stable
92
- resource ID; `target.artifact_digest`, when supplied, is a SHA-256 hex digest.
93
- Credential, password, secret, token, private-key, authorization, and env keys
94
- are rejected recursively. Operation arguments remain structured JSON sent to
95
- the executor; no caller-provided shell command or argv is accepted.
96
-
97
- | Option | Purpose |
98
- |--------|---------|
99
- | `--project` | Project stable ID |
100
- | `--assignment` | Assignment ID |
101
- | `--attempt` | Active managed attempt ID |
102
- | `--operation` | Configured named operation |
103
- | `--input` | Structured JSON file |
104
- | `--authorization` | Exact decision or configured automation reference |
105
- | `--request-id` | Idempotency key; use the same ID for retries/status |
106
- | `--dry-run` | Validate and show scope without an effect |
107
-
108
- ### `ace-lab service status`
109
-
110
- ```sh
111
- ace-lab service status --request REQUEST --format json
112
- ```
84
+ ### `ace-lab status [--format table|json] [-q]`
113
85
 
114
- Expected output after a lost executor receipt:
115
-
116
- ```json
117
- {"status":"ok","data":{"request_id":"REQUEST","outcome":"uncertain","state":"uncertain"}}
86
+ ```bash
87
+ ace-lab status # readable table (default)
88
+ ace-lab status --format json # full report as JSON
118
89
  ```
119
90
 
120
- Only the original OS caller identity with project visibility can read the
121
- request. The authoritative state survives loss of the local assignment cache
122
- because it lives in the separate assignment evidence Git ref.
123
-
124
- ### `ace-lab service listen --mode ordinary-merge`
125
-
126
- The installed ordinary service owns the Assign, HITL and merge endpoints in one
127
- process. Start it as the configured service account:
128
-
129
- ```sh
130
- ace-lab service listen --mode ordinary-merge --project atlas \
131
- --config /etc/lab/ace-lab/atlas-receiver.yml
132
- ```
133
-
134
- The receiver configuration selects its existing forge, repository, principal and
135
- merge socket. `/etc/lab/ace-lab/assign-owners.yml` separately selects the canonical
136
- Assign journal and native execution owner. Both selections must agree with the
137
- ordinary project mapping in the trusted authorization document. `repo_path` is
138
- the merge candidate repository; it does not choose the execution journal.
139
-
140
- Each project entry in the root-installed assignment document contains:
141
-
142
- | Entry | Installed selection |
143
- |---|---|
144
- | `service` | Non-root owner UID/GID, assignment socket and private state root |
145
- | `journal` | Service-owned repository, execution ref and private evidence checkout |
146
- | `assignment_root`, `lifecycle_root` | Separate private prepared-input and exclusion directories |
147
- | `runtime_binary`, `native` | Installed executable and exact native server/socket generation from the installation receipt |
148
- | `worktrees` | Allowed worktree root and Git common directory |
149
- | `participants` | OS UID/GID/actor, native workspace and supervisor panes |
150
- | `hitl` | Private store root, distinct socket, dedicated group, trusted grants path and optional installed TLS authority reference |
151
-
152
- For example, the `hitl` entry inside an already selected project can be:
153
-
154
- ```yaml
155
- hitl:
156
- root: /var/lib/lab/assign/atlas/hitl
157
- socket_path: /run/lab/assign/atlas/hitl.sock
158
- group: atlas-lifecycle
159
- grants_path: /etc/lab/ace-lab/authorization.yml
91
+ `status` is read-only local observability (task 8xg.t.sbz.6.0): it reads the
92
+ ADR-022 lab configuration cascade and the file-based state-owner locations
93
+ (ADR-034) — nothing is started, written, or installed, and there is no
94
+ authorization gate. Live terminal layout belongs to `ace-herdr status`; the
95
+ enriched worktree view with task associations is `ace-git-worktree list`
96
+ (delivered by k9y) — `status` shows only presence/count for worktrees and does
97
+ not duplicate that surface.
98
+
99
+ Lab configuration is reported in three distinct states — they are never
100
+ conflated:
101
+
102
+ - `configured` — at least one overlay (`.ace/lab/config.yml` or
103
+ `~/.ace/lab/config.yml`) present and valid, with topology counts
104
+ (0/0/0 is a *valid empty set*).
105
+ - `missing` — defaults only: no overlay exists. Informational; exit code 0.
106
+ Never rendered as a ready-but-empty lab.
107
+ - `invalid` — an unparsable/non-mapping document or a schema violation;
108
+ carries the path-bearing message.
109
+
110
+ Runtime *configuration* is echoed (project/user `runtime/config.yml`, then the
111
+ `ACE_RUNTIME` environment variable) with its value and origin. No runtime
112
+ detection and no `ace-runtime` dependency: runtime availability is
113
+ `ace-herdr status`'s dimension.
114
+
115
+ Local state covers `.ace-tasks/`, `.ace-tasks/_overseer/` (presence only —
116
+ private, never enumerated), `.ace-local/assign/`, `.ace-local/hitl/`,
117
+ `.ace-wt/`, and `.ace-local/coworker/` (presence only — reserved L3). Absence
118
+ of overseer or coworker state is information, not a failure.
119
+
120
+ Exit codes: 0 for `configured` and `missing` (empty or not); a source error —
121
+ invalid configuration or an unreadable location — prints the **full report
122
+ first**, then fails non-zero with the error, so available results stay visible
123
+ next to the failure.
124
+
125
+ ## Prepare (L8.1)
126
+
127
+ ```bash
128
+ ace-lab prepare # table: one line per manifest item
129
+ ace-lab prepare --dry-run # predict (would_*) without mutating anything
130
+ ace-lab prepare --format json
160
131
  ```
161
132
 
162
- The store must be a separate child of that project's `service.state_root`; the
163
- installer creates `service.state_root` with mode `0700`, the HITL root with its
164
- maintained mode `0711`, and socket access before startup. The private parent keeps
165
- the HITL tree inaccessible to other accounts; Store maintains private request and
166
- secret subdirectories. Startup and restart validate the same layout.
167
- `hitl.service_uid` in the grants document must equal the Assign owner UID. Native
168
- identities come from the installed receipt and live observation, not the project
169
- configuration cascade. HITL requests, decisions and merge claims use the same
170
- canonical Assign journal and lifecycle exclusion.
171
-
172
- For the existing remote Hermes transport, install its project-scoped mTLS
173
- authority and add its absolute configuration path to this same `hitl` entry:
133
+ `prepare` reads the trusted install manifest from the fixed deployment
134
+ path `/etc/lab/ace-lab/install.yml` and makes the machine match it,
135
+ idempotently, in a fixed section order: accounts → directories → keys →
136
+ roles. There is deliberately no `--manifest` flag: a caller-selected
137
+ manifest could manufacture privileges (accounts, directories, keys), so
138
+ the manifest is a root-owned deployment document verified through the
139
+ same fail-closed traversal as the authorization file (every element on
140
+ the path root-owned and not group/world-writable, `O_NOFOLLOW` on the
141
+ final component). It is never edited through the `.ace/` cascade and is
142
+ not task state (ADR-034): nothing outside the manifest is touched, and
143
+ no progress journal is written — re-running `prepare` simply converges.
174
144
 
175
145
  ```yaml
176
- tls_authority: /etc/lab/ace-hitl/atlas-tls.json
177
- ```
178
-
179
- The referenced file uses the maintained `ace.hitl.tls.authority/v1` schema and
180
- credential provisioning layout from `ace-hitl`. Its project must match this
181
- owner; `service_user` and `service_group` must resolve to the owner's actual UID
182
- and primary GID. The dedicated Unix control group remains `hitl.group`. Root
183
- installs the credential references and Incus TCP proxy; callers cannot choose
184
- them. Invalid ownership, identity, project or TLS material prevents startup.
185
-
186
- The TLS listener runs in this same service process and shares its exact HITL
187
- binding, store, decision policy and in-memory one-time OTP vault. Hermes uses its
188
- verified project certificate as a remote transport identity; it does not need a
189
- synthetic Unix transport UID. Keep `transport_uids` empty when no local transport
190
- is installed. Configure the actual local proposer separately with
191
- `proposal_uids` and its project grant. A proposer grant never authorizes replies
192
- or delegates silence-based decisions. Restart the composed service after changing
193
- its TLS authority or credential selection, retaining the canonical journal.
194
-
195
- All selected endpoints must become ready before assignment ingress opens. A
196
- failed endpoint shuts down the complete owner. A stale assignment endpoint needs
197
- installer inspection; startup does not remove it or recover merge effects first.
198
-
199
- ## Ordinary assignment client
200
-
201
- `ace-lab assignment` selects the root-installed project owner and uses the
202
- existing Assign protocol. Assign remains the only accepted execution journal.
203
- Preparation captures reviewed task context and the existing managed assignment
204
- graph; it does not start a worker or register accepted work.
205
-
206
- ```sh
207
- ace-lab assignment prepare --project atlas --task 8wr.t.abc --pane w1:pS \
208
- --output .ace-local/prepared-task
209
- ace-lab assignment request --project atlas --input .ace-local/prepared-task/request.json
146
+ # /etc/lab/ace-lab/install.yml — root-owned deployment document
147
+ schema_version: 1 # required, must be 1
148
+ machine: lab01 # required identity (role/HITL addressing)
149
+ accounts: # optional
150
+ - username: ace-worker # ^[a-z_][a-z0-9_-]{0,31}$, unique
151
+ role: worker # informational binding
152
+ create: false # default; true requires administrator privileges
153
+ home: /home/ace-worker # default: /Users/<u> (darwin), /home/<u> (linux)
154
+ directories: # optional
155
+ - path: /home/ace-overseer/labs # absolute
156
+ owner: ace-overseer # optional; omitted = installer ownership
157
+ mode: "0750" # optional octal string, default "0755"
158
+ keys: # optional, ed25519 only (v1)
159
+ - account: ace-worker # must reference a declared account
160
+ path: /home/ace-worker/.ssh/id_ed25519 # optional, default <home>/.ssh/id_ed25519
161
+ roles: # optional
162
+ - id: worker # ^[a-z][a-z0-9_-]{0,31}$, unique
163
+ account: ace-worker
164
+ instructions: wfi://assign/work # wfi://|prompt://|tmpl:// or an absolute readable file
165
+ projects: # optional (consumed by ace-lab install)
166
+ - id: atlas # ^[a-z][a-z0-9_-]{0,31}$, unique
167
+ path: /Users/ace/lab/atlas # absolute, unique
168
+ remote: git@forge:atlas/atlas.git # optional clone source
169
+ label: Atlas platform # optional display label
170
+ grants: # optional: trusted-tier principal grants
171
+ ace-overseer: # principal → declared project ids
172
+ - atlas
173
+ gemset: # optional (gad.2): target runtime + required gem set
174
+ gem_home: /opt/lab/gems # absolute; converged as GEM_HOME and GEM_PATH
175
+ gems: # exact versions, unique names
176
+ - name: ace-lab
177
+ version: 0.13.0 # must be an exact gem version (no ranges)
210
178
  ```
211
179
 
212
- The output directory must not exist. Preparation retains the definition, bundle,
213
- and registration request with one stable mutation ID. `--dependency-reports FILE`
214
- selects an explicit JSON array of `{task_id, assignment_id, number}` entries for
215
- dependency reports. A failed preparation keeps partial files for inspection;
216
- it never rebuilds an existing directory. In this source checkout use
217
- `bundle exec ace-lab` because no `bin/ace-lab` binstub is committed.
218
-
219
- Every request file has exactly these fields:
220
-
221
- ```json
222
- {
223
- "version": 1,
224
- "project_id": "atlas",
225
- "operation": "start_attempt",
226
- "mutation_id": "original-start-001",
227
- "params": {
228
- "assignment_id": "assignment-id",
229
- "attempt_id": "attempt-id",
230
- "expected_generation": 0,
231
- "scope": "010",
232
- "head": "0123456789012345678901234567890123456789",
233
- "pane": "w1:p2"
234
- },
235
- "parts": []
236
- }
180
+ Statuses: accounts `created|exists|would_create|failed`; directories
181
+ `created|exists|mode_fixed|would_create|would_fix_mode|failed`; keys
182
+ `created|exists|would_create|would_repair_public|would_fix_mode|failed`
183
+ (with the public `fingerprint`); roles `verified|failed`; gems
184
+ `installed|updated|exists|would_install|would_update|failed` (one item
185
+ per declared gem). `ok` in the
186
+ JSON report means zero failed items. Key/role items whose account
187
+ failed are reported as dependency failures instead of being attempted;
188
+ gem items fail as dependencies when the declared runtime did not
189
+ prepare.
190
+
191
+ `--dry-run` is a faithful prediction of the wet run (`would_*`
192
+ statuses, zero mutations): keys and roles of an account the account
193
+ phase *would create* are predicted (`would_create` keys, `verified`
194
+ roles) even though the account does not exist in passwd yet; an
195
+ existing private key with a missing `.pub` predicts
196
+ `would_repair_public` (the wet run regenerates it); mode drift on an
197
+ owner-matching pair predicts `would_fix_mode`; a planted `.ssh`
198
+ symlink is refused in dry-run exactly as the wet run refuses it, and a
199
+ missing directory whose declared owner account *failed* the account
200
+ phase is predicted as the wet run's owner-unavailable failure (the
201
+ orchestrator threads its computed account set into the directory
202
+ stage). Four prediction limits: an *existing* directory whose declared
203
+ owner account does not exist yet cannot have its ownership checked,
204
+ and is reported as a failed item with a rerun hint; likewise, a role's
205
+ instruction *file* readability cannot be checked against an account
206
+ that does not exist yet (its uid is unknown), so such bindings are
207
+ predicted `verified` from the file's existence and type alone — the
208
+ wet run performs the full readability check; and an *existing key
209
+ pair* under an account the phase would create cannot have its
210
+ ownership checked either (the future uid is unknown) — a pair owned
211
+ by neither the installer nor the to-be-created account is
212
+ conservatively predicted as the wet run's takeover refusal, which is
213
+ pessimistic only in the pathological case where account creation
214
+ reuses exactly that uid. A fourth, smaller limit: when the account
215
+ phase *would create* the account, an existing installer-owned pair (or
216
+ installer-owned `.ssh`) is predicted as a plain `exists` even though the
217
+ wet run would chown it to the account while converging — the transfer
218
+ itself is not predicted.
219
+
220
+ Behavior and guarantees:
221
+
222
+ - **Idempotent convergence.** Existing resources are verified, not
223
+ recreated; only mode drift on an owner-matching directory or
224
+ owner-matching key pair is repaired. A pre-existing directory — or
225
+ key pair — owned by a different account is refused (`refusing
226
+ takeover`), never silently chowned, and that policy covers `.ssh`
227
+ itself: a pre-existing `.ssh` owned by neither the account nor the
228
+ installer is refused on both the create and the existing-key path
229
+ (wet and dry-run). Symlinks and
230
+ non-directories at declared paths are refused. Residual policy:
231
+ symlink refusal applies to the *declared leaf* path only —
232
+ intermediate path components are followed (and created with default
233
+ mode/ownership, since only the leaf carries declared values); the
234
+ manifest is root-owned, so this trusts the deployment document, not
235
+ the account.
236
+ - **Account creation** uses the platform's own tooling (darwin
237
+ `dscl . -create` + `createhomedir`, linux `useradd -m`), always with
238
+ argv arrays. Unsupported platforms are reported with a manual remedy
239
+ (`create: false`). Missing administrator privileges are classified
240
+ with an actionable message, as is a missing account without
241
+ `create: true`. For an *existing* account whose passwd home differs
242
+ from the manifest home, the drift is reported in the message; the
243
+ manifest home stays authoritative (key default paths derive from
244
+ it).
245
+ - **Secret hygiene.** Key pairs are generated with `ssh-keygen -t
246
+ ed25519`; `.ssh` is 0700, the private key 0600, the public key 0644,
247
+ ownership assigned to the account — and `.ssh` mode/ownership is
248
+ *converged* on the existing-key path too (drift predicts
249
+ `would_fix_mode` in dry-run and is repaired in the wet run; a
250
+ foreign-owned `.ssh` is a takeover refusal, see above). A pair is
251
+ ready only when the `.pub` fingerprint matches the public key
252
+ derived from the private key (`ssh-keygen -y`); unreadable or
253
+ mismatched pairs are `failed` items, never usable-looking
254
+ `exists`/`created` with no fingerprint — the unknown pair itself is
255
+ left untouched for manual resolution. Reports
256
+ carry the public
257
+ fingerprint only — private key material never appears in any status,
258
+ message, report, or diagnostic argument (the derived public key
259
+ exists only inside a private tmpdir for the duration of one
260
+ read-only comparison). Key paths inside a git
261
+ worktree are refused outright — judged by their PHYSICAL location
262
+ (existing ancestors resolved with `realpath`), so a symlinked
263
+ ancestor cannot smuggle a key into a repository. Planted symlinks on the
264
+ account-controlled paths — `.ssh` (checked with `lstat` before any
265
+ `mkdir`/chmod/chown, on the *existing-key* path and in dry-run too)
266
+ and `<key>.pub` (checked with `lstat` — including *before*
267
+ `ssh-keygen` on the create path, since keygen writes the `.pub` by
268
+ path — and written with `O_NOFOLLOW`) —
269
+ are refused, so a privileged run cannot be turned into an arbitrary
270
+ chmod/chown/write by the account.
271
+ - **Symlink-race hardening.** Leaf directories (`.ssh` and declared
272
+ directories) are created with component-wise `Dir.mkdir` — whose
273
+ `EEXIST` on a planted symlink refuses instead of following it — and
274
+ re-`lstat`ed immediately after creation: a leaf that is not a real
275
+ directory the installer just created is refused before any
276
+ chmod/chown, and a directory swapped between the owner check and a
277
+ mode fix is re-checked before the chmod. Key *files* never carry a
278
+ path-based mutation: after `ssh-keygen` the pair is re-`lstat`ed
279
+ (a keygen writing through a swapped-in symlink is refused before any
280
+ ownership transfer), and every chmod/chown on the private key and
281
+ the `.pub` — regenerated or not — goes through an
282
+ `O_NOFOLLOW`-opened file handle, so a mid-flight symlink swap
283
+ surfaces as `ELOOP` and is refused. On the create path the `.ssh`
284
+ ownership transfer is *deferred* until after the post-keygen
285
+ verification: while `ssh-keygen` writes by path, a freshly created
286
+ `.ssh` is still installer-owned 0700 and unwritable by the account,
287
+ so nothing can be planted inside it during the keygen window.
288
+ Accepted residuals: a microscopic
289
+ window remains between that final re-`lstat` and the path-based
290
+ `chmod`/`chown` on a *directory* leaf (Ruby stdlib offers no fd-based
291
+ directory chmod); closing it would require exotic APIs for no
292
+ practical gain over the guard chain above — and `ssh-keygen`
293
+ necessarily writes by path, so the keygen step itself retains the
294
+ *account-owned-home rename race*: an account that already owns the
295
+ parent home directory can rename the installer-owned `.ssh`
296
+ mid-flight and substitute its own (likewise, an `.ssh` that
297
+ pre-exists account-owned stays writable by the account throughout).
298
+ The post-keygen re-`lstat` then detects the substitution after the
299
+ write and refuses before any ownership transfer; whatever was
300
+ written lands root-owned 0600 — a file-creation primitive at an
301
+ attacker-chosen location, not an escalation, and inside the same
302
+ accepted-threat envelope as the directory residual above.
303
+ - **Role instruction reachability.** A file instruction source is
304
+ `verified` only when the account can actually reach it: the file's
305
+ read bits AND the search (`x`) bit of every ancestor directory of
306
+ the resolved path (ancestor symlinks followed) must grant the
307
+ account's governing POSIX class — owner, primary or supplementary
308
+ group, or other. A readable file behind an unsearchable directory
309
+ (e.g. 0644 inside a 0700 directory owned by someone else) is
310
+ `failed`, not `verified`. ACLs remain out of scope.
311
+ - **Gem-set convergence (gad.2).** When the manifest declares a
312
+ `gemset`, prepare converges the declared gem set into the declared
313
+ target runtime (`gem_home`): a gem whose exact declared version is
314
+ already present is a no-op (`exists`), a missing gem is installed
315
+ (`installed`), a gem present at a different version is updated (the
316
+ declared version is installed; RubyGems keeps versions side by side
317
+ and verify judges the declared set). The `gem` subprocesses run with
318
+ `GEM_HOME`/`GEM_PATH` pinned to the runtime (through the POSIX `env`
319
+ command, argv arrays only) and with the invoking process's bundler
320
+ context stripped (`RUBYOPT`, `RUBYLIB`, `BUNDLE_*`/`BUNDLER_*`), so
321
+ a `bundle exec ace-lab prepare` converges the LAB runtime, never its
322
+ own bundle. The runtime directory is created only with
323
+ administrator privileges; a symlink or non-directory at `gem_home`
324
+ is refused (takeover posture). Installs fetch from the `gem`
325
+ command's configured sources (provisioning network access, like
326
+ cloning a declared remote) with `--no-document`. `--dry-run` is a
327
+ faithful prediction: the read-only probe runs, statuses are
328
+ `would_install`/`would_update`/`exists`, nothing is created or
329
+ installed, and a runtime that would need privileges predicts the wet
330
+ failure.
331
+ - **Exit codes.** A wet run with failed items prints the report, then
332
+ exits non-zero enumerating each `<section>/<item>: <message>`;
333
+ `--dry-run` exits 0 whenever a report renders (conflicts appear as
334
+ predicted failures). A missing, untrusted or invalid manifest is a
335
+ classified error naming the path, never its content.
336
+
337
+ Boundaries (what `prepare` is not):
338
+
339
+ - No project onboarding in `prepare` itself (`ace-lab project add`,
340
+ L8.2, is the dedicated command) — the manifest's role catalog
341
+ declares where each role lives and under which account; the
342
+ project-add step consumes that converged environment.
343
+ - No full `ace-lab install` orchestration (L8.3, delivered as
344
+ `ace-lab install`): no project onboarding, no trusted-tier grants
345
+ configuration, no foreign-lab refusal — `prepare` converges the
346
+ machine and leaves the composition to the installer.
347
+ - No interrupted-install resume bookkeeping (L8.4): re-running `prepare`
348
+ is the resume mechanism; install-level resumption composes it.
349
+ - No end-to-end verification under the target accounts (L8.5): real
350
+ multi-account proof belongs to the readiness verification step.
351
+
352
+ ## Project add (L8.2)
353
+
354
+ ```bash
355
+ ace-lab project add atlas --path /Users/ace/lab/atlas --remote git@forge:atlas/atlas.git
356
+ ace-lab project add atlas --path /Users/ace/lab/atlas --dry-run
357
+ ace-lab project add atlas --path /Users/ace/lab/atlas --format json
237
358
  ```
238
359
 
239
- Use actual retained IDs, scope, head and native pane. The actual worker executes
240
- its start request: the owner verifies caller ancestry, live process and checkout.
241
- A supervisor cannot impersonate another pane by changing `pane`. A new terminal
242
- or prompt acknowledgement alone is not running proof.
243
-
244
- | Operation | `params` beyond `assignment_id` | Transfer / authority |
245
- |---|---|---|
246
- | `register_assignment` | Generated by prepare | Exact prepared bundle; supervisor |
247
- | `registration_status` | None | Read; `mutation_id: null` |
248
- | `start_attempt` | `attempt_id`, `expected_generation: 0`, `scope`, `head`, `pane` | Actual worker start |
249
- | `assignment_snapshot` | `attempt_id` (or null), `kind: queue` or `prepared_work` | Download; `mutation_id: null` |
250
- | `queue_start` | `attempt_id`, `expected_generation`, `pane`, `step` (or null for next) | Original worker |
251
- | `queue_finish` | Same as queue_start | One UTF-8 report part |
252
- | `queue_fail` | `attempt_id`, `expected_generation`, `pane`, `message` | Original worker; unfinished work stays failed |
253
- | `submit_result` | `attempt_id`, `expected_generation`, `pane`, `head`, `receipt_sha256` | Receipt first, then its ordered artifacts; original worker |
254
- | `result_status` | `attempt_id` | Read; `mutation_id: null` |
255
- | `result_snapshot` | `attempt_id`, `result_id` | Canonical receipt/artifacts download; `mutation_id: null` |
256
- | `finish` | `attempt_id`, `expected_generation`, `pane`, `head`, `result_id`, `receipt_digest` | Supervisor accepts exact submitted result after settlement |
257
-
258
- Upload `parts` are ordered `{file, bytes, sha256}` entries. Relative files resolve
259
- beside the retained request. Changed, oversized or symlinked inputs refuse before
260
- connection. Requesters never supply a wire `transfer` descriptor. Downloads require
261
- `--output NEW_DIRECTORY`; numbered byte files and `response.json` are private
262
- projections of the authenticated reply, not another execution store. For queue
263
- snapshots, the first byte file contains the current step instructions and reports
264
- as JSON; prepared-work snapshots contain the original Git bundle.
265
-
266
- Retain each request and its exact parts before invoking it. On response loss,
267
- inspect `registration_status` or `result_status` and reuse the same original
268
- mutation when replay is appropriate. Never refresh its generation, replace its
269
- bytes, or create a second worker because a reply was lost. Queue completion and
270
- `submit_result` keep the attempt running; only the canonical accepted `finish`
271
- reply proves terminal completion. Unknown effect or Inbox settlement refuses it.
272
-
273
- These commands do not silently route to a local `ace-assign` cache. Missing or
274
- changed installed owner selection is an explicit failure. Native wake consumers
275
- must read the same canonical status and adopt its original attempt/result IDs.
276
- Changed owner selection refuses new admission and dispatch until a controlled
277
- restart. Existing canonical receipts remain the source for reconciliation.
278
-
279
- ## Ordinary candidate, review and merge evidence
280
-
281
- Keep the real delivery attempt running through review, test execution and merge.
282
- Finish it only after settlement. A completed coder attempt can seed a new real
283
- registered delivery attempt; never reopen its old authority.
284
-
285
- The existing `assignment request --input request.json` interface also supports:
286
-
287
- | Operation | Params in addition to assignment_id/attempt_id/head/candidate_generation | Authority / transfer |
288
- |---|---|---|
289
- | `submit_candidate` | `pane`, `expected_generation` | Original worker; one complete Git bundle; candidate_generation is the previous generation (0 initially) |
290
- | `assign_review` | `pane`, `expected_generation`, `reviewer_assignment_id`, `reviewer_attempt_id` | Current canonical supervisor selects an already started independent reviewer |
291
- | `export_candidate` | `pane`, `review_id` | Assigned reviewer; null mutation; `--output NEW_DIRECTORY` downloads canonical bundle |
292
- | `accept_review` | `pane`, `expected_generation`, `review_id`, `receipt_sha256` | Assigned reviewer uploads exact receipt followed by its ordered report artifacts |
293
- | `cancel_review` | `pane`, `expected_generation`, `review_id` | Current supervisor revokes that exact review |
294
- | `review_status` | None | Null mutation; revalidates accepted original receipt and artifacts |
295
- | `merge_evidence` | `test_execution_ids` (nonempty array) | Null mutation; projects exact accepted review and canonical executed-test references |
296
-
297
- First submit the candidate, retaining its returned head, tree and new generation.
298
- Prepare/register/start a separate review assignment through the ordinary native
299
- entry. Its agent must have a different actual process generation, session UUID
300
- and pane. The supervisor assigns that exact reviewer assignment/attempt at its
301
- honestly observed starting base. Export gives the assigned agent canonical candidate
302
- bytes; it then materializes/checks out the candidate. Receipt acceptance requires
303
- that exact candidate HEAD and the unchanged original native reviewer binding. A receipt uses the delivery attempt's identifiers,
304
- scope and actual producer; its `review.reviewer.actor` is the real reviewer's
305
- account. The owner derives independence from authenticated native origins, so
306
- shared-account agents do not need invented actor aliases.
307
-
308
- `accept_review` retains the original receipt and up to 15 report artifacts in
309
- CanonicalEvidence, verifies their exact digests, and returns the accepted review
310
- ID/digest. The receipt declares operation review, succeeded, approved at the exact
311
- candidate head, and executed review checks. These review checks are not test
312
- execution authority. Missing/corrupt reports, a different caller, terminal delivery
313
- attempt, changed head/generation or cancellation refuse. Retrying the exact
314
- accepted mutation cannot accept a second review; changed bytes under that mutation
315
- refuse. This first ingress accepts direct independent review of the exact candidate
316
- generation. Campaign receipts and accepted-head carry are deliberately unavailable
317
- until the existing ace-review campaign/assessment owner is connected; a head change
318
- currently refuses prior approval. This is a remaining delivery capability, not the
319
- final policy of requiring a full independent review after every SHA change.
320
-
321
- Execute the project's root-selected test command through the existing command
322
- transport. Only an explicit `purpose: test` definition with exact immutable
323
- candidate head/tree, root-witnessed child, and canonical exit0/no signal can supply
324
- test evidence. `merge_evidence` resolves those execution IDs and returns
325
- `review_evidence` and `test_evidence` for the fixed merge request. Copy those exact
326
- projections into the retained request before obtaining its digest-bound decision.
327
- Plain worker check strings or generic allowed commands are not substitutes.
328
-
329
- The sole owner revalidates evidence in the merge admission CAS and before each
330
- Forge effect. Assignment exclusion prevents candidate changes, review cancellation
331
- or finish racing an executing merge. Policy revocation or missing canonical bytes
332
- refuses without dispatch. Existing unknown outcomes retain their original merge
333
- reference and require reconciliation.
334
-
335
- The dynamic candidate-to-immutable-command-view binding and installed two-worker
336
- acceptance remain separate required gates. Static source fixtures do not authorize
337
- per-PR administrative head/path edits or claim installed autonomous merging.
338
-
339
- ## Ordinary driver and supervisor recovery
340
-
341
- `ace-overseer ordinary --operation prepare` retains one context per fresh worker.
342
- From that worker's native tool process, drive only the installed owner:
343
-
344
- ```sh
345
- ace-lab assignment drive --project atlas --context /private/work/context.json --operation status
346
- ace-lab assignment drive --project atlas --context /private/work/context.json --operation start
347
- ace-lab assignment drive --project atlas --context /private/work/context.json --operation bootstrap --output /private/work/accepted-context
348
- ace-lab assignment drive --project atlas --context /private/work/context.json --operation next
349
- ace-lab assignment drive --project atlas --context /private/work/context.json --operation step-start --generation 1 --step 010.01.01
350
- ace-lab assignment drive --project atlas --context /private/work/context.json --operation step-finish --generation 2 --step 010.01.01 --report /private/work/check.md
360
+ `project add` makes a project ready for team work (task 8xg.t.sbz.7.1),
361
+ assuming `ace-lab prepare` (L8.1) already converged the machine-level
362
+ accounts, directories, keys and roles. Three steps run in a fixed order:
363
+
364
+ 1. **repository** — the target path becomes a Git repository with the
365
+ declared origin remote. A missing or empty path is cloned from
366
+ `--remote`; an existing repository without an origin gets one; an
367
+ existing repository with a *different* origin is refused as a
368
+ contradictory registration, and a non-empty non-repository path is
369
+ refused as a foreign location — never taken over, never overwritten.
370
+ The path itself must be the repository toplevel: a directory nested
371
+ inside another repository's work tree is refused as foreign.
372
+ 2. **overseer** — the private, untracked `.ace-tasks/_overseer/` folder
373
+ is created (ADR-034) and its `.gitignore` entry ensured. Existing
374
+ `.gitignore` content and folder content are never touched; the
375
+ folder is never enumerated.
376
+ 3. **registry** — the project is registered in `<path>/.ace/lab/config.yml`
377
+ (the project tier of the ordinary ADR-022 lab-config cascade) with
378
+ its `id`, `label`, absolute `path` and `remote`. Because every
379
+ consumer already reads that cascade, `ace-lab status` shows the
380
+ project (`configured`, topology counts) and `ace-lab projects` /
381
+ `ace-overseer projects` list it — no second registry exists, and
382
+ `ace-overseer` needed no read-side change. Two prerequisites for
383
+ that listing: grants come exclusively from the trusted tier of the
384
+ cascade, so listing the new project id requires a trusted-tier
385
+ principal grant covering it — configuration the installer (L8.3)
386
+ owns, not `project add` — and the project-tier document only enters
387
+ the cascade when the command runs from the project root (cascade
388
+ tiering). The `path` and `remote` registry fields are
389
+ installer-owned deployment data and are not exposed in the public
390
+ projection.
391
+
392
+ The planner chain consumes the result directly: `ace-coworker prepare`
393
+ provisions task worktrees from the registered repository, and the
394
+ overseer's private queue lives in the prepared `_overseer` folder —
395
+ no manual directory fixing is needed.
396
+
397
+ Statuses per step: `created|exists|updated|would_create|would_update|failed`.
398
+ `ok` means
399
+ zero failed steps; a wet run with failures prints the report first,
400
+ then exits non-zero naming each step. `--dry-run` predicts
401
+ (`would_*`) with zero mutations and exits 0.
402
+
403
+ Idempotence and refusals:
404
+
405
+ - Re-adding an already-registered project re-validates every step and
406
+ reports `exists` without rewriting the registry, the `.gitignore`,
407
+ or anything inside `_overseer`. A changed `--label` on re-add
408
+ updates the recorded label (display data, not identity) and reports
409
+ `updated`; an omitted `--label` keeps the recorded one.
410
+ - The same id registered with a different `path` or `remote`, or the
411
+ same absolute `path` registered under a different id, is refused
412
+ before any write.
413
+ - An unparseable registry document or a foreign `schema_version` is
414
+ refused rather than overwritten.
415
+ - A failed repository step fails the overseer and registry steps as
416
+ dependency failures instead of attempting them.
417
+
418
+ Boundaries: `project add` does not clone role accounts or keys (L8.1
419
+ owns those), does not configure HITL adapters or run the full install
420
+ (L8.3/`ace-lab install`), and adds no new state owner — the only
421
+ persisted artifact is the project's own lab-config cascade document.
422
+
423
+ ## Install (L8.3)
424
+
425
+ ```bash
426
+ ace-lab install # full installation from the trusted manifest
427
+ ace-lab install --dry-run # predict every phase without mutating
428
+ ace-lab install --format json
351
429
  ```
352
430
 
353
- Use returned generations and actual step numbers, not the illustrative values.
354
- `HERDR_PANE` supplies the caller pane unless `--pane` is explicit; the selected
355
- owner authenticates actual native ancestry independently. `bootstrap` returns
356
- accepted context paths, byte counts, SHA256 values and manifest digest. `next`
357
- reads the canonical queue including reports and returns current/next instructions.
358
- `fail --generation N --message REASON` records a failed scope; it never fabricates
359
- successful completion. The workflow `wfi://assign/drive` selects this protocol
360
- before local assignment resolution when `--ordinary`/the ordinary environment
361
- is present.
362
-
363
- Submit one bounded execution receipt JSON with the following fields, using
364
- actual observed values (no placeholders in submitted evidence):
365
-
366
- ```json
367
- {
368
- "assignment_id": "original-assignment", "attempt_id": "original-attempt",
369
- "project_id": "atlas", "scope": "010.01", "operation": "check",
370
- "producer": {"actor": "actual-user", "role": "worker", "runtime": "herdr"},
371
- "head": "0123456789012345678901234567890123456789",
372
- "verdict": "succeeded", "checks": [{"name": "tests", "verdict": "passed"}],
373
- "artifacts": [{"path": "check.md", "sha256": "actual-64-hex-artifact-digest"}]
374
- }
431
+ `install` performs the full new-lab installation (task 8xg.t.sbz.7.2)
432
+ from the same trusted install manifest as `prepare`, by composing the
433
+ existing molecules in a fixed order:
434
+
435
+ 1. **Foreign-lab guard — before any mutation.** The trusted
436
+ authorization document `/etc/lab/ace-lab/authorization.yml` (when
437
+ present) must not grant projects the manifest does not declare: a
438
+ ready or foreign lab is refused with a classified error and zero
439
+ writes, identically in wet runs and `--dry-run` (prediction parity).
440
+ 2. **prepare (L8.1)** — accounts → directories → keys → roles → gems,
441
+ exactly as `ace-lab prepare` (same organism, same per-item report;
442
+ the declared gem set converges into the declared target runtime).
443
+ 3. **project add (L8.2) per declared project** — repository, private
444
+ `_overseer`, project registry, exactly as `ace-lab project add`.
445
+ Projects are isolated: one failing project does not block the
446
+ others. When `prepare` did not converge, every project is reported
447
+ as a dependency failure instead of being attempted.
448
+ 4. **grants** — the trusted-tier principal grants from the manifest's
449
+ `grants` section are converged into the `principals` mapping of the
450
+ authorization document: this is the installer-owned configuration
451
+ that makes the installed projects visible to `ace-lab projects` and
452
+ `ace-overseer projects` (see the L8.2 boundary above). Grants only
453
+ grow: a manifest that would remove an existing grant is refused.
454
+ Principals the manifest does not mention and every other section of
455
+ the document (e.g. HITL adapter policy) are preserved untouched —
456
+ and a converged re-run writes nothing at all, so the document's
457
+ admin-authored comments stay byte-identical. A manifest with no
458
+ `grants` over an absent document is a no-op (`skipped`): no empty
459
+ authorization file is ever created. Before any write the existing
460
+ document's principals are validated exactly like the read side
461
+ (shared validation with the grants resolver): a malformed policy is
462
+ refused with a classified error instead of silently rewritten, and
463
+ whitespace-padded principal names are normalized so they cannot
464
+ coexist with a stripped twin.
465
+ The write is atomic (temp file + rename, mode 0644). Grants are
466
+ configured only when the earlier phases converged — never over a
467
+ partially failed install (no pretend success).
468
+
469
+ Statuses: `prepare` contributes its own per-item statuses; each project
470
+ contributes `created|exists|updated|would_*|failed` per step; grants
471
+ report `created|updated|exists|skipped|would_*|failed` plus a
472
+ per-principal breakdown and the `kept` list (present and possibly
473
+ empty, so machine consumers see a stable schema). `ok` means every phase converged. A wet run with failures
474
+ prints the report first, then exits non-zero enumerating each
475
+ `<phase>/<item>: <message>`; `--dry-run` predicts with zero mutations
476
+ and exits 0.
477
+
478
+ Idempotence and resume design: every step is idempotent, so an
479
+ interrupted re-run converges where steps already completed (accounts
480
+ `exists`, repositories `exists`, grants `exists`) without duplicates or
481
+ lost configuration — the per-step record is the resume mechanism; no
482
+ journal exists or is needed for convergence. See
483
+ [Resuming an interrupted install](#resuming-an-interrupted-install-l84).
484
+
485
+ ### Resuming an interrupted install (L8.4)
486
+
487
+ ```bash
488
+ ace-lab install # same command: recognizes and finishes its own partial state
489
+ ace-lab install --dry-run # previews exactly which steps exist and which would run
375
490
  ```
376
491
 
377
- The producer comes from canonical start status; checks and artifacts must describe
378
- executed work. Run `--operation result --generation N --receipt receipt.json
379
- --artifact check.md` with files in receipt order. The existing receipt verifier
380
- and canonical import validate attribution, evidence and exact bytes. A result
381
- submission leaves the worker running until its canonical supervisor executes
382
- `--operation adopt` and receives terminal status. Keep the actual worker process
383
- and checkout alive until that acceptance; a terminal pane without the required
384
- native binding is insufficient.
385
-
386
- Mutations retain their exact request and transferred bytes under the original
387
- context directory before sending. A lost response is recovered with canonical
388
- `status`/`next`, then `--operation replay --request /original/requests/.../request.json`.
389
- Never update an existing request's generation, mutation ID or bytes to force a
390
- retry. The requester directory is not an accepted-state store. The result hint
391
- file is ignored by canonical reads/adoption, including when forged or stale.
392
- Inbound Inbox settlement is not provisioned by this driver; nonempty references
393
- still require the separately selected existing Inbox owner and fail closed otherwise.
394
-
395
- A replacement coordinator reads `--operation supervisor`, then from its actual
396
- installed native supervisor placement uses:
397
-
398
- ```sh
399
- ace-lab assignment drive --project atlas --context /private/work/context.json \
400
- --operation adopt-supervisor --generation 1 --previous-binding-sha256 EXACT_OLD_DIGEST
401
- ```
402
-
403
- The definition chain records old→new bindings under its original generation CAS.
404
- Only a positively ended old generation (kernel process absence or a different
405
- exact process birth) permits adoption; live, inaccessible, remote-host and unknown
406
- identities refuse. A lost adoption response reuses its original retained request.
407
- No worker is re-registered or relaunched. Existing pending results, uncertain
408
- operations, attempts and request IDs remain unchanged. Finish, including replay,
409
- checks the current canonical supervisor binding and rejects the displaced one.
410
-
411
- Wire additions are `supervisor_status` (assignment only, null mutation), and
412
- `adopt_supervisor` (assignment, actual pane, expected definition generation,
413
- previous_binding_sha256; original mutation). They use the same installed owner,
414
- project policy, journal and lifecycle exclusion as ordinary execution.
415
-
416
- ## Error semantics
417
-
418
- Errors are one deterministic JSON document on stdout plus a non-zero exit.
419
-
420
- | Code | Meaning |
421
- |------|---------|
422
- | `missing` | No entry with that stable ID, or no capable service in the project |
423
- | `ambiguous` | Several equally capable services without a configured `default_for` |
424
- | `stale` | Binding is inactive, unattested, or the instance identity was replaced |
425
- | `unauthorized` | Caller's verified local identity has no principal for the project |
426
- | `invalid_configuration` | Topology config violates the schema; message is actionable |
427
- | `invalid_input` | Service input or request ID is malformed |
428
- | `invalid_attempt` | Assignment attempt is absent, unmanaged, terminal, or has a stale candidate head |
429
- | `conflict` | Request ID is already bound to different input or scope |
430
- | `evidence_unavailable` | Assignment evidence ref cannot be written or read |
431
-
432
- Example:
433
-
434
- $ ace-lab resolve --id atlas-coder --format json; echo $?
435
- {"status":"error","error":{"code":"stale","message":"stable ID \"atlas-coder\" has a stale runtime binding; a replaced process must re-attest before routing","id":"atlas-coder","project":"atlas"}}
436
- 1
437
-
438
- ## Configuration
439
-
440
- Schema version 1, resolved through the ADR-022 cascade (project `.ace/lab/`
441
- over user `~/.ace/lab/` over gem defaults; gem defaults are an empty, safe
442
- topology). Cascade documents carry **topology only**:
443
-
444
- ```yaml
445
- schema_version: 1
446
- topology:
447
- projects:
448
- - id: atlas # globally unique across all entries
449
- label: Display name # display only; never resolves
450
- agents:
451
- - id: atlas-planner
452
- project: atlas # must reference an existing project
453
- role: planner
454
- capabilities: [planning] # non-empty; normalized (stripped, lowercased)
455
- binding:
456
- kind: runtime # agents bind to runtimes, services to services
457
- state: active # routeable only when active
458
- instance_id: i1 # runtime identity fact
459
- attested_instance_id: i1 # must equal instance_id to be fresh
460
- services:
461
- - id: atlas-search
462
- project: atlas
463
- capabilities: [search]
464
- default_for: [search] # exactly one default may win among candidates
465
- endpoint:
466
- kind: http # http or https
467
- url: "https://search.example.internal/query?token=secret" # stored, never published
468
- binding: { kind: service, state: active, instance_id: i2, attested_instance_id: i2 }
469
- ```
470
-
471
- ### Authorization grants
472
-
473
- Grants never live in the cascade: project and user documents are
474
- caller-writable, so an `authorization` section there is rejected as
475
- `invalid_configuration`. Grants come from a single deployment-controlled
476
- file at the **fixed path** `/etc/lab/ace-lab/authorization.yml` (installed
477
- by the `lab-config` deployment). The location is not caller-selectable --
478
- there is no flag or environment override. At every query the tool verifies
479
- the file and every directory on its real path are root-owned and not
480
- group/world-writable, and opens the file `O_NOFOLLOW`; any failed
481
- verification fails closed:
482
-
483
- ```yaml
484
- principals:
485
- "<verified-local-uid>": # passwd username or numeric uid of the caller
486
- projects: ["atlas"]
487
- ```
488
-
489
- The grants file is machine-global: it is validated structurally only, so a
490
- principal may reference projects absent from the current directory's
491
- topology. Such grants are valid but never match a locally configured
492
- project. All-digit principal keys are matched as uids only -- an all-digit
493
- passwd username is authorized solely through its uid, so it can never
494
- consume a different account's numeric-uid grant.
495
-
496
- A missing trusted file means nobody is authorized (fail closed).
497
-
498
- ### Trusted service policy
499
-
500
- The same deployment-owned file may contain `operations` and
501
- `authorizations`. An operation names one exact project and stable service ID,
502
- an executor OS UID, a valid lease, and either a fixed local argv or an absolute
503
- Unix socket path. The domain deployment owns actual handlers and credentials.
504
- The Unix service must authenticate the client's OS peer identity, validate
505
- the exact authorization and current lease itself, and return a structured
506
- receipt; ACE verifies the service peer UID before accepting that response.
507
-
508
- ```yaml
509
- operations:
510
- forge-sync:
511
- project: atlas
512
- service_id: atlas-sync
513
- transport: unix
514
- socket_path: /run/lab/atlas-sync.sock
515
- executor_uid: 997
516
- lease_expires_at: '2026-10-02T16:00:00Z'
517
- authorizations:
518
- DECISION:
519
- operation: forge-sync
520
- project_id: atlas
521
- assignment_id: A
522
- attempt_id: ATT
523
- input_digest: <sha256-of-canonical-json>
524
- target: { resource: forge/repository, artifact_digest: null }
525
- candidate_head: <exact-git-head>
526
- caller_uid: 1000
527
- expires_at: '2026-10-02T16:00:00Z'
492
+ An installation interrupted mid-run (crash after accounts, directories,
493
+ keys, a project, or part of the grants) is resumed by running the same
494
+ `ace-lab install` command again. Recognition is derived from the real
495
+ artifacts the steps read and write — there is no journal, marker file
496
+ or second state owner:
497
+
498
+ - already-done steps report `exists`/`verified` and are not reworked:
499
+ the converged directory and key pair keep their inode and content, an
500
+ already-cloned repository keeps its worktree (untracked files
501
+ included) and a converged trusted document stays byte-identical;
502
+ - missing steps complete: a key pair whose public half is missing
503
+ regenerates only that half from the surviving private key; a project
504
+ whose repository exists finishes only `_overseer` and the registry;
505
+ partially granted own principals converge while principals and
506
+ sections the manifest does not mention are kept;
507
+ - grants are configured only when prepare and every project converged
508
+ (no pretend success over a partial install).
509
+
510
+ Own-partial vs foreign is distinguished per step, not only for grants:
511
+ each already-present artifact is checked against what the manifest
512
+ declares before it is touched, and mismatching content is refused —
513
+ never taken over, never cleaned:
514
+
515
+ | Step | Foreign state | Result |
516
+ | --- | --- | --- |
517
+ | declared directory | symlink, non-directory, or foreign ownership | step fails with a resolve-manually message; projects and grants are gated as dependency failures |
518
+ | key pair | foreign-owned key files or `.ssh`, non-regular file | step fails with a resolve-manually message |
519
+ | gem set | symlinked or non-directory runtime, missing privileges, or a failed probe/install | every gem item fails with a classified message; projects and grants are gated as dependency failures |
520
+ | project repository | non-empty non-Git directory, or a Git repository with a different origin | step fails: "refusing to take over a foreign location" / "refusing to overwrite an existing registration"; the other projects still run (isolation) |
521
+ | grants | trusted document granting projects outside the manifest, or a document that fails verification/parsing | the whole install refuses before any mutation ("refusing to install over a foreign lab"), wet and in `--dry-run` alike |
522
+
523
+ When resume is not possible, the wet failure names every blocking step
524
+ concretely (`install failed (N step(s)): <phase>/<item>: <message>`);
525
+ `--dry-run` predicts the same per-step refusals with zero mutations
526
+ and exits 0.
527
+
528
+ Boundaries: `install` does not verify readiness under the target
529
+ accounts (that is `verify`, below); HITL adapter configuration beyond
530
+ preserving the existing trusted document sections is not orchestrated
531
+ by this slice.
532
+
533
+ ## Verify (L8.5)
534
+
535
+ ### `ace-lab verify [--format table|json] [-q]`
536
+
537
+ ```bash
538
+ ace-lab verify # per-check table; exit 0 only when ready
539
+ ace-lab verify --format json # stable machine report for the overseer / next tool
528
540
  ```
529
541
 
530
- The decision must match every listed field exactly and remain valid. A
531
- decision never supplies missing executor capability or credentials. For
532
- authenticated host maintenance, the policy also identifies an executable and
533
- evidence sink outside the deployment being replaced; its domain handler must
534
- enforce quiescence for all other product work. Environment `review-approval`
535
- is a named operation and never substitutes for an independent code review.
536
-
537
- Validation of the topology cascade rejects duplicate IDs (globally unique
538
- across projects, agents, services), unknown project references, malformed
539
- capabilities, defaults for undeclared capabilities, unusable endpoints
540
- (absolute http(s) URL with a host and a port in 1–65535; `endpoint.kind` is
541
- `http` or `https`), unsupported binding kinds, and principals referencing
542
- unknown projects.
543
-
544
- **Error messages are value-free by design:** validation runs before
545
- authorization, so messages use positional field locations
546
- (`topology.agents[0].project references an unknown project`) and never echo
547
- configured IDs, project names, or principal names -- configuration defects
548
- cannot disclose topology to unauthorized callers.
549
-
550
- **Ownership:** deployed topology and authorization values are maintained by
551
- the `lab-config` repository (`8wl.t.gad`). `ace-lab` defines the schema and
552
- reads; it never provisions.
553
-
554
- ## Guarantees and boundaries
555
-
556
- - Public output is built from a strict allowlist: `id`, `project`, `label`,
557
-
558
- `role`, `capabilities`, `default_for`, binding `kind` + availability state,
559
- and a sanitized endpoint identity. Tokens, auth-file paths, URL userinfo,
560
- paths, query parameters, fragments, pane/session IDs, and instance
561
- identities never appear in any command output.
562
-
563
- - Caller identity comes from the verified local process owner
564
-
565
- (username or uid). `--role`, `--principal`, and `--caller` flags are
566
- rejected: authorization is a property of the caller, not an input.
567
-
568
- - A replaced process keeps its stable ID; until re-attested, queries
569
-
570
- classify it `stale` -- an explicit unavailable result, never a routeable
571
- answer.
572
-
573
- - No Works or scheduling. Service request claims and receipts use
574
-
575
- `ace-assign`'s evidence journal. The Lab execution binary
576
- (`/usr/local/bin/lab`) is never invoked or required.
577
-
578
- ### Protected handler cleanup boundary
579
-
580
- Handlers retain the fixed closed environment and 30-second execution deadline, with 16KiB stdout and 8KiB stderr caps. Owned cleanup adds at most one second of confirmed reaping; unresolved cleanup remains uncertain and cannot return a service receipt. It cannot establish an arbitrary domain target or surviving writer is absent. The gad.b operation-specific fresh inspector remains required source work; installed verification is centralized in gad.2.
581
-
582
- ### Selected receiver no-effect recovery
583
-
584
- The existing protected receiver exposes `recover_no_effect(binding:, input_bytes:, mutation_id:, expected_generation:)`. Supply the original assignment/attempt/candidate/head/request binding, original structured input, stable mutation ID and unchanged original positive authority generation. The receiver snapshots those inputs, authenticates canonical status and input before any settled return, and adopts an already accepted current challenge without claiming again. A needed claim uses the supplied generation unchanged. Lost acknowledgements remain uncertain; status retry never repeats the original effect.
585
-
586
- Trusted operation configuration must provide a distinct fixed `no_effect_argv` for the original project/service/executor. Inspection may proceed after the effect lease expires, but it never renews effect permission or selects normal effect argv. The selected inspector must produce operation-specific original target/handler/writer observations and existing strict challenge-bound artifacts; only the authority's canonical import can establish `failed-settled`. This source API and controlled composition do not deliver the outstanding gad.b inspector or installed acceptance.
587
-
588
- ## Project commands
589
-
590
- `ace-lab run --project ace -- /usr/bin/id` invokes one exact installed command
591
- policy. `--as publisher` selects a permitted executor; repeated `--service NAME`
592
- replaces the configured default set. Every service and the entire combination
593
- must be granted. Arguments after `--` remain literal, including command options.
594
- No shell is added. Duplicate `--project`/`--as` or empty argv are usage errors.
595
-
596
- The root-owned `/etc/lab/ace-lab/project-commands.yml` is the only command-policy
597
- source. Project preferences and caller environment cannot create grants. Its
598
- version1 project entries contain `launcher`, `default_executor`, `default_services`,
599
- `executors`, `services`, `commands`, and `grants`. Executors declare UID/GID/groups,
600
- HOME/cwd/PATH and nonsecret environment; services declare nonsecret environment
601
- and secret references; commands bind exact argv, executable SHA256, source
602
- head/tree and required `purpose: command` or `purpose: test`. Test purpose requires
603
- nonnull source head/tree and is never inferred from executable name or exit0.
604
- With `purpose: command`, source head/tree may both be null for a non-Git probe;
605
- Git source uses two exact object IDs. Grants bind requester kind/UID, command, executor
606
- and the exact service combination. Accepted code/cwd must be root-owned and
607
- unwritable by the executor. Missing or changed context refuses execution.
608
-
609
- An operator grant authorizes an OS account. Agents sharing that account share
610
- its operator authority; an environment hint does not prove human presence.
611
- Agent-selected invocation additionally checks actual current native ancestry.
612
- Separate identities are required when operator authority must exclude agents.
613
-
614
- The CLI retains an invocation under `.ace-local/lab-commands/` and prints its
615
- execution reference before contacting the canonical Assign owner. Inspect it with
616
- `ace-lab run-status --project ace --reference REF`. Inspection starts no command
617
- and remains available to the original requester account after a grant is revoked.
618
- After response loss, inspect that original reference before requesting new work.
619
- An existing claim or a replayed acknowledgment cannot release another child.
620
-
621
- Interactive terminals preserve no-echo input, dimensions and signals. Pipe mode
622
- preserves EOF, separate stdout/stderr and actual exit/signal status. Socket closure
623
- without the authenticated result is uncertainty. Root launch/exit observations
624
- remain separate from accepted product delivery evidence; exit0 alone does not
625
- prove publication, installation or completed delivery.
626
-
627
- This implementation slice admits nonsecret commands. A nonempty selected secret
628
- reference set is explicitly refused before claim until the secret execution
629
- capability is supplied. Publication also requires the publisher's writable
630
- artifact/cache destination outside immutable accepted source, or retained exact
631
- artifacts. These remaining joins and installed acceptance are tracked by ei9 .5;
632
- source/kernel fixture success does not assert installed autonomous publication.
633
-
634
- In interactive mode, input EOF sends the terminal's conventional Ctrl-D byte; a
635
- program that changes its terminal to raw mode may interpret that byte itself.
636
- Noninteractive EOF closes the command input pipe. The dedicated Linux helper owns
637
- and reaps detached command descendants as well as the original process group.
542
+ `verify` is the read-only end-of-install check that the lab is ready
543
+ UNDER THE TARGET ACCOUNTS — not merely for the installer's account. It
544
+ reads the same artifacts `install`/`prepare` derive truth from (the
545
+ trusted install manifest, the filesystem's permission bits and
546
+ ownership, the repositories, the per-project registry and the trusted
547
+ authorization document) and writes nothing anywhere: no journal, no
548
+ second state owner. (The key fingerprint probe may use an ephemeral
549
+ tmpdir scratch — 0600, removed afterwards — never a durable write.)
550
+ Because it judges converged artifacts only, a
551
+ fresh install and a resumed one verify identically — there is no
552
+ hidden gap after resume.
553
+
554
+ Per-check sections, each reported as `pass` / `fail` / `unknown` with
555
+ a concrete reason (never a bare boolean):
556
+
557
+ | Section | Checks, as the target account |
558
+ | --- | --- |
559
+ | accounts | account exists; home matches the manifest; login shell is usable (non-login shells block readiness) |
560
+ | directories | declared directory exists with the declared mode and owner |
561
+ | keys | the pair is usable: modes (0600/0644/`.ssh` 0700), account ownership, and the public fingerprint matches the key derived from the private half (the same probe `prepare` applies) |
562
+ | roles | instruction source readable by the role's account (the `prepare` role verifier) |
563
+ | repositories | the path is the repository toplevel, the origin matches the manifest remote; local origins are probed with `git ls-remote`, network origins are reported `unknown` (not dialed by the read-only verifier) |
564
+ | grants | the trusted document is present and verifiable, and every manifest principal's grants resolve through the real read-side mechanics (`CallerAuthorizer`) to the declared projects |
565
+ | registry | each project is registered in its `.ace/lab/config.yml` (visibility through the configuration cascade) |
566
+ | overseer | `.ace-tasks/_overseer/` exists and is ignored by Git (`git check-ignore`) |
567
+ | adapters | configuration is a mapping; a declared local command resolves (probed for the target executors); `kind: hitl` routes are installed in every project's `.ace/hitl/config.yml` and satisfy the runtime loader's rules |
568
+ | tools | each required tool resolves to an executable the target executors can run (presence + permission probe, no execution) |
569
+ | gems | each declared gem is installed at its EXACT declared version in the declared target runtime — judged through the same read-only probe prepare converges from; a missing runtime, a missing gem or a version mismatch blocks readiness (a missing `gem` executable reports `unknown`, the offline posture) |
570
+
571
+ The overall verdict is `ready` only when no check failed. A failure
572
+ never hides healthy data: the full report renders first (table or
573
+ JSON), then the command exits non-zero naming every failed check
574
+ concretely (`lab is not ready (N failed check(s)): <section>/<item>:
575
+ <reason>`). `unknown` (a network remote) is reported and counted but
576
+ does not block readiness — it is not a failure, and treating it as one
577
+ would make `verify` unusable offline. The JSON report shape is stable:
578
+ `ready`, `machine`, `manifest`, `authorization_path`, `checks`
579
+ (per-section lists), and `counts` (`pass`/`fail`/`unknown`).
580
+
581
+ Key material hygiene is inherited from the provisioner: only the
582
+ public fingerprint ever appears in a report; private key bytes stay
583
+ inside the `ssh-keygen` subprocess.
584
+
585
+ ## Planned waves
586
+
587
+ > **L7 remainder:** `ace-lab status` is delivered; the remaining L7 scope
588
+ > (configurable `ace-overseer status` aggregation, pi continuation) lives in
589
+ > its own packages and is not available yet.
590
+ >
591
+ > **Planned (L8):** the preparation phase is delivered as `ace-lab
592
+ > prepare` (L8.1), project onboarding as `ace-lab project add` (L8.2),
593
+ > the full installer as `ace-lab install` (L8.3), resuming the
594
+ > installer's own interrupted installation as the same `ace-lab
595
+ > install` re-run (L8.4), and end-to-end readiness verification under
596
+ > the target accounts as `ace-lab verify` (L8.5).
597
+
598
+ ## Boundaries
599
+
600
+ `ace-lab` owns no task, assignment, HITL, or worktree state. There is no
601
+ journal, authority/admission service, daemon, queue engine, command kernel, or
602
+ extra state cache in this package.
603
+
604
+ The installed authority/admission/journal/command path is removed in L1: the
605
+ `service request|status|listen`, `ace-lab assignment …`, `run`, `run-status`,
606
+ and `command execute` commands no longer exist. The local file-based flow does
607
+ not use them. The Forgejo merge adapter and its trusted configuration loader
608
+ remain as provider building blocks; the local flow does not invoke them.