ace-lab 0.8.0 → 0.13.0
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +363 -92
- data/README.md +66 -100
- data/docs/usage.md +529 -404
- data/lib/ace/lab/atoms/install_schema.rb +373 -0
- data/lib/ace/lab/cli/commands/install.rb +161 -0
- data/lib/ace/lab/cli/commands/prepare.rb +118 -0
- data/lib/ace/lab/cli/commands/project_add.rb +101 -0
- data/lib/ace/lab/cli/commands/status.rb +109 -0
- data/lib/ace/lab/cli/commands/verify.rb +115 -0
- data/lib/ace/lab/cli.rb +21 -13
- data/lib/ace/lab/install_manifest_path.rb +23 -0
- data/lib/ace/lab/models/status_report.rb +38 -0
- data/lib/ace/lab/molecules/account_provisioner.rb +196 -0
- data/lib/ace/lab/molecules/config_state_classifier.rb +145 -0
- data/lib/ace/lab/molecules/directory_provisioner.rb +208 -0
- data/lib/ace/lab/molecules/grant_provisioner.rb +250 -0
- data/lib/ace/lab/molecules/grant_resolver.rb +49 -23
- data/lib/ace/lab/molecules/hitl_routes.rb +263 -0
- data/lib/ace/lab/molecules/install_manifest_loader.rb +64 -0
- data/lib/ace/lab/molecules/key_provisioner.rb +736 -0
- data/lib/ace/lab/molecules/lab_state_scanner.rb +55 -0
- data/lib/ace/lab/molecules/overseer_provisioner.rb +188 -0
- data/lib/ace/lab/molecules/project_registry.rb +251 -0
- data/lib/ace/lab/molecules/repository_provisioner.rb +141 -0
- data/lib/ace/lab/molecules/role_verifier.rb +223 -0
- data/lib/ace/lab/organisms/install.rb +213 -0
- data/lib/ace/lab/organisms/prepare.rb +156 -0
- data/lib/ace/lab/organisms/project_add.rb +170 -0
- data/lib/ace/lab/organisms/status_service.rb +70 -0
- data/lib/ace/lab/organisms/verify.rb +768 -0
- data/lib/ace/lab/version.rb +1 -1
- data/lib/ace/lab.rb +34 -6
- metadata +34 -102
- data/lib/ace/lab/atoms/protected_publication_input.rb +0 -50
- data/lib/ace/lab/atoms/protected_workspace_prune_input.rb +0 -114
- data/lib/ace/lab/atoms/protected_workspace_prune_preview.rb +0 -86
- data/lib/ace/lab/atoms/service_input.rb +0 -92
- data/lib/ace/lab/cli/commands/assignment.rb +0 -45
- data/lib/ace/lab/cli/commands/service.rb +0 -240
- data/lib/ace/lab/models/service_receipt.rb +0 -18
- data/lib/ace/lab/molecules/hitl_authorizer.rb +0 -63
- data/lib/ace/lab/molecules/ordinary_admission_resolver.rb +0 -131
- data/lib/ace/lab/molecules/ordinary_listener.rb +0 -262
- data/lib/ace/lab/molecules/ordinary_merge_read.rb +0 -45
- data/lib/ace/lab/molecules/ordinary_native_read.rb +0 -106
- data/lib/ace/lab/molecules/ordinary_owner_admission.rb +0 -139
- data/lib/ace/lab/molecules/ordinary_owner_config.rb +0 -232
- data/lib/ace/lab/molecules/ordinary_receiver_config.rb +0 -94
- data/lib/ace/lab/molecules/ordinary_socket.rb +0 -126
- data/lib/ace/lab/molecules/ordinary_worktree_read.rb +0 -216
- data/lib/ace/lab/molecules/protected_cleanup_owner_admission.rb +0 -163
- data/lib/ace/lab/molecules/protected_cleanup_owner_client.rb +0 -165
- data/lib/ace/lab/molecules/protected_cleanup_owner_identity.rb +0 -239
- data/lib/ace/lab/molecules/protected_publication_publisher.rb +0 -110
- data/lib/ace/lab/molecules/protected_service_handler.rb +0 -43
- data/lib/ace/lab/molecules/protected_service_policy.rb +0 -156
- data/lib/ace/lab/molecules/service_executor.rb +0 -99
- data/lib/ace/lab/molecules/service_policy.rb +0 -289
- data/lib/ace/lab/organisms/authority_composition.rb +0 -87
- data/lib/ace/lab/organisms/ordinary_assignment_client.rb +0 -149
- data/lib/ace/lab/organisms/ordinary_authority_composition.rb +0 -123
- data/lib/ace/lab/organisms/ordinary_merge_receiver.rb +0 -688
- data/lib/ace/lab/organisms/ordinary_merge_request.rb +0 -456
- data/lib/ace/lab/organisms/protected_cleanup_owner.rb +0 -361
- data/lib/ace/lab/organisms/protected_service_client.rb +0 -129
- data/lib/ace/lab/organisms/protected_service_listener.rb +0 -181
- data/lib/ace/lab/organisms/protected_service_publication.rb +0 -268
- data/lib/ace/lab/organisms/protected_service_receiver.rb +0 -267
- data/lib/ace/lab/organisms/protected_service_recovery.rb +0 -185
- data/lib/ace/lab/organisms/protected_service_request.rb +0 -128
- data/lib/ace/lab/organisms/protected_service_worker.rb +0 -89
- data/lib/ace/lab/organisms/service_request_service.rb +0 -249
data/docs/usage.md
CHANGED
|
@@ -1,449 +1,574 @@
|
|
|
1
1
|
# ace-lab usage
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
9
|
+
The file-based ownership model is documented once in
|
|
10
|
+
[../../docs/lab-flow.md](../../docs/lab-flow.md).
|
|
8
11
|
|
|
9
|
-
Topology
|
|
10
|
-
`-q/--quiet`. Service status accepts `--format json`; service request always
|
|
11
|
-
emits JSON.
|
|
12
|
+
## Topology
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
$ ace-lab projects --format json
|
|
18
|
-
{"status":"ok","data":{"projects":[{"id":"atlas","label":"Atlas platform"},{"id":"borealis"}]}}
|
|
19
|
-
|
|
20
|
-
A caller with no configured principal receives:
|
|
21
|
-
|
|
22
|
-
{"status":"error","error":{"code":"unauthorized","message":"caller has no configured authorization for any lab project"}}
|
|
23
|
-
|
|
24
|
-
### `ace-lab agents --project PROJECT`
|
|
25
|
-
|
|
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"}}]}}
|
|
30
|
-
|
|
31
|
-
`binding.state` is `available` when the configured runtime binding is active
|
|
32
|
-
and its instance identity matches the attestation; otherwise `stale`.
|
|
33
|
-
|
|
34
|
-
### `ace-lab services --project PROJECT`
|
|
35
|
-
|
|
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"}}}]}}
|
|
40
|
-
|
|
41
|
-
The endpoint is a sanitized identity: scheme, host, and explicit port only.
|
|
42
|
-
|
|
43
|
-
### `ace-lab resolve --id ID`
|
|
44
|
-
|
|
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"}}}}
|
|
49
|
-
|
|
50
|
-
Labels are display values and never resolve: `resolve "Atlas platform"` is a
|
|
51
|
-
`missing` error.
|
|
52
|
-
|
|
53
|
-
### `ace-lab route --project PROJECT --capability CAPABILITY`
|
|
54
|
-
|
|
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"}}
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
Run a preview, then submit the exact request:
|
|
72
|
-
|
|
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
|
-
```
|
|
81
|
-
|
|
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
|
-
```
|
|
113
|
-
|
|
114
|
-
Expected output after a lost executor receipt:
|
|
115
|
-
|
|
116
|
-
```json
|
|
117
|
-
{"status":"ok","data":{"request_id":"REQUEST","outcome":"uncertain","state":"uncertain"}}
|
|
118
|
-
```
|
|
119
|
-
|
|
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
|
|
160
|
-
```
|
|
161
|
-
|
|
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:
|
|
174
|
-
|
|
175
|
-
```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
|
|
210
|
-
```
|
|
211
|
-
|
|
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
|
-
}
|
|
237
|
-
```
|
|
238
|
-
|
|
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
|
-
## Error semantics
|
|
280
|
-
|
|
281
|
-
Errors are one deterministic JSON document on stdout plus a non-zero exit.
|
|
282
|
-
|
|
283
|
-
| Code | Meaning |
|
|
284
|
-
|------|---------|
|
|
285
|
-
| `missing` | No entry with that stable ID, or no capable service in the project |
|
|
286
|
-
| `ambiguous` | Several equally capable services without a configured `default_for` |
|
|
287
|
-
| `stale` | Binding is inactive, unattested, or the instance identity was replaced |
|
|
288
|
-
| `unauthorized` | Caller's verified local identity has no principal for the project |
|
|
289
|
-
| `invalid_configuration` | Topology config violates the schema; message is actionable |
|
|
290
|
-
| `invalid_input` | Service input or request ID is malformed |
|
|
291
|
-
| `invalid_attempt` | Assignment attempt is absent, unmanaged, terminal, or has a stale candidate head |
|
|
292
|
-
| `conflict` | Request ID is already bound to different input or scope |
|
|
293
|
-
| `evidence_unavailable` | Assignment evidence ref cannot be written or read |
|
|
294
|
-
|
|
295
|
-
Example:
|
|
296
|
-
|
|
297
|
-
$ ace-lab resolve --id atlas-coder --format json; echo $?
|
|
298
|
-
{"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"}}
|
|
299
|
-
1
|
|
300
|
-
|
|
301
|
-
## Configuration
|
|
302
|
-
|
|
303
|
-
Schema version 1, resolved through the ADR-022 cascade (project `.ace/lab/`
|
|
304
|
-
over user `~/.ace/lab/` over gem defaults; gem defaults are an empty, safe
|
|
305
|
-
topology). Cascade documents carry **topology only**:
|
|
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`.
|
|
306
18
|
|
|
307
19
|
```yaml
|
|
20
|
+
# .ace/lab/config.yml -- topology only
|
|
308
21
|
schema_version: 1
|
|
309
22
|
topology:
|
|
310
23
|
projects:
|
|
311
|
-
- id: atlas
|
|
312
|
-
label: Display name # display only; never resolves
|
|
24
|
+
- { id: atlas, label: Atlas platform }
|
|
313
25
|
agents:
|
|
314
26
|
- id: atlas-planner
|
|
315
|
-
project: atlas
|
|
27
|
+
project: atlas
|
|
316
28
|
role: planner
|
|
317
|
-
capabilities: [planning]
|
|
318
|
-
binding:
|
|
319
|
-
kind: runtime # agents bind to runtimes, services to services
|
|
320
|
-
state: active # routeable only when active
|
|
321
|
-
instance_id: i1 # runtime identity fact
|
|
322
|
-
attested_instance_id: i1 # must equal instance_id to be fresh
|
|
29
|
+
capabilities: [planning]
|
|
323
30
|
services:
|
|
324
31
|
- id: atlas-search
|
|
325
32
|
project: atlas
|
|
326
33
|
capabilities: [search]
|
|
327
|
-
default_for: [search]
|
|
328
|
-
endpoint:
|
|
329
|
-
kind: http # http or https
|
|
330
|
-
url: "https://search.example.internal/query?token=secret" # stored, never published
|
|
331
|
-
binding: { kind: service, state: active, instance_id: i2, attested_instance_id: i2 }
|
|
34
|
+
default_for: [search]
|
|
35
|
+
endpoint: { kind: http, url: "https://search.example.internal/query" }
|
|
332
36
|
```
|
|
333
37
|
|
|
334
|
-
###
|
|
335
|
-
|
|
336
|
-
Grants never live in the cascade: project and user documents are
|
|
337
|
-
caller-writable, so an `authorization` section there is rejected as
|
|
338
|
-
`invalid_configuration`. Grants come from a single deployment-controlled
|
|
339
|
-
file at the **fixed path** `/etc/lab/ace-lab/authorization.yml` (installed
|
|
340
|
-
by the `lab-config` deployment). The location is not caller-selectable --
|
|
341
|
-
there is no flag or environment override. At every query the tool verifies
|
|
342
|
-
the file and every directory on its real path are root-owned and not
|
|
343
|
-
group/world-writable, and opens the file `O_NOFOLLOW`; any failed
|
|
344
|
-
verification fails closed:
|
|
38
|
+
### `ace-lab projects`
|
|
345
39
|
|
|
346
|
-
```
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
projects: ["atlas"]
|
|
40
|
+
```bash
|
|
41
|
+
ace-lab projects --format json
|
|
42
|
+
# {"status":"ok","data":{"projects":[{"id":"atlas","label":"Atlas platform"}]}}
|
|
350
43
|
```
|
|
351
44
|
|
|
352
|
-
|
|
353
|
-
principal may reference projects absent from the current directory's
|
|
354
|
-
topology. Such grants are valid but never match a locally configured
|
|
355
|
-
project. All-digit principal keys are matched as uids only -- an all-digit
|
|
356
|
-
passwd username is authorized solely through its uid, so it can never
|
|
357
|
-
consume a different account's numeric-uid grant.
|
|
45
|
+
### `ace-lab agents --project PROJECT`
|
|
358
46
|
|
|
359
|
-
|
|
47
|
+
```bash
|
|
48
|
+
ace-lab agents --project atlas --format json
|
|
49
|
+
```
|
|
360
50
|
|
|
361
|
-
|
|
51
|
+
`binding.state` is `available` when the configured runtime binding is active;
|
|
52
|
+
otherwise `stale`.
|
|
362
53
|
|
|
363
|
-
|
|
364
|
-
`authorizations`. An operation names one exact project and stable service ID,
|
|
365
|
-
an executor OS UID, a valid lease, and either a fixed local argv or an absolute
|
|
366
|
-
Unix socket path. The domain deployment owns actual handlers and credentials.
|
|
367
|
-
The Unix service must authenticate the client's OS peer identity, validate
|
|
368
|
-
the exact authorization and current lease itself, and return a structured
|
|
369
|
-
receipt; ACE verifies the service peer UID before accepting that response.
|
|
54
|
+
### `ace-lab services --project PROJECT`
|
|
370
55
|
|
|
371
|
-
```
|
|
372
|
-
|
|
373
|
-
forge-sync:
|
|
374
|
-
project: atlas
|
|
375
|
-
service_id: atlas-sync
|
|
376
|
-
transport: unix
|
|
377
|
-
socket_path: /run/lab/atlas-sync.sock
|
|
378
|
-
executor_uid: 997
|
|
379
|
-
lease_expires_at: '2026-10-02T16:00:00Z'
|
|
380
|
-
authorizations:
|
|
381
|
-
DECISION:
|
|
382
|
-
operation: forge-sync
|
|
383
|
-
project_id: atlas
|
|
384
|
-
assignment_id: A
|
|
385
|
-
attempt_id: ATT
|
|
386
|
-
input_digest: <sha256-of-canonical-json>
|
|
387
|
-
target: { resource: forge/repository, artifact_digest: null }
|
|
388
|
-
candidate_head: <exact-git-head>
|
|
389
|
-
caller_uid: 1000
|
|
390
|
-
expires_at: '2026-10-02T16:00:00Z'
|
|
56
|
+
```bash
|
|
57
|
+
ace-lab services --project atlas --format json
|
|
391
58
|
```
|
|
392
59
|
|
|
393
|
-
The
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
evidence sink outside the deployment being replaced; its domain handler must
|
|
397
|
-
enforce quiescence for all other product work. Environment `review-approval`
|
|
398
|
-
is a named operation and never substitutes for an independent code review.
|
|
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.
|
|
399
63
|
|
|
400
|
-
|
|
401
|
-
across projects, agents, services), unknown project references, malformed
|
|
402
|
-
capabilities, defaults for undeclared capabilities, unusable endpoints
|
|
403
|
-
(absolute http(s) URL with a host and a port in 1–65535; `endpoint.kind` is
|
|
404
|
-
`http` or `https`), unsupported binding kinds, and principals referencing
|
|
405
|
-
unknown projects.
|
|
64
|
+
### `ace-lab resolve --id ID`
|
|
406
65
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
configured IDs, project names, or principal names -- configuration defects
|
|
411
|
-
cannot disclose topology to unauthorized callers.
|
|
66
|
+
```bash
|
|
67
|
+
ace-lab resolve --id atlas-planner --format json
|
|
68
|
+
```
|
|
412
69
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
reads; it never provisions.
|
|
70
|
+
Labels are display values and never resolve: resolving `"Atlas platform"` is a
|
|
71
|
+
`missing` error.
|
|
416
72
|
|
|
417
|
-
|
|
73
|
+
### `ace-lab route --project PROJECT --capability CAPABILITY`
|
|
418
74
|
|
|
419
|
-
|
|
75
|
+
```bash
|
|
76
|
+
ace-lab route --project atlas --capability search --format json
|
|
77
|
+
```
|
|
420
78
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
paths, query parameters, fragments, pane/session IDs, and instance
|
|
424
|
-
identities never appear in any command output.
|
|
79
|
+
Routing selects a capable service in the requested project. It never invokes
|
|
80
|
+
the service and never grants credentials.
|
|
425
81
|
|
|
426
|
-
|
|
82
|
+
## Status
|
|
427
83
|
|
|
428
|
-
|
|
429
|
-
rejected: authorization is a property of the caller, not an input.
|
|
84
|
+
### `ace-lab status [--format table|json] [-q]`
|
|
430
85
|
|
|
431
|
-
|
|
86
|
+
```bash
|
|
87
|
+
ace-lab status # readable table (default)
|
|
88
|
+
ace-lab status --format json # full report as JSON
|
|
89
|
+
```
|
|
432
90
|
|
|
433
|
-
|
|
434
|
-
|
|
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
|
|
131
|
+
```
|
|
435
132
|
|
|
436
|
-
|
|
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.
|
|
437
144
|
|
|
438
|
-
|
|
439
|
-
|
|
145
|
+
```yaml
|
|
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
|
+
```
|
|
440
174
|
|
|
441
|
-
|
|
175
|
+
Statuses: accounts `created|exists|would_create|failed`; directories
|
|
176
|
+
`created|exists|mode_fixed|would_create|would_fix_mode|failed`; keys
|
|
177
|
+
`created|exists|would_create|would_repair_public|would_fix_mode|failed`
|
|
178
|
+
(with the public `fingerprint`); roles `verified|failed`. `ok` in the
|
|
179
|
+
JSON report means zero failed items. Key/role items whose account
|
|
180
|
+
failed are reported as dependency failures instead of being attempted.
|
|
181
|
+
|
|
182
|
+
`--dry-run` is a faithful prediction of the wet run (`would_*`
|
|
183
|
+
statuses, zero mutations): keys and roles of an account the account
|
|
184
|
+
phase *would create* are predicted (`would_create` keys, `verified`
|
|
185
|
+
roles) even though the account does not exist in passwd yet; an
|
|
186
|
+
existing private key with a missing `.pub` predicts
|
|
187
|
+
`would_repair_public` (the wet run regenerates it); mode drift on an
|
|
188
|
+
owner-matching pair predicts `would_fix_mode`; a planted `.ssh`
|
|
189
|
+
symlink is refused in dry-run exactly as the wet run refuses it, and a
|
|
190
|
+
missing directory whose declared owner account *failed* the account
|
|
191
|
+
phase is predicted as the wet run's owner-unavailable failure (the
|
|
192
|
+
orchestrator threads its computed account set into the directory
|
|
193
|
+
stage). Four prediction limits: an *existing* directory whose declared
|
|
194
|
+
owner account does not exist yet cannot have its ownership checked,
|
|
195
|
+
and is reported as a failed item with a rerun hint; likewise, a role's
|
|
196
|
+
instruction *file* readability cannot be checked against an account
|
|
197
|
+
that does not exist yet (its uid is unknown), so such bindings are
|
|
198
|
+
predicted `verified` from the file's existence and type alone — the
|
|
199
|
+
wet run performs the full readability check; and an *existing key
|
|
200
|
+
pair* under an account the phase would create cannot have its
|
|
201
|
+
ownership checked either (the future uid is unknown) — a pair owned
|
|
202
|
+
by neither the installer nor the to-be-created account is
|
|
203
|
+
conservatively predicted as the wet run's takeover refusal, which is
|
|
204
|
+
pessimistic only in the pathological case where account creation
|
|
205
|
+
reuses exactly that uid. A fourth, smaller limit: when the account
|
|
206
|
+
phase *would create* the account, an existing installer-owned pair (or
|
|
207
|
+
installer-owned `.ssh`) is predicted as a plain `exists` even though the
|
|
208
|
+
wet run would chown it to the account while converging — the transfer
|
|
209
|
+
itself is not predicted.
|
|
210
|
+
|
|
211
|
+
Behavior and guarantees:
|
|
212
|
+
|
|
213
|
+
- **Idempotent convergence.** Existing resources are verified, not
|
|
214
|
+
recreated; only mode drift on an owner-matching directory or
|
|
215
|
+
owner-matching key pair is repaired. A pre-existing directory — or
|
|
216
|
+
key pair — owned by a different account is refused (`refusing
|
|
217
|
+
takeover`), never silently chowned, and that policy covers `.ssh`
|
|
218
|
+
itself: a pre-existing `.ssh` owned by neither the account nor the
|
|
219
|
+
installer is refused on both the create and the existing-key path
|
|
220
|
+
(wet and dry-run). Symlinks and
|
|
221
|
+
non-directories at declared paths are refused. Residual policy:
|
|
222
|
+
symlink refusal applies to the *declared leaf* path only —
|
|
223
|
+
intermediate path components are followed (and created with default
|
|
224
|
+
mode/ownership, since only the leaf carries declared values); the
|
|
225
|
+
manifest is root-owned, so this trusts the deployment document, not
|
|
226
|
+
the account.
|
|
227
|
+
- **Account creation** uses the platform's own tooling (darwin
|
|
228
|
+
`dscl . -create` + `createhomedir`, linux `useradd -m`), always with
|
|
229
|
+
argv arrays. Unsupported platforms are reported with a manual remedy
|
|
230
|
+
(`create: false`). Missing administrator privileges are classified
|
|
231
|
+
with an actionable message, as is a missing account without
|
|
232
|
+
`create: true`. For an *existing* account whose passwd home differs
|
|
233
|
+
from the manifest home, the drift is reported in the message; the
|
|
234
|
+
manifest home stays authoritative (key default paths derive from
|
|
235
|
+
it).
|
|
236
|
+
- **Secret hygiene.** Key pairs are generated with `ssh-keygen -t
|
|
237
|
+
ed25519`; `.ssh` is 0700, the private key 0600, the public key 0644,
|
|
238
|
+
ownership assigned to the account — and `.ssh` mode/ownership is
|
|
239
|
+
*converged* on the existing-key path too (drift predicts
|
|
240
|
+
`would_fix_mode` in dry-run and is repaired in the wet run; a
|
|
241
|
+
foreign-owned `.ssh` is a takeover refusal, see above). A pair is
|
|
242
|
+
ready only when the `.pub` fingerprint matches the public key
|
|
243
|
+
derived from the private key (`ssh-keygen -y`); unreadable or
|
|
244
|
+
mismatched pairs are `failed` items, never usable-looking
|
|
245
|
+
`exists`/`created` with no fingerprint — the unknown pair itself is
|
|
246
|
+
left untouched for manual resolution. Reports
|
|
247
|
+
carry the public
|
|
248
|
+
fingerprint only — private key material never appears in any status,
|
|
249
|
+
message, report, or diagnostic argument (the derived public key
|
|
250
|
+
exists only inside a private tmpdir for the duration of one
|
|
251
|
+
read-only comparison). Key paths inside a git
|
|
252
|
+
worktree are refused outright — judged by their PHYSICAL location
|
|
253
|
+
(existing ancestors resolved with `realpath`), so a symlinked
|
|
254
|
+
ancestor cannot smuggle a key into a repository. Planted symlinks on the
|
|
255
|
+
account-controlled paths — `.ssh` (checked with `lstat` before any
|
|
256
|
+
`mkdir`/chmod/chown, on the *existing-key* path and in dry-run too)
|
|
257
|
+
and `<key>.pub` (checked with `lstat` — including *before*
|
|
258
|
+
`ssh-keygen` on the create path, since keygen writes the `.pub` by
|
|
259
|
+
path — and written with `O_NOFOLLOW`) —
|
|
260
|
+
are refused, so a privileged run cannot be turned into an arbitrary
|
|
261
|
+
chmod/chown/write by the account.
|
|
262
|
+
- **Symlink-race hardening.** Leaf directories (`.ssh` and declared
|
|
263
|
+
directories) are created with component-wise `Dir.mkdir` — whose
|
|
264
|
+
`EEXIST` on a planted symlink refuses instead of following it — and
|
|
265
|
+
re-`lstat`ed immediately after creation: a leaf that is not a real
|
|
266
|
+
directory the installer just created is refused before any
|
|
267
|
+
chmod/chown, and a directory swapped between the owner check and a
|
|
268
|
+
mode fix is re-checked before the chmod. Key *files* never carry a
|
|
269
|
+
path-based mutation: after `ssh-keygen` the pair is re-`lstat`ed
|
|
270
|
+
(a keygen writing through a swapped-in symlink is refused before any
|
|
271
|
+
ownership transfer), and every chmod/chown on the private key and
|
|
272
|
+
the `.pub` — regenerated or not — goes through an
|
|
273
|
+
`O_NOFOLLOW`-opened file handle, so a mid-flight symlink swap
|
|
274
|
+
surfaces as `ELOOP` and is refused. On the create path the `.ssh`
|
|
275
|
+
ownership transfer is *deferred* until after the post-keygen
|
|
276
|
+
verification: while `ssh-keygen` writes by path, a freshly created
|
|
277
|
+
`.ssh` is still installer-owned 0700 and unwritable by the account,
|
|
278
|
+
so nothing can be planted inside it during the keygen window.
|
|
279
|
+
Accepted residuals: a microscopic
|
|
280
|
+
window remains between that final re-`lstat` and the path-based
|
|
281
|
+
`chmod`/`chown` on a *directory* leaf (Ruby stdlib offers no fd-based
|
|
282
|
+
directory chmod); closing it would require exotic APIs for no
|
|
283
|
+
practical gain over the guard chain above — and `ssh-keygen`
|
|
284
|
+
necessarily writes by path, so the keygen step itself retains the
|
|
285
|
+
*account-owned-home rename race*: an account that already owns the
|
|
286
|
+
parent home directory can rename the installer-owned `.ssh`
|
|
287
|
+
mid-flight and substitute its own (likewise, an `.ssh` that
|
|
288
|
+
pre-exists account-owned stays writable by the account throughout).
|
|
289
|
+
The post-keygen re-`lstat` then detects the substitution after the
|
|
290
|
+
write and refuses before any ownership transfer; whatever was
|
|
291
|
+
written lands root-owned 0600 — a file-creation primitive at an
|
|
292
|
+
attacker-chosen location, not an escalation, and inside the same
|
|
293
|
+
accepted-threat envelope as the directory residual above.
|
|
294
|
+
- **Role instruction reachability.** A file instruction source is
|
|
295
|
+
`verified` only when the account can actually reach it: the file's
|
|
296
|
+
read bits AND the search (`x`) bit of every ancestor directory of
|
|
297
|
+
the resolved path (ancestor symlinks followed) must grant the
|
|
298
|
+
account's governing POSIX class — owner, primary or supplementary
|
|
299
|
+
group, or other. A readable file behind an unsearchable directory
|
|
300
|
+
(e.g. 0644 inside a 0700 directory owned by someone else) is
|
|
301
|
+
`failed`, not `verified`. ACLs remain out of scope.
|
|
302
|
+
- **Exit codes.** A wet run with failed items prints the report, then
|
|
303
|
+
exits non-zero enumerating each `<section>/<item>: <message>`;
|
|
304
|
+
`--dry-run` exits 0 whenever a report renders (conflicts appear as
|
|
305
|
+
predicted failures). A missing, untrusted or invalid manifest is a
|
|
306
|
+
classified error naming the path, never its content.
|
|
307
|
+
|
|
308
|
+
Boundaries (what `prepare` is not):
|
|
309
|
+
|
|
310
|
+
- No project onboarding in `prepare` itself (`ace-lab project add`,
|
|
311
|
+
L8.2, is the dedicated command) — the manifest's role catalog
|
|
312
|
+
declares where each role lives and under which account; the
|
|
313
|
+
project-add step consumes that converged environment.
|
|
314
|
+
- No full `ace-lab install` orchestration (L8.3, delivered as
|
|
315
|
+
`ace-lab install`): no project onboarding, no trusted-tier grants
|
|
316
|
+
configuration, no foreign-lab refusal — `prepare` converges the
|
|
317
|
+
machine and leaves the composition to the installer.
|
|
318
|
+
- No interrupted-install resume bookkeeping (L8.4): re-running `prepare`
|
|
319
|
+
is the resume mechanism; install-level resumption composes it.
|
|
320
|
+
- No end-to-end verification under the target accounts (L8.5): real
|
|
321
|
+
multi-account proof belongs to the readiness verification step.
|
|
322
|
+
|
|
323
|
+
## Project add (L8.2)
|
|
324
|
+
|
|
325
|
+
```bash
|
|
326
|
+
ace-lab project add atlas --path /Users/ace/lab/atlas --remote git@forge:atlas/atlas.git
|
|
327
|
+
ace-lab project add atlas --path /Users/ace/lab/atlas --dry-run
|
|
328
|
+
ace-lab project add atlas --path /Users/ace/lab/atlas --format json
|
|
329
|
+
```
|
|
442
330
|
|
|
443
|
-
|
|
331
|
+
`project add` makes a project ready for team work (task 8xg.t.sbz.7.1),
|
|
332
|
+
assuming `ace-lab prepare` (L8.1) already converged the machine-level
|
|
333
|
+
accounts, directories, keys and roles. Three steps run in a fixed order:
|
|
334
|
+
|
|
335
|
+
1. **repository** — the target path becomes a Git repository with the
|
|
336
|
+
declared origin remote. A missing or empty path is cloned from
|
|
337
|
+
`--remote`; an existing repository without an origin gets one; an
|
|
338
|
+
existing repository with a *different* origin is refused as a
|
|
339
|
+
contradictory registration, and a non-empty non-repository path is
|
|
340
|
+
refused as a foreign location — never taken over, never overwritten.
|
|
341
|
+
The path itself must be the repository toplevel: a directory nested
|
|
342
|
+
inside another repository's work tree is refused as foreign.
|
|
343
|
+
2. **overseer** — the private, untracked `.ace-tasks/_overseer/` folder
|
|
344
|
+
is created (ADR-034) and its `.gitignore` entry ensured. Existing
|
|
345
|
+
`.gitignore` content and folder content are never touched; the
|
|
346
|
+
folder is never enumerated.
|
|
347
|
+
3. **registry** — the project is registered in `<path>/.ace/lab/config.yml`
|
|
348
|
+
(the project tier of the ordinary ADR-022 lab-config cascade) with
|
|
349
|
+
its `id`, `label`, absolute `path` and `remote`. Because every
|
|
350
|
+
consumer already reads that cascade, `ace-lab status` shows the
|
|
351
|
+
project (`configured`, topology counts) and `ace-lab projects` /
|
|
352
|
+
`ace-overseer projects` list it — no second registry exists, and
|
|
353
|
+
`ace-overseer` needed no read-side change. Two prerequisites for
|
|
354
|
+
that listing: grants come exclusively from the trusted tier of the
|
|
355
|
+
cascade, so listing the new project id requires a trusted-tier
|
|
356
|
+
principal grant covering it — configuration the installer (L8.3)
|
|
357
|
+
owns, not `project add` — and the project-tier document only enters
|
|
358
|
+
the cascade when the command runs from the project root (cascade
|
|
359
|
+
tiering). The `path` and `remote` registry fields are
|
|
360
|
+
installer-owned deployment data and are not exposed in the public
|
|
361
|
+
projection.
|
|
362
|
+
|
|
363
|
+
The planner chain consumes the result directly: `ace-coworker prepare`
|
|
364
|
+
provisions task worktrees from the registered repository, and the
|
|
365
|
+
overseer's private queue lives in the prepared `_overseer` folder —
|
|
366
|
+
no manual directory fixing is needed.
|
|
367
|
+
|
|
368
|
+
Statuses per step: `created|exists|updated|would_create|would_update|failed`.
|
|
369
|
+
`ok` means
|
|
370
|
+
zero failed steps; a wet run with failures prints the report first,
|
|
371
|
+
then exits non-zero naming each step. `--dry-run` predicts
|
|
372
|
+
(`would_*`) with zero mutations and exits 0.
|
|
373
|
+
|
|
374
|
+
Idempotence and refusals:
|
|
375
|
+
|
|
376
|
+
- Re-adding an already-registered project re-validates every step and
|
|
377
|
+
reports `exists` without rewriting the registry, the `.gitignore`,
|
|
378
|
+
or anything inside `_overseer`. A changed `--label` on re-add
|
|
379
|
+
updates the recorded label (display data, not identity) and reports
|
|
380
|
+
`updated`; an omitted `--label` keeps the recorded one.
|
|
381
|
+
- The same id registered with a different `path` or `remote`, or the
|
|
382
|
+
same absolute `path` registered under a different id, is refused
|
|
383
|
+
before any write.
|
|
384
|
+
- An unparseable registry document or a foreign `schema_version` is
|
|
385
|
+
refused rather than overwritten.
|
|
386
|
+
- A failed repository step fails the overseer and registry steps as
|
|
387
|
+
dependency failures instead of attempting them.
|
|
388
|
+
|
|
389
|
+
Boundaries: `project add` does not clone role accounts or keys (L8.1
|
|
390
|
+
owns those), does not configure HITL adapters or run the full install
|
|
391
|
+
(L8.3/`ace-lab install`), and adds no new state owner — the only
|
|
392
|
+
persisted artifact is the project's own lab-config cascade document.
|
|
393
|
+
|
|
394
|
+
## Install (L8.3)
|
|
395
|
+
|
|
396
|
+
```bash
|
|
397
|
+
ace-lab install # full installation from the trusted manifest
|
|
398
|
+
ace-lab install --dry-run # predict every phase without mutating
|
|
399
|
+
ace-lab install --format json
|
|
400
|
+
```
|
|
444
401
|
|
|
445
|
-
|
|
402
|
+
`install` performs the full new-lab installation (task 8xg.t.sbz.7.2)
|
|
403
|
+
from the same trusted install manifest as `prepare`, by composing the
|
|
404
|
+
existing molecules in a fixed order:
|
|
405
|
+
|
|
406
|
+
1. **Foreign-lab guard — before any mutation.** The trusted
|
|
407
|
+
authorization document `/etc/lab/ace-lab/authorization.yml` (when
|
|
408
|
+
present) must not grant projects the manifest does not declare: a
|
|
409
|
+
ready or foreign lab is refused with a classified error and zero
|
|
410
|
+
writes, identically in wet runs and `--dry-run` (prediction parity).
|
|
411
|
+
2. **prepare (L8.1)** — accounts → directories → keys → roles, exactly
|
|
412
|
+
as `ace-lab prepare` (same organism, same per-item report).
|
|
413
|
+
3. **project add (L8.2) per declared project** — repository, private
|
|
414
|
+
`_overseer`, project registry, exactly as `ace-lab project add`.
|
|
415
|
+
Projects are isolated: one failing project does not block the
|
|
416
|
+
others. When `prepare` did not converge, every project is reported
|
|
417
|
+
as a dependency failure instead of being attempted.
|
|
418
|
+
4. **grants** — the trusted-tier principal grants from the manifest's
|
|
419
|
+
`grants` section are converged into the `principals` mapping of the
|
|
420
|
+
authorization document: this is the installer-owned configuration
|
|
421
|
+
that makes the installed projects visible to `ace-lab projects` and
|
|
422
|
+
`ace-overseer projects` (see the L8.2 boundary above). Grants only
|
|
423
|
+
grow: a manifest that would remove an existing grant is refused.
|
|
424
|
+
Principals the manifest does not mention and every other section of
|
|
425
|
+
the document (e.g. HITL adapter policy) are preserved untouched —
|
|
426
|
+
and a converged re-run writes nothing at all, so the document's
|
|
427
|
+
admin-authored comments stay byte-identical. A manifest with no
|
|
428
|
+
`grants` over an absent document is a no-op (`skipped`): no empty
|
|
429
|
+
authorization file is ever created. Before any write the existing
|
|
430
|
+
document's principals are validated exactly like the read side
|
|
431
|
+
(shared validation with the grants resolver): a malformed policy is
|
|
432
|
+
refused with a classified error instead of silently rewritten, and
|
|
433
|
+
whitespace-padded principal names are normalized so they cannot
|
|
434
|
+
coexist with a stripped twin.
|
|
435
|
+
The write is atomic (temp file + rename, mode 0644). Grants are
|
|
436
|
+
configured only when the earlier phases converged — never over a
|
|
437
|
+
partially failed install (no pretend success).
|
|
438
|
+
|
|
439
|
+
Statuses: `prepare` contributes its own per-item statuses; each project
|
|
440
|
+
contributes `created|exists|updated|would_*|failed` per step; grants
|
|
441
|
+
report `created|updated|exists|skipped|would_*|failed` plus a
|
|
442
|
+
per-principal breakdown and the `kept` list (present and possibly
|
|
443
|
+
empty, so machine consumers see a stable schema). `ok` means every phase converged. A wet run with failures
|
|
444
|
+
prints the report first, then exits non-zero enumerating each
|
|
445
|
+
`<phase>/<item>: <message>`; `--dry-run` predicts with zero mutations
|
|
446
|
+
and exits 0.
|
|
447
|
+
|
|
448
|
+
Idempotence and resume design: every step is idempotent, so an
|
|
449
|
+
interrupted re-run converges where steps already completed (accounts
|
|
450
|
+
`exists`, repositories `exists`, grants `exists`) without duplicates or
|
|
451
|
+
lost configuration — the per-step record is the resume mechanism; no
|
|
452
|
+
journal exists or is needed for convergence. See
|
|
453
|
+
[Resuming an interrupted install](#resuming-an-interrupted-install-l84).
|
|
454
|
+
|
|
455
|
+
### Resuming an interrupted install (L8.4)
|
|
456
|
+
|
|
457
|
+
```bash
|
|
458
|
+
ace-lab install # same command: recognizes and finishes its own partial state
|
|
459
|
+
ace-lab install --dry-run # previews exactly which steps exist and which would run
|
|
460
|
+
```
|
|
446
461
|
|
|
447
|
-
|
|
462
|
+
An installation interrupted mid-run (crash after accounts, directories,
|
|
463
|
+
keys, a project, or part of the grants) is resumed by running the same
|
|
464
|
+
`ace-lab install` command again. Recognition is derived from the real
|
|
465
|
+
artifacts the steps read and write — there is no journal, marker file
|
|
466
|
+
or second state owner:
|
|
467
|
+
|
|
468
|
+
- already-done steps report `exists`/`verified` and are not reworked:
|
|
469
|
+
the converged directory and key pair keep their inode and content, an
|
|
470
|
+
already-cloned repository keeps its worktree (untracked files
|
|
471
|
+
included) and a converged trusted document stays byte-identical;
|
|
472
|
+
- missing steps complete: a key pair whose public half is missing
|
|
473
|
+
regenerates only that half from the surviving private key; a project
|
|
474
|
+
whose repository exists finishes only `_overseer` and the registry;
|
|
475
|
+
partially granted own principals converge while principals and
|
|
476
|
+
sections the manifest does not mention are kept;
|
|
477
|
+
- grants are configured only when prepare and every project converged
|
|
478
|
+
(no pretend success over a partial install).
|
|
479
|
+
|
|
480
|
+
Own-partial vs foreign is distinguished per step, not only for grants:
|
|
481
|
+
each already-present artifact is checked against what the manifest
|
|
482
|
+
declares before it is touched, and mismatching content is refused —
|
|
483
|
+
never taken over, never cleaned:
|
|
484
|
+
|
|
485
|
+
| Step | Foreign state | Result |
|
|
486
|
+
| --- | --- | --- |
|
|
487
|
+
| declared directory | symlink, non-directory, or foreign ownership | step fails with a resolve-manually message; projects and grants are gated as dependency failures |
|
|
488
|
+
| key pair | foreign-owned key files or `.ssh`, non-regular file | step fails with a resolve-manually message |
|
|
489
|
+
| 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) |
|
|
490
|
+
| 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 |
|
|
491
|
+
|
|
492
|
+
When resume is not possible, the wet failure names every blocking step
|
|
493
|
+
concretely (`install failed (N step(s)): <phase>/<item>: <message>`);
|
|
494
|
+
`--dry-run` predicts the same per-step refusals with zero mutations
|
|
495
|
+
and exits 0.
|
|
496
|
+
|
|
497
|
+
Boundaries: `install` does not verify readiness under the target
|
|
498
|
+
accounts (that is `verify`, below); HITL adapter configuration beyond
|
|
499
|
+
preserving the existing trusted document sections is not orchestrated
|
|
500
|
+
by this slice.
|
|
501
|
+
|
|
502
|
+
## Verify (L8.5)
|
|
503
|
+
|
|
504
|
+
### `ace-lab verify [--format table|json] [-q]`
|
|
505
|
+
|
|
506
|
+
```bash
|
|
507
|
+
ace-lab verify # per-check table; exit 0 only when ready
|
|
508
|
+
ace-lab verify --format json # stable machine report for the overseer / next tool
|
|
509
|
+
```
|
|
448
510
|
|
|
449
|
-
|
|
511
|
+
`verify` is the read-only end-of-install check that the lab is ready
|
|
512
|
+
UNDER THE TARGET ACCOUNTS — not merely for the installer's account. It
|
|
513
|
+
reads the same artifacts `install`/`prepare` derive truth from (the
|
|
514
|
+
trusted install manifest, the filesystem's permission bits and
|
|
515
|
+
ownership, the repositories, the per-project registry and the trusted
|
|
516
|
+
authorization document) and writes nothing anywhere: no journal, no
|
|
517
|
+
second state owner. (The key fingerprint probe may use an ephemeral
|
|
518
|
+
tmpdir scratch — 0600, removed afterwards — never a durable write.)
|
|
519
|
+
Because it judges converged artifacts only, a
|
|
520
|
+
fresh install and a resumed one verify identically — there is no
|
|
521
|
+
hidden gap after resume.
|
|
522
|
+
|
|
523
|
+
Per-check sections, each reported as `pass` / `fail` / `unknown` with
|
|
524
|
+
a concrete reason (never a bare boolean):
|
|
525
|
+
|
|
526
|
+
| Section | Checks, as the target account |
|
|
527
|
+
| --- | --- |
|
|
528
|
+
| accounts | account exists; home matches the manifest; login shell is usable (non-login shells block readiness) |
|
|
529
|
+
| directories | declared directory exists with the declared mode and owner |
|
|
530
|
+
| 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) |
|
|
531
|
+
| roles | instruction source readable by the role's account (the `prepare` role verifier) |
|
|
532
|
+
| 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) |
|
|
533
|
+
| 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 |
|
|
534
|
+
| registry | each project is registered in its `.ace/lab/config.yml` (visibility through the configuration cascade) |
|
|
535
|
+
| overseer | `.ace-tasks/_overseer/` exists and is ignored by Git (`git check-ignore`) |
|
|
536
|
+
|
|
537
|
+
The overall verdict is `ready` only when no check failed. A failure
|
|
538
|
+
never hides healthy data: the full report renders first (table or
|
|
539
|
+
JSON), then the command exits non-zero naming every failed check
|
|
540
|
+
concretely (`lab is not ready (N failed check(s)): <section>/<item>:
|
|
541
|
+
<reason>`). `unknown` (a network remote) is reported and counted but
|
|
542
|
+
does not block readiness — it is not a failure, and treating it as one
|
|
543
|
+
would make `verify` unusable offline. The JSON report shape is stable:
|
|
544
|
+
`ready`, `machine`, `manifest`, `authorization_path`, `checks`
|
|
545
|
+
(per-section lists), and `counts` (`pass`/`fail`/`unknown`).
|
|
546
|
+
|
|
547
|
+
Key material hygiene is inherited from the provisioner: only the
|
|
548
|
+
public fingerprint ever appears in a report; private key bytes stay
|
|
549
|
+
inside the `ssh-keygen` subprocess.
|
|
550
|
+
|
|
551
|
+
## Planned waves
|
|
552
|
+
|
|
553
|
+
> **L7 remainder:** `ace-lab status` is delivered; the remaining L7 scope
|
|
554
|
+
> (configurable `ace-overseer status` aggregation, pi continuation) lives in
|
|
555
|
+
> its own packages and is not available yet.
|
|
556
|
+
>
|
|
557
|
+
> **Planned (L8):** the preparation phase is delivered as `ace-lab
|
|
558
|
+
> prepare` (L8.1), project onboarding as `ace-lab project add` (L8.2),
|
|
559
|
+
> the full installer as `ace-lab install` (L8.3), resuming the
|
|
560
|
+
> installer's own interrupted installation as the same `ace-lab
|
|
561
|
+
> install` re-run (L8.4), and end-to-end readiness verification under
|
|
562
|
+
> the target accounts as `ace-lab verify` (L8.5).
|
|
563
|
+
|
|
564
|
+
## Boundaries
|
|
565
|
+
|
|
566
|
+
`ace-lab` owns no task, assignment, HITL, or worktree state. There is no
|
|
567
|
+
journal, authority/admission service, daemon, queue engine, command kernel, or
|
|
568
|
+
extra state cache in this package.
|
|
569
|
+
|
|
570
|
+
The installed authority/admission/journal/command path is removed in L1: the
|
|
571
|
+
`service request|status|listen`, `ace-lab assignment …`, `run`, `run-status`,
|
|
572
|
+
and `command execute` commands no longer exist. The local file-based flow does
|
|
573
|
+
not use them. The Forgejo merge adapter and its trusted configuration loader
|
|
574
|
+
remain as provider building blocks; the local flow does not invoke them.
|