ace-lab 0.1.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 +7 -0
- data/.ace-defaults/lab/config.yml +41 -0
- data/CHANGELOG.md +22 -0
- data/LICENSE +21 -0
- data/README.md +110 -0
- data/Rakefile +12 -0
- data/docs/usage.md +178 -0
- data/exe/ace-lab +19 -0
- data/lib/ace/lab/atoms/binding_freshness.rb +33 -0
- data/lib/ace/lab/atoms/public_projection.rb +98 -0
- data/lib/ace/lab/atoms/topology_schema.rb +280 -0
- data/lib/ace/lab/cli/commands/agents.rb +43 -0
- data/lib/ace/lab/cli/commands/projects.rb +43 -0
- data/lib/ace/lab/cli/commands/resolve.rb +44 -0
- data/lib/ace/lab/cli/commands/route.rb +48 -0
- data/lib/ace/lab/cli/commands/services.rb +43 -0
- data/lib/ace/lab/cli/commands/support.rb +57 -0
- data/lib/ace/lab/cli.rb +71 -0
- data/lib/ace/lab/models/query_result.rb +55 -0
- data/lib/ace/lab/models/runtime_binding.rb +44 -0
- data/lib/ace/lab/models/topology_entry.rb +74 -0
- data/lib/ace/lab/molecules/caller_authorizer.rb +63 -0
- data/lib/ace/lab/molecules/capability_router.rb +69 -0
- data/lib/ace/lab/molecules/exact_resolver.rb +56 -0
- data/lib/ace/lab/molecules/grant_resolver.rb +180 -0
- data/lib/ace/lab/molecules/inventory_query.rb +78 -0
- data/lib/ace/lab/molecules/topology_loader.rb +49 -0
- data/lib/ace/lab/organisms/topology_service.rb +105 -0
- data/lib/ace/lab/version.rb +7 -0
- data/lib/ace/lab.rb +136 -0
- metadata +203 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 54eee52265f20d22376fcdf7ac0a748be0c8ec5d7400259310a1a5a37d2f373e
|
|
4
|
+
data.tar.gz: e9b3746081e8790a2764f8dcbbd54650ddb3a23890cdb653c689c5a5d6ad7d5c
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 78d706d9b60416afdd98a12bbf98dfe90a12e2cd4105c29f3bab5b177dcdea70f8ac0753e35926107790afbdd1fc03c9e792b9c4fd85a45b1dac716122032060
|
|
7
|
+
data.tar.gz: 646f3f2fae98bc011fabf589a1175c6248f36cd4cd89a7dc2172e0010287affbb16bebe4dc33ce8af5b53e263d2743d1af7c4410c2a307c0e08b215a57aad18f
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# ace-lab default configuration
|
|
2
|
+
# Override in ~/.ace/lab/config.yml or .ace/lab/config.yml
|
|
3
|
+
#
|
|
4
|
+
# Deployed topology values are owned by the lab-config repository; this
|
|
5
|
+
# default ships a deliberately empty, safe structure so a fresh install
|
|
6
|
+
# resolves to "no topology" instead of guessed values.
|
|
7
|
+
#
|
|
8
|
+
# AUTHORIZATION GRANTS DO NOT LIVE HERE. Caller-writable cascade documents
|
|
9
|
+
# must never define who may see what (review round 4, F3). Grants come from
|
|
10
|
+
# the single deployment-controlled file /etc/lab/ace-lab/authorization.yml —
|
|
11
|
+
# a fixed path; the file and every directory on its real path must be
|
|
12
|
+
# root-owned and not group/world-writable, or queries fail closed:
|
|
13
|
+
#
|
|
14
|
+
# { "principals": { "<verified-local-uid>": { "projects": ["project-id"] } } }
|
|
15
|
+
#
|
|
16
|
+
# Topology schema (version 1):
|
|
17
|
+
# topology:
|
|
18
|
+
# projects:
|
|
19
|
+
# - { id: project-id, label: Display name }
|
|
20
|
+
# agents:
|
|
21
|
+
# - id: agent-id
|
|
22
|
+
# project: project-id
|
|
23
|
+
# role: planner
|
|
24
|
+
# capabilities: ["planning"]
|
|
25
|
+
# binding: { kind: runtime, state: active, instance_id: iid,
|
|
26
|
+
# attested_instance_id: iid }
|
|
27
|
+
# services:
|
|
28
|
+
# - id: service-id
|
|
29
|
+
# project: project-id
|
|
30
|
+
# capabilities: ["search"]
|
|
31
|
+
# default_for: ["search"]
|
|
32
|
+
# endpoint: { kind: http, url: "https://service.example/internal" }
|
|
33
|
+
# binding: { kind: service, state: active, instance_id: iid,
|
|
34
|
+
# attested_instance_id: iid }
|
|
35
|
+
|
|
36
|
+
schema_version: 1
|
|
37
|
+
|
|
38
|
+
topology:
|
|
39
|
+
projects: []
|
|
40
|
+
agents: []
|
|
41
|
+
services: []
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.1.0] - 2026-09-28
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Topology/routing CLI (`ace-lab`) addressing projects, agents, and services by
|
|
13
|
+
stable IDs: `projects`, `agents`, `services`, `resolve`, `route` commands with
|
|
14
|
+
deterministic JSON output (`--format json`).
|
|
15
|
+
- ADR-022 configuration contract for Lab topology (`schema_version: 1`) with
|
|
16
|
+
validated stable IDs, project references, capability normalization, binding
|
|
17
|
+
attestation fields, and verified-local-principal authorization mapping.
|
|
18
|
+
- Classified query outcomes: `missing`, `ambiguous`, `stale`, `unauthorized`,
|
|
19
|
+
`invalid_configuration`.
|
|
20
|
+
- Allowlist-based public projection; tokens, auth-file paths, endpoint
|
|
21
|
+
credentials, URL userinfo, query parameters, fragments, and raw pane/session
|
|
22
|
+
identifiers never appear in public output.
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Michal Czyz
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
## ace-lab
|
|
2
|
+
|
|
3
|
+
Topology and routing CLI for the Lab: address projects, agents, and services
|
|
4
|
+
by stable IDs. Part of ACE (Agentic Coding Environment).
|
|
5
|
+
|
|
6
|
+
`ace-lab` answers one question -- *what is the stable ID of the project, agent,
|
|
7
|
+
or service I mean, and what is it authorized and able to do?* -- from
|
|
8
|
+
configuration alone. It never invokes Lab, never reads credentials, tracks no
|
|
9
|
+
work, and never guesses transient pane or session identifiers.
|
|
10
|
+
|
|
11
|
+
### Install
|
|
12
|
+
|
|
13
|
+
gem install ace-lab
|
|
14
|
+
|
|
15
|
+
### Usage
|
|
16
|
+
|
|
17
|
+
Configure topology via the ADR-022 cascade (`~/.ace/lab/config.yml` or
|
|
18
|
+
`.ace/lab/config.yml`; deployed values are owned by the `lab-config`
|
|
19
|
+
repository). Authorization grants live in a separate deployment-controlled
|
|
20
|
+
file at the fixed path `/etc/lab/ace-lab/authorization.yml` — root-owned and
|
|
21
|
+
not group/world-writable, verified at every query — never in the
|
|
22
|
+
caller-writable cascade and never at a caller-selected location:
|
|
23
|
+
|
|
24
|
+
```yaml
|
|
25
|
+
# .ace/lab/config.yml — topology only
|
|
26
|
+
schema_version: 1
|
|
27
|
+
topology:
|
|
28
|
+
projects:
|
|
29
|
+
- { id: atlas, label: Atlas platform }
|
|
30
|
+
agents:
|
|
31
|
+
- id: atlas-planner
|
|
32
|
+
project: atlas
|
|
33
|
+
role: planner
|
|
34
|
+
capabilities: [planning]
|
|
35
|
+
binding: { kind: runtime, state: active, instance_id: i1, attested_instance_id: i1 }
|
|
36
|
+
services:
|
|
37
|
+
- id: atlas-search
|
|
38
|
+
project: atlas
|
|
39
|
+
capabilities: [search]
|
|
40
|
+
default_for: [search]
|
|
41
|
+
endpoint: { kind: http, url: "https://search.example.internal/query?token=secret" }
|
|
42
|
+
binding: { kind: service, state: active, instance_id: i2, attested_instance_id: i2 }
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
```yaml
|
|
46
|
+
# trusted grants file (deployment-owned)
|
|
47
|
+
principals:
|
|
48
|
+
"<verified-local-uid>": { projects: ["atlas"] }
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Query it:
|
|
52
|
+
|
|
53
|
+
ace-lab projects --format json
|
|
54
|
+
ace-lab agents --project atlas --format json
|
|
55
|
+
ace-lab services --project atlas --format json
|
|
56
|
+
ace-lab resolve --id atlas-planner --format json
|
|
57
|
+
ace-lab route --project atlas --capability search --format json
|
|
58
|
+
|
|
59
|
+
Every command prints one deterministic JSON document:
|
|
60
|
+
|
|
61
|
+
{"status":"ok","data":{"entry":{"id":"atlas-search","project":"atlas",
|
|
62
|
+
"capabilities":["search"],"default_for":["search"],
|
|
63
|
+
"binding":{"kind":"service","state":"available",
|
|
64
|
+
"endpoint":{"kind":"http","url":"https://search.example.internal"}}}}}
|
|
65
|
+
|
|
66
|
+
Failures are classified: `missing`, `ambiguous`, `stale`, `unauthorized`,
|
|
67
|
+
`invalid_configuration` -- for example a replaced agent process:
|
|
68
|
+
|
|
69
|
+
{"status":"error","error":{"code":"stale","message":"stable ID
|
|
70
|
+
\"atlas-planner\" has a stale runtime binding; ...","id":"atlas-planner",
|
|
71
|
+
"project":"atlas"}}
|
|
72
|
+
|
|
73
|
+
### Guarantees
|
|
74
|
+
|
|
75
|
+
- **Stable IDs are canonical.** Labels are display values and never resolve.
|
|
76
|
+
|
|
77
|
+
Exact match or a classified `missing` error.
|
|
78
|
+
|
|
79
|
+
- **Public output never leaks.** Tokens, auth-file paths, endpoint userinfo,
|
|
80
|
+
|
|
81
|
+
query parameters, fragments, pane/session identifiers, and instance
|
|
82
|
+
identities are excluded by an allowlist projection, not field redaction.
|
|
83
|
+
|
|
84
|
+
- **Authorization comes from the verified local process owner**, matched
|
|
85
|
+
|
|
86
|
+
against configured principals. There is no `--role` or `--principal` flag;
|
|
87
|
+
identity flags are rejected outright.
|
|
88
|
+
|
|
89
|
+
- **Routing is project-local.** Candidates are capable services of the
|
|
90
|
+
|
|
91
|
+
requested project with fresh (active + attested) bindings. Zero candidates
|
|
92
|
+
are `missing`; several equal candidates need a configured `default_for`, or
|
|
93
|
+
the result is `ambiguous`. Another project's service is never picked.
|
|
94
|
+
|
|
95
|
+
- **Read-only by contract.** No Lab invocation, no execution state, no
|
|
96
|
+
|
|
97
|
+
scheduling, no credentials -- service invocation and execution state belong
|
|
98
|
+
to separate tools.
|
|
99
|
+
|
|
100
|
+
### Non-goals
|
|
101
|
+
|
|
102
|
+
`ace-lab` is not an execution-state engine. It does not invoke services
|
|
103
|
+
(`8wr.t.qjx` owns the service contract), track Works or assignment state, or
|
|
104
|
+
schedule anything. Deployed topology values are owned by `lab-config`
|
|
105
|
+
(`8wl.t.gad`); this package defines and validates the schema.
|
|
106
|
+
|
|
107
|
+
### Development
|
|
108
|
+
|
|
109
|
+
bundle install
|
|
110
|
+
ace-test ace-lab # or: bundle exec rake test from ace-lab/
|
data/Rakefile
ADDED
data/docs/usage.md
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# ace-lab usage
|
|
2
|
+
|
|
3
|
+
Topology and routing CLI for the Lab. Resolves stable project, agent, and
|
|
4
|
+
service IDs from configuration -- without knowing transient pane IDs or
|
|
5
|
+
holding service credentials.
|
|
6
|
+
|
|
7
|
+
## Commands
|
|
8
|
+
|
|
9
|
+
All commands accept `--format json` (the only supported format; anything else
|
|
10
|
+
is rejected) and `-q/--quiet` (suppress stdout; exit semantics unchanged).
|
|
11
|
+
|
|
12
|
+
### `ace-lab projects`
|
|
13
|
+
|
|
14
|
+
List projects visible to the verified caller.
|
|
15
|
+
|
|
16
|
+
$ ace-lab projects --format json
|
|
17
|
+
{"status":"ok","data":{"projects":[{"id":"atlas","label":"Atlas platform"},{"id":"borealis"}]}}
|
|
18
|
+
|
|
19
|
+
A caller with no configured principal receives:
|
|
20
|
+
|
|
21
|
+
{"status":"error","error":{"code":"unauthorized","message":"caller has no configured authorization for any lab project"}}
|
|
22
|
+
|
|
23
|
+
### `ace-lab agents --project PROJECT`
|
|
24
|
+
|
|
25
|
+
List agents in one project.
|
|
26
|
+
|
|
27
|
+
$ ace-lab agents --project atlas --format json
|
|
28
|
+
{"status":"ok","data":{"agents":[{"id":"atlas-planner","project":"atlas","role":"planner","capabilities":["planning"],"binding":{"kind":"runtime","state":"available"}}]}}
|
|
29
|
+
|
|
30
|
+
`binding.state` is `available` when the configured runtime binding is active
|
|
31
|
+
and its instance identity matches the attestation; otherwise `stale`.
|
|
32
|
+
|
|
33
|
+
### `ace-lab services --project PROJECT`
|
|
34
|
+
|
|
35
|
+
List services in one project.
|
|
36
|
+
|
|
37
|
+
$ ace-lab services --project atlas --format json
|
|
38
|
+
{"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"}}}]}}
|
|
39
|
+
|
|
40
|
+
The endpoint is a sanitized identity: scheme, host, and explicit port only.
|
|
41
|
+
|
|
42
|
+
### `ace-lab resolve --id ID`
|
|
43
|
+
|
|
44
|
+
Resolve one entry by exact stable ID.
|
|
45
|
+
|
|
46
|
+
$ ace-lab resolve --id atlas-planner --format json
|
|
47
|
+
{"status":"ok","data":{"entry":{"id":"atlas-planner","project":"atlas","role":"planner","capabilities":["planning"],"binding":{"kind":"runtime","state":"available"}}}}
|
|
48
|
+
|
|
49
|
+
Labels are display values and never resolve: `resolve "Atlas platform"` is a
|
|
50
|
+
`missing` error.
|
|
51
|
+
|
|
52
|
+
### `ace-lab route --project PROJECT --capability CAPABILITY`
|
|
53
|
+
|
|
54
|
+
Select a configured capable service in the requested project.
|
|
55
|
+
|
|
56
|
+
$ ace-lab route --project atlas --capability search --format json
|
|
57
|
+
{"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"}}}}}
|
|
58
|
+
|
|
59
|
+
Routing never invokes the service and never grants credentials.
|
|
60
|
+
|
|
61
|
+
## Error semantics
|
|
62
|
+
|
|
63
|
+
Errors are one deterministic JSON document on stdout plus a non-zero exit.
|
|
64
|
+
|
|
65
|
+
| Code | Meaning |
|
|
66
|
+
|------|---------|
|
|
67
|
+
| `missing` | No entry with that stable ID, or no capable service in the project |
|
|
68
|
+
| `ambiguous` | Several equally capable services without a configured `default_for` |
|
|
69
|
+
| `stale` | Binding is inactive, unattested, or the instance identity was replaced |
|
|
70
|
+
| `unauthorized` | Caller's verified local identity has no principal for the project |
|
|
71
|
+
| `invalid_configuration` | Topology config violates the schema; message is actionable |
|
|
72
|
+
|
|
73
|
+
Example:
|
|
74
|
+
|
|
75
|
+
$ ace-lab resolve --id atlas-coder --format json; echo $?
|
|
76
|
+
{"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"}}
|
|
77
|
+
1
|
|
78
|
+
|
|
79
|
+
## Configuration
|
|
80
|
+
|
|
81
|
+
Schema version 1, resolved through the ADR-022 cascade (project `.ace/lab/`
|
|
82
|
+
over user `~/.ace/lab/` over gem defaults; gem defaults are an empty, safe
|
|
83
|
+
topology). Cascade documents carry **topology only**:
|
|
84
|
+
|
|
85
|
+
```yaml
|
|
86
|
+
schema_version: 1
|
|
87
|
+
topology:
|
|
88
|
+
projects:
|
|
89
|
+
- id: atlas # globally unique across all entries
|
|
90
|
+
label: Display name # display only; never resolves
|
|
91
|
+
agents:
|
|
92
|
+
- id: atlas-planner
|
|
93
|
+
project: atlas # must reference an existing project
|
|
94
|
+
role: planner
|
|
95
|
+
capabilities: [planning] # non-empty; normalized (stripped, lowercased)
|
|
96
|
+
binding:
|
|
97
|
+
kind: runtime # agents bind to runtimes, services to services
|
|
98
|
+
state: active # routeable only when active
|
|
99
|
+
instance_id: i1 # runtime identity fact
|
|
100
|
+
attested_instance_id: i1 # must equal instance_id to be fresh
|
|
101
|
+
services:
|
|
102
|
+
- id: atlas-search
|
|
103
|
+
project: atlas
|
|
104
|
+
capabilities: [search]
|
|
105
|
+
default_for: [search] # exactly one default may win among candidates
|
|
106
|
+
endpoint:
|
|
107
|
+
kind: http # http or https
|
|
108
|
+
url: "https://search.example.internal/query?token=secret" # stored, never published
|
|
109
|
+
binding: { kind: service, state: active, instance_id: i2, attested_instance_id: i2 }
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Authorization grants
|
|
113
|
+
|
|
114
|
+
Grants never live in the cascade: project and user documents are
|
|
115
|
+
caller-writable, so an `authorization` section there is rejected as
|
|
116
|
+
`invalid_configuration`. Grants come from a single deployment-controlled
|
|
117
|
+
file at the **fixed path** `/etc/lab/ace-lab/authorization.yml` (installed
|
|
118
|
+
by the `lab-config` deployment). The location is not caller-selectable —
|
|
119
|
+
there is no flag or environment override. At every query the tool verifies
|
|
120
|
+
the file and every directory on its real path are root-owned and not
|
|
121
|
+
group/world-writable, and opens the file `O_NOFOLLOW`; any failed
|
|
122
|
+
verification fails closed:
|
|
123
|
+
|
|
124
|
+
```yaml
|
|
125
|
+
principals:
|
|
126
|
+
"<verified-local-uid>": # passwd username or numeric uid of the caller
|
|
127
|
+
projects: ["atlas"]
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
The grants file is machine-global: it is validated structurally only, so a
|
|
131
|
+
principal may reference projects absent from the current directory's
|
|
132
|
+
topology. Such grants are valid but never match a locally configured
|
|
133
|
+
project. All-digit principal keys are matched as uids only — an all-digit
|
|
134
|
+
passwd username is authorized solely through its uid, so it can never
|
|
135
|
+
consume a different account's numeric-uid grant.
|
|
136
|
+
|
|
137
|
+
A missing trusted file means nobody is authorized (fail closed).
|
|
138
|
+
|
|
139
|
+
Validation of the topology cascade rejects duplicate IDs (globally unique
|
|
140
|
+
across projects, agents, services), unknown project references, malformed
|
|
141
|
+
capabilities, defaults for undeclared capabilities, unusable endpoints
|
|
142
|
+
(absolute http(s) URL with a host and a port in 1–65535; `endpoint.kind` is
|
|
143
|
+
`http` or `https`), unsupported binding kinds, and principals referencing
|
|
144
|
+
unknown projects.
|
|
145
|
+
|
|
146
|
+
**Error messages are value-free by design:** validation runs before
|
|
147
|
+
authorization, so messages use positional field locations
|
|
148
|
+
(`topology.agents[0].project references an unknown project`) and never echo
|
|
149
|
+
configured IDs, project names, or principal names — configuration defects
|
|
150
|
+
cannot disclose topology to unauthorized callers.
|
|
151
|
+
|
|
152
|
+
**Ownership:** deployed topology and authorization values are maintained by
|
|
153
|
+
the `lab-config` repository (`8wl.t.gad`). `ace-lab` defines the schema and
|
|
154
|
+
reads; it never provisions.
|
|
155
|
+
|
|
156
|
+
## Guarantees and boundaries
|
|
157
|
+
|
|
158
|
+
- Public output is built from a strict allowlist: `id`, `project`, `label`,
|
|
159
|
+
|
|
160
|
+
`role`, `capabilities`, `default_for`, binding `kind` + availability state,
|
|
161
|
+
and a sanitized endpoint identity. Tokens, auth-file paths, URL userinfo,
|
|
162
|
+
paths, query parameters, fragments, pane/session IDs, and instance
|
|
163
|
+
identities never appear in any command output.
|
|
164
|
+
|
|
165
|
+
- Caller identity comes from the verified local process owner
|
|
166
|
+
|
|
167
|
+
(username or uid). `--role`, `--principal`, and `--caller` flags are
|
|
168
|
+
rejected: authorization is a property of the caller, not an input.
|
|
169
|
+
|
|
170
|
+
- A replaced process keeps its stable ID; until re-attested, queries
|
|
171
|
+
|
|
172
|
+
classify it `stale` -- an explicit unavailable result, never a routeable
|
|
173
|
+
answer.
|
|
174
|
+
|
|
175
|
+
- No Works, assignment state, or scheduling. Service invocation belongs to
|
|
176
|
+
|
|
177
|
+
`8wr.t.qjx`; execution state to `8wr.t.qjl`. The Lab execution binary
|
|
178
|
+
(`/usr/local/bin/lab`) is never invoked or required.
|
data/exe/ace-lab
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "ace/lab"
|
|
5
|
+
|
|
6
|
+
# No args -> show help
|
|
7
|
+
args = ARGV.empty? ? ["--help"] : ARGV
|
|
8
|
+
|
|
9
|
+
# Start CLI with exception-based exit code handling (per ADR-023).
|
|
10
|
+
# No bundler/setup here: an installed gem must run without a Gemfile;
|
|
11
|
+
# use `bundle exec` in development to resolve workspace sources.
|
|
12
|
+
begin
|
|
13
|
+
exit_code = Ace::Lab::CLI.start(args)
|
|
14
|
+
exit(exit_code) if exit_code.is_a?(Integer) && exit_code.nonzero?
|
|
15
|
+
rescue Ace::Support::Cli::Error => e
|
|
16
|
+
# e.to_s carries the documented "Error: " prefix (Ace::Support::Cli::Error)
|
|
17
|
+
warn e.to_s # standard:disable Lint/RedundantStringCoercion
|
|
18
|
+
exit(e.exit_code)
|
|
19
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ace
|
|
4
|
+
module Lab
|
|
5
|
+
module Atoms
|
|
6
|
+
# Pure freshness classification for a runtime binding (spec 8wq.t.1w4).
|
|
7
|
+
# A binding is fresh only when it is explicitly active AND the configured
|
|
8
|
+
# instance identity exactly matches its attestation. Absent, inactive,
|
|
9
|
+
# or mismatched identities classify as stale — never guessed fresh.
|
|
10
|
+
module BindingFreshness
|
|
11
|
+
FRESH = "fresh"
|
|
12
|
+
STALE = "stale"
|
|
13
|
+
|
|
14
|
+
class << self
|
|
15
|
+
# @param binding [Ace::Lab::Models::RuntimeBinding, nil]
|
|
16
|
+
# @return [String] FRESH or STALE
|
|
17
|
+
def classify(binding)
|
|
18
|
+
return STALE if binding.nil?
|
|
19
|
+
return STALE unless binding.state == "active"
|
|
20
|
+
return STALE if binding.instance_id.nil? || binding.instance_id.empty?
|
|
21
|
+
return STALE unless binding.instance_id == binding.attested_instance_id
|
|
22
|
+
|
|
23
|
+
FRESH
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def fresh?(binding)
|
|
27
|
+
classify(binding) == FRESH
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "uri"
|
|
4
|
+
|
|
5
|
+
module Ace
|
|
6
|
+
module Lab
|
|
7
|
+
module Atoms
|
|
8
|
+
# Allowlist-based public projection of topology entries (spec 8wq.t.1w4).
|
|
9
|
+
# Public output is constructed exclusively from the fields below, so raw
|
|
10
|
+
# configuration values can never leak: no tokens, auth-file paths,
|
|
11
|
+
# endpoint credentials, URL userinfo, path, query parameters, fragments,
|
|
12
|
+
# pane/session identifiers, or instance identities.
|
|
13
|
+
module PublicProjection
|
|
14
|
+
class << self
|
|
15
|
+
# @return [Hash] {id, label?}
|
|
16
|
+
def project(entry)
|
|
17
|
+
public_entry = {"id" => entry.id}
|
|
18
|
+
public_entry["label"] = entry.label if entry.label
|
|
19
|
+
public_entry
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# @return [Hash] {id, project, label?, role, capabilities,
|
|
23
|
+
# binding: {kind, state}}
|
|
24
|
+
def agent(entry)
|
|
25
|
+
public_entry = {
|
|
26
|
+
"id" => entry.id,
|
|
27
|
+
"project" => entry.project,
|
|
28
|
+
"role" => entry.role,
|
|
29
|
+
"capabilities" => entry.capabilities,
|
|
30
|
+
"binding" => binding_summary(entry.binding)
|
|
31
|
+
}
|
|
32
|
+
public_entry["label"] = entry.label if entry.label
|
|
33
|
+
public_entry
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# @return [Hash] {id, project, label?, capabilities, default_for,
|
|
37
|
+
# binding: {kind, state, endpoint: {kind, url?}}}
|
|
38
|
+
def service(entry)
|
|
39
|
+
public_entry = {
|
|
40
|
+
"id" => entry.id,
|
|
41
|
+
"project" => entry.project,
|
|
42
|
+
"capabilities" => entry.capabilities,
|
|
43
|
+
"default_for" => entry.default_for,
|
|
44
|
+
"binding" => binding_summary(entry.binding).merge(endpoint_summary(entry))
|
|
45
|
+
}
|
|
46
|
+
public_entry["label"] = entry.label if entry.label
|
|
47
|
+
public_entry
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Project an entry of any kind
|
|
51
|
+
def entry(entry)
|
|
52
|
+
case entry.kind
|
|
53
|
+
when "project" then project(entry)
|
|
54
|
+
when "agent" then agent(entry)
|
|
55
|
+
else service(entry)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
# Availability state derived from freshness; never exposes
|
|
62
|
+
# instance identities or raw configured state
|
|
63
|
+
def binding_summary(binding)
|
|
64
|
+
{
|
|
65
|
+
"kind" => binding&.kind,
|
|
66
|
+
"state" => binding_freshness(binding)
|
|
67
|
+
}
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def binding_freshness(binding)
|
|
71
|
+
fresh = Ace::Lab::Atoms::BindingFreshness.fresh?(binding)
|
|
72
|
+
fresh ? "available" : "stale"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Safe endpoint identity: scheme, host, and explicit port only.
|
|
76
|
+
# Userinfo, path, query, and fragment are dropped by construction;
|
|
77
|
+
# an unparseable URL projects no endpoint identity at all.
|
|
78
|
+
def endpoint_summary(entry)
|
|
79
|
+
endpoint = entry.endpoint
|
|
80
|
+
return {} unless endpoint.is_a?(Hash)
|
|
81
|
+
|
|
82
|
+
{"endpoint" => {"kind" => endpoint["kind"], "url" => sanitize_url(endpoint["url"])}}
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def sanitize_url(url)
|
|
86
|
+
uri = URI.parse(url)
|
|
87
|
+
return nil unless uri.is_a?(URI::HTTP) || uri.is_a?(URI::HTTPS)
|
|
88
|
+
return nil if uri.host.nil? || uri.host.empty?
|
|
89
|
+
|
|
90
|
+
(uri.port == uri.default_port) ? "#{uri.scheme}://#{uri.host}" : "#{uri.scheme}://#{uri.host}:#{uri.port}"
|
|
91
|
+
rescue URI::Error, ArgumentError
|
|
92
|
+
nil
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|