- add public concept guides and homepage access guidance\n- clarify granting-party and ticket terminology\n- warn about identity file migration removal in v0.6.0\n- package plugin and CLI metadata as v0.5.0
- commit
- e72a56d6dee0f61410ea9cb58be8f69a93d1bcb8
- parent
- 1d24d9f6d5c9c7a6965bf09f036b34afdd143ada
- author
- Dami <dami.etoile@gmail.com>
| M | .github/ | 77 | ++++----- |
| A | AGENTS.MD | 42 | +++++ |
| M | Cargo.lock | 10 | +- |
| M | Cargo.toml | 2 | +- |
| M | README.md | 43 | +++-- |
| M | crates/ | 22 | +- |
| M | crates/ | 8 | +- |
| M | docs/ | 54 | +++--- |
| A | docs/ | 107 | ++++++++++++ |
| M | docs/ | 6 | +- |
| A | docs/ | 131 | +++++++++++++++ |
| A | docs/ | 106 | ++++++++++++ |
| M | plugins/ | 6 | +- |
| M | plugins/ | 2 | +- |
| M | plugins/ | 22 | +- |
| M | skills/ | 26 | ++-- |
| M | web/ | 66 | ++++++++- |
| A | web/ | 257 | ++++++++++++++++++++++++++++++ |
| M | web/ | 27 | +++- |
@@ -1,51 +1,44 @@
1-dev.site v0.4.0 makes the CLI an agent-ready control surface, adds scoped brokered access2-for sandboxed workers, and separates the application identity boundary from its default3-OIDC provider.1+dev.site v0.5.0 improves the public documentation and homepage guidance for access, tickets,2+keys, and service transport.43 54 ## highlights65 7-- devsite resources list --json now reconciles remote resources with local hosting state.8-- Resource upserts and removals support --plan / --dry-run, and devsite doctor --json9- reports state-aware recovery actions without probing hosted services.10-- Endpoint identity files now use the semantic devsite-endpoint.key and11- devsite-endpoint.pub names. Existing files in the devsite configuration directory are12- migrated automatically.13-- A portable, model- and harness-neutral CLI skill documents the JSON and resident-process14- contracts. Release archives now include the complete docs, skills, and15- plugins/devsite-access bundles.16-17-## scoped service broker18-19-A sandboxed requester can generate a signed service-keyword request and retain its private20-endpoint key. A machine enrolled with the explicit service_grants:issue scope can resolve21-that keyword among services its account may access, review a dry-run, and issue a22-short-lived grant bound to the requester endpoint.23-24-Grant application requires a server-signed dsp_ approval token over the exact broker25-credential, request, resource, endpoint, and expiry. Request ids are single-use, grants26-expire within 15 minutes, ambiguous service matches require an explicit resource id, and27-revoking the broker credential removes grant sessions it issued.28-29-The optional devsite-access plugin exposes the same workflow through a portable MCP30-2026-07-28 stdio adapter. It uses server/discover and per-request protocol metadata, has31-no initialize handshake or connection-scoped session state, supports request cancellation,32-and prevents its generic CLI tool from bypassing grant planning.33-34-## authentication portability35-36-Shoo remains the default login provider, but the server now treats it as an OIDC adapter37-rather than part of the account model. Deployments can configure another public OIDC38-provider with explicit issuer, authorization, token, JWKS, client, scope, and algorithm39-settings. Accounts are keyed by verified (issuer, subject) identity pairs, and the browser40-never receives provider tokens.6+- New concept guides explain tickets, keys and endpoint identities, and the ALPN wire7+ protocol.8+- The homepage shows the delegated access workflow from request through service connection.9+- Command examples use colour to distinguish commands, subcommands, options, values, and10+ ticket values.11+- Documentation now calls the approving human or automated process the granting party.12+13+## tickets, keys, and protocol14+15+The ticket guide explains each ticket type, its issuer, use count, lifetime, storage, and16+revocation rules.17+18+The key guide explains endpoint identity, requester keys, control-plane signing keys, and19+private key storage.20+21+The ALPN guide describes `devsite/tcp/1`, its QUIC handshake, frame format, capability22+checks, and version boundary.23+24+## identity file migration25+26+Version 0.5.0 continues to move legacy `identity.key` and `identity.pub` files to27+`devsite-endpoint.key` and `devsite-endpoint.pub`.28+29+The migration checks only the dev.site config directory. It moves a legacy file only when30+the destination file does not exist.31+32+`devsite doctor` now warns that version 0.6.0 removes legacy file support.33+34+Before you upgrade to version 0.6.0, run `devsite login` or `devsite daemon run` with35+version 0.5.0 or an earlier version.4136 4237 ## upgrade notes4338 44-- Database migrations are automatic.45-- Existing CLI and daemon enrollment continues to work. Broker authority is not added to46- existing machine credentials; create a new machine ticket with the service-grant option47- enabled when a machine should act as a broker.48-- Deploy the v0.4.0 control plane before using the new access commands.39+- Database migrations are not required.40+- Existing `devsite-endpoint.key` and `devsite-endpoint.pub` files continue to work.41+- Version 0.6.0 will not read or move `identity.key` or `identity.pub`.4942 5043 Homebrew users can upgrade with:5144 @@ -0,0 +1,42 @@
1+# Role2+Write prose in ASD-STE100 Simplified Technical English. This applies to documentation, READMEs, pull-request text, error messages, release notes, and comments. It does not apply to code, identifiers, or command syntax. It is not for marketing copy, essays, or anything that needs a voice. STE strips voice on purpose.3+4+## Rules5+WORDS6+- Use one name for one thing. Do not call the same item by two different names.7+- Use the short common word: start (not begin/commence/initiate), use (not utilize/leverage), help (not facilitate), make sure (not ensure), before (not prior to), after (not subsequent to), about (not regarding/concerning), get (not obtain/acquire), show (not demonstrate), also (not additionally/furthermore/moreover).8+- Give each word one meaning. "fall" means to move down, not to decrease.9+- No marketing adjectives: seamless, robust, powerful, cutting-edge, effortless, world-class, next-generation, revolutionary.10+- British English spelling (e.g., standardise, optimise, colour).11+12+VERBS13+- Active voice. "the parser reads the file", not "the file is read by the parser".14+- Use a verb for an action. "analyse the log", not "perform an analysis of the log".15+- No stacked auxiliaries. Not "it is important to note that this may help to improve". Write "this improves X".16+- No "-ing" main verb where a simple tense works.17+18+SENTENCES19+- One instruction per sentence. Max 20 words (instruction), max 25 (descriptive).20+- No contractions. Use articles: a, an, the, this, these.21+22+PUNCTUATION23+- No semicolons. Write two sentences.24+- No em dashes or en dashes used as punctuation.25+26+STRUCTURE27+- One topic per paragraph, max six sentences.28+- For steps, use a numbered vertical list, one action per item, imperative form.29+- Put a condition before its command.30+- Write only the requested text. No preamble, no summary, no closing remarks.31+32+## Modes33+- strict: procedures, runbooks, safety text, error messages. Apply every rule and both length caps.34+- STE-flavoured: general prose (READMEs, PR descriptions, docs). Apply the sentence, paragraph, active-voice, and verb discipline. Relax the dictionary lockdown so the text keeps enough range to read naturally.35+36+## Self-Linting Checklist (Run silently before outputting)37+1. Is any sentence over 20 words? Split it.38+2. Are there any semicolons or dash punctuation marks? Remove them.39+3. Are there any contractions? Expand them.40+4. Is there passive voice with a known actor? Make it active.41+5. Is there an "-ing" main verb, nominalisation, or phrasal verb? Replace with a plain verb.42+6. Is the same concept named two different ways? Pick one name.@@ -821,7 +821,7 @@ dependencies = [
821821 822822 [[package]]823823 name = "devsite-cli"824-version = "0.4.0"824+version = "0.5.0"825825 dependencies = [826826 "anyhow",827827 "clap",@@ -842,7 +842,7 @@ dependencies = [
842842 843843 [[package]]844844 name = "devsite-client"845-version = "0.4.0"845+version = "0.5.0"846846 dependencies = [847847 "anyhow",848848 "devsite-proto",@@ -854,7 +854,7 @@ dependencies = [
854854 855855 [[package]]856856 name = "devsite-daemon"857-version = "0.4.0"857+version = "0.5.0"858858 dependencies = [859859 "anyhow",860860 "data-encoding",@@ -873,7 +873,7 @@ dependencies = [
873873 874874 [[package]]875875 name = "devsite-proto"876-version = "0.4.0"876+version = "0.5.0"877877 dependencies = [878878 "data-encoding",879879 "ed25519-dalek 2.2.0",@@ -885,7 +885,7 @@ dependencies = [
885885 886886 [[package]]887887 name = "devsite-server"888-version = "0.4.0"888+version = "0.5.0"889889 dependencies = [890890 "anyhow",891891 "axum",@@ -72,6 +72,12 @@ expiry, and one-use nonce before opening the configured loopback target.
7272 7373 The transport ALPN is `devsite/tcp/1`.7474 75+Concept guides explain the security boundaries and lifecycle in detail:76+77+- [Tickets](docs/tickets.md)78+- [Keys and endpoint identities](docs/keys-and-identities.md)79+- [ALPN and the service wire protocol](docs/alpn-and-wire-protocol.md)80+7581 ## Services and links7682 7783 Services default to private:@@ -158,31 +164,32 @@ agent skill that teaches these intent-based workflows through the CLI's JSON con
158164 at [`skills/devsite-cli/SKILL.md`](skills/devsite-cli/SKILL.md); it has no dependency on a159165 particular model or agent harness.160166 161-## Brokered agent access167+## Delegated agent access162168 163169 A sandboxed worker can request temporary service access without receiving a logged-in164170 machine's credential or endpoint key:165171 166172 ```bash167-# Requester: keep requester.key private; send only request.json to the broker.173+# Requester: keep requester.key private; send only request.json to the granting party.168174 devsite access request postgres --request request.json --key requester.key --json169175 170-# Broker: requires a machine ticket enrolled with service_grants:issue.176+# Granting party: requires a machine ticket enrolled with service_grants:issue.171177 devsite access resolve postgres --json172178 devsite access grant --request request.json --plan --json173179 devsite access grant --request request.json --approved-plan dsp_... --json174180 175-# Requester: use the server and dss_ grant returned by the broker.181+# Requester: use the server and dss_ grant returned by the granting party.176182 devsite --server https://dev.site access connect dss_... --key requester.key --json177183 ```178184 179185 The request signs the service keyword, requester endpoint, request id, and expiry. The180-broker resolves that intent only among services its account may access, signs the chosen181-resource and expiry, and receives a single-use, short-lived grant bound to the requester's182-endpoint. Requests expire within 10 minutes; grants within 15 minutes. Replaying an issued183-request id is rejected. Apply requires the server-signed `dsp_…` token returned by the184-exact reviewed plan, so changing the request, resource, endpoint, or expiry after approval185-is rejected. The dashboard exposes broker authority as an explicit186+granting party resolves that intent only among services its account may access, signs the187+chosen resource and expiry, and receives a short-lived session bound to the requester's188+endpoint. Requests expire within 10 minutes; sessions within 15 minutes. Replaying an189+issued request id is rejected, while the resulting session can authorize multiple TCP190+connections during its lifetime. Apply requires the server-signed `dsp_…` ticket returned191+by the exact reviewed plan, so changing the request, resource, endpoint, or expiry after192+approval is rejected. The dashboard exposes grant authority as an explicit186193 `service_grants:issue` scope on the single-use machine enrollment ticket, and revoking that187194 machine credential removes the grant sessions it issued.188195 @@ -190,7 +197,7 @@ The optional [`plugins/devsite-access`](plugins/devsite-access) bundle packages
190197 harness-neutral skill with a local MCP stdio adapter. The adapter implements MCP191198 `2026-07-28` discovery and request-scoped metadata: there is no `initialize` exchange,192199 negotiated session, or call-order state. Any compatible MCP client can use the adapter; the193-plugin manifest is only a packaging layer. See [brokered access](docs/access-broker.md) for200+plugin manifest is only a packaging layer. See [delegated access](docs/access-broker.md) for194201 the trust model and integration contract.195202 196203 Homebrew installs can supervise it at login on macOS or Linux:@@ -205,12 +212,14 @@ Native Linux packages may install `packaging/systemd/devsite.service` as a user
205212 systemctl --user enable --now devsite.service206213 ```207214 208-The endpoint identity persists in the devsite config directory as209-`devsite-endpoint.key`, with its Ed25519 public key written to210-`devsite-endpoint.pub`. The daemon registers that public endpoint id with the control plane211-once at startup, publishes its address through Iroh, and reloads service targets and212-authorizations every two seconds. Adding or removing a service does not require restarting213-it.215+Before you upgrade to version 0.6.0, run `devsite login` or `devsite daemon run` with an216+earlier version. That version moves legacy `identity.key` and `identity.pub` files only217+from the dev.site config directory. Version 0.6.0 uses `devsite-endpoint.key` and218+`devsite-endpoint.pub`. It does not read or move the legacy files. See219+[Keys and endpoint identities](docs/keys-and-identities.md) for the migration rules. The220+daemon registers that public endpoint id with the control plane at startup, publishes its221+address through Iroh, and reloads service targets and authorizations every two seconds.222+Adding or removing a service does not require a restart.214223 215224 `devsite daemon status` reports whether this config directory has a live daemon. The same216225 liveness appears in `devsite status` alongside the config location, pinned signing key, and@@ -61,7 +61,7 @@ enum Command {
6161 /// Host local TCP services.6262 6363 Service(ServiceCommand),64- /// Request and broker short-lived endpoint-bound service access.64+ /// Request and grant short-lived endpoint-bound service access.6565 6666 Access(AccessCommand),6767 /// Forward a local TCP port to a service you may access.@@ -157,9 +157,9 @@ enum ServiceCommand {
157157 enum AccessCommand {158158 /// Create a signed request without contacting the control plane.159159 Request {160- /// Human service keyword for a trusted broker to resolve.160+ /// Human service keyword for a trusted granting party to resolve.161161 service: String,162- /// Public JSON request to hand to the broker.162+ /// Public JSON request to give to the granting party.163163 164164 request: std::path::PathBuf,165165 /// Private endpoint key retained by the requesting sandbox.@@ -169,7 +169,7 @@ enum AccessCommand {
169169 170170 ttl: u64,171171 },172- /// Find services this broker credential may delegate.172+ /// Find services that this granting party may delegate.173173 Resolve { keyword: String },174174 /// Validate or issue a grant for a signed request.175175 Grant {@@ -189,9 +189,9 @@ enum AccessCommand {
189189 190190 approved_plan: Option<String>,191191 },192- /// Forward a local TCP port using an endpoint-bound broker grant.192+ /// Forward a local TCP port with an endpoint-bound delegated access grant.193193 Connect {194- /// Session grant returned by the broker.194+ /// Session grant returned by the granting party.195195 grant: String,196196 /// Private endpoint key created with `access request`.197197 @@ -1251,7 +1251,7 @@ fn create_access_request(
12511251 } else {12521252 println!("created signed request for {service}");12531253 println!(1254- " request {} (share with the broker)",1254+ " request {} (give to the granting party)",12551255 request_path.display()12561256 );12571257 println!(@@ -1344,7 +1344,7 @@ async fn grant_access(
13441344 .iter()13451345 .any(|item| item.resource_id == claims.resource_id)13461346 {1347- bail!("the approved service is no longer accessible to this broker");1347+ bail!("the approved service is no longer accessible to this granting party");13481348 }13491349 claims.resource_id.clone()13501350 }@@ -1453,7 +1453,7 @@ async fn connect_grant(
14531453 output: Output,14541454 ) -> Result<()> {14551455 if !grant.starts_with("dss_") {1456- bail!("broker grant must start with dss_");1456+ bail!("delegated access grant must start with dss_");14571457 }14581458 let raw = std::fs::read(key_path).with_context(|| format!("reading {}", key_path.display()))?;14591459 let bytes: [u8; 32] = raw@@ -1466,7 +1466,7 @@ async fn connect_grant(
14661466 let session: TunnelSessionInfo = api14671467 .get("/api/tunnel/session")14681468 .await1469- .context("validating broker grant")?;1469+ .context("validating delegated access grant")?;14701470 if session.requester_endpoint_id != client.endpoint_id().to_string() || !session.brokered {14711471 bail!("the grant is not bound to the supplied requester endpoint key");14721472 }@@ -2164,7 +2164,7 @@ async fn doctor(paths: &Paths, output: Output) -> Result<()> {
21642164 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {21652165 let legacy = paths.root.join("identity.key");21662166 let message = if legacy.exists() {2167- "A legacy identity.key exists and will migrate on the next login or daemon start."2167+ "Legacy identity.key and identity.pub support ends in version 0.6.0. Run `devsite login` or `devsite daemon run` before you upgrade. The command moves these files."21682168 } else {21692169 "No endpoint identity exists yet."21702170 };@@ -192,8 +192,10 @@ impl Paths {
192192 Ok(key)193193 }194194 195- /// Move endpoint identity files written by CLI versions before the filenames196- /// identified their purpose. Existing destination files always win.195+ /// Move legacy endpoint identity files to purpose-specific names.196+ /// Remove this migration helper in version 0.6.0.197+ /// The migration reads files only from this dev.site config directory.198+ /// Existing destination files take precedence.197199 fn migrate_legacy_identity_files(&self) -> Result<()> {198200 for (legacy, current) in [199201 (self.root.join("identity.key"), self.identity()),@@ -306,7 +308,7 @@ mod tests {
306308 }307309 308310 309- fn legacy_identity_filenames_migrate_without_changing_the_key() {311+ fn legacy_identity_files_migrate_before_version_0_6_0_removes_compatibility() {310312 let paths = test_paths();311313 std::fs::create_dir_all(&paths.root).unwrap();312314 let original = SecretKey::generate();@@ -1,24 +1,24 @@
1-# Brokered endpoint access1+# Delegated endpoint access22 3-Brokered access lets a sandboxed worker obtain a narrowly bounded service grant without4-giving that worker a person's browser session, a machine credential, or the broker3+Delegated access lets a sandboxed worker obtain narrowly bounded service access without4+giving that worker a person's browser session, a machine credential, or the granting55 machine's endpoint key.66 77 ## Actors and authority88 99 - The **requester** owns a fresh, temporary Ed25519 endpoint key and creates a signed service10- request. Its public request is safe to hand to a broker; its private key is not.11-- The **broker** is an ordinary enrolled machine whose single-use enrollment ticket carried12- the `service_grants:issue` scope. The server stores that scope on the resulting revocable13- machine credential.14-- The **control plane** verifies both signatures, enforces the broker scope and account10+ request. Its public request is safe to hand to the granting party; its private key is not.11+- The **granting party** is a human or automated process operating an ordinary enrolled12+ machine whose single-use enrollment ticket carried the `service_grants:issue` scope. The13+ server stores that scope on the resulting revocable machine credential.14+- The **control plane** verifies both signatures, enforces the grant scope and account1515 permissions, rejects replays, and issues an endpoint-bound tunnel session.1616 - The **service daemon** still verifies the resulting one-use transport capability before1717 connecting to its local loopback target.1818 19-Broker scope is delegated at enrollment instead of inferred from possession of any machine19+Grant scope is delegated at enrollment instead of inferred from possession of any machine2020 credential. The dashboard must explicitly include it when minting the registration ticket.21-Revoking the resulting machine credential removes grant sessions issued by that broker.21+Revoking the resulting machine credential removes grant sessions issued by it.2222 2323 ## Signed handoff2424 @@ -30,40 +30,41 @@ devsite --json access request postgres \
3030 --key /private/requester.key3131 ```3232 33-The request signature covers a domain separator, schema version, request id, service34-keyword, requester endpoint id, and expiry. The command creates new files rather than35-overwriting paths and keeps both private by default. Only the request JSON crosses the36-sandbox boundary.33+The server separately requires schema version 1. The request signature covers a domain34+separator, request id, service keyword, requester endpoint id, and expiry. The command35+creates new files rather than overwriting paths and keeps both private by default. Only the36+request JSON crosses the sandbox boundary.3737 38-The broker first resolves and plans:38+The granting party first resolves and plans:3939 4040 ```sh4141 devsite --json access resolve postgres4242 devsite --json access grant --request /inbox/request.json --plan4343 ```4444 45-Resolution searches only owned services and accepted service shares visible to the broker.45+Resolution searches only owned services and accepted service shares visible to the granting46+account.4647 An exact unique name is selected automatically. Ambiguous matches require an explicit4748 `--resource res_…`. The plan verifies the requester signature and reports the exact4849 resource, owner, requester endpoint, request id, and expiry without issuing a grant.4950 50-After user or policy approval, the broker applies the same intent:51+After user or policy approval, the granting party applies the same intent:5152 5253 ```sh5354 devsite --json access grant --request /inbox/request.json \5455 --approved-plan dsp_…5556 ```5657 57-The broker's persistent `devsite-endpoint.key` signs the request id, selected resource,58-requester endpoint, and grant expiry. The server also verifies that the signed request's58+The granting machine's persistent `devsite-endpoint.key` signs the request id, selected59+resource, requester endpoint, and grant expiry. The server also verifies that the signed request's5960 keyword still resolves to that resource. It rejects expired requests, grants longer than 1560-minutes, duplicate request ids, credentials without broker scope, and resources outside the61-broker account's current access. The `dsp_…` token is signed by the control plane over the62-broker credential, request id and keyword, resource, endpoint, request expiry, and grant61+minutes, duplicate request ids, credentials without grant scope, and resources outside the62+granting account's current access. The `dsp_…` token is signed by the control plane over the63+granting credential, request id and keyword, resource, endpoint, request expiry, and grant6364 expiry. Apply requires it, preventing a request file or selected target from being swapped6465 after approval.6566 66-The broker returns only `result.grant` and `result.server`. The requester then runs:67+The granting party returns only `result.grant` and `result.server`. The requester then runs:6768 6869 ```sh6970 devsite --server SERVER --json access connect GRANT \@@ -71,13 +72,14 @@ devsite --server SERVER --json access connect GRANT \
7172 ```7273 7374 The connector recreates the requested endpoint from that private key, validates that the74-grant is broker-issued and bound to the same endpoint, and emits resident NDJSON lifecycle75+grant is delegated and bound to the same endpoint, and emits resident NDJSON lifecycle7576 events. It refuses non-loopback listeners.7677 7778 ## Expiry and revocation7879 79-Requests expire within 10 minutes and request ids are single-use at issuance. Grants expire80-within 15 minutes. A broker credential has a bounded number of simultaneous active grant80+Requests expire within 10 minutes and request ids are single-use at issuance. Grant81+sessions expire within 15 minutes and may mint one capability per TCP connection during82+that window. A granting credential has a bounded number of simultaneous active grant8183 sessions. Credential revocation deletes those sessions and prevents further capabilities8284 from being minted. A one-use transport capability already minted immediately before8385 revocation remains limited by its own short expiry and nonce.@@ -0,0 +1,107 @@
1+# ALPN and the service wire protocol2+3+dev.site currently defines one application protocol for service traffic:4+5+| ALPN | Purpose | Transport |6+| --- | --- | --- |7+| `devsite/tcp/1` | Authorize and carry arbitrary TCP byte streams | Iroh QUIC, direct or relayed |8+9+ALPN means Application-Layer Protocol Negotiation. During the QUIC/TLS handshake, the10+client requests `devsite/tcp/1` and the daemon advertises that exact value. This selects the11+wire protocol before either side interprets an application frame.12+13+## Where it fits14+15+The dev.site control plane and service path are separate:16+17+```text18+CLI ── HTTPS ──► dev.site control plane19+ │ session validation + capability issuance20+ │21+ └── Iroh QUIC, ALPN devsite/tcp/1 ──► owner's daemon ── TCP ──► loopback service22+ encrypted service bytes23+```24+25+`devsite/tcp/1` is not the protocol used by the HTTPS API, OIDC login, the local MCP stdio26+adapter, or the hosted application. The application behind a service can speak PostgreSQL,27+HTTP, SSH, Redis, or any other TCP protocol; after authorization, dev.site treats its bytes28+as opaque.29+30+## One connection and one stream31+32+The client keeps one authenticated QUIC connection per daemon and can open many33+bidirectional streams on it. Each forwarded local TCP connection uses a new QUIC stream:34+35+1. The client opens a bidirectional stream.36+2. It sends one length-prefixed Postcard `Connect` request containing a signed capability.37+3. The daemon verifies the control-plane signature, daemon audience, authenticated client38+ endpoint, expiry, permission, resource, and one-use nonce.39+4. The daemon connects to the resource's fixed loopback TCP target.40+5. It sends `Connected`, or a deliberately coarse error.41+6. After `Connected`, both sides copy uninterpreted TCP bytes until half-close or shutdown.42+43+The control plane is contacted to mint the capability, but it is not on this data path and44+never sees the service bytes or local target port.45+46+## Frame format47+48+The request and response each use this envelope:49+50+```text51+4-byte little-endian payload length52+Postcard-encoded payload53+```54+55+The length is checked before allocation and may not exceed 4 MiB. Version 1 has one request56+variant, `Connect { capability }`, and these responses:57+58+- `Connected`;59+- `Error { Denied }` for invalid authorization, unknown resources, or capability replay;60+- `Error { BadRequest }` for malformed protocol input; and61+- `Error { UpstreamUnavailable }` when the configured local TCP service does not accept a62+ connection.63+64+Authorization failures intentionally collapse to `Denied`. A remote peer cannot use error65+differences to discover whether a resource exists, whether it is hosted, or which66+capability check failed.67+68+Once `Connected` is sent, there are no more dev.site frames on that stream. Everything69+after it is the service's raw byte stream.70+71+## Security properties72+73+Iroh QUIC encrypts the network and authenticates endpoint public keys. The capability then74+adds application authorization:75+76+- its server signature identifies the control plane that issued it;77+- its audience binds it to one daemon endpoint;78+- its client key must match the connection's authenticated remote endpoint;79+- its resource maps only to a fixed loopback target configured by the host;80+- its permission is currently only `TcpConnect`;81+- its expiry limits it to three minutes; and82+- its nonce is accepted only once by the daemon.83+84+ALPN contributes protocol separation and version selection, not user authorization. A85+client that negotiates `devsite/tcp/1` still gets no service access without a valid86+capability. Conversely, capability bytes sent under some other protocol are never parsed87+as a dev.site request.88+89+Iroh may establish a direct path or use a relay. Relays carry encrypted QUIC traffic and do90+not terminate the dev.site application stream. The final daemon-to-service hop is local TCP91+on the host and uses whatever encryption, if any, the service protocol provides.92+93+## Versioning94+95+The `/1` suffix is a compatibility boundary for framing and semantics. A change that old96+peers cannot safely decode or interpret should use a new ALPN rather than silently changing97+version 1. The current client requests one ALPN and the daemon advertises one ALPN; there is98+no dev.site protocol fallback or multi-version negotiation today.99+100+The public `/api/config` response reports the expected daemon protocol. `devsite doctor`101+compares it with the CLI's compiled `devsite/tcp/1` value so a control-plane/client mismatch102+is visible before debugging the data path.103+104+Adding another ALPN should define its exact request and response frames, authorization105+checks, maximum sizes, transition from framing to application bytes, compatibility policy,106+and diagnostic behavior. A new service application protocol alone does not require a new107+ALPN because version 1 already transports arbitrary TCP bytes.@@ -53,7 +53,7 @@ file permissions, server/API compatibility, the pinned signing key, credential b
5353 daemon registration, and remote/local resource drift. It does not open connections to5454 hosted services.5555 56-## Brokered access56+## Delegated access5757 5858 `devsite access request SERVICE --request FILE --key FILE --json` creates a public signed5959 request and a separate private requester endpoint key. Its result contains@@ -68,12 +68,12 @@ name, optional owner handle, and exact-name-match state.
6868 `devsite access grant --request FILE --plan --json` validates and resolves the signed6969 request without issuing a grant. If resolution is ambiguous, supply an approved7070 `--resource res_…`. The plan returns a server-signed `approved_plan` token covering the71-exact broker credential, request, resource, endpoint, and expiry. Apply with71+exact granting party credential, request, resource, endpoint, and expiry. Apply with7272 `--approved-plan dsp_…`; changed inputs are rejected. A successful apply returns the request7373 id, resource id, name and owner, requester endpoint id, expiry, short-lived `dss_…` grant,7474 and `server`. Treat the grant as a secret and return only the grant and server to the7575 requester.7676 7777 `devsite access connect GRANT --key FILE --json` is resident NDJSON. It validates that the78-grant is broker-issued and bound to the supplied key, then emits the same `listening`,78+grant was issued by the granting party and is bound to the supplied key. It then emits the same `listening`,7979 `connection`, error, and shutdown lifecycle records as ordinary `connect`.@@ -0,0 +1,131 @@
1+# Keys and endpoint identities2+3+dev.site uses Ed25519 keys for three jobs: identifying Iroh endpoints, proving that a4+request came from the endpoint it names, and signing control-plane authorization. A5+public endpoint id is safe to share. Its matching private key is what proves possession.6+7+## Key inventory8+9+| Key | Lifetime and storage | What it proves |10+| --- | --- | --- |11+| Machine endpoint key | Persistent; `devsite-endpoint.key` in the dev.site config directory | The enrolled machine and its daemon are the same endpoint |12+| Machine endpoint public key | Persistent; `devsite-endpoint.pub` and the endpoint id registered with the server | Where to address the daemon and which key capabilities name as their audience |13+| Ordinary connector key | Ephemeral; process memory only | Which client redeemed a `dst_…` ticket and opened the Iroh connection |14+| Delegated requester key | Temporary; a caller-selected file such as `requester.key` | Which endpoint signed the request and may use the resulting grant |15+| Control-plane signing key | Persistent operator secret; environment secret or `capability_signing.key` | That a plan or stream capability was issued by this control plane |16+| Pinned control-plane public key | Persistent; stored in the machine's private `config.json` | Which control-plane signatures the daemon will trust |17+18+All are 32-byte Ed25519 private keys or their public halves. A displayed Iroh endpoint id19+is the encoded public key, not a separate secret identifier.20+21+## Persistent machine endpoint identity22+23+The CLI creates the machine endpoint key during first login or daemon startup and keeps it24+across restarts:25+26+```text27+dev.site config directory/28+├── devsite-endpoint.key private, raw 32 bytes29+├── devsite-endpoint.pub public endpoint id, text30+└── config.json private machine credential + pinned server public key31+```32+33+Set `DEVSITE_HOME` to choose the config directory explicitly. Otherwise the CLI uses the34+operating system's application config location. On Unix, private files are created with35+mode `0600`; the public `.pub` file does not need private permissions.36+37+During enrollment, the CLI signs a domain-separated endpoint-proof message. The server38+checks that proof before binding the returned machine credential to the public endpoint id.39+Daemon registration repeats a possession proof. A scoped granting party also uses this40+same key to sign the chosen resource, requester endpoint, request id, and grant expiry.41+42+Capabilities sent to this daemon name its public endpoint key as the audience. The daemon43+rejects a capability issued for another endpoint even if every other claim is valid.44+45+### Prepare for version 0.6.046+47+Before you upgrade to version 0.6.0, run `devsite login` or `devsite daemon run` with an48+earlier version. That version moves `identity.key` and `identity.pub` to the new names.49+50+The earlier version checks only these exact names in the resolved dev.site config directory.51+It does not scan the current directory, home directory, SSH directory, or filesystem for52+similar names.53+54+The earlier version moves a legacy file only when the target file does not exist:55+56+```text57+identity.key → devsite-endpoint.key58+identity.pub → devsite-endpoint.pub59+```60+61+An existing `devsite-endpoint.key` or `devsite-endpoint.pub` takes priority. Version 0.6.062+does not read or move `identity.key` or `identity.pub`.63+64+## Client endpoint keys65+66+For ordinary `devsite connect dst_…`, the CLI creates a fresh Iroh key in memory. The67+public half is supplied when the ticket is redeemed, the resulting `dss_…` session is68+bound to it, and every per-stream capability repeats that binding. The daemon compares the69+capability's client key with the authenticated remote endpoint from the QUIC connection.70+A copied session or capability is therefore insufficient without the private key.71+72+The key disappears when the connector process exits. A new process cannot resume that73+session, even if it has copied the `dss_…` ticket.74+75+## Delegated requester keys76+77+`devsite access request` creates a temporary requester key in a caller-selected file:78+79+```sh80+devsite access request postgres \81+ --request request.json \82+ --key requester.key83+```84+85+Both paths are created without overwriting existing files and use private permissions on86+Unix. The request JSON contains the public endpoint id and a signature over the request id,87+service keyword, endpoint id, and expiry. Only `request.json` should cross the sandbox or88+approval boundary. `requester.key` stays with the requester and is later supplied to89+`devsite access connect`.90+91+The key file itself does not expire, but every request and resulting session does. Delete92+it after the request and any granted connection are finished. If the key leaks while a93+grant is active, revoke the issuing machine credential or underlying service access and94+stop the connector; waiting for the grant's 15-minute maximum lifetime also bounds the95+exposure.96+97+## Control-plane signing key98+99+The control plane has one Ed25519 signing key shared by two authorization formats:100+101+- `dsp_…` service-grant approval plans; and102+- one-stream service capabilities.103+104+Production can supply the private key through `DEVSITE_SIGNING_KEY`. A local deployment105+without that setting loads or creates `capability_signing.key` in the server state106+directory with private permissions. The private key must never be distributed to clients107+or daemons.108+109+At login, the CLI fetches the public half from `/api/pubkey` and pins it in `config.json`.110+The daemon refuses to start without a valid pinned key and verifies every capability111+against it. `devsite doctor` compares the currently published server key with the pinned112+value and reports a change rather than silently trusting it.113+114+Losing the private signing key prevents the server from minting artifacts that existing115+daemons trust. Compromise permits forging plans and capabilities until clients move to a116+new trust root. Back it up as an operator secret and treat rotation as an explicit117+re-enrollment or re-pinning event, not a routine file replacement.118+119+## What compromise permits120+121+| Compromised material | Consequence | Recovery |122+| --- | --- | --- |123+| Machine endpoint private key only | Endpoint impersonation is possible, but control-plane API calls still require the machine credential | Revoke the named machine credential, remove the key, and enroll a new endpoint |124+| Machine credential only | Authenticated control-plane actions within its scopes; it cannot satisfy endpoint proofs without the key | Revoke the named machine credential and log in again |125+| Machine key and credential | Full authority of that enrolled machine and its scopes | Revoke immediately and re-enroll with a new key |126+| Ephemeral connector key | Useful only with its still-live bound session or capability | Stop the connector/delete its session; ordinary key vanishes with the process |127+| Delegated requester key | Use of a still-live grant bound to that requester | Stop/revoke the grant path and delete the key |128+| Control-plane signing key | Forged approval plans and service capabilities | Rotate the trust root and re-pin or re-enroll every daemon |129+130+A `dsm_…` machine ticket is a bearer ticket, not a cryptographic key, even though the CLI131+stores it beside key material. See [Tickets](tickets.md) for its lifecycle.@@ -0,0 +1,106 @@
1+# Tickets2+3+A ticket is a serializable object that packages the information required for an operation.4+dev.site passes tickets between the browser, CLI, control plane, and daemon. Each ticket5+type defines its own use count, lifetime, binding, storage, and revocation rules.6+7+The control plane issues most tickets. It stores a SHA-256 hash for opaque bearer tickets.8+It cannot recover their plaintext values. Signed tickets carry their own claims and signature.9+10+## Ticket inventory11+12+| Ticket | Purpose | Issuer | Consumer | Uses | Lifetime | Binding | Storage | Revocation |13+| --- | --- | --- | --- | --- | --- | --- | --- | --- |14+| `dmt_…` | Enroll one machine | Control plane, at a signed-in user's request | CLI and control plane | One | Until enrollment or revocation | The enrolled machine endpoint after redemption | Control plane hash, then CLI input | Revoke the named machine entry before use |15+| `dsm_…` | Authenticate one enrolled machine | Control plane after enrollment | CLI, daemon, and control plane | Many | No automatic expiry | Bearer ticket for one named machine. Enrollment and daemon registration also require endpoint proof. | CLI private `config.json`, control plane hash | Revoke the machine ticket |16+| `dst_…` | Start one service connection | Control plane, at an authorised browser's request | `devsite connect` and control plane | One | Two minutes | One viewer and service. The session then binds to the client endpoint. | CLI input, control plane hash | Consume or expire the ticket. Removing access also prevents redemption. |17+| `dss_…` | Request stream capabilities | Control plane after `dst_…` redemption or approved delegated access | Connector and control plane | Many | Eight hours. Delegated sessions last at most 15 minutes. | Client endpoint. Also tied to its issuer credential when delegated. | Connector memory, control plane hash | Delete or expire the session. Revoke its issuer credential, resource, account, or access. |18+| `dsp_…` | Carry one reviewed delegated approval | Control plane signing key | Granting client and control plane | One grant for one request id | Limited by request and grant expiries | Request id, service, issuer ticket, resource, requester endpoint, and expiries | Caller-provided plan file or memory. It has no server-side plaintext row. | The ticket cannot create a grant after its request or grant expires. Revoke the issuer ticket or underlying access. |19+| Stream ticket (signed capability) | Open one service stream | Control plane signing key | Daemon | One | Three minutes | Client endpoint, daemon endpoint, viewer, resource, and `TcpConnect` permission | Connector memory and stream handshake | Its nonce is consumed once. The daemon closes streams after access revocation or a stale authorisation snapshot. |20+| Browser session | Keep a user signed in to the website | Control plane after sign-in | Browser and control plane | Many | Seven days | Bearer ticket with no endpoint-key binding | HttpOnly browser cookie and control plane hash | Log out or let the ticket expire |21+22+## Machine enrollment ticket: `dmt_…`23+24+A signed-in user creates a named `dmt_…` ticket on the dashboard. The ticket can carry25+machine scopes such as `service_grants:issue`.26+27+The CLI presents the ticket with a public endpoint key and proof from its matching private28+key. The control plane atomically consumes the ticket, binds the machine entry to that29+endpoint, and returns a `dsm_…` machine ticket.30+31+`dmt_…` tickets have no automatic expiry. Create one when needed and use it promptly. A32+leak can enroll one endpoint until use or revocation. Successful enrollment prevents reuse.33+34+## Machine ticket: `dsm_…`35+36+The `dsm_…` ticket lets the CLI and daemon make authenticated control-plane requests. It37+does not travel over an Iroh service connection. Enrollment and daemon registration also38+require proof from the persistent Ed25519 endpoint key.39+40+The CLI stores this ticket in private `config.json`. The control plane stores only its hash.41+Revoking it stops later authenticated use, unregisters its bound daemon, and deletes its42+delegated tunnel sessions.43+44+## Service connection ticket: `dst_…`45+46+An authorised browser creates a `dst_…` ticket for one viewer and service. It expires after47+two minutes. `devsite connect` creates a new Iroh endpoint key in memory and redeems the48+ticket with that public endpoint id.49+50+The control plane atomically deletes the ticket, checks current access, and returns a51+client-bound `dss_…` tunnel ticket. The connector does not save it. It keeps the value only52+in memory and deletes the session on a clean shutdown.53+54+## Tunnel ticket: `dss_…`55+56+A `dss_…` ticket lets one connector request a capability for each local TCP connection. An57+ordinary connection ticket creates an eight-hour session. A delegated approval creates a58+session that lasts at most 15 minutes.59+60+The control plane binds the ticket to the client endpoint key. A delegated session also61+links to the issuer credential. The control plane stores its hash and checks access before62+it issues every capability.63+64+## Approval ticket: `dsp_…`65+66+A requester creates a signed request with an `agr_…` id, a service keyword, a temporary67+endpoint, an expiry, and a private-key signature. The granting party asks the control plane68+to resolve the request and issue a `dsp_…` ticket.69+70+This ticket is a signed approval snapshot. It names the request id, service, issuer ticket,71+chosen resource, requester endpoint, and expiries. The control plane verifies the request72+proof again when it plans or applies the grant. The ticket cannot open a service stream.73+Applying it creates at most one `dss_…` ticket for its request id.74+75+The ticket has no server-side plaintext row. The caller can keep it in a plan file or76+memory. Expiry, issuer-credential revocation, and access revocation stop a later grant.77+78+## Stream ticket: signed capability79+80+The connector uses a `dss_…` ticket to request one signed capability for one local TCP81+connection. The control plane signs the capability with its private signing key. The daemon82+pins the matching public key and uses it during the Iroh service handshake.83+84+The ticket names the viewer, resource, client endpoint, daemon endpoint, and `TcpConnect`85+permission. It expires after three minutes. The daemon accepts its nonce once and rejects a86+different endpoint. It closes active streams when its authorisation snapshot is stale or no87+longer permits the stream.88+89+## Browser session ticket90+91+A browser session ticket keeps a user signed in to the dashboard for seven days. The browser92+stores it in an HttpOnly cookie. The control plane stores its hash. Logout or expiry ends93+its use.94+95+## Tickets and secrets96+97+Treat complete `dmt_…`, `dsm_…`, `dst_…`, and `dss_…` values, browser cookies, and live98+signed capabilities as secrets. Redact them from logs, screenshots, shell history, and agent99+transcripts.100+101+Keep a `dsp_…` ticket inside its approval workflow. It cannot open a connection by itself,102+but it records a reviewed decision. `acct_…`, `res_…`, `machine_…`, `agr_…`, handles, and103+endpoint ids are identifiers. They do not grant access by themselves.104+105+See [Keys and endpoint identities](keys-and-identities.md) for the private keys that bind106+some tickets to Iroh endpoints.@@ -1,14 +1,14 @@
11 {22 "name": "devsite-access",3- "version": "0.4.0",4- "description": "Operate devsite and broker endpoint-bound service access through a portable MCP adapter and skill.",3+ "version": "0.5.0",4+ "description": "Operate devsite and grant delegated endpoint-bound service access through a portable MCP adapter and skill.",55 "author": {66 "name": "dev.site"77 },88 "skills": "./skills/",99 "interface": {1010 "displayName": "Devsite Access",11- "shortDescription": "Manage devsite and broker scoped service grants.",11+ "shortDescription": "Manage devsite and delegated service grants.",1212 "longDescription": "Provides the devsite CLI workflow skill and a local MCP adapter for status, resources, diagnostics, signed access requests, resolution, planning, and endpoint-bound grant issuance.",1313 "developerName": "dev.site",1414 "category": "Productivity",@@ -53,7 +53,7 @@ const tools = [
5353 },5454 {5555 name: 'devsite_access_resolve',56- description: 'Resolve a service keyword against services this scoped broker credential may delegate.',56+ description: 'Resolve a service keyword against services this scoped granting party credential may delegate.',5757 inputSchema: {5858 type: 'object',5959 properties: { keyword: { type: 'string' } },@@ -1,6 +1,6 @@
11 ---22 name: devsite-cli3-description: Operate and explain the devsite command-line interface. Use for publishing profile links, hosting or connecting to private TCP services, requesting or brokering endpoint-bound service access, inspecting resources, checking setup, managing the foreground daemon, or troubleshooting devsite CLI behavior.3+description: Operate and explain the devsite command-line interface. Use for publishing profile links, hosting or connecting to private TCP services, requesting or granting delegated endpoint-bound service access, inspecting resources, checking setup, managing the foreground daemon, or troubleshooting devsite CLI behavior.44 ---55 66 # devsite CLI@@ -22,7 +22,7 @@ Use the installed CLI as the source of truth. This skill is portable: use any sh
2222 2323 ## Plan mutations2424 25-Use `--plan` before upserts, removals, or broker grant issuance. `--dry-run` is an alias.25+Use `--plan` before upserts, removals, or delegated grant issuance. `--dry-run` is an alias.2626 2727 ```sh2828 devsite --json link set --name docs --url https://example.com/docs --public --plan@@ -40,13 +40,13 @@ Explain the exact target, visibility, recipients, destination, and effects. Appl
4040 | Host loopback TCP | Plan `service host PORT --name NAME`; add shares only as intended, apply, then keep `daemon run` alive. |4141 | Remove a resource | Inspect `resources list`, plan `link remove NAME` or `service remove NAME`, explain effects, then apply. |4242 | Connect normally | Use the approved, short-lived single-use ticket with `connect TICKET`; choose only a loopback `--listen` address. |43-| Request sandbox access | Create a signed request and separate endpoint key with `access request`; send only the request JSON to an authorized broker. |44-| Broker sandbox access | Resolve the signed keyword, plan the endpoint-bound grant, obtain policy or user approval, then issue it with a scoped broker credential. |43+| Request sandbox access | Create a signed request and separate endpoint key with `access request`; send only the request JSON to an authorised granting party. |44+| Grant delegated sandbox access | Resolve the signed keyword, plan the endpoint-bound grant, obtain policy or user approval, then issue it with a scoped granting party credential. |4545 | Diagnose | Run `doctor`; if hosting is configured but stopped, supervise `daemon run` as a resident process. |4646 4747 Folders are presentation, not authorization. Public entries are public; shared entries require acceptance. Services are protocol-neutral TCP byte streams to the owner's loopback target, not HTTP reverse proxies.4848 49-## Broker endpoint-bound access49+## Delegated endpoint-bound access5050 5151 Requester:5252 @@ -57,10 +57,10 @@ devsite --json access request postgres \
5757 ```5858 5959 - Create both paths in a requester-private directory. The command refuses collisions and writes private files.60-- Send only `request.json` to the broker. Never send `requester.key`.60+- Send only `request.json` to the granting party. Never send `requester.key`.6161 - The signature binds the service keyword, requester endpoint, request id, and expiry. Requests expire within 10 minutes and issuance consumes each request id once.6262 63-Broker:63+Granting party:6464 6565 ```sh6666 devsite --json access resolve postgres@@ -69,10 +69,10 @@ devsite --json access grant --request /inbox/request.json \
6969 --approved-plan dsp_…7070 ```7171 72-- These commands require a machine credential enrolled with the `service_grants:issue` scope. Being logged in alone does not grant broker authority.73-- Resolution contains only services the broker account owns or has an accepted share for. If several candidates match, present them and pass the authorized `--resource res_…`; never guess an ambiguous match.72+- These commands require a machine credential with the `service_grants:issue` scope. A login alone does not give that credential authority to grant access.73+- Resolution contains only services the granting party account owns or has an accepted share for. If several candidates match, present them and pass the authorised `--resource res_…`; never guess an ambiguous match.7474 - Show the exact service/resource, owner, requester endpoint, request id, and expiry to the user or policy engine. Issue only with that plan's server-signed `approved_plan` token; changed inputs will be rejected.75-- Grants last at most 15 minutes and are bound to the requester's endpoint. Return only the JSON result's `grant` and `server`; never return broker credentials or endpoint keys.75+- Grants last at most 15 minutes and are bound to the requester's endpoint. Return only the JSON result's `grant` and `server`; never return granting party credentials or endpoint keys.7676 7777 Requester:7878 @@ -81,7 +81,7 @@ devsite --server https://dev.site --json access connect dss_… \
8181 --key /private/requester.key8282 ```8383 84-`access connect` is resident NDJSON. It proves possession of the requester key, validates the endpoint binding, and opens only a loopback listener. Keep requester and broker roles distinct even if one harness can execute both.84+`access connect` is resident NDJSON. It proves possession of the requester key, validates the endpoint binding, and opens only a loopback listener. Keep requester and granting party roles distinct even if one harness can execute both.8585 8686 ## Resident commands8787 @@ -1,6 +1,6 @@
11 ---22 name: devsite-cli3-description: Operate and explain the devsite command-line interface. Use when a user asks to publish or change a dev.site profile link, share or stop sharing a local TCP service, connect to a service shared with them, request or broker endpoint-bound service access, inspect hosted resources, check devsite setup, manage the foreground daemon, customize profile presentation, or troubleshoot devsite CLI behavior. Also use for intent such as “share localhost,” “expose my local database,” “request database access,” “publish my docs,” or “what can dev.site do?”3+description: Operate and explain the devsite command-line interface. Use when a user asks to publish or change a dev.site profile link, share or stop sharing a local TCP service, connect to a service shared with them, request or grant delegated endpoint-bound service access, inspect hosted resources, check devsite setup, manage the foreground daemon, customize profile presentation, or troubleshoot devsite CLI behavior. Also use for intent such as “share localhost,” “expose my local database,” “request database access,” “publish my docs,” or “what can dev.site do?”44 ---55 66 # devsite CLI@@ -46,17 +46,17 @@ devsite --json service remove postgres --plan
4646 | Stop serving a service | Inspect `resources list`, plan `service remove NAME`, explain remote-access and local-mapping effects, then apply after confirmation. |4747 | Remove a link | Inspect `resources list`, plan `link remove NAME`, explain access/profile effects, then apply after confirmation. |4848 | Reach a shared service | Obtain a user-provided, short-lived single-use connection ticket from the approved profile entry. Run `connect TICKET` and wait for its ready event; use `--listen 127.0.0.1:PORT` only for a deliberate loopback port. |49-| Request sandboxed access | Create a signed request and separate endpoint key with `access request`; send only the request JSON to an authorized broker. Keep the key in the requester sandbox and use it with `access connect` after receiving the grant. |50-| Broker service access | Resolve the signed keyword, plan the exact endpoint-bound grant, obtain the required policy or user approval, then issue it with a machine credential enrolled for `service_grants:issue`. Return only the grant and its server to the requester. |49+| Request sandboxed access | Create a signed request and separate endpoint key with `access request`; send only the request JSON to an authorised granting party. Keep the key in the requester sandbox and use it with `access connect` after receiving the grant. |50+| Grant delegated service access | Resolve the signed keyword, plan the exact endpoint-bound grant, obtain the required policy or user approval, then issue it with a machine credential enrolled for `service_grants:issue`. Return only the grant and its server to the requester. |5151 | Enroll this machine | Obtain a user-provided single-use machine ticket and run `login TICKET`. This creates or uses the local endpoint identity and stores a revocable machine credential. |5252 | Diagnose availability | Run `doctor`; use its checks and proposed actions. For a configured but stopped host, start `daemon run` under the harness’s normal long-running-process supervision. |5353 | Change profile presentation | Discover exact syntax with `theme --json --help`; use `theme properties` before producing a theme declaration file, then `theme set FILE` or `theme clear` as authorized. |5454 5555 Folders are display grouping, not authorization boundaries. A public profile entry is public; shared entries require the invitee to accept the share. The control plane manages identity and permissions, while service traffic is a peer-to-peer TCP connection to the owner’s local loopback target.5656 57-## Broker endpoint-bound access57+## Delegated endpoint-bound access5858 59-Use this workflow when a sandboxed or otherwise untrusted worker needs temporary access without receiving the broker machine's identity or credential.59+Use this workflow when a sandboxed or untrusted worker needs temporary access. Do not give it the granting party machine's identity or credential.6060 6161 Requester:6262 @@ -67,10 +67,10 @@ devsite --json access request postgres \
6767 ```6868 6969 - Create both paths in a requester-private directory. The command refuses existing files and writes them with private permissions.70-- Send only `request.json` to the broker. Its signature binds the service keyword, requester endpoint, request id, and expiry. Never send `requester.key`.70+- Send only `request.json` to the granting party. Its signature binds the service keyword, requester endpoint, request id, and expiry. Never send `requester.key`.7171 - A request expires within 10 minutes. Issuance consumes its request id once, so retry by creating a new request rather than weakening replay checks.7272 73-Broker:73+Granting party:7474 7575 ```sh7676 devsite --json access resolve postgres@@ -79,10 +79,10 @@ devsite --json access grant --request /inbox/request.json \
7979 --approved-plan dsp_…8080 ```8181 82-- Broker commands require a machine credential whose enrollment ticket included the `service_grants:issue` scope. This is explicit delegated authority, not a capability inferred from being logged in.83-- Resolution is limited to services the broker account owns or has an accepted share for. If multiple services match, present the candidates and pass the approved `--resource res_…`; never choose an ambiguous fuzzy match silently.84-- Always plan first. Show the signed request identity, exact service/resource, owner, requester endpoint, and expiry to the user or policy engine that controls issuance. Apply only with that plan's server-signed `approved_plan` token; it pins those fields so a replaced request file or changed target is rejected.85-- Grants last at most 15 minutes and are bound to the requester endpoint. Give the requester only the `dss_…` grant and `server` from the JSON result. Do not give it the broker credential, broker endpoint key, or requester's copied key.82+- Granting party commands require a machine credential with the `service_grants:issue` scope. A login alone does not give that credential authority to grant access.83+- Resolution is limited to services the granting party account owns or has an accepted share for. If multiple services match, present the candidates and pass the approved `--resource res_…`; never choose an ambiguous fuzzy match silently.84+- Always plan first. Show the signed request identity, exact service/resource, owner, requester endpoint, and expiry to the user or policy engine that controls issuance. Apply only with that plan's server-signed `approved_plan` token. It pins these fields so a replaced request file or changed target is rejected.85+- Grants last at most 15 minutes and are bound to the requester endpoint. Give the requester only the `dss_…` grant and `server` from the JSON result. Do not give it the granting party credential, granting party endpoint key, or requester's copied key.8686 8787 Requester connection:8888 @@ -91,9 +91,9 @@ devsite --server https://dev.site --json access connect dss_… \
9191 --key /private/requester.key9292 ```9393 94-`access connect` is resident NDJSON like ordinary `connect`. It proves possession of the requester key, verifies the grant is broker-issued and bound to that endpoint, and then exposes only a loopback listener. A broker credential can be revoked independently; revocation deletes its unredeemed grant sessions, while any already minted one-use transport capability remains bounded by its existing short expiry.94+`access connect` is resident NDJSON like ordinary `connect`. It proves possession of the requester key, verifies the grant is issued by the granting party and bound to that endpoint, then opens only a loopback listener. A granting party credential can be revoked independently. Revocation deletes its unredeemed grant sessions. Any minted one-use transport capability remains limited by its short expiry.9595 96-Keep the requester and broker roles distinct even if one harness can execute both. The skill is a protocol and CLI workflow: any model, shell runner, policy engine, or MCP client can implement the handoff. Do not depend on a named agent framework, vendor callback, or stateful model session.96+Keep the requester and granting party roles distinct even if one harness can execute both. The skill is a protocol and CLI workflow. Any model, shell runner, policy engine, or MCP client can implement the handoff. Do not depend on a named agent framework, vendor callback, or stateful model session.9797 9898 ## Manage resident commands9999 @@ -473,6 +473,42 @@ function bindInstallControl() {
473473 });474474 }475475 476+const CLI_VERBS = new Set([477+ 'access', 'connect', 'daemon', 'grant', 'host', 'link', 'login', 'request',478+ 'resolve', 'run', 'service', 'set',479+]);480+481+/// Lightweight highlighting for the static examples below. This deliberately482+/// understands only devsite's command vocabulary: it is presentation, not a483+/// shell parser, and preserving the original text keeps selection/copy honest.484+function highlightCliExamples(container) {485+ container.querySelectorAll('pre > code').forEach((code) => {486+ const source = code.textContent;487+ if (!source.trimStart().startsWith('devsite ')) return;488+489+ const highlighted = document.createDocumentFragment();490+ let wordIndex = 0;491+ source.split(/(\s+)/).forEach((token) => {492+ if (!token || /^\s+$/.test(token)) {493+ highlighted.append(token);494+ if (token.includes('\n')) wordIndex = 0;495+ return;496+ }497+498+ const span = document.createElement('span');499+ if (wordIndex === 0 && token === 'devsite') span.className = 'command-executable';500+ else if (wordIndex <= 2 && CLI_VERBS.has(token)) span.className = 'command-verb';501+ else if (token.startsWith('--')) span.className = 'command-option';502+ else if (/^(?:dmt|dsp|dss|dst)_/.test(token)) span.className = 'command-secret';503+ else span.className = 'command-value';504+ span.textContent = token;505+ highlighted.append(span);506+ wordIndex += 1;507+ });508+ code.replaceChildren(highlighted);509+ });510+}511+476512 function renderSignedOut() {477513 setPageTitle('home');478514 clearTheme();@@ -538,11 +574,38 @@ function renderSignedOut() {
538574 </blockquote>539575 <pre><code>devsite connect dst_***</code></pre>540576 </article>577+ <article>578+ <h3>Delegate Granular Access</h3>579+ <blockquote><p>A sandboxed agent can request a service by keyword. The request is matched to an accessible service, and a scoped grant is presented for approval.</p>580+ <p>The requester receives only a short-lived grant bound to the selected581+ service and its temporary endpoint.</p></blockquote>582+ <pre><code>devsite access request postgres --request request.json --key requester.key</code></pre>583+ <blockquote>Resolution is limited to services available to the approving account. Grants584+ are short-lived and revocable.</blockquote>585+ <pre><code>devsite access resolve postgres586+devsite access grant --request request.json --plan587+devsite access grant --request request.json --approved-plan dsp_***</code></pre>588+ <blockquote>The requester opens an on-demand, endpoint-bound connection to the selected service.</blockquote>589+ <pre><code>devsite access connect dss_*** --key requester.key</code></pre>590+ </article>541591 <article>542592 <h3>Persist the Daemon</h3>543593 <pre><code>devsite daemon run</code></pre>544- <blockquote>autorun it with Homebrew services or systemd</blockquote>594+ <blockquote>autorun it with Homebrew services or systemd to keep services available for595+ on-demand connections</blockquote>545596 </article>597+ </details>598+599+ <details>600+ <summary>Important Concepts</summary>601+ <p>Tickets, keys, and the ALPN tell the story of how it works</p>602+ <nav aria-label="Security concept guides">603+ <ul>604+ <li><a href="/concepts.html#tickets">Tickets</a></li>605+ <li><a href="/concepts.html#keys">Keys</a></li>606+ <li><a href="/concepts.html#protocol">ALPN</a></li>607+ </ul>608+ </nav>546609 </details>`;547610 548611 pageFooter.innerHTML = `@@ -552,6 +615,7 @@ function renderSignedOut() {
552615 </nav>`;553616 pageFooter.hidden = false;554617 bindInstallControl();618+ highlightCliExamples(main);555619 }556620 557621 function renderClaimHandle() {@@ -0,0 +1,257 @@
1+2+<html lang="en">3+<head>4+<meta charset="utf-8">5+<meta name="viewport" content="width=device-width, initial-scale=1">6+<title>dev.site - concepts</title>7+<link rel="stylesheet" href="/vendor/pico.min.css">8+<link rel="stylesheet" href="/site.css">9+</head>10+<body>11+12+<header class="container">13+ <nav>14+ <ul>15+ <li>16+ <a href="/" class="contrast brand" aria-label="dev.site home">17+ <svg width="24" height="24" viewBox="2 2 20 20" aria-hidden="true"18+ xmlns="http://www.w3.org/2000/svg">19+ <path d="M16,15,12,7,8,15Zm-1,0L11.5,8"20+ style="fill:none;stroke:var(--pico-primary);stroke-linecap:round;stroke-linejoin:round;stroke-width:2"></path>21+ <circle cx="12" cy="12" r="9"22+ style="fill:none;stroke:var(--devsite-logo-ring, rgb(0,0,0));stroke-linecap:round;stroke-linejoin:round;stroke-width:2"></circle>23+ </svg>24+ <strong>dev.site</strong>25+ </a>26+ </li>27+ </ul>28+ <ul><li><a href="/">Home</a></li></ul>29+ </nav>30+</header>31+32+<main class="container">33+ <article>34+ <hgroup>35+ <h1>Important concepts</h1>36+ </hgroup>37+ <nav aria-label="On this page">38+ <ul>39+ <li><a href="#tickets">Tickets</a></li>40+ <li><a href="#keys">Keys</a></li>41+ <li><a href="#protocol">ALPN</a></li>42+ </ul>43+ </nav>44+ </article>45+46+ <section id="tickets" aria-labelledby="tickets-title">47+ <h2 id="tickets-title">Tickets</h2>48+ <p>A ticket is a serializable object that packages the information required for an49+ operation. dev.site passes tickets between the browser, CLI, control plane, and50+ daemon. Each ticket type defines its own use count, lifetime, binding, storage, and51+ revocation rules.</p>52+53+ <figure>54+ <table class="concepts-table">55+ <thead><tr><th>Ticket</th><th>Purpose</th><th>Issuer and consumer</th><th>Uses and lifetime</th><th>Binding, storage, and revocation</th></tr></thead>56+ <tbody>57+ <tr><td><code>dmt_…</code></td><td>Enroll one machine</td><td>Control plane issues it. The CLI and control plane use it.</td><td>One use. No automatic expiry.</td><td>It binds the enrolled endpoint after redemption. The control plane stores its hash. Revoke the machine entry.</td></tr>58+ <tr><td><code>dsm_…</code></td><td>Authenticate one machine</td><td>The control plane issues it. The CLI, daemon, and control plane use it.</td><td>Many uses. No automatic expiry.</td><td>It is a bearer ticket. Enrollment and daemon registration also need endpoint proof. Private config stores it. The server stores its hash. Revoke the ticket.</td></tr>59+ <tr><td><code>dst_…</code></td><td>Start one service connection</td><td>The control plane issues it for an authorised browser. The connector redeems it.</td><td>One use. Two minutes.</td><td>It names one viewer and service. The control plane stores its hash. Consume it, let it expire, or revoke access.</td></tr>60+ <tr><td><code>dss_…</code></td><td>Request stream capabilities</td><td>The control plane issues it. The connector and control plane use it.</td><td>Many uses. Eight hours, or at most 15 minutes when delegated.</td><td>It binds to the client endpoint. The connector keeps it in memory and the server stores its hash. Delete, expire, or revoke it.</td></tr>61+ <tr><td><code>dsp_…</code></td><td>Carry one delegated approval</td><td>The control-plane signing key issues it. A granting client and the control plane use it.</td><td>One grant per request id. Request and grant expiries limit it.</td><td>It binds the request id, service, issuer ticket, resource, endpoint, and expiries. The caller stores it. Revoke the issuer ticket or access.</td></tr>62+ <tr><td>Stream ticket<br><small>signed capability</small></td><td>Open one service stream</td><td>The control-plane signing key issues it. The daemon uses it.</td><td>One use. Three minutes.</td><td>It binds both endpoints, viewer, resource, and permission. The connector holds it in memory. Its nonce prevents reuse.</td></tr>63+ <tr><td>Browser session</td><td>Keep a user signed in</td><td>The control plane issues it after sign-in. The browser and control plane use it.</td><td>Many uses. Seven days.</td><td>It is a bearer ticket with no endpoint-key binding. An HttpOnly cookie holds it. The server stores its hash. Log out or let it expire.</td></tr>64+ </tbody>65+ </table>66+ </figure>67+68+ <details class="concept-detail">69+ <summary>Machine enrollment ticket</summary>70+ <p>A signed-in user creates a named <code>dmt_…</code> ticket. The CLI presents it with71+ a public endpoint key and proof from the matching private key. The server atomically72+ consumes the ticket, binds the machine entry to that endpoint, and returns a revocable73+ <code>dsm_…</code> machine ticket. Machine scopes such as74+ <code>service_grants:issue</code> originate on this enrollment ticket.</p>75+ <p><code>dmt_…</code> currently has no automatic expiry. Create it when needed and use it76+ promptly. A leak can enroll one endpoint until the ticket is used or revoked, but the77+ ticket cannot be used again after successful enrollment.</p>78+ </details>79+80+ <details class="concept-detail">81+ <summary>Service connection tickets</summary>82+ <p>An authorised browser creates a two-minute <code>dst_…</code> for one viewer and83+ service. <code>devsite connect</code> generates an in-memory client endpoint and84+ redeems that ticket for an eight-hour <code>dss_…</code> session bound to the client85+ key. Each local TCP connection then exchanges the session for a fresh, three-minute,86+ one-use stream ticket bound to both the client and daemon endpoints.</p>87+ <pre><code>dst_… → dss_… → stream ticket → encrypted service stream</code></pre>88+ <p>A <code>dss_…</code> is a reusable ticket. It can mint one stream ticket per TCP89+ connection until it expires, is deleted, or loses current authorisation.</p>90+ </details>91+92+ <details class="concept-detail">93+ <summary>Delegated access grant</summary>94+ <p>A requester creates a signed request containing an <code>agr_…</code> id, service95+ keyword, temporary public endpoint, and expiry. A human or automated granting party96+ resolves that keyword only among services its account may access. The server returns97+ a <code>dsp_…</code> plan signed over the exact request, issuer credential, selected98+ resource, endpoint, and expiries.</p>99+ <p>The plan is an approval ticket. It cannot open a stream. It binds the request id,100+ service, issuer ticket, resource, requester endpoint, and expiries. The control plane101+ verifies the request proof again. Applying the ticket returns a requester-bound102+ <code>dss_…</code> lasting at most 15 minutes. The request id permits one grant. The103+ resulting ticket can serve multiple TCP connections during that window.</p>104+ </details>105+106+ <details class="concept-detail">107+ <summary>Ticket security and storage</summary>108+ <p>Treat complete <code>dmt_…</code>, <code>dsm_…</code>, <code>dst_…</code>, and109+ <code>dss_…</code> values, browser cookies, and live capabilities as secrets. The110+ control plane stores hashes rather than plaintext bearer values. A111+ <code>dsp_…</code> is decodable and cannot connect by itself, but should stay inside112+ the approval workflow.</p>113+ <p><code>acct_…</code>, <code>res_…</code>, <code>machine_…</code>, <code>agr_…</code>,114+ handles, and endpoint ids are identifiers rather than tickets. Current access is115+ rechecked when tickets are redeemed and capabilities are requested. Resource,116+ sharing, session, and machine-ticket revocation narrow or terminate later use.</p>117+ </details>118+ </section>119+120+ <hr>121+122+ <section id="keys" aria-labelledby="keys-title">123+ <h2 id="keys-title">Keys and Endpoint Identities</h2>124+ <p>Ed25519 keys identify Iroh endpoints, prove that a request came from the endpoint it125+ names, and sign control-plane authorization. A public endpoint id is safe to share.126+ Its matching private key is what proves possession.</p>127+128+ <figure>129+ <table class="concepts-table">130+ <thead><tr><th>Key</th><th>Storage</th><th>Job</th></tr></thead>131+ <tbody>132+ <tr><td>Machine endpoint</td><td><code>devsite-endpoint.key</code></td><td>Persistent enrolled machine and daemon identity</td></tr>133+ <tr><td>Machine public endpoint</td><td><code>devsite-endpoint.pub</code> and server registration</td><td>Daemon address and capability audience</td></tr>134+ <tr><td>Ordinary connector</td><td>Process memory only</td><td>Binds a redeemed ticket and QUIC peer</td></tr>135+ <tr><td>Delegated requester</td><td>A caller-selected file such as <code>requester.key</code></td><td>Signs the request and uses the resulting grant</td></tr>136+ <tr><td>Control-plane signing key</td><td>Operator secret or <code>capability_signing.key</code></td><td>Signs approval plans and stream capabilities</td></tr>137+ <tr><td>Pinned server public key</td><td>Private machine <code>config.json</code></td><td>Defines which control-plane signatures the daemon trusts</td></tr>138+ </tbody>139+ </table>140+ </figure>141+142+ <details class="concept-detail">143+ <summary>Persistent machine identity</summary>144+ <p>The CLI creates <code>devsite-endpoint.key</code> in the resolved dev.site config145+ directory and preserves it across restarts. On Unix, private files are created with146+ mode <code>0600</code>. Its public half is written to147+ <code>devsite-endpoint.pub</code>, registered with the control plane, and used as the148+ daemon's Iroh endpoint id.</p>149+ <p>Before you upgrade to version 0.6.0, run <code>devsite login</code> or150+ <code>devsite daemon run</code> with an earlier version. That version moves151+ <code>identity.key</code> and <code>identity.pub</code> only from the dev.site config152+ directory. It does not scan the current directory, home directory, SSH directory, or153+ filesystem. It moves a legacy file only when its target file does not exist. Version154+ 0.6.0 uses <code>devsite-endpoint.key</code> and155+ <code>devsite-endpoint.pub</code>. It does not read or move the legacy files.</p>156+ </details>157+158+ <details class="concept-detail">159+ <summary>Connector and requester keys</summary>160+ <p>An ordinary connector's key exists only in memory, so copying its bound session to a161+ different process is insufficient. Delegated access persists a temporary key at the162+ requested <code>--key</code> path. The CLI creates it without overwriting an existing163+ file and keeps it private on Unix. Share <code>request.json</code>, never164+ <code>requester.key</code>. Delete the key after the request and granted connection165+ finish.</p>166+ </details>167+168+ <details class="concept-detail">169+ <summary>Control-plane trust root</summary>170+ <p>The server uses one signing key for <code>dsp_…</code> plans and one-stream171+ capabilities. Production can provide it through <code>DEVSITE_SIGNING_KEY</code>. A172+ local server otherwise keeps <code>capability_signing.key</code> in its state173+ directory. Login pins only its public half. The daemon refuses to start without that174+ pin and rejects artifacts signed by another key.</p>175+ <p><code>devsite doctor</code> reports when the server's published public key differs176+ from the pinned value. Losing the private signing key prevents existing daemons from177+ trusting new capabilities. Compromise permits forged authorization. Rotation178+ therefore requires an explicit trust migration rather than a silent file change.</p>179+ </details>180+181+ <details class="concept-detail">182+ <summary>Key compromise boundaries</summary>183+ <p>A machine endpoint key cannot make authenticated API calls without its184+ <code>dsm_…</code>. The bearer ticket cannot satisfy endpoint proofs without the185+ key. Possession of both grants the enrolled machine's full authority and scopes, so186+ revoke the named machine entry and enroll a new endpoint if either pair may be187+ compromised. A temporary client key matters only while a matching session or188+ capability remains live.</p>189+ </details>190+ </section>191+192+ <hr>193+194+ <section id="protocol" aria-labelledby="protocol-title">195+ <h2 id="protocol-title">ALPN and the service wire protocol</h2>196+ <p>ALPN means Application-Layer Protocol Negotiation. dev.site currently defines one197+ service protocol: <code>devsite/tcp/1</code>. The client requests it and the daemon198+ advertises it during the Iroh QUIC handshake, selecting the wire format before either199+ side interprets an application frame.</p>200+201+ <pre><code>CLI ── HTTPS ──► dev.site control plane202+ │ session validation + capability issuance203+ │204+ └── Iroh QUIC, ALPN devsite/tcp/1 ──► owner daemon ── TCP ──► loopback service205+ encrypted service bytes</code></pre>206+207+ <p>The ALPN does not apply to the HTTPS API, OIDC, MCP adapter, or hosted application's208+ own protocol. PostgreSQL, HTTP, SSH, Redis, and other TCP protocols are opaque bytes209+ after dev.site authorizes the stream.</p>210+211+ <details class="concept-detail">212+ <summary>Service wire sequence</summary>213+ <ol>214+ <li>The client opens a bidirectional stream on an authenticated QUIC connection.</li>215+ <li>It sends a Postcard <code>Connect</code> request containing one signed capability.</li>216+ <li>The daemon verifies signature, audience, authenticated client key, expiry,217+ permission, resource, and one-use nonce.</li>218+ <li>The daemon connects to the resource's fixed loopback target.</li>219+ <li>It returns <code>Connected</code> or a deliberately coarse error.</li>220+ <li>After <code>Connected</code>, the remainder is the service's raw TCP byte stream.</li>221+ </ol>222+ <p>Request and response frames start with a four-byte little-endian length followed by223+ a Postcard payload. Length is checked before allocation and is capped at 4 MiB.224+ Authorization failures collapse to <code>Denied</code> so a peer cannot distinguish a225+ bad signature from an unknown or unshared resource.</p>226+ </details>227+228+ <details class="concept-detail">229+ <summary>ALPN security scope</summary>230+ <p>Iroh QUIC encrypts the network and authenticates endpoint keys. ALPN separates and231+ versions the application protocol. It does not grant service access: every stream232+ still requires a server-signed capability addressed to the daemon and bound to the233+ authenticated client. Relays carry encrypted QUIC traffic and do not terminate the234+ dev.site application stream. The local daemon-to-service TCP hop uses whatever235+ encryption the service protocol provides.</p>236+ </details>237+238+ <details class="concept-detail">239+ <summary>ALPN version compatibility</summary>240+ <p>The <code>/1</code> suffix is the compatibility boundary. An incompatible framing or241+ semantic change should use a new ALPN. Today the client requests one dev.site ALPN and242+ the daemon advertises one. There is no protocol fallback or multi-version negotiation.243+ <code>devsite doctor</code> compares the server's reported daemon protocol with the244+ CLI's compiled value.</p>245+ </details>246+ </section>247+</main>248+249+<footer class="container">250+ <nav>251+ <ul><li><a href="https://github.com/FelineStateMachine/devsite"252+ target="_blank" rel="noopener">GitHub</a></li></ul>253+ </nav>254+</footer>255+256+</body>257+</html>@@ -123,6 +123,22 @@ details:not(.dropdown)[open] > summary::after {
123123 transform: rotate(45deg);124124 }125125 126+/* Keep each concepts table header visible until its table leaves the viewport. */127+.concepts-table thead th {128+ position: sticky;129+ top: 0;130+ z-index: 1;131+ background-color: var(--pico-background-color);132+}133+134+.concept-detail {135+ margin-block: calc(var(--pico-spacing) * 0.75);136+}137+138+.concept-detail > summary {139+ line-height: 1.175;140+}141+126142 .brand {127143 display: inline-flex;128144 gap: calc(var(--pico-spacing) * 0.4);@@ -269,10 +285,17 @@ details:not(.dropdown)[open] > summary::after {
269285 font-weight: 700;270286 }271287 .command-verb {272- color: var(--pico-code-color);288+ color: var(--pico-ins-color);289+ font-weight: 700;290+}291+.command-option {292+ color: var(--pico-primary-hover);293+}294+.command-value {295+ color: var(--pico-color);273296 }274297 .command-secret {275- color: var(--pico-muted-color);298+ color: var(--pico-del-color);276299 }277300 278301 /* The sequence diagram is the signed-out page's one bespoke element. */