From 60741043026c19168a6dda225ba1132c9742a6d4 Mon Sep 17 00:00:00 2001 From: Jerry Combs Date: Fri, 23 Oct 2026 13:24:07 -0500 Subject: [PATCH 1/2] Docs: remove built design notes, rewrite pull requests and per-person data as references, fix stale docs Removed: architecture-view.md, trace-viewer.md, simple-install.md, event-logger.md (built; the two event-logger steps left are #164 and #165). pull-requests.md and multi-user.md are user references with their API fields. data-model.md gains people's own connections, Git and agent tokens, sessions, the overrides file's names, servers and jobs, and known differences. design.md marked historical, connections-ui.md marked built, limits.md's trace buffer and development.md's release steps corrected, the lab's internal details out of hosted-install.md. Code comments and the usage text point at the docs that remain. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 18 ++ README.md | 10 +- bin/smoke-hosted.sh | 2 +- docs/architecture-view.md | 163 ------------- docs/connections-ui.md | 5 + docs/data-model.md | 62 +++-- docs/design.md | 22 +- docs/development.md | 2 +- docs/event-logger.md | 158 ------------ docs/features.md | 2 +- docs/hosted-install.md | 39 +-- docs/limits.md | 4 +- docs/multi-user.md | 228 +++++------------- docs/pull-requests.md | 217 +++++------------ docs/simple-install.md | 148 ------------ docs/trace-viewer.md | 129 ---------- .../pointblue/dirxml/web/EventsService.java | 2 +- .../com/pointblue/dirxml/web/LocalSetup.java | 2 +- .../java/com/pointblue/dirxml/web/Serve.java | 2 +- .../com/pointblue/dirxml/web/TraceFiles.java | 2 +- .../com/pointblue/dirxml/web/TreeService.java | 4 +- .../pointblue/dirxml/web/LocalSetupTest.java | 2 +- web/src/components/ArchitectureView.tsx | 2 +- web/src/components/DeveloperView.tsx | 2 +- web/src/types.ts | 2 +- 25 files changed, 241 insertions(+), 988 deletions(-) delete mode 100644 docs/architecture-view.md delete mode 100644 docs/event-logger.md delete mode 100644 docs/simple-install.md delete mode 100644 docs/trace-viewer.md diff --git a/CHANGELOG.md b/CHANGELOG.md index ae574d5..71ada29 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,8 +7,26 @@ and this project uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html) ## [Unreleased] +### Changed + +- **Docs brought up to date.** + - pull-requests.md and multi-user.md are user references now, not design notes, each with its + API fields. + - data-model.md has people's own connections, Git and agent tokens, sessions, the overrides + file's names, servers and jobs, and known differences. + - design.md is marked historical (W7 built; rollback, secrets and driver deletion are on the + page), and connections-ui.md says it is built. + - limits.md gives the trace view's real buffer (50,000 records, 15,000 shown, adjustable), and + development.md the release steps as they are. + - hosted-install.md no longer describes Point Blue's own lab. + ### Removed +- **Design notes for features that are built:** docs/architecture-view.md, docs/trace-viewer.md, + docs/simple-install.md and docs/event-logger.md. What they describe is in features.md, + quickstart.md and upgrading.md; the two event-logger steps still to build are issues #164 (the + logger as a catalog package) and #165 (`pbt-eventlog.js`, capture samples at a stage). Git + history keeps the notes. - docs/hosted.md, the hosted-mode design note of 2026-10-05: its body described the design before it was built (one shared vault bind, compare-page pull requests, no merging on the page, decisions still open), and hosted-mode.md, hosted-install.md, security.md, multi-user.md, diff --git a/README.md b/README.md index 094c832..4b1615a 100644 --- a/README.md +++ b/README.md @@ -137,18 +137,16 @@ for developing it or for a quick look at a tree. Upgrading an install is running | [docs/features.md](docs/features.md) | every view, by area | | [docs/agents.md](docs/agents.md) | agents: tokens, connecting Claude Code or Cursor, the setup prompt, every MCP tool and its arguments, the CLIs, the agent kit | | [docs/data-model.md](docs/data-model.md) | projects, workspaces, trees, environments and vaults: the entity relationship diagram | -| [docs/multi-user.md](docs/multi-user.md) | design note: per-user connections, data and traces on a hosted server | -| [docs/architecture-view.md](docs/architecture-view.md) | design note: the Architecture (Designer Modeler) and Developer views | +| [docs/multi-user.md](docs/multi-user.md) | what's yours and what's shared on a hosted server: vault sign-ins, tokens, traces, the files they live in | +| [docs/pull-requests.md](docs/pull-requests.md) | pull requests from the page: providers, whose account, the workstation's `gh` / `glab`, the API | | [docs/hosted-install.md](docs/hosted-install.md) | the container for a team: image, host layout, proxy, Keycloak, the lab | | [docs/hosted-mode.md](docs/hosted-mode.md) | hosted configuration: projects, repositories, identity (`token`, `oidc`, `none`), roles, agents | | [docs/security.md](docs/security.md) | what the server refuses, where credentials live, what never leaves it; how the workstation and hosted modes differ | | [docs/secret-managers.md](docs/secret-managers.md) | AWS, Azure, GCP, CyberArk, BeyondTrust, Bitwarden, Terraform: a credential as a command | | [docs/development.md](docs/development.md) | running from a checkout, layout, tests, releasing | | [docs/limits.md](docs/limits.md) | what still goes through Operations or the CLI | -| [docs/design.md](docs/design.md) | the assessment and the phases | -| [docs/event-logger.md](docs/event-logger.md) | design note: the Event Logger integration: events as simulator cases, an Events view, the logger as a package | -| [docs/connections-ui.md](docs/connections-ui.md) | design note: the Connections page: one flow per vault (address, certificate, sign-in, driver set), required before optional, certificate pinning, rotated passwords made obvious and easy | -| [docs/trace-viewer.md](docs/trace-viewer.md) | design note: the DirXML Trace Viewer in the workbench: a shared trace library, live and file trace with colours, filters, find and links into the tree | +| [docs/design.md](docs/design.md) | historical: the assessment and phases from before it was built, and why it is built this way | +| [docs/connections-ui.md](docs/connections-ui.md) | design note (built in 0.61.0): the Connections page, and the layout rules every form follows: required before optional, one status and one action per row, rotated passwords made obvious | | [CHANGELOG.md](CHANGELOG.md) | every release | ## License diff --git a/bin/smoke-hosted.sh b/bin/smoke-hosted.sh index ef8e6d4..5f33387 100755 --- a/bin/smoke-hosted.sh +++ b/bin/smoke-hosted.sh @@ -428,7 +428,7 @@ check("her agent's calls are hers, marked via its token", any(l["user"] == "alic and any(l["user"] == "alice" and l["event"] == "agent-token-create" for l in lines) and all(made["token"] not in json.dumps(l) for l in lines)) print("smoke-hosted: all passed") PY -# the local install's `setup` (docs/simple-install.md), written into a temporary folder +# the local install's `setup` (docs/quickstart.md), written into a temporary folder mkdir -p "$WORK/setup" "$HERE/bin/idmweb" setup --dir "$WORK/setup" --port 18099 --user smoke > "$WORK/setup.out" 2>&1 || { cat "$WORK/setup.out"; echo "FAIL setup"; exit 1; } if grep -q "container_name: idmweb" "$WORK/setup/compose.yml" && grep -q "^IDMWEB_TOKEN=" "$WORK/setup/idmweb.env" \ diff --git a/docs/architecture-view.md b/docs/architecture-view.md deleted file mode 100644 index d959f0d..0000000 --- a/docs/architecture-view.md +++ /dev/null @@ -1,163 +0,0 @@ -# Architecture and Developer views: design note - -Status: **decided 2026-10-09** (section 6) for issue #32; A1 built (the Architecture view with the live overlay and export; DirXMLDev 0.18.0), A2 built (the Developer view); #32 complete. Jerry: "the visuals are -important for marketing the solution; ideally we want parity with the views in Designer." - -## 1. What Designer shows, and what the page shows today - -Designer has one canvas, the **Modeler**, in two modes, and an outline beside it: - -| Designer | What it draws | The page today | -|---|---|---| -| Modeler, **Architect** mode | the Identity Vault as a tall panel with its driver set; one box per **application** (icon chosen by the driver's application type, the application's name); a line from vault to application with the **driver** drawn on it | Home: a grid of driver cards (name, shim class, counts) | -| Modeler, **Developer** mode | the same, with each driver line opened into its Subscriber and Publisher channels and their policy sets | Flow: one driver's fishbone at a time | -| **Outline** (Developer perspective) | the whole project as a tree: driver set, libraries, each driver's channels, policy sets, policies, resources, entitlements, jobs, forms, PRDs | the sidebar tree: drivers → driver/subscriber/publisher policies, resources, entitlements | - -The page has the data for all of it: the outline, each driver's fishbone model (policy sets in -engine order, policies in linkage order), the driver's Designer **application type** (the core's -`ApplicationType`, read from Designer's own model-item definitions by shim class, else by base -package) and the driver's **icon bytes** (`Driver.icon`, the vault's `DirXML-DriverImage`, the -same file Designer shows). What is missing is the two pictures. - -## 2. The Architecture view - -`#type=architecture`, a nav entry under Home. One SVG canvas, drawn by the page from an -`/api/architecture` answer, pan and zoom, *Fit*. - -**Layout, as Designer's Architect mode:** - -- The **Identity Vault** in the centre: a tall rounded panel with the vault icon, the tree's name - and the driver set's DN, the servers listed small at the bottom; the **driver set** as a band - across it with its name and the counts (drivers, library items, packages). -- **Applications** in two columns, left and right of the vault, sorted by name and alternated so - the columns balance: a card with the application's **icon** (the driver's own; the generic - application glyph when it has none), the **application name** (the type's display name — - *Active Directory*, *SCIM*, *JDBC* — or the driver's name when the type is unknown) and the - driver's name beneath when it differs. -- A **connector** from the vault's edge to each application, with the **driver badge** on it: a - small round node holding the driver's icon, as Designer draws the driver object on the line. -- **Vault-only drivers** — those with no external system: User Application, Loopback, Null, - Entitlements Service, Role and Resource Service, Managed System Gateway, Data Collection — take - the same columns, **at the foot of each column** after the applications, so the connectors all - read the same way; the hover text says they serve the vault (decision 2, revised 2026-10-09: the - earlier row beneath the vault did not connect cleanly and its caption overran the cards). -- **Hover** on an application or badge: shim class, package, policy and entitlement counts, - and, when an environment is chosen, the driver's state and cache size. **Click** an application - → the driver's Flow view; **click** a badge → its Configuration; the User Application card also - offers Roles and resources. -- **Live overlay** (decision 4): with an environment chosen in the shared picker, one - `/api/vault/status` call colours each badge — running green, stopped grey, disabled red, - unknown hollow — and the vault panel shows which server answered. Off by default; a *Show - state* toggle. -- **Filter** box: a name or shim fragment dims everything else, as the sidebar's filter does. -- **Export**: *Download SVG* and *Download PNG (2×)*. The SVG is self-contained — styles inlined, - icons embedded as data URLs, fonts named with fallbacks — so it drops into a document or a - slide as it looks on the page. Light and dark follow the page's theme; the export takes the - current one. - -Drawn with the page's own tokens: the vault in the accent colour, cards on the surface colour -with the border token, connectors in the muted text colour, badge dots in `--ok` / `--err`. No -diagram library; the fishbone renderer shows the way (plain SVG, computed layout). - -## 3. The Developer view - -`#type=developer`, a nav entry beside Architecture. The whole driver set as one tree, full page, -the way Designer's outline reads in the Developer perspective (decision 3): - -``` -Driver Set (name, DN) -├─ Configuration: GCVs, servers, packages, linkage -├─ Library: policies, ECMAScript, mapping tables, GCV objects -└─ Drivers - └─ Active Directory Driver [Active Directory] running - ├─ Configuration: driver.xml, filter, schema map, GCVs, ECVs, shim settings, app schema - ├─ Subscriber - │ ├─ Event Transformation (policies in linkage order) - │ ├─ Matching · Creation · Placement · Command Transformation · Output Transformation - ├─ Publisher - │ ├─ Input Transformation · Event Transformation · Matching · Creation · Placement · Command Transformation - ├─ Resources: ECMAScript, mapping tables, GCV objects - ├─ Entitlements - ├─ Jobs - └─ Forms · Provisioning request definitions (the User Application) -``` - -- Policy sets in the engine's order with the policies in **linkage order**, the same model the - fishbone draws (`Fishbone.model` per driver, built once by the server for the whole set). -- Every node shows its count; *Expand all* / *Collapse all*; a filter box that keeps the - matching nodes and their ancestors; a click opens what the sidebar would open (artifact, - config editor, entitlement, form, PRD); the keyboard moves as in a tree. -- The state column at the driver level follows the same live overlay as the Architecture view. - -The sidebar tree stays as it is (navigation); the Developer view is the whole set at once, -for orientation and the round-trip check the issue names. - -## 4. Server - -- `GET /api/architecture`: `{ driverSet: {name, dn, servers, counts}, applications: [ {driver, - dn, shimClass, type, typeName, vaultOnly, icon (bool), counts: {policies, entitlements, - resources, forms, prds}} ], developer: [tree nodes] }` — the outline plus, per driver, the - application type (`ApplicationType.of(shimClass, basePackage)`), whether an icon is present, - and the fishbone's policy sets for the Developer tree. Built per request from the tree (the - outline already is); cached by revision like the outline. -- `GET /api/driver-icon?driver=…`: the driver's icon bytes with the right `Content-Type` - (`gif`/`png` by the recorded extension or the magic number); 404 when none; the page draws - its generic glyph then. Served with a long cache header keyed by revision. -- Nothing is written. - -## 5. Not in this change - -- Designer's Developer **mode on the canvas** (channels and policy sets drawn on the driver - lines): the Developer tree covers the content; a canvas version can follow if the picture is - wanted for marketing too. -- Editing from the canvas (Designer lets you drag applications about): the layout is computed. -- Printing to PDF beyond the SVG/PNG export. - -## 6. Decisions (taken 2026-10-09) - -1. **Two columns around the vault**, Designer's own arrangement. -2. **Vault-only drivers beneath the vault.** Revised 2026-10-09: at the foot of the two columns - instead — the row beneath did not connect cleanly to the set and its caption overran the cards. -3. **Developer view as a full-page tree** now; the canvas Developer mode is a possible follow-up. -4. **Live state overlay in the first release.** -5. **Export both SVG and PNG.** - -## 7. Phases - -| Phase | What | -|---|---| -| A1 | `/api/architecture`, `/api/driver-icon`; the Architecture view with hover, click, filter, fit, export | -| A2 | the Developer view | -| A3 | the live state overlay on both | - -## 8. As built (A1) - -- `GET /api/architecture`: the set (name, DN, servers, drivers, library items, installed - packages) and one application per driver (`type`, `typeName`, `vaultOnly`, `icon`, counts). - `GET /api/driver-icon?driver=…`: the icon bytes with their image type, 404 when none. -- `ArchitectureView`: `archLayout` places every driver in two columns alternated by name order, - the applications first and the vault-only drivers at the foot of each column; cards carry the driver's icon (an initials glyph - when it has none), the application type's name and the driver name with its shim class; the - driver badge sits on the connector. Hover: shim, counts, state. Click a card: the Flow view; - click a badge: Configuration. Filter dims. Zoom and Fit. *show state* (remembered) reads - `/api/vault/status` for the chosen environment and colours the badges. *Download SVG* embeds - the page's colour tokens and the icons as data URLs; *Download PNG* renders that at 2×. -- A driver whose type the tree does not record (a Remote Loader shim with no - `designer.driver-type`) is labelled by its driver name with the generic glyph, as Designer's - importer would without the base package; carrying the type from the base package stamp is a - follow-up. - -## 9. As built (A2) - -- `GET /api/developer`: one tree — the set (its configuration: GCVs, vault schema, overrides; - the library's policies and resources), then every driver: Configuration (driver.xml, filter, - shim settings, engine control values, GCVs, application schema), Subscriber (Event, Matching, - Creation, Placement, Command, Output), Publisher (Input, Event, Matching, Creation, Placement, - Command), Schema Mapping, Resources (the driver-level sets with policies, and the resource - artifacts), Entitlements, Forms, Provisioning request definitions. Policy sets come from the - same model the fishbone draws, so the policies are in linkage order; every node carries what - the page opens when clicked and every folder its leaf count. -- `DeveloperView`: the tree with the set and the drivers open by default, *Expand all* and - *Collapse all*, a filter that keeps matching nodes and their ancestors (name, type or shim), - the driver's icon and type name on its row, *unresolved* on a policy link the tree cannot - resolve, and *show state* for the chosen environment on the driver rows. diff --git a/docs/connections-ui.md b/docs/connections-ui.md index 7b754be..ea9227f 100644 --- a/docs/connections-ui.md +++ b/docs/connections-ui.md @@ -1,5 +1,10 @@ # Design note: the Connections page +Status: **built** in DirXMLDevWeb 0.61.0 with DirXMLDev 0.43.0 (the page, certificate pinning, +rotated passwords, servers, the project's repository); #131 closed. The rules in *Rules the layout +follows* and *Words* still hold for any form added to the page. What the page does now is in +[features.md](features.md) (Connections, Projects) and [multi-user.md](multi-user.md). + Issue #131. This note covers how the Connections page should look and behave, and the one core change it needs: trusting a vault's certificate by its fingerprint instead of accepting any certificate. What stays the same: diff --git a/docs/data-model.md b/docs/data-model.md index 22f9d46..63de388 100644 --- a/docs/data-model.md +++ b/docs/data-model.md @@ -12,7 +12,7 @@ project's tree is deployed to its own environments and no others. erDiagram HOSTED_SERVER { string public_url - path data_dir "workspaces, traces, audit.log" + path data_dir "workspaces, users, audit.log, sessions" path environments_file "one per server" string identity "none | token | oidc" list editor_groups @@ -40,6 +40,21 @@ erDiagram list groups secret token } + AGENT_TOKEN { + string label PK "per person" + string hash "SHA-256 only" + date expires + } + CONNECTION { + string environment PK "per person" + string bind_dn + secret password "theirs alone" + bool trust_all + } + GIT_TOKEN { + string host PK "per person" + secret token + } WORKSPACE { string id PK "project/user/branch, or project/in-place" string branch @@ -65,6 +80,13 @@ erDiagram OVERRIDES { string environment PK "overrides/.properties" map values "gcv, ecv, shim, auth server and id" + map names "drivers/.name: the driver's name there" + map servers "servers. = the server's DN there" + map jobs "schedule, disabled, trace per job" + } + KNOWN_DIFFERENCES { + string environment PK "overrides/.known.json" + list accepted "differences recorded as intended" } CASE { string name PK "cases//" @@ -79,15 +101,14 @@ erDiagram path snapshot "deploy-snapshots//" } ENVIRONMENT { - string name PK "the Connection the page edits" + string name PK "the project's definition" string url "ldaps://host:636" - string bind_dn - secret password "literal, env, command, Keychain or manager" + string cert_sha256 "the pinned certificate" string driver_set_dn enum tier "dev | stg | prd" string requires "an environment deployed first" - bool trust_all string ssh_host + bool from_any_branch "production deploys from any branch" path secrets_file "secrets-.properties" } DRIVER_SECRETS { @@ -128,6 +149,10 @@ erDiagram HOSTED_SERVER ||--o{ PROJECT : holds HOSTED_SERVER ||--o{ PRINCIPAL : "signs in" HOSTED_SERVER ||--o{ AGENT : "names" + PRINCIPAL ||--o{ AGENT_TOKEN : "makes (acts as them)" + PRINCIPAL ||--o{ CONNECTION : "signs in to vaults with" + PRINCIPAL ||--o{ GIT_TOKEN : "creates pull requests with" + CONNECTION }o--|| ENVIRONMENT : "for" HOSTED_SERVER ||--o{ ENVIRONMENT : "reads from its environments file" PROJECT ||--o{ WORKSPACE : "one per person and branch" PRINCIPAL ||--o{ WORKSPACE : opens @@ -136,6 +161,8 @@ erDiagram PROJECT ||--o{ TRACE_FILE : "records and uploads" TREE ||--o{ DRIVER : contains TREE ||--o{ OVERRIDES : "one per environment" + TREE ||--o{ KNOWN_DIFFERENCES : "one per environment" + KNOWN_DIFFERENCES }o--|| ENVIRONMENT : "accepted for" TREE ||--o{ CASE : "simulator corpus" TREE ||--o{ DEPLOY_RECORD : "one per deploy" OVERRIDES }o--|| ENVIRONMENT : "values for" @@ -145,7 +172,7 @@ erDiagram ENVIRONMENT |o--o| ENVIRONMENT : requires ENVIRONMENT ||--o| DRIVER_SECRETS : "deploy reads" ENVIRONMENT ||--o| EVENT_STORE : "may have" - ENVIRONMENT ||--|| SECRET_SOURCE : "password from" + CONNECTION ||--|| SECRET_SOURCE : "password from" DRIVER_SECRETS ||--o{ SECRET_SOURCE : "each value from" VAULT ||--o{ VAULT_DRIVER_SET : holds VAULT_DRIVER_SET ||--o{ VAULT_DRIVER : runs @@ -166,14 +193,18 @@ operations act on the one the environment names. | **Hosted server** | `hosted.properties` | One web server: its public URL, data directory, identity provider, the groups that make an editor or a deployer, *the* environments file it reads, the projects it holds. Local mode has none of this: one process, one tree, one person. | | **Project** | `project..*` in `hosted.properties` | One tree repository the server holds: a git remote (or a checkout used in place), the tree and cases directories inside it, the base branch, its own token or deploy key, and the environments its tree is deployed to. | | **Principal** | the identity provider (OIDC groups), `token.user` (`identity=token`, the local container: every role), or `none.users` (tests) | A signed-in person: subject, groups, and from the groups the editor and deployer roles. A **session** is a principal plus the workspace they have open. | -| **Agent** | `agents=` and `agent..token*` | A named bearer token with groups, for an automation or an AI agent; it opens workspaces like a person. | +| **Agent** | `agents=` and `agent..token*` | A named bearer token with groups, for an automation that acts for no one (CI); it opens workspaces like a person, with connections of its own. | +| **Agent token** | `data.dir/users//agent-tokens.json` (a SHA-256 of each) | A token a person makes on the page for their own agent: the agent **acts as that person**, with their roles, connections and workspaces, through the API or the MCP endpoint, marked `via` its label in the audit log ([agents.md](agents.md)). | +| **Connection** | `data.dir/users//environments.properties` | One person's sign-in to one environment's vault: bind DN, where the password comes from, certificate trust, the event store's password. Theirs alone ([multi-user.md](multi-user.md)). | +| **Git provider token** | `data.dir/users//git.properties` | One person's token for one provider host, used for their pull requests ([pull-requests.md](pull-requests.md)). | | **Workspace** | `data.dir/workspaces///` | A checkout of one project on one branch for one person, with the services over it (tree, vault, simulator, packages, credentials, events, traces). In-place projects have one workspace, `/in-place`. Local mode has exactly one: the tree on the command line. | | **Tree** | `tree/` in the repository | **One driver set** as files: `driverset.xml`, set GCVs, the drivers, the shared library, entitlements, provisioning (see the core's `docs/tree-layout.md`). | | **Driver** | `tree/drivers//` | A driver's `driver.xml`, filter, shim configuration, engine control values, GCVs, channel policies, and on a multi-server set the per-server settings under `servers//`. | -| **Overrides** | `overrides/.properties` beside the tree | The values that differ per environment (GCVs, ECVs, shim parameters, the authentication server and id). The tree keeps the base value; a deploy to that environment applies its file. | +| **Overrides** | `overrides/.properties` beside the tree | What differs per environment: GCVs, ECVs, shim parameters, the authentication server and id; a driver's name there (`drivers/.name`); which of the environment's servers stands for each of the tree's (`servers. = `); jobs' schedule, enabled state and trace. The tree keeps the base value; a deploy to that environment applies its file. | +| **Known differences** | `overrides/.known.json` beside the tree | Differences between an environment and the tree (or another environment) recorded as intended, so *Vault → Environments* stops listing them ([environment-practices.md](environment-practices.md)). | | **Case** | `cases//` | A simulator case: `input.xds`, the recorded `expected-output.xds`, `case.properties` naming the driver and channel and where the input came from (an event-store row, a cache event). | | **Deploy record** | `deploy-log/.jsonl`, `deploy-snapshots//` | One line per deploy or driver operation, and the LDIF (or cache XDS) snapshot taken before the write. The production gate (`requires`) reads the log. | -| **Environment** | the environments file: `.*` (hosted: the definitions there, and each person's bind DN and password in `data.dir/users//environments.properties`) | **A place a driver set is deployed to**: a vault URL, a bind DN, where the password comes from, the driver-set DN, the tier, an optional prerequisite environment, trust, the SSH host for the trace tail, the secrets file, the event store. The page calls it a **Connection**; the Connections view writes this entry. | +| **Environment** | the environments file: `.*` (hosted: the project's definitions, no secrets; workstation: the whole entry, sign-in included) | **A place a driver set is deployed to**: a vault URL and its pinned certificate, the driver-set DN, the tier, an optional prerequisite environment, the SSH host for the trace tail, the secrets file, the event store. On a hosted server each person adds their own **connection** to it; the Connections page shows both. A driver or object can name the environments it belongs in (`environments=`) and the ones it is planned for (`planned=`). | | **Driver secrets** | `secrets-.properties` | The shim, Remote Loader and named passwords a deploy to that environment needs, each a stored value or a reference to a secret manager. Never read back. | | **Secret source** | the suffix of a key: `password`, `passwordEnv`, `passwordCommand`, `passwordKeychain`, or a manager reference | Where any secret comes from. On a macOS workstation the web stores a typed value in the Keychain by default, else in the owner-only file; the container has no Keychain, and on a hosted server a person's own reference is a deployer's (docs/security.md). | | **Event store** | `.events*` on the environment | The Event Logger's PostgreSQL table for that environment: JDBC URL, read-only user, table, whether people are pseudonymised on the way out, the tree name its DNs start with. | @@ -189,6 +220,8 @@ operations act on the one the environment names. | Chosen environment | one remembered choice per browser (`localStorage`), offered from the project's list | the same, from the file | | Credentials | no Keychain in the container; a person's password goes to their own owner-only file (a reference, a deployer's only) | the macOS Keychain by default, else the file or a reference | | Workspaces | one per project, person and branch | one | +| Vault sign-ins, Git provider tokens, agent tokens | each person's own files under `data.dir/users//` | the environments file (the Keychain by default); `gh` or `glab`; no agent tokens | +| Sessions | `data.dir/sessions.json`, surviving a restart | none | | Trace recordings | `data.dir/users//traces/` | `~/.idm/traces/local` | | Audit log | `data.dir/audit.log`: session, workspace, event, outcome | none | @@ -199,8 +232,9 @@ operations act on the one the environment names. with a driver set it is not deployed to. - Two projects may share a vault (two driver sets in one tree) and still have separate environments, because an environment is the pair (vault, driver set). -- An environment's secrets are the tree's concern (the secrets file sits beside the tree and the - overrides file names the environment), while the bind password is the environments file's. - Neither is ever committed. -- Per-person environments (issue #47) would add a second environments file, read after the - server's, with the same names and the same project filter. +- An environment's driver secrets are the environment's (the secrets file beside the environments + file, deployers only), while each bind password is a person's own (their connection). Neither + is ever committed. +- Two environments of one tree can differ in names, servers and what they hold; the overrides + file maps the names and servers, the per-object lists say what belongs where, and the known + differences file records the rest as intended. diff --git a/docs/design.md b/docs/design.md index be9fad6..f03a56d 100644 --- a/docs/design.md +++ b/docs/design.md @@ -1,11 +1,19 @@ # DirXMLDevWeb: assessment and design -Status: **decided 2026-10-05; W0–W6 built** (this repository). The note was written in DirXMLDev as `docs/web-designer.md`; this is its home now. - -> This note describes the workstation install (local mode: loopback, one token, the macOS -> Keychain, `idm serve`). The hosted server came later and differs on sign-in, binding, -> credentials and roles: see [hosted-mode.md](hosted-mode.md) and the table in -> [security.md](security.md). +Status: **decided 2026-10-05; W0–W7 built** (this repository). The note was written in DirXMLDev as `docs/web-designer.md`; this is its home now. + +> **Historical: why the product is built this way.** This is the assessment and plan from before +> it was built, kept for its reasoning: why not port Designer, why not WebAssembly, why a server +> over the DirXMLDev core. It is not a description of the product today: +> - **The server's name:** it is `bin/idmweb serve`, not `idm serve`. +> - **The hosted server** came later and differs on sign-in, binding, credentials and roles +> ([hosted-mode.md](hosted-mode.md), the table in [security.md](security.md)). +> - **Vault access per person:** each person signs in to each vault for themselves +> ([multi-user.md](multi-user.md)). +> - **Agents** work through the server's MCP endpoint, with tokens people make +> ([agents.md](agents.md)). +> +> What it does now is in [features.md](features.md); what it doesn't, in [limits.md](limits.md). The question: build a web version of Designer, perhaps with WebAssembly. The answer in one paragraph: **yes to a web workbench, no to porting Designer, @@ -192,7 +200,7 @@ Each is usable on its own and stops cleanly. | **W0 spike** | `idm serve` and a read-only explorer: outline, artifact view, fishbone, validation panel, references. Proves the catalog-as-API, the watcher and the token model | | **W1 configuration editing** | GCVs, filter, schema map, mapping tables, link order by drag, stage overrides; raw XML editing with validate-on-save; every write previewed as a diff | | **W2 policy builder** | Rules, conditions, actions and tokens as structured editors over the DirXML Script grammar, with the engine's compile check live | -| **W3 compare, deploy, operate** | Vault diff per stage, plan review, confirm, verify; driver state and control; live trace. Built; rollback, secrets and driver deletion stay with the CLI | +| **W3 compare, deploy, operate** | Vault diff per stage, plan review, confirm, verify; driver state and control; live trace. Built, with rollback, the secrets modes and deleting a driver only in the vault on the page too; the step-by-step deploy stays with the CLI ([limits.md](limits.md)) | | **W4 simulator** | Cases, run, stage-by-stage output, comparison against another tree or stage | | **W5 provisioning** | PRD flow diagram (read, then edit through `flow.*`), form builder embedded, roles and resources | | **W6 packages** | Status, install, upgrade, build | diff --git a/docs/development.md b/docs/development.md index 1687986..6fc15b6 100644 --- a/docs/development.md +++ b/docs/development.md @@ -86,4 +86,4 @@ bin/smoke.sh # every endpoint, the security gates and the write path, ## Releasing -A release bumps `pom.xml`, `Serve.VERSION` (a test pins the two together) and the CHANGELOG heading, merges `--no-ff`, tags `vX.Y.Z` and publishes the jar and the page zip on GitHub. Publishing the release runs `.github/workflows/image.yml`, which builds the image for amd64 and arm64 from the released jars, pushes it to GHCR. The lab pulls its image from Harbor (see hosted-install.md). +A release bumps `pom.xml`, `Serve.VERSION` (a test pins the two together) and the CHANGELOG heading, merges `--no-ff`, tags `vX.Y.Z` and publishes the jar, the page zip and `THIRD-PARTY.md` on GitHub. Before tagging, run `bin/smoke.sh`, `bin/smoke-hosted.sh` and `bin/smoke-container.sh` from `main`. Publishing the release runs `.github/workflows/image.yml`, which builds the image for amd64 and arm64 from the released jars and pushes it to GHCR. The image build also checks out DirXMLSimulator at the release the pom names, for the `sim` CLI and the `dirxml-policy-testing` skill in the agent kit (`hosted/agent-kit`, assembled by `hosted/docker/build.sh`). diff --git a/docs/event-logger.md b/docs/event-logger.md deleted file mode 100644 index 8a90966..0000000 --- a/docs/event-logger.md +++ /dev/null @@ -1,158 +0,0 @@ -# Event Logger integration: design note - -Status: **decided 2026-10-07** (Jerry took the proposals in section 6). Built: the event store in DirXMLDev 0.14.0 with DirXMLDevWeb 0.17.0, the Events view in 0.19.0. - -The question: can [DIrXMLEventLogger](https://github.com/jcombs-pointblue/DIrXMLEventLogger) -be used by, and folded into, the DirXMLDev / DirXMLDevWeb / DirXMLSimulator stack? -The answer: yes, and the best fit is the one its README names — captured events as -test input. The logger becomes the stack's **event store**: the simulator takes its -cases from it, the workbench shows it with links into the tree, and the logger -itself ships as a package. The Flask UI is not ported; its pages are re-made in the -workbench where they can link to drivers, policies and cases. - -## 1. What the logger is - -- A driver shim (`com.pointblue.idm.eventlogger.EventLoggerDriver`) on the subscriber - channel of its own driver. Every add, modify, delete, sync, rename and move that passes - its filter is written to PostgreSQL: one row per event in `dxmlevent` — `eventid` (the - engine's `event-id`, primary key), `classname`, `srcdn`, `srcentryid`, `eventtype`, - `eventjson` (jsonb, from the `xds2json` converters), `xmlevent` (the raw XDS, when - `storeXML=true`), `cachedtime`, `srcdriver`. Indexes on `REVERSE(srcdn)` (subtree - queries) and `srcdriver`. -- `PolicyLogger`: a static registry keyed by the EventLogger driver's DN. Any driver's - policy can log a document from ECMAScript — `PolicyLogger.logEvent(eventLoggerDN, - thisDriverDN, channel, policyName, xml)` — without database credentials. That is a - per-policy, per-channel capture of a real document at a named point in the flow. -- A Flask UI (timeline per object, modify diff, recent, full-text search, stats, CSV). -- Offline test jigs for the shim, a JSON→XML converter, a load generator. -- Retention: size-based cleanup with pg_cron (latest commit). - -It already runs on edir3: the tree imported from edir3 carries the `EventLogger` driver, -pointed at a PostgreSQL on another host. - -## 2. Where it plugs in - -Five integration points, in the order they should be built. 1 and 2 are the phase this -note proposes; 3–5 are listed so the shape of 1 and 2 leaves room for them. - -### 2.1 Simulator cases from logged events (the phase) - -A simulator case is a directory: `case.properties` (driver, channel), `input.xds`, and -optionally a golden `expected-output.xds` or `expected.assertions`. A logged event's raw -XML is exactly an `input.xds`. Two sources: - -- **An event row** (`xmlevent`): the document as it reached the EventLogger driver's - subscriber channel — the vault's event after the EventLogger's own filter, before any - other driver's policies. Replayed through driver *X*'s subscriber chain, it answers - "what would X do with this real event?". -- **A PolicyLogger row**: the document at a named policy of a named driver and channel. - Replayed from that stage, it is a regression input for exactly that policy set. - -"Save as case" takes a row, the target driver (default: the row's `srcdriver`, or for -PolicyLogger rows the driver that logged it) and the channel, writes -`cases//{case.properties,input.xds}` beside the tree, and optionally runs it at -once and records the golden. Cases built this way carry `source=events:` in -`case.properties` so the corpus stays traceable to the store. - -Rows without `xmlevent` (`storeXML=false`) are rebuilt from `eventjson` with the -logger's `JsonToXmlConverter`; the case is marked `reconstructed=true`, because the -converters do not keep everything (operation-data order, comments, unknown elements). - -### 2.2 Events in the workbench - -- **Core** (DirXMLDev): an `EventStore` over JDBC, read-only, with `query events` - (by DN or subtree, driver, type, class, time window, free text over the JSON) and - `query event `; `events.case` is the one write op, and it writes to the cases - directory, never to the store. The PostgreSQL JDBC driver (BSD-2) is an ordinary - dependency; nothing proprietary. -- **Environment settings** (the environments file, same shape as the vault bind): - `.eventsUrl=jdbc:postgresql://host:5432/idmEvent`, `.eventsUser`, - `.eventsPassword=` (or `…PasswordEnv=` / `…PasswordCommand=`, or `…PasswordKeychain=` - on a macOS workstation, like the other secrets; on a hosted server the reader password is in each - person's own file, and a reference is a deployer's), `.eventsTable` (default `public.dxmlevent`). The read-only - `eventlogger_reader` account the logger's README already asks for is the one to use. -- **Web**: `/api/events/*` on `VaultService`'s model (per environment, no credentials - on the page), an **Events** view with the Flask pages re-made — object timeline with - the modify diff, recent, search, stats — plus what Flask cannot do: the source driver - opens the driver, a PolicyLogger row's policy name opens that policy, an event has - *Save as case* and *Run through driver…* (the simulator view with the case - pre-filled). The agent (MCP) gets the same queries. - -### 2.3 The logger as a package - -Build the EventLogger driver into a package in the catalog (the existing -`package.build` from the driver in the tree), so any lab gets it with the New Driver -flow. Two things our deploy cannot do and the docs must say: the shim jar and the JDBC -jar go on the engine host's classpath (`/opt/novell/eDirectory/lib/dirxml/classes/`, -restart), and the database must exist with the DDL applied. The containerised logger -(the other session's goal) should ship a compose that mounts the two jars into the -engine container and brings up PostgreSQL with the DDL and the reader account — then -the lab's "install" is one `docker compose up`. - -### 2.4 A Library resource for PolicyLogger - -An ECMAScript resource (`pbt-eventlog.js`, wrapping `PolicyLogger.logEvent` with the -EventLogger DN from a driver-set GCV) and a one-rule template policy "log the document -here". Linked into any policy set from the flow's set panel, it makes "capture samples -at this stage" one reviewed change. Needs 2.1 so the captured rows turn into cases. - -### 2.5 Liveness on the cards (later) - -Event counts per driver for the last day from the store on the home cards and the -driver-set view. Cheap once 2.2 exists; not part of the phase. - -## 3. Changes the logger repository needs first - -These are in the hand-off prompt for the session working on the logger; the stack's -side depends on the first two. - -1. **Redact passwords.** `BaseEventConverter.processPassword` copies the `` - text into the JSON, and `xmlevent` keeps the raw document. Both must be masked before - the store is read by a workbench or an agent. Also ``, - `nspmDistributionPassword`-style attributes, and operation-data passwords. -2. **A build and a release.** A `pom.xml` (the engine jars `provided`, as DirXMLDev does - it), tests for the converters (round trip through `JsonToXmlConverter`), a GitHub - release with the shim jar attached — so CI here can fetch it like the core jar, and - the package in 2.3 can name a version. -3. **PolicyLogger rows need their own columns.** Today channel and policy name travel - inside the JSON (or not at all); the store needs `channel` and `policy` columns (and - `stage`: `input`/`output`) so 2.1 can find "the document at policy P of driver D". - Migration: `ALTER TABLE … ADD COLUMN`, nullable. -4. **Keep `storeXML=true` as the recommendation** for development stores; the raw - document is what the simulator wants. -5. Stable JSON shape: a `schemaVersion` in the JSON (or a column) so readers can tell - old rows from new. - -## 4. What is *not* proposed - -- Porting the Flask UI as is. Its pages come back inside the workbench (2.2) where they - can link into the tree; the Flask app stays usable for teams without the workbench. -- Writing to the store from the stack. Only the engine (through the logger) writes - events; the stack reads, and writes cases to the tree. -- Replacing the trace stream. The engine trace (`vault.trace`) stays the live view; the - store is the persistent, queryable one. - -## 5. Phase plan - -| Step | Where | What | -|---|---|---| -| L1 | logger repo | password redaction, pom + tests + release, `channel`/`policy`/`stage` columns | -| C1 | DirXMLDev | `EventStore`, `query events`, `query event`, `events.case`; env keys; docs | -| W1 | DirXMLDevWeb | `/api/events/*`, Events view (timeline, diff, recent, search, stats), *Save as case*, *Run through driver*, links to driver/policy | -| P1 | catalog / docs | EventLogger package; install notes (jars on the engine classpath, DDL, reader account); compose from the logger repo | -| R1 | Library | `pbt-eventlog.js` + template policy, "log here" in the set panel | - -C1 and W1 follow L1's first two items; P1 follows the containerised logger; R1 last. - -## 6. Decisions (taken 2026-10-07) - -1. **Store per environment.** `.events*` keys beside the vault bind; dev and production - stores differ and the credentials are already per environment. -2. **Captured cases go in the tree's `cases/` directory**, committed with the tree, so a - production sample becomes a regression test in the repository. A case that must not be - committed is saved outside the tree by choice, not by default. -3. **Any signed-in user may read the store** through the read-only reader account; when a - store holds production names, the DN search and the saved cases go through the same - `pseudonymise` option the vault clone has. -4. **The Flask UI stays in the logger repository** as the standalone viewer; the workbench's - Events view does not replace it for teams without the workbench. diff --git a/docs/features.md b/docs/features.md index c2c2a03..ff0b528 100644 --- a/docs/features.md +++ b/docs/features.md @@ -250,7 +250,7 @@ Every view of the page, by area, in every install; the items marked *local and s its application type, the driver as a badge on the connector, the drivers that serve the vault itself at the foot of each column; hover for facts, click for the flow or the configuration, filter, zoom; *show state* colours each driver by its state in the chosen environment; *Download SVG / PNG* for a - slide or a document (docs/architecture-view.md). + slide or a document. - **Developer**: the whole driver set as one tree, as Designer's outline reads in the Developer perspective — each driver's configuration, its Subscriber and Publisher policy sets in engine order with the policies in linkage order, schema mapping, resources, entitlements, forms and diff --git a/docs/hosted-install.md b/docs/hosted-install.md index fef04a5..be298b9 100644 --- a/docs/hosted-install.md +++ b/docs/hosted-install.md @@ -27,17 +27,17 @@ the image holds nothing proprietary and must stay that way, so it can live in an ```bash # publish: both architectures, pushed (needs a buildx builder and a login to the registry) -PUSH=1 ALSO_TAG=latest hosted/docker/build.sh REGISTRY/PROJECT/dirxml-dev-web:0.11.0 +PUSH=1 ALSO_TAG=latest hosted/docker/build.sh REGISTRY/PROJECT/dirxml-dev-web: # or a local image for one machine -hosted/docker/build.sh dirxml-dev-web:0.11.0 # PLATFORM=linux/amd64 for an x86 host from a Mac -docker save dirxml-dev-web:0.11.0 | gzip > dirxml-dev-web-0.11.0.tar.gz # then docker load on the host +hosted/docker/build.sh dirxml-dev-web: # PLATFORM=linux/amd64 for an x86 host from a Mac +docker save dirxml-dev-web: | gzip > dirxml-dev-web-.tar.gz # then docker load on the host ``` A registry with a self-signed certificate valid longer than 825 days cannot be pushed to from a Mac (macOS refuses the certificate; the buildx client checks it, not only the builder). Push from a Linux host that trusts it instead — ship the gathered context and run -`hosted/docker/push-on-host.sh CONTEXT REGISTRY/PROJECT/dirxml-dev-web:0.11.0 latest` there; +`hosted/docker/push-on-host.sh CONTEXT REGISTRY/PROJECT/dirxml-dev-web: latest` there; it makes a buildx builder that knows the certificate and installs binfmt for arm64 once. The hosts then run `docker compose pull && docker compose up -d` with `IDMWEB_IMAGE` naming the @@ -48,9 +48,8 @@ registry image. the page from the releases (this repo's, DirXMLDev's, the simulator's), checks out DirXMLDev at its tag for the helpers and the Terraform module, and runs the same `build.sh` — the engine-jar check included. Nothing is built from source there, so the image is reproducible from the tags. -The package is public (since 2026-10-14), so `docker compose pull` needs no credentials, while -the repository itself stays private. To build one by hand: -`gh workflow run image.yml -f version=0.12.0`. +The package is public, so `docker compose pull` needs no credentials. To build one by hand: +`gh workflow run image.yml -f version=`. ## 2. Lay out the host @@ -70,7 +69,7 @@ the repository itself stays private. To build one by hand: token, the OIDC client secret, agent tokens. Under `config/`, only the driver secrets hold values. Each person's vault password and Git provider token are in their own owner-only file in the data volume (`users//`), and a token typed to clone a project is in `secrets/.repo.token` -there; see §7 for what the volume holds. `environments.properties` must hold no password: the +there; see §6 for what the volume holds. `environments.properties` must hold no password: the server will not start with one (docs/multi-user.md). - `.env`: `PUBLIC_HOST` (the DNS name), `ACME_EMAIL`, the Keycloak admin credentials. @@ -124,29 +123,7 @@ An agent (CI, a coding agent, a script) does not install anything: idmweb idm vault.status --env stg` (`ls /var/lib/idmweb/users` lists the subjects; that person's password goes to the vault). An agent reaches a vault through the API, with its own connection. -## 6. The lab (Point Blue) - -`https://idmweb.pointbluetech.com:8443` on DockerHost1 (`192.168.103.99`, `~/idmweb/`): Caddy on -8443 with an internal certificate (443 is Harbor's; the name is private), Keycloak under -`/idp` with the example realm, the server on the `edir-test3_default` network so `edir3` is -`ldaps://edir-test3-engine:636`, the engine jars from that container, the repository -`PointBlueTechnology/idm-lab-config` (synthetic tree) through a deploy key. The generated -secrets are in Jerry's Keychain under service `idmweb-lab`. Deployed 2026-10-06. - -The image comes from the host's Harbor: `harbor.pointbluetech.com/dirxmldev/dirxml-dev-web:` -(multi-arch), pushed by the robot account `robot$dirxmldev+ci` (token in the same Keychain -service, item `harbor-robot`). Harbor's certificate is self-signed for ten years, which macOS -rejects, so the push runs on DockerHost1 itself: - -```bash -hosted/docker/build.sh x 2>/dev/null || true # gather the context (the tag is unused here) -tar -C hosted/docker -czf - . | ssh debian@192.168.103.99 'rm -rf ~/idmweb/build && mkdir -p ~/idmweb/build && tar -C ~/idmweb/build -xzf -' -scp hosted/docker/push-on-host.sh debian@192.168.103.99:~/idmweb/ -ssh debian@192.168.103.99 '~/idmweb/push-on-host.sh ~/idmweb/build harbor.pointbluetech.com/dirxmldev/dirxml-dev-web:0.11.0 latest' -ssh debian@192.168.103.99 'cd ~/idmweb && sed -i "s#^IDMWEB_IMAGE=.*#IDMWEB_IMAGE=harbor.pointbluetech.com/dirxmldev/dirxml-dev-web:0.11.0#" .env && docker compose pull -q idmweb && docker compose up -d idmweb' -``` - -## 7. Operate +## 6. Operate - Upgrade: set `IDMWEB_IMAGE` in `.env` to the new version, then `docker compose pull idmweb && docker compose up -d idmweb` ([upgrading.md](upgrading.md): before you upgrade, rolling back). - The `idmweb-data` volume holds the workspaces, `audit.log`, `sessions.json`, each person's diff --git a/docs/limits.md b/docs/limits.md index af9d402..40ead75 100644 --- a/docs/limits.md +++ b/docs/limits.md @@ -19,7 +19,9 @@ Ticking files picks among those the review listed, and within a file the merge is line by line; a deleted file is taken or left whole. A deploy always takes the whole scope (a driver or the set): the vault matches a commit, never part of one. -- The trace view reads over the environment's LDAPS connection and keeps the last 5,000 lines. +- The trace view reads over the environment's LDAPS connection. It keeps the last 50,000 records + and shows the last 15,000 by default; both can be changed in its settings. Longer captures go to + a file (*record to a file*), opened from *Trace files*. - The simulator view creates plain cases (a driver, a channel, an input document). A case's other keys (schema, a fake directory, assertions, a live query source) are honoured when run but are edited in its files; comparing with another tree stays with `idm simulate --against`. diff --git a/docs/multi-user.md b/docs/multi-user.md index 4a43367..845825c 100644 --- a/docs/multi-user.md +++ b/docs/multi-user.md @@ -1,181 +1,79 @@ -# Per-user data: design note +# What's yours and what's shared -Status: **decided 2026-10-08** (section 7); U1 built (core 0.16.0, web 0.22.0), U2 built (per-person credentials, the split Connections view), U3 built (traces per person, unclaimed-directory notice), U4 built (definitions in full from the page, "set up your connection" in the pickers); see "As built" at the end. Jerry's direction: "each user's workspace -needs to be unique to them. They get their own data, their own connections, nothing shared — -except perhaps secrets." +On a hosted server (a team server, or the local container), each person has their own copy of +every branch they open, their own vault sign-ins, their own Git provider tokens, agent tokens and +trace files. What describes an environment is the project's, and only the driver secrets are +shared. The workstation install (`serve `) is one person and uses one environments file, +as the `idm` CLI does. -## 1. What is shared today +## Three kinds of thing, three owners -Hosted mode already gives each person their own checkout per branch -(`data.dir/workspaces///`), so the tree, its cases, overrides, deploy -snapshots and the simulator corpus are already theirs until they commit. Everything about the -*vault side* is shared: - -| Today | Where | Shared by | +| Kind | Owner | Holds | |---|---|---| -| Connections (every environment: URL, bind DN, password source, driver set, tier, trust, SSH, event store) | the server's one `environments.file` | every person, every project | -| Driver secrets (shim, Remote Loader, named passwords) | `secrets-.properties` beside it | every person | -| Trace recordings and uploads | `data.dir/traces//` | everyone on the project | -| The choice of environment | the browser's `localStorage` | nobody (per browser) | -| Sessions, audit log, package catalog, the repositories | `data.dir` | everyone, by design | +| **Environment definition** | the project (deployers edit) | name, tier, vault URL, driver-set DN, `requires`, SSH host and user, the event store's URL, table and options. No secrets. | +| **Connection** | the person | per environment: bind DN, where the password comes from, certificate trust, the event store's reader password. Theirs alone: never returned, never copied. | +| **Driver secret** | the environment (deployers edit, deploys read) | shim, Remote Loader and named passwords, each a value or a secret-manager reference. Every deploy to an environment needs the same ones. | -The core reads the environments file through a process-wide path (`IDM_ENVIRONMENTS`, or -`Environments.load()` with no argument). Seven call sites in `CredentialService`, `VaultService`, -`EventsService` and `SetupService` go through that. That is the one real obstacle: the file has -to become a per-request choice. +An environment, as one person sees it, is the project's definition plus their own connection for +that name. Without a connection it is listed but marked *set up your connection*, linking to +**Connections**. A person may also define environments of their own (a private lab) in full, in +their own file; nobody else sees those. -## 2. The model +This split keeps the vault's own audit honest (each person binds as themselves, so the vault +records who deployed), means the server never holds a shared production password, and stops two +people deploying one tree to different driver sets. -Three kinds of thing, three owners: +## Where it lives -| Kind | Owner | Holds | -|---|---|---| -| **Environment definition** | the project (deployers edit) | name, tier, vault URL, driver-set DN, `requires`, SSH host, event-store URL and table. No secrets. | -| **Credential** | the person | per environment name: bind DN, where the password comes from, `trustAll`, the event-store account. Theirs alone; never returned, never copied. | -| **Driver secret** | the environment (deployers edit, deploys read) | `secrets-.properties`: shim, Remote Loader and named passwords, each a value or a secret-manager reference. Jerry's exception: these are facts about the environment, and every deploy to it needs the same ones. | +``` +data.dir/ + users// 0700: the person's own, reached only through their session + environments.properties their connections by environment name, and environments of their own + git.properties their Git provider tokens by host (pull-requests.md) + agent-tokens.json a SHA-256 of each of their agent tokens (agents.md) + profile.properties name, email, last seen + traces// their trace recordings and uploads + workspaces//// their copy of each branch they open + audit.log, sessions.json, projects/, archive/ the server's +environments.file the project's definitions (no password keys) +secrets-.properties beside it: the driver secrets, deployers only +``` -An *effective environment* for one person is a definition plus that person's credential for its -name. No credential means the environment is listed but "not connected" for them. A person may -also define environments of their own (a private lab) in their file with every key; those are -theirs entirely and no one else sees them. +`` is the identity provider's subject (an OIDC `sub`, `token.user` on the local +container, an agent's name). Files are `0600` and directories `0700`. The server runs as one OS +user, so the separation is the server's to enforce: every read and write of a person's directory +goes through their session, and nothing in another person's directory is listed, read or +returned. -Why split the definition from the credential rather than keep whole entries per person: +**Agents.** An agent named in `agents=` (CI) is a subject too, with its own connections, set +through the same API with its token. A person's own agent token acts as that person, with their +connections ([agents.md](agents.md)). -- a URL and a driver-set DN are facts about where the project deploys, not about a person; with - whole entries per person two people can drift (one deploys to the wrong driver set); -- the bind identity is the thing that should be personal: the vault's own audit then shows who - deployed, and the server never holds a shared production password; -- a new person sees the project's environments at once and only has to supply who they are. +**The server refuses to start** while `environments.file` holds a password key, and names the keys +to move into people's own files, so a shared password can't stay there unnoticed. -The pure alternative (every entry per person, no definitions) is a smaller change and is what -"nothing shared" says literally. It costs each person retyping the same URLs and DNs, and nothing -stops two people deploying the same tree to different driver sets. Decision 1 below. +## On the page -## 3. Layout +- **Connections** lists the project's vaults. Deployers add and edit definitions; each person + signs in to each vault for themselves (*Sign in*, *Update password*, *Forget*), and sees their + password's state (rejected, expiring, grace logins left). +- **Every environment picker** (Vault, Schema, App schema, Events, Trace) lists the person's + environments, with *set up your connection* on the ones they haven't signed in to. +- **Trace files** lists only the person's own recordings. -``` -data.dir/ - users// 0700, the person's own data - profile.properties name, email, last sign-in (for the admin's listing; subjects are opaque ids) - environments.properties their credentials by environment name, and any environments of their own - git.properties their own Git provider tokens by host (#35 P2; docs/pull-requests.md) - traces// their recordings and uploads - workspaces//// as today (snapshots stay inside the checkout) - environments/ - definitions.properties the shared definitions (today's environments.file, minus every password key) - secrets-.properties the shared driver secrets, deployer-only -``` +## The API -`` is the identity provider's subject (an OIDC `sub`, an agent's name). The server -process is one OS user, so separation is the server's to enforce: every read and write of a -person's directory goes through their session, and nothing in another person's directory is ever -listed, read or returned. Files are created `0600`, directories `0700`, as the credential files -are today. - -Agents (`agents=`) are subjects too: the CI agent sets its own credentials through the same API -with its token, and the Claude agent its own. An agent's credentials are not a person's. - -## 4. What changes - -**Core** (small): `Environments` gains `load(Path definitions, Path credentials)` returning the -merged view, with the credential file's keys for a name overlaying the definition's and names only -in the credential file taken whole. `Environments.load()` with no argument stays for the CLI. -`SecretSource.warnIfShared` runs on both files. - -**Web server**: - -- `Workspace.open(...)` takes the person's environments file; `VaultService`, `CredentialService`, - `EventsService` and `SetupService` hold the paths they were opened with and never call the - no-argument `load()`. The `IDM_ENVIRONMENTS` warning in `Serve.hosted` goes. -- `CredentialService` splits: *definitions* (list for everyone; add, change, remove for deployers, - audited) and *my credentials* (the signed-in person only: set bind DN and password source, test - the bind, remove). Driver secrets stay as they are, deployer-only, in the shared directory. - Nothing ever returns a value; reads describe the kind of source, as today. -- `TraceFiles` moves to `users//traces/`. -- A `UserData` helper: resolve and create `users/`, write `profile.properties` at sign-in, - list subjects with their profiles for the admin's view (later). -- Hosted keys: `environments.file` is read once more at start; if it holds password keys the - server refuses to start and says which keys to move (so an upgrade cannot silently keep a shared - password). `secrets.dir` (default `data.dir/environments`) names where the driver secrets live. - -**Page**: - -- **Connections** becomes two panels: *Environments* (the project's definitions; deployers edit) - and *My connections* (one row per environment: connected / not connected, bind DN, password - source, *Test*, *Set up…*, *Forget*). The project's `environments=` list still says which names - this tree is deployed to. -- Every view that lists environments shows the person's effective ones; an environment without a - credential is shown greyed with "set up your connection". -- **Trace files** lists only the person's own. - -**Local mode** is one person and is unchanged: one file, every key, as the CLI reads it. - -## 5. Migration - -- On first start after the change the server reads today's `environments.file`: entries' password - keys are *not* copied anywhere; the file is rewritten as `definitions.properties` without them, - and the old file is left in place renamed `.before-per-user`. The operator tells each person to - set up their connection, or does it for the lab's test accounts. -- Driver secrets files move to `secrets.dir` unchanged. -- Existing `traces/` recordings cannot be attributed to a person; they are left where they - are, read-only, under a *Shared (before per-user)* heading until someone deletes them, or the - operator removes the directory. -- The lab has orphan workspace directories from before project names were part of the path - (`workspaces/agent_claude`, `workspaces/`); the upgrade removes directories that no - project claims. - -## 6. Not in this change - -- An admin view of people and their last sign-in (`profile.properties` makes it possible). -- Server-side user settings (the chosen environment stays in the browser). -- Encrypting the credential files at rest. The password of record for production should be a - secret-manager reference, not a value; the server's own cloud identity runs the manager - commands, and that identity is shared by nature. Say so in `security.md`. -- Removing a person's directory when they leave (an admin operation, later). - -## 7. Decisions (taken 2026-10-08) - -1. **Definitions + per-person credentials** (section 2): the project's definitions hold no - secrets; each person's file holds their bind DN and password source per environment name. -2. **Driver secrets are shared** per environment, deployer-only, in `secrets.dir`. -3. **Agents hold their own credentials**, like a person. -4. **The server refuses to start** when the shared definitions file still holds a password key, - naming the keys to move. - -## 8. Phases - -| Phase | What | Needs | +| Call | Who | Fields | |---|---|---| -| U1 | core: `Environments.load(definitions, credentials)`; web services take explicit paths; `IDM_ENVIRONMENTS` no longer used in hosted mode | core release | -| U2 | `UserData`, per-person credential files, the split Connections view, migration | U1 | -| U3 | traces per person; orphan workspace cleanup | U2 | -| U4 | definitions editing by deployers in the page | U2 | - -## 9. As built (U2) - -- `environments.file` keeps its name and becomes the **definitions** file; the server refuses to - start while it holds a secret key (`Environments.secretKeys`), naming them. The driver secrets - stay **beside it** (`secrets-.properties` in the same directory), so an existing install - moves nothing; `secrets.dir` was not added. -- `data.dir/users//` (`UserData`, `0700`): `environments.properties` (the person's - credentials: `bindDn`, `password…`, `trustAll`, or whole environments of their own) and - `profile.properties` (subject, name, email, last seen). Created on first use; the profile is - touched whenever their environments are opened. -- `CredentialService` with two files: `list` marks each environment `defined`, `own`, - `connected`; `save` (deployers, `/api/credentials/environment`) writes the definition fields - only and refuses a password; `saveMine` / `removeMine` (`/api/credentials/mine`, - `/api/credentials/mine/remove`, any signed-in role) write the person's file, and for a name the - project does not define take the definition fields too. Driver secrets and `.secrets` - stay in the definitions, deployer-only. -- **U3:** trace recordings and uploads are the person's: `users//traces//`. - Recordings from before (`data.dir/traces//`) are not attributed; the server lists - such directories at start-up (with `workspaces/` of no project) as a NOTICE and never removes - them — the operator moves a recording under the person who made it, or deletes it. -- **U4:** the definition form holds everything a definition can: URL, driver set, tier, - `requires`, SSH host and user, and the event store (`eventsUrl`, `eventsUser`, `eventsTable`, - `eventsPseudonymise`, `eventsTree`); the person's connection holds the bind DN, trust, the - vault password and the event store's reader password (`eventsPassword…`), each from a file, a - variable, a command or a manager. The environment pickers (Vault, Schema, App schema, Events, - Trace) mark an environment the project defines but the person has not connected to with *set up - your connection*, linking to Connections; `GET /api/vault/environments` carries `split`, - `defined`, `connected`, `own`. +| `GET /api/vault/environments` | anyone signed in | answers each environment's `tier`, and whether it is `defined` (by the project), `own` (the person's), `connected` | +| `POST /api/credentials/environment` | deployers | **`name`**; the definition's fields (`url`, `driverSet`, `tier`, `requires`, `sshHost`, `sshUser`, `eventsUrl`, …); a password is refused | +| `POST /api/credentials/mine` | anyone signed in | **`name`**; `bindDn`, `password` with `passwordStore` (`file`; or `env:VAR`, `command:…` for deployers), `trustAll`, `eventsPassword` with `eventsPasswordStore`; for an environment of their own, the definition's fields too | +| `POST /api/credentials/mine/remove` | anyone signed in | **`name`** | + +Nothing returns a password: reads say what kind of source holds it. + +## Traces from before per-person data + +Trace recordings made before per-person data (in `data.dir/traces//`) belong to no one. +The server lists such directories at start-up as a NOTICE, along with workspaces no project +claims, and never removes them. Move a recording under the person who made it, or delete it. diff --git a/docs/pull-requests.md b/docs/pull-requests.md index 5d20406..9c3031a 100644 --- a/docs/pull-requests.md +++ b/docs/pull-requests.md @@ -1,178 +1,89 @@ -# Pull requests through the provider's API: design note (#35) +# Pull requests -Status: **decided 2026-10-13** (section 6): the recommendations, all four. **P1, P2 and P3 built** (sections 8–10): #35 complete. +On a branch that differs from the base, the repository bar offers **Create pull request…** (a merge +request on GitLab). The request is made through the provider's API, without leaving the page. +Once it exists, the bar shows it as a link with its number and state (`#12 open ↗`, `!12` on +GitLab), and no second one is offered. -## 1. Today +## Creating one -The repository bar shows *Pull request ↗* once a branch differs from the base. It is a link to -the provider's compare page (`Git.compareUrl`): GitHub's `/compare/...`, GitLab's -`/-/merge_requests/new`. The person leaves the app, signs in to the provider and fills the form -there. Any other host gets no link at all. +1. **Push the branch.** The button stays disabled until the branch is pushed. +2. **Create pull request…** opens a dialog: + - **Title**, required. It starts as the branch's last commit subject. + - **Description.** It starts as the list of the branch's commit subjects. + - **Draft**, unticked by default. -What the server already has: + The dialog says which account will be the author before you choose **Create**. +3. **Later pushes** update the request, since the provider follows the branch. -- **The project's repository token** (`repo.token`, `project..repo.token`, or a - `*.tokenEnv` / `*.tokenCommand` reference; `*.tokenKeychain` on a macOS host only). The server pushes with it over https. -- **Each person's own file** (`data.dir/users//`, docs/multi-user.md). Today it holds - their vault credentials by environment, and nobody else reads it. -- **Who is asking.** The signed-in person's name and email, and the editor and deployer roles. +Reviewers, labels, assignees and merging stay on the provider. A repository with no GitHub or +GitLab in front of it is merged on the page instead ([git-workflow.md](git-workflow.md)). -## 2. What it does +## Providers -1. On a branch that differs from the base, *Create pull request…* opens a short dialog: - - a title, defaulting to the branch's last commit subject; - - a body, defaulting to the branch's commit subjects as a list; - - the base branch (the project's), and a *draft* tick. -2. *Create* calls the provider's API. The bar then shows the request as a link with its - number and state, e.g. *#12 open ↗*, instead of the create button. -3. When the branch already has an open request, the bar shows that one, and no second one is - offered. On a page load the bar asks the provider for the branch's open request. -4. A push to the branch updates the request as usual, since the provider follows the branch. -5. The compare link stays as the fallback. It covers a host the server cannot reach or has no - credential for, and a provider it does not speak. - -| Provider | Create | The branch's open request | -|---|---|---| -| GitHub (github.com; Enterprise at `https://HOST/api/v3`) | `POST /repos/{owner}/{repo}/pulls` with `title`, `head`, `base`, `body`, `draft` | `GET /repos/{owner}/{repo}/pulls?head={owner}:{branch}&state=open` | -| GitLab (gitlab.com; self-managed at `https://HOST/api/v4`) | `POST /projects/{url-encoded path}/merge_requests` with `source_branch`, `target_branch`, `title` (a draft is a `Draft:` prefix), `description` | `GET /projects/{path}/merge_requests?source_branch={branch}&state=opened` | - -The provider and the API's address are inferred from the remote: `github.com`, or a host whose -name contains `gitlab`. A project can name them instead: +GitHub (github.com and Enterprise) and GitLab (gitlab.com and self-managed). The provider and its +API are worked out from the remote: `github.com`, or a host whose name contains `gitlab`. A project +on another host name says so in `hosted.properties`: ```properties -project..repo.provider=github # github | gitlab +project..repo.provider=github # github | gitlab project..repo.apiUrl=https://git.example.com/api/v3 +project..repo.caFile=/etc/idmweb/git-ca.crt # a self-hosted provider's private CA (else oidc.caFile) ``` -The call goes out through the server's TLS trust, the same trust the sign-in uses, so a -self-hosted provider with a private CA works when its root is trusted. - -## 3. Whose credential - -A pull request is created **as someone**. The provider records that account as its author and -sends the notifications. +Any other provider (Azure DevOps, Bitbucket), a project without a token, or a provider the server +can't reach gets a link to the provider's compare page instead. -| Option | Author on the provider | What people must do | Notes | -|---|---|---|---| -| A. the project's repository token | the token's account (a bot or service user) | nothing | the body ends with "Opened by Alice Example from DirXMLDevWeb" | -| B. each person's own provider token | the person | each adds a token to their own file, once | tokens with *pull requests: write* (GitHub) or *api* (GitLab) | -| C. B when the person has one, else A | the person, or the bot | optional | **recommended** | +## Whose account -With C, *My connection* gains one field, *Your Git provider token*. It is stored write-only in -the person's own file, like their vault password, and can be a secret-manager reference. The -dialog says which account will be the author before *Create*. +A request is created as someone: the provider names that account as its author and sends its +notifications. -The project token must allow it. On GitHub that is *Pull requests: read and write* on top of -*Contents* for the push. On GitLab it is the *api* scope. A token without it gets the provider's -own answer back, shown as it is, and the compare link is offered. +- **Your own provider token, when you have one.** *Connections → My Git provider tokens* holds one + per provider host: **Add**, **Replace**, **Forget**. The request is then yours on the provider. + - **Where it's kept:** the token stays in your own owner-only file + (`data.dir/users//git.properties`). Nothing gives its value back. + - **References:** a deployer may keep it as a reference instead (an environment variable, a + command or a secret manager). An editor's reference is refused when saved, and one saved + earlier is not used ([security.md](security.md)). +- **Otherwise, the project's repository token**, the one the server pushes with. The request is + the token's account's (a bot or service user), and its description ends with "Opened by + from DirXMLDevWeb". +- **A token that can't be read** (an unset variable, a failing command): the project's is used + and the dialog says why. -## 4. Workstation installs +The token needs to be allowed to create requests: on GitHub *Pull requests: read and write* (with +*Contents* for pushing), on GitLab the *api* scope. Otherwise the provider's answer is shown as it +is, with the compare link. -A workstation install (`serve `, local mode) has no repository token. The local container -is a hosted server, so it uses sections 2 and 3. Its pushes use the person's own git credentials. -Two ways to create a request there: +## The workstation install -- **The provider's CLI**: `gh pr create` or `glab mr create`, when one is installed and signed - in (`gh auth status`). It uses the person's existing sign-in and stores nothing new. - **Recommended.** -- A token in the Keychain (`IDMWEB_GIT_TOKEN`, or a `--git-token-keychain` flag). +A workstation install (`serve `) has no repository token. It uses the provider's own CLI, +`gh` for GitHub or `glab` for GitLab, when it is installed and signed in (`gh auth status`). The +request is then yours, through your existing sign-in, and nothing new is stored. When the CLI is +missing or signed out, the dialog says what to run (`gh auth login --hostname `) and offers +the compare link. The local container is a hosted server and works as above. -Without either, the compare link stays. +## Roles and audit -## 5. Roles, audit, failure +- **Who may create one:** editors and deployers, who may push. Readers see the link. +- **Audit:** each creation is in the audit log (`pull-request`), with the provider, the request's + number and address, the branch and base, and whose account was used. Never a token. -- **Roles.** Editors may create a request, since they may push. Readers see the link. -- **Audit.** Each creation is recorded: the provider, the request number and URL, the base and - the branch, the account used (bot or person), and the outcome. A token never is. -- **Failure.** The provider's message is shown, e.g. "A pull request already exists" or "Not - Found" for a token that cannot see the repository, and the compare link is offered. -- **Not in this change.** Reviewers, labels, assignees, merging from the page, Azure DevOps and - Bitbucket. +## The API -## 6. Decisions needed +For scripts and agents, with a session or `Authorization: Bearer `: -1. **Whose credential** (section 3): A, B, or C? (Recommended: C.) -2. **Providers in version one**: GitHub and GitLab together, or GitHub first and GitLab - second? (Recommended: together. They differ only in two calls.) -3. **Workstation installs** (section 4): the provider's CLI, a Keychain token, or the compare link - only? (Recommended: the CLI.) -4. **Draft by default?** (Recommended: no; the tick is there.) - -Decided 2026-10-13 (Jerry: "Go with the recommendations"): - -1. **C**: the person's own provider token when they have one, otherwise the project's repository - token, with "Opened by …" in the body. -2. **GitHub and GitLab together**, self-hosted included. -3. **Workstation installs through the provider's CLI** (`gh` / `glab`) when it is installed and signed in. -4. **Not a draft by default**; the tick is there. +| Call | Fields | Answer | +|---|---|---| +| `GET /api/git/pull-request` | | `available` (or the `reason` it isn't), `provider`, `as` (whose account), `via` (a CLI, on a workstation), `pushed`, the branch's open `request`, and a `title` and `body` to start from | +| `POST /api/git/pull-request` | **`title`** (required); `body`; `draft` (`true` or `false`, default `false`) | `ok` and the `request`, or the provider's `error` | +| `GET /api/credentials/git` | | the provider hosts of the server's projects, each with its projects and how your token is kept (`file`, `env:VAR`, `command`), or none | +| `POST /api/credentials/git` | **`host`**, **`store`**: `file` (with **`token`**), `env:VAR` or `command:` (references: deployers only) | saved, or why not | +| `POST /api/credentials/git/remove` | **`host`** | forgotten | -## 7. Phases +For example, on the branch the token's session has open: -| Phase | What | Needs | -|---|---|---| -| P1 | `PullRequests` (GitHub and GitLab clients, provider detection), `POST /api/git/pull-request`, `GET /api/git/pull-request`, the dialog and the bar's link, audit | decisions 1–2 | -| P2 | the person's provider token in *My connection* (if C or B) | P1, decision 1 | -| P3 | workstation installs through `gh` / `glab` (if chosen) | P1, decision 3 | - -Each phase is tested against a stub provider in the unit tests and the smoke run. P1 is also -checked against this repository on GitHub with a scratch branch, whose request is closed -afterwards. - -## 8. As built (P1) - -- `PullRequests` (web): GitHub and GitLab clients and the provider detection. `repo.provider`, - `repo.apiUrl` and `repo.caFile` (else `oidc.caFile`) are the project's settings. -- `GET /api/git/pull-request` answers `available` (or the `reason` it is not), the `provider`, whose - account it would be created `as`, whether the branch is `pushed`, the branch's open `request`, and - a `title` and `body` to start from. `POST /api/git/pull-request` takes `title` (required), `body` - and `draft`, needs an editor and a pushed branch, and answers `ok` with the `request`, or the - provider's `error`. Each creation is audited (`pull-request`), without the token. -- The bar asks only when the branch, its upstream or its unpushed count changes. It shows the open - request as `#12 open ↗` (`!12` on GitLab), else *Create pull request…* (disabled until pushed), - else the compare link. -- Found on the way: a workspace is a single-branch clone, so a push of its own branch never - recorded `origin/` (no "to push" count, no upstream). A workspace now tracks its own - branch when it is opened and before each push. -- Tested: `PullRequestsTest` against a stub of each provider, the hosted smoke against a stub - GitHub (fallback, create, the open one shown, audit, the description's "Opened by" line), the - page against the same stub, and the client against this repository on GitHub (a scratch draft - request, closed and its branch deleted). - -## 9. As built (P2) - -- `GitTokens`: a person's provider tokens in `data.dir/users//git.properties` (owner-only), - by host: `.token` (kept in that file), `.tokenEnv`, `.tokenCommand` (a secret - manager's CLI, through the same picker as a vault password). Write-only: nothing gives a value back. - A reference (an environment variable, a command or a secret manager) is a deployer's (commands - 2026-10-14, variables in 0.47.0): an editor's is refused when saved, and one already in their file - is never resolved (the project's token stands in, with the reason). An editor keeps the token in - their file. -- `GET /api/credentials/git` lists the provider hosts of the server's projects, with each host's - projects and how the person's token is kept (`file`, `env:VAR`, `command`) or none. - `POST /api/credentials/git` takes `host`, `store` (`file` with `token`, `env:VAR`, `command:…`); - `POST /api/credentials/git/remove` takes `host`. Both are audited with the host and the form, - never the value. Any signed-in editor manages their own. -- A pull request uses the person's token for the remote's host when there is one: the request is - theirs on the provider and the description carries no "Opened by" line. A token that cannot be - read (an unset variable, a failing command) falls back to the project's, and the dialog says why. -- The page: *My Git provider tokens* under *My connections* (Add, Replace, Forget), and the dialog - points there when the project's token would be used. -- Tested: `GitTokensTest` (forms, the owner-only file, refusals, the host list, no value given back), - the hosted smoke (her token used and sent, no "Opened by", the fallback with its reason, forget, - the audit without values), and the page against a project on a GitHub-style remote. - -## 10. As built (P3) - -- `CliPullRequests`: on a workstation install (`serve `) the remote's provider names the CLI, `gh` for GitHub and - `glab` for GitLab, found on `PATH` or in the usual install places (Homebrew included, for a server - started from a launcher). `gh auth status --hostname ` (or `glab`'s) says whether it is - signed in; its account is the author ("jcombs (through gh)"). It runs in the checkout, prompts and - pagers off: `gh pr list --head --state open --json …`, `gh pr create --base --head - --title --body [--draft]`; `glab mr list --source-branch --output json`, `glab mr create - --source-branch --target-branch --title --description [--draft] --yes`. Nothing is stored. -- The same routes as on a hosted server: `GET /api/git/pull-request` answers `via` (the CLI) and, when - it cannot be used, the reason with what to run (`gh auth login --hostname `). The base is - `main` or `master`, whichever the remote has. No "Opened by" line: the request is the person's. -- Tested: `CliPullRequestsTest` against stub `gh` and `glab` scripts (the calls made, signed out, - missing, the CLI's own error), the local smoke (a tree outside a checkout), and the page against - this repository through the real `gh`: a scratch draft request (#104), closed and its branch deleted. +```bash +curl -s -X POST https://idm.example.com/api/git/pull-request -H "Authorization: Bearer $IDMWEB_AGENT" -H "Content-Type: application/json" -d '{"title":"Contractors to their own OU","body":"- placement: contractors\n- GCV contractor.ou","draft":true}' +``` diff --git a/docs/simple-install.md b/docs/simple-install.md deleted file mode 100644 index e490799..0000000 --- a/docs/simple-install.md +++ /dev/null @@ -1,148 +0,0 @@ -# Setup in the image: one install for every operating system (design note) - -**Status: built 2026-10-15 (S1–S3, S4 on macOS and Linux); confirmed by Jerry with the recommended decisions below.** - -## Why - -The local install (one person, one container) is a zip with `setup.sh`, a 190-line shell script. -It needs a POSIX shell, `git`, `openssl` and `sed` on the host, so on Windows it runs only in WSL -or Git Bash, and its helper `~/idmweb/idm` is a shell script too. The image already carries -everything setup needs: a shell, `git`, `python3`, Java, and the server's own code. Setup moves -into the image, so the host needs **Docker and nothing else**. The commands are then the same in -PowerShell, macOS and Linux, and there is no zip. - -## The install, after - -```bash -docker run --rm --pull always -v "$HOME/idmweb:/idmweb" ghcr.io/pointbluetechnology/dirxml-dev-web:latest setup -docker compose -f "$HOME/idmweb/compose.yml" up -d -``` - -In PowerShell, the same two lines work: `$HOME` is your profile folder, and Docker Desktop -accepts the Windows path in `-v`. On Linux, create the folder first and run `setup` as yourself, -so the files are yours: - -```bash -mkdir -p ~/idmweb && docker run --rm --pull always -u "$(id -u):$(id -g)" -v "$HOME/idmweb:/idmweb" ghcr.io/pointbluetechnology/dirxml-dev-web:latest setup -``` - -The first command writes `~/idmweb` (the configuration, the tokens, the compose file, and the -repository when you give a tree) and prints the sign-in link. The second starts the server. -Options go after `setup`: - -- `--tree /tree`, with the tree mounted read-only: `-v /path/to/tree:/tree:ro` -- `--port 8081` -- `--image ` (the compose file's image; default: the image that ran setup) - -Then: - -- **CLI:** `docker exec idmweb idm validate tree`. The image's own `idm` launcher finds the - workspace and your credentials file the way the host wrapper does today, so no wrapper script - is needed on any OS. -- **Engine jars** (optional): copy them into `~/idmweb/lib` (Finder, Explorer or `cp`), then - `docker restart idmweb`. -- **Upgrade:** run the `setup` line with the new tag, then the `compose up` line. Your folder is - kept. -- **Stop:** `docker compose -f "$HOME/idmweb/compose.yml" down`. - -## How - -### `setup` in the image - -A new subcommand of the server, `idmweb setup` (Java, in `Serve`), reached through the -entrypoint's `case`. It does what `setup.sh` does today, from inside the container, on `/idmweb`: - -1. Writes `compose.yml` from a template in the jar: the image (the one running, unless - `--image`), the port, `container_name: idmweb`, and the four volumes, relative to the - compose file. Docker resolves them on the host, so the same file works from any OS. -2. Writes `config/hosted.properties` from the template (token identity, `token.user`, - `public.url`) unless one exists. -3. Tokens: generates `IDMWEB_TOKEN` and `IDMWEB_AGENT_TOKEN` with `SecureRandom` into - `idmweb.env` (mode 600) unless they exist. -4. With `--tree`: seeds `repo.git` with the image's `git` (the tree under `tree/`, `cases/` - beside it, the same `.gitignore`), as today. -5. Environments: the definitions and credentials split that `setup.sh` does with `grep` and - `sed` moves into Java. That code already parses the same files (`CredentialService`). -6. Prints the sign-in link (`/auth/login#signin=…`, as #123 adds) and the next command. - -**File ownership** (revised while building: the image runs as `idmweb`, and a plain `docker run` -cannot become root, so there is no `chown`). Docker Desktop on macOS and Windows maps ownership: -`setup` runs as the image's user and the files are yours. On Linux the command passes -`-u "$(id -u):$(id -g)"`, so `setup` writes as you. Either way, `compose.yml` records the uid:gid -`setup` ran as for the server's `user:`, as `IDMWEB_UID` does today. When `/idmweb` is not -writable, `setup` says so and gives the Linux command. Nothing runs as root. - -### The `idm` launcher finds the workspace - -`/opt/idmweb/bin/idm` gains what the host wrapper computes today: -- the first project and branch `main` (`IDMWEB_PROJECT` and `IDMWEB_BRANCH` override them); -- the workspace directory under `/var/lib/idmweb/workspaces/…`; -- `IDM_CREDENTIALS` from `token.user`. - -It does this only when the current directory is not already a tree, so `docker exec -w …` keeps -working. - -### What stays - -- `setup.sh` remains for developers working from a checkout. Its local-build path is - unchanged. -- The team-server install (`hosted-install.md`) is unchanged. -- Podman: the same commands with `podman` (`podman compose`). Rootless Podman keeps your uid, so - no `chown` is needed. - -### Why not one command - -`setup` could also start the server if the container mounted the Docker socket, but that hands a -setup step root-equivalent control of the host. Two commands, the second plain `docker compose`, -keep the install inspectable: you can read `compose.yml` before you start it. - -## Decided (Jerry, 2026-10-15) - -1. **The zip bundle is dropped** once `setup` ships. The release notes give the two commands. -2. **`container_name: idmweb`**: one local install per machine, so `docker exec idmweb …` works. -3. **`setup` is Java**: unit-testable, and it reuses the server's configuration code. - -## Verification - -- **Unit tests** for `setup`: a fresh folder, a re-run that keeps tokens and configuration, a tree - seeded, an environments file split, ownership on Linux. -- **The quickstart run** on macOS, on Linux (a Debian host), and in **Windows PowerShell** with - Docker Desktop. Each time: setup, compose up, the sign-in link, `docker exec idmweb idm validate - tree`, adding the jars and restarting, then an upgrade to a newer tag that keeps the folder. -- `bin/smoke-hosted.sh` is unchanged, and a new smoke step runs `setup` into a temporary folder. - -## Steps - -- **S1** `idmweb setup` (Java), with templates and tests. -- **S2** The `idm` launcher finds the workspace; `container_name`. -- **S3** Docs: the quickstart becomes the two commands, with PowerShell, macOS and Linux side by - side, and the release notes carry them too. -- **S4** The verification runs above, then a release without the zip. - -## Results (2026-10-15) - -- **S1.** `idmweb setup` (`LocalSetup`) is built, with `LocalSetupTest` (6 tests): a fresh folder, - a re-run that keeps tokens and configuration and takes a new port and image (`public.url` - follows the port), a tree and cases seeded, the environments split (including an older - `config/` with passwords), refusals, and the templates in the jar. The templates are - `hosted/docker/single-user`'s files, copied into the jar by the build: one source with - `setup.sh`. -- **S2.** The image's `idm` finds the workspace and your credentials file. The compose file - names the container `idmweb`. -- **S3.** The quickstart is the two commands, for macOS, Windows (PowerShell) and Linux. The - single-user README is the checkout reference. The release no longer carries the zip. -- **S4, macOS** (Docker Desktop), with an image built from the checkout: - - `setup` with a tree mounted read-only wrote the folder, owned by you; - - `compose up` started it, and the printed link signed in; - - opening the branch and running `docker exec idmweb idm validate tree` ran in the workspace; - - adding the jars and `docker restart idmweb` gave the full validation; - - re-running `setup` with a newer tag, then `compose up`, kept the token, the project and the - workspace. -- **S4, Linux** (DockerHost1, Debian): - - without `-u`, `setup` refused with the Linux command, including how to take back a folder - Docker had made as root; - - with it, every file was owned by the user (uid 1000) and the server ran as that uid; - - the token signed in, and `docker exec idmweb idm` ran. -- **Found on the way:** `idm validate` crashed without the engine jars (`NoClassDefFoundError`). - DirXMLDev#77 skips the checks that need them, with an info finding. -- **Not yet run:** Windows PowerShell (no Windows machine here) and Podman. diff --git a/docs/trace-viewer.md b/docs/trace-viewer.md deleted file mode 100644 index 177841f..0000000 --- a/docs/trace-viewer.md +++ /dev/null @@ -1,129 +0,0 @@ -# Trace viewer in the workbench: design note - -Status: **decided 2026-10-07** (Jerry took the proposals in section 7). Built: the shared library in DirXMLTraceViewer 1.5.0 (`dirxml-trace-core`), the Trace view in DirXMLDevWeb 0.16.0. - -The question: provide a web version of the -[DirXML Trace Viewer](https://github.com/PointBlueTechnology/DirXMLTraceViewer) (desktop, Swing, -MIT, 1.4.0) as part of this stack. The answer: yes, by sharing its engine rather than porting its -window. The viewer's `model` package — the DSTrace parser, formatter, highlighter, filter, -compactor, file reader — has no UI dependency and is already covered by tests; its `ldap` package -is the event-monitoring source over jldap. Those become a small library both products use. The -workbench's present Trace tab (raw lines over SSE with a text filter) is replaced by a viewer -built on the library, with the desktop viewer's features: per-driver and per-channel filtering, -syntax colouring with the engine's own DSTrace colours, find, compact whitespace, timestamp -formats, pause, record, save, and opening trace files. The desktop viewer stays; it keeps its -window, its DMG and its `driver.trace view` hand-off, and both render the same trace the same way. - -## 1. What exists today - -| Where | What | -|---|---| -| Desktop viewer | `model/`: `TraceParser` (headers, continuations attributed to the last header per server, DSTrace `%` directives), `DsTraceFormatter` + `FormattedText` (the `%+C`/`%nC`/`%-C` colour spans), `TraceRecord` (server, time, driver, channel, text, colour spans, header positions), `TraceHighlighter` (runs: XML markup, status keywords, DSTrace colours, header), `TraceFilter` (channels, driver names or DNs, text), `TraceCompactor`, `TimestampFormat`, `TraceFileReader`/`Writer`. `ldap/`: `LdapTraceEventSource` (DirXML + DirXML Drivers debug categories through `EdirEventSource`), `IdmExtendedOperations` (start/stop/restart), `DriverStatus`, `ServerInfo`, certificate pinning, legacy TLS. `ui/`: Swing. Tests on the model, the extended operations, the LDAP session, certificate trust. | -| DirXMLDev core | `operate/LdapTrace` over the simulator's `EdirTraceStream`: one driver's lines, continuation lines attributed to the last driver line, `driver.trace tail`, and `driver.trace view` which launches the desktop viewer. | -| DirXMLDevWeb | `GET /api/vault/trace?env&driver&server&engine` — server-sent events, one raw line each, keep-alive comments, deployer-only. `VaultView`'s Trace tab: driver and server pickers, start/stop, a text filter, a follow toggle, a `
` holding the last lines. The Drivers tab shows each driver's state and trace level per server and starts/stops drivers. |
-
-The web tab has none of the viewer's parsing: no channel attribution, no colours, no find, no
-files. Doing those on raw lines would re-implement the viewer; sharing the model does not.
-
-## 2. The library
-
-The viewer repository gains a second Maven module, `dirxml-trace-core` (groupId `com.pointbluetech`,
-MIT, Java 21): the `model` package as is, and the `ldap` package minus the Swing dialogs
-(`CertificateTrust` keeps its pinning store; the prompts move behind an interface the desktop
-implements with dialogs and the web answers from its per-environment settings). Its only
-dependency is jldap 4.3 from Maven Central. The desktop app module depends on it and keeps
-`ui/`, `update/`, `Main`. Releases attach both jars; the web's CI fetches `dirxml-trace-core` by
-version the way it fetches the core jar, and the hosted image gathers it with the others.
-
-Why a module and not a copy: one parser, one palette, one set of tests. A DSTrace quirk fixed for
-the desktop is fixed for the web in the next release, and the two never disagree about what a
-line means.
-
-jldap is open source and on Maven Central; it is not one of the engine jars the build refuses in
-an image. From DirXMLDev 0.37.0 it is the core's LDAP client too (the engine install's `ldap.jar`
-is no longer needed), and the image carries it.
-
-## 3. Server side (DirXMLDevWeb)
-
-- **Live trace.** `/api/vault/trace` keeps its URL and its SSE shape, but each event becomes a
-  parsed record, not a line: `{server, receivedAt, driver, channel, header:{ts,name,tag}, text,
-  runs:[[start,end,token,dsColor]…]}` — `TraceRecord` plus `TraceHighlighter.highlight`, serialised
-  once on the server. The source is `LdapTraceEventSource` per server of the driver set (the
-  environment's LDAPS settings, its CA or `trustAll` answering the certificate interface), one
-  parser per stream. `driver` is optional: without it the stream carries every driver of the set
-  and the page filters, which is how the desktop works and what a multi-driver investigation
-  needs. The core's `LdapTrace` stays for the CLI and the agent (`driver.trace tail`).
-- **Recording.** `record=true` on the stream also writes the formatted text through
-  `TraceFileWriter` to `/users//traces///-.trace` (the person's own; `~/.idm/traces/local/` on a workstation), unfiltered
-  (the desktop records what passes the filter; the page can save the displayed text itself, so
-  the server keeps everything). `GET /api/traces` lists them, `GET /api/traces/` serves one,
-  `DELETE` removes one; recordings are per person and project.
-- **Trace files.** `POST /api/traces/upload` (multipart, streamed to the traces directory, size
-  limit configurable, default 1 GB) and `GET /api/traces//records?from&count&channels&driver&text`
-  — the file is parsed once with `TraceFileReader`/`TraceParser.parseText` into an index (record
-  offsets, driver, channel, a lower-cased text hash for search) kept beside the file, so a 650 MB
-  file is paged and filtered on the server and the page never holds it whole. `GET
-  …/find?text&after` returns the next match positions.
-- **Who may use it.** Viewing trace, live or from a file, is for any signed-in user (today the
-  stream is deployer-only, which was a stand-in). Start, stop and restart stay deployer-only, on
-  the Drivers tab, through the core's operations as now; the viewer's `IdmExtendedOperations`
-  are not used because the core already has these. **Setting a trace level is missing from the
-  stack** (the Drivers tab only shows it): T2 adds `driver.trace-level` to the core — per server,
-  `DirXML-TraceLevel` on a driver, `DirXML-DriverTraceLevel` on the set, the typed environment
-  name like every vault write — and the Drivers tab gets the control beside start/stop.
-- **Audit.** A trace session and an upload are audit lines, as the stream already is.
-
-## 4. The page
-
-A `TraceViewer` component used in two places: the Vault view's Trace tab (live, per
-environment) and a **Trace** entry in the sidebar for files (no environment, so it works in the
-workstation install and for a customer's file sent by mail).
-
-- **Rendering.** A virtualised list (the page keeps up to the buffer size, 50,000 records,
-  shows 15,000, both settable as on the desktop); each record is a block of ``s from its
-  runs, with CSS classes per `TraceHighlighter.Token` and per DSTrace colour index — the desktop's
-  `TracePalette` becomes CSS variables, light and dark. Monospace, wrap toggle, font size,
-  auto-scroll.
-- **Filters** (hide): channels (Subscriber, Publisher, Service, Engine/Other), drivers (the
-  driver set's list from the outline, multi-select; the driver's trace name, not its object name,
-  as the parser sees it), contains text. `TraceFilter` logic runs on the page for live trace
-  (records already carry driver and channel) and on the server for files.
-- **Find** (highlight, do not hide): every match marked, Enter and Shift-Enter step, count shown.
-- **Compact whitespace**, **timestamp format** (U.S., European, ISO 8601, time only — rewritten
-  from `receivedAt` for viewer-stamped records as `withTimestampFormat` does), **pause** (the
-  stream keeps collecting into the buffer; the view freezes), **record** (server-side, section 3),
-  **save displayed** (download of the text on screen, filters applied), **copy** (plain text, as
-  the desktop's `TraceCopy`).
-- **Links into the tree.** This is what the web adds over the desktop: a policy name in a trace
-  header (`Applying policy %+C%14CNOVLADDCFG-sub-ctp-…%-C`) opens that policy; a driver name
-  opens the driver; a `` block links to the Validation panel's module.
-  Resolution is by the names in the outline, on the page.
-- **Keyboard**: the desktop's shortcuts where the browser allows them (find, pause, wrap, font).
-
-## 5. What stays with the desktop viewer
-
-Its window, the DMG and zip releases, its own connection dialog and certificate prompts, the
-update notice, `--demo`, and the `driver.trace view` hand-off. The desktop is for someone who
-has no workbench; the web is for someone who has the tree open and wants the policy under the
-cursor.
-
-## 6. Phases
-
-| Step | Where | What |
-|---|---|---|
-| T1 | viewer repo | split into `dirxml-trace-core` + app modules; the certificate prompt behind an interface; release 1.5.0 with both jars |
-| T2 | DirXMLDev + DirXMLDevWeb | `driver.trace-level` in the core; `/api/vault/trace` emits parsed records from `LdapTraceEventSource`; `TraceViewer` live: colours, filters, find, compact, timestamps, pause, save, copy; the Vault Trace tab uses it; trace viewing for any signed-in user |
-| T3 | DirXMLDevWeb | recordings and files: `/api/traces/*`, upload, server-side index and paging, the sidebar Trace entry |
-| T4 | DirXMLDevWeb | links into the tree (policy, driver, status → validation); buffer settings |
-
-T1 is Jerry's repository; T2 starts when 1.5.0 exists.
-
-## 7. Decisions (taken 2026-10-07)
-
-1. **A `dirxml-trace-core` module in the viewer repository**, released beside the app; the web
-   depends on it by version. No copy of the model.
-2. **Trace viewing for any signed-in user**; start, stop, restart and trace level stay
-   deployer-only.
-3. **Recordings and uploads per project under the data directory**, 1 GB limit by default.
-4. **The live stream carries the whole driver set** and the page filters; one event registration
-   per server.
diff --git a/src/main/java/com/pointblue/dirxml/web/EventsService.java b/src/main/java/com/pointblue/dirxml/web/EventsService.java
index 32931d5..986eea8 100644
--- a/src/main/java/com/pointblue/dirxml/web/EventsService.java
+++ b/src/main/java/com/pointblue/dirxml/web/EventsService.java
@@ -17,7 +17,7 @@
 import java.util.Map;
 
 /**
- * The Event Logger's store for the page (docs/event-logger.md): the rows an environment's store
+ * The Event Logger's store for the page (features.md, Events; DirXMLDev docs/event-store.md): the rows an environment's store
  * holds, one row with its modify diff and the other rows of the same engine event, and a row
  * saved as a simulator case in the tree's {@code cases/}. Reads go through the core's read-only
  * {@code EventStore}; the only write is the case directory. A store flagged
diff --git a/src/main/java/com/pointblue/dirxml/web/LocalSetup.java b/src/main/java/com/pointblue/dirxml/web/LocalSetup.java
index 9084d87..45f74e1 100644
--- a/src/main/java/com/pointblue/dirxml/web/LocalSetup.java
+++ b/src/main/java/com/pointblue/dirxml/web/LocalSetup.java
@@ -20,7 +20,7 @@
 
 /**
  * {@code idmweb setup}: the local install, one person and one container, written from inside the
- * image so the host needs only Docker (docs/simple-install.md). On {@code /idmweb} (the host's
+ * image so the host needs only Docker (docs/quickstart.md). On {@code /idmweb} (the host's
  * folder, mounted) it writes what {@code setup.sh} writes from a checkout: {@code compose.yml} and
  * {@code .env}, {@code config/hosted.properties}, the tokens in {@code idmweb.env}, the
  * environments split into definitions and your own credentials, and {@code repo.git} seeded with
diff --git a/src/main/java/com/pointblue/dirxml/web/Serve.java b/src/main/java/com/pointblue/dirxml/web/Serve.java
index 2be12d2..b9afa9a 100644
--- a/src/main/java/com/pointblue/dirxml/web/Serve.java
+++ b/src/main/java/com/pointblue/dirxml/web/Serve.java
@@ -33,7 +33,7 @@ public static void main(String[] args) throws Exception {
             return;
         }
         if (args.length < 2 || !args[0].equals("serve")) {
-            System.err.println("usage: idmweb serve  [--port N] [--no-open] [--web DIR] [--cases DIR] [--catalog DIR]\n       idmweb serve --hosted \n       idmweb setup [--tree DIR] [--port N] …   (in the image: the local install, docs/simple-install.md)\n       idmweb version");
+            System.err.println("usage: idmweb serve  [--port N] [--no-open] [--web DIR] [--cases DIR] [--catalog DIR]\n       idmweb serve --hosted \n       idmweb setup [--tree DIR] [--port N] …   (in the image: the local install, docs/quickstart.md)\n       idmweb version");
             System.exit(2);
         }
         Path tree = Path.of(args[1]).toAbsolutePath().normalize();
diff --git a/src/main/java/com/pointblue/dirxml/web/TraceFiles.java b/src/main/java/com/pointblue/dirxml/web/TraceFiles.java
index f3139e4..ab37a46 100644
--- a/src/main/java/com/pointblue/dirxml/web/TraceFiles.java
+++ b/src/main/java/com/pointblue/dirxml/web/TraceFiles.java
@@ -24,7 +24,7 @@
 import java.util.stream.Stream;
 
 /**
- * Trace files of one workspace (docs/trace-viewer.md, T3): recordings the live stream writes and
+ * Trace files of one workspace (features.md, Trace files): recordings the live stream writes and
  * files people upload, kept per project under the data directory. A file is parsed once with the
  * viewer library's reader into an index the page reads in pages, filtered on the server, so a
  * large file never travels whole. One parsed file is kept in memory at a time.
diff --git a/src/main/java/com/pointblue/dirxml/web/TreeService.java b/src/main/java/com/pointblue/dirxml/web/TreeService.java
index 2d1c737..3bbf1ed 100644
--- a/src/main/java/com/pointblue/dirxml/web/TreeService.java
+++ b/src/main/java/com/pointblue/dirxml/web/TreeService.java
@@ -320,7 +320,7 @@ static String language(String file) {
     }
 
     /**
-     * The driver set as Designer's Modeler draws it (#32, docs/architecture-view.md): the set with
+     * The driver set as Designer's Modeler draws it (#32): the set with
      * its servers and counts, and one application per driver — its Designer application type and
      * display name, whether it serves the vault itself, whether it carries an icon, its counts.
      */
@@ -380,7 +380,7 @@ public synchronized Map architecture() throws IOException {
 
     /**
      * The whole driver set as one tree, the way Designer's outline reads in the Developer
-     * perspective (#32 A2, docs/architecture-view.md §3): the set's configuration and library,
+     * perspective (#32): the set's configuration and library,
      * then each driver's configuration, channels with their policy sets in engine order and
      * policies in linkage order (the fishbone's model), resources, entitlements, forms, PRDs.
      * Every node says what the page opens when clicked.
diff --git a/src/test/java/com/pointblue/dirxml/web/LocalSetupTest.java b/src/test/java/com/pointblue/dirxml/web/LocalSetupTest.java
index 290d149..5bfc0ac 100644
--- a/src/test/java/com/pointblue/dirxml/web/LocalSetupTest.java
+++ b/src/test/java/com/pointblue/dirxml/web/LocalSetupTest.java
@@ -16,7 +16,7 @@
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
-/** {@code idmweb setup}, the local install written from inside the image (docs/simple-install.md). */
+/** {@code idmweb setup}, the local install written from inside the image (docs/quickstart.md). */
 public class LocalSetupTest {
 
     @Rule
diff --git a/web/src/components/ArchitectureView.tsx b/web/src/components/ArchitectureView.tsx
index 6683df5..71f8ece 100644
--- a/web/src/components/ArchitectureView.tsx
+++ b/web/src/components/ArchitectureView.tsx
@@ -4,7 +4,7 @@ import { EnvPicker, useEnvironment } from "../env";
 import type { ArchApplication, Architecture, DriverState, Selection } from "../types";
 
 /**
- * The driver set as Designer's Modeler draws it in Architect mode (#32, docs/architecture-view.md):
+ * The driver set as Designer's Modeler draws it in Architect mode (#32):
  * the Identity Vault in the centre with its driver set, one application per driver in two
  * columns with the driver's own icon and its application type's name, the driver drawn as a
  * badge on the connector, the drivers that serve the vault itself at the foot of each column. Hover for the
diff --git a/web/src/components/DeveloperView.tsx b/web/src/components/DeveloperView.tsx
index 5ad1ba5..e77a4e8 100644
--- a/web/src/components/DeveloperView.tsx
+++ b/web/src/components/DeveloperView.tsx
@@ -5,7 +5,7 @@ import type { DevNode, DriverState, Selection } from "../types";
 
 /**
  * The whole driver set as one tree, the way Designer's outline reads in the Developer
- * perspective (#32 A2, docs/architecture-view.md §3): the set's configuration and library, each
+ * perspective (#32): the set's configuration and library, each
  * driver's configuration, its channels with the policy sets in engine order and the policies in
  * linkage order, resources, entitlements, forms, PRDs. Every node opens what the sidebar would;
  * a filter keeps the matching nodes and their ancestors; the driver rows can carry the live
diff --git a/web/src/types.ts b/web/src/types.ts
index 377367e..8c5d65d 100644
--- a/web/src/types.ts
+++ b/web/src/types.ts
@@ -1215,7 +1215,7 @@ export interface GitDiff {
   files: { file: string; status: string; from?: string | null; language?: string; before?: string; after?: string }[];
 }
 
-/** The Event Logger's store, per environment (docs/event-logger.md). */
+/** The Event Logger's store, per environment (DirXMLDev docs/event-store.md). */
 export interface EventsDescribe {
   environment: string;
   configured: boolean;

From c6d49ae2a1e30053023c7cdbcb3205a63eea3d62 Mon Sep 17 00:00:00 2001
From: Jerry Combs 
Date: Fri, 23 Oct 2026 13:27:24 -0500
Subject: [PATCH 2/2] Release 0.75.0

Co-Authored-By: Claude Opus 5.5 
---
 CHANGELOG.md                                      | 4 +++-
 pom.xml                                           | 2 +-
 src/main/java/com/pointblue/dirxml/web/Serve.java | 2 +-
 3 files changed, 5 insertions(+), 3 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 87f2b00..50d83b8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,7 +5,9 @@ All notable changes to DirXMLDevWeb are documented in this file.
 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
 and this project uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
-## [Unreleased]
+## [0.75.0] - 2026-10-23
+
+Needs DirXMLDev 0.50.0.
 
 ### Changed
 
diff --git a/pom.xml b/pom.xml
index ff7e0b7..dc7feee 100644
--- a/pom.xml
+++ b/pom.xml
@@ -6,7 +6,7 @@
 
     com.pointblue.dirxml
     dirxml-dev-web
-    0.74.0
+    0.75.0
     
         
             MIT License
diff --git a/src/main/java/com/pointblue/dirxml/web/Serve.java b/src/main/java/com/pointblue/dirxml/web/Serve.java
index 7e5709a..e889d18 100644
--- a/src/main/java/com/pointblue/dirxml/web/Serve.java
+++ b/src/main/java/com/pointblue/dirxml/web/Serve.java
@@ -14,7 +14,7 @@
  */
 public final class Serve {
 
-    public static final String VERSION = "0.74.0";
+    public static final String VERSION = "0.75.0";
 
     private Serve() {
     }