ace-lab 0.9.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 -104
- data/README.md +66 -100
- data/docs/usage.md +529 -592
- 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 -23
- 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 +34 -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 -108
- 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 -92
- data/lib/ace/lab/cli/commands/command_execute.rb +0 -28
- data/lib/ace/lab/cli/commands/run.rb +0 -65
- 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/command_owner_admission.rb +0 -183
- data/lib/ace/lab/molecules/command_policy.rb +0 -158
- 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 -152
- 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/command_executor.rb +0 -220
- data/lib/ace/lab/organisms/command_run_client.rb +0 -131
- data/lib/ace/lab/organisms/ordinary_assignment_client.rb +0 -120
- data/lib/ace/lab/organisms/ordinary_authority_composition.rb +0 -153
- data/lib/ace/lab/organisms/ordinary_merge_receiver.rb +0 -679
- 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,637 +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
|
-
|
|
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
|
-
|
|
18
|
-
|
|
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
|
-
|
|
38
|
+
### `ace-lab projects`
|
|
21
39
|
|
|
22
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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: `
|
|
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
|
-
|
|
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
|
-
|
|
79
|
+
Routing selects a capable service in the requested project. It never invokes
|
|
80
|
+
the service and never grants credentials.
|
|
72
81
|
|
|
73
|
-
|
|
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
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
|
210
173
|
```
|
|
211
174
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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
|
|
237
329
|
```
|
|
238
330
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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
|
|
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
|
|
351
400
|
```
|
|
352
401
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
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
|
|
375
460
|
```
|
|
376
461
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
`
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
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'
|
|
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
|
|
528
509
|
```
|
|
529
510
|
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
unknown
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
the
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
`ace-lab
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
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.
|
|
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.
|