diff --git a/.github/workflows/image.yml b/.github/workflows/image.yml index 6c755e6..625450d 100644 --- a/.github/workflows/image.yml +++ b/.github/workflows/image.yml @@ -40,6 +40,12 @@ jobs: repository: PointBlueTechnology/DirXMLDev ref: v${{ steps.v.outputs.core }} path: DirXMLDev + - name: DirXMLSimulator (its policy-testing skill, for the agent kit) + uses: actions/checkout@v4 + with: + repository: PointBlueTechnology/DirXMLSimulator + ref: v${{ steps.v.outputs.sim }} + path: DirXMLSimulator - name: DirXMLDev and simulator jars env: GH_TOKEN: ${{ github.token }} @@ -70,6 +76,7 @@ jobs: - name: Build and push env: IDM_HOME: ${{ github.workspace }}/DirXMLDev + SIM_HOME: ${{ github.workspace }}/DirXMLSimulator IDM_SIM_VERSION: ${{ steps.v.outputs.sim }} PUSH: "1" ALSO_TAG: latest diff --git a/CHANGELOG.md b/CHANGELOG.md index 60c6057..789e0d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,30 @@ 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.72.0] - 2026-10-22 + +Needs DirXMLDev 0.50.0. + +### Added + +- **Agents get everything a DirXMLDev install gives them** (docs/agents.md). + - **MCP:** the server is an MCP server at `/mcp`, with 22 tools: orientation, edit operations + (preview, then apply), commit, compare, compare environments, plan, deploy, rollback, driver + control, simulate, trace. Each tool is a call to the server's own API with the caller's token: + the same sign-in, roles, audit, write lock and production gate as the page. + - **Personal agent tokens:** your name at the top right → *agent tokens…*. An agent with your + token acts as you (your roles, connections, workspace), marked `via` the token in the audit log. + Only a hash is kept; roles follow you; an agent cannot make tokens. The dialog shows the command + that connects Claude Code. `agent.tokens` / `agent.tokens.maxDays` in `hosted.properties`. + - **Discoverable over MCP:** the agent kit is served as MCP resources (`dirxmldev://kit/…`), and + two prompts: `setup` (read the rules, install the skills, orient; `/mcp__dirxmldev__setup` in + Claude Code) and `orient`. The token dialog's *Set up your agent* gives the same text to paste + into agents without MCP prompts; it holds no token. + - **The agent kit:** this version's `AGENTS.md`, `CLAUDE.md`, the `dirxml-dev` and + `dirxml-policy-testing` skills and the agent docs. In the image; the local install's `setup` + writes it to `agent/`; *Download the agent kit* gives it with the server's MCP address. + - **`sim` and `apps` in the image**, beside `idm`: `docker exec idmweb sim …` (the policy + simulator's CLI), `docker exec idmweb apps …` (Identity Applications), in your workspace. ## [0.71.0] - 2026-10-20 diff --git a/bin/smoke-container.sh b/bin/smoke-container.sh index 2f1353e..56637ea 100755 --- a/bin/smoke-container.sh +++ b/bin/smoke-container.sh @@ -82,6 +82,25 @@ s, c = call("POST", "/api/git/commit", {"message": "From the container", "push": check("commit and push reach the host's repository", s == 200 and c["pushed"] and "From the container" in subprocess.run(["git", "--git-dir", os.path.join(work, "repo.git"), "log", "feature/container", "-1", "--format=%s"], capture_output=True, text=True).stdout) cli = subprocess.run(["docker", "exec", name, "idm", "validate", "/var/lib/idmweb/workspaces/repo/agent_ci/feature_container/tree", "--json"], capture_output=True, text=True) check("the idm CLI inside the container validates the same workspace", cli.returncode == 0 and '"ok":true' in cli.stdout.replace(" ", "")) +# agents (docs/agents.md): the simulator's and the Identity Applications' CLIs, the MCP endpoint, the agent kit +sim = subprocess.run(["docker", "exec", name, "sim"], capture_output=True, text=True) +check("the simulator's CLI is in the image", "run" in sim.stdout + sim.stderr and "usage" in (sim.stdout + sim.stderr).lower()) +apps = subprocess.run(["docker", "exec", name, "apps", "--help"], capture_output=True, text=True) +check("the Identity Applications helper is in the image", apps.returncode == 0 and "--env" in apps.stdout) +s, init = call("POST", "/mcp", {"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {"protocolVersion": "2025-06-18", "capabilities": {}, "clientInfo": {"name": "smoke", "version": "1"}}}) +check("the container answers MCP at /mcp", s == 200 and init["result"]["serverInfo"]["name"] == "dirxmldevweb") +s, rl = call("POST", "/mcp", {"jsonrpc": "2.0", "id": 2, "method": "resources/list"}) +uris = [x["uri"] for x in rl["result"]["resources"]] +check("its MCP resources are the kit: the rules and both skills", "dirxmldev://kit/AGENTS.md" in uris and "dirxmldev://kit/.claude/skills/dirxml-dev/SKILL.md" in uris + and "dirxmldev://kit/.claude/skills/dirxml-policy-testing/SKILL.md" in uris) +req = urllib.request.Request(base + "/api/agent-kit", headers={"Authorization": "Bearer " + TOKEN}) +with urllib.request.urlopen(req) as r: + import io, zipfile + z = zipfile.ZipFile(io.BytesIO(r.read())) +names = z.namelist() +check("the agent kit carries the rules, both skills and the docs, with this server's MCP address", + "agent-kit/AGENTS.md" in names and "agent-kit/.claude/skills/dirxml-dev/SKILL.md" in names and "agent-kit/.claude/skills/dirxml-policy-testing/SKILL.md" in names + and "agent-kit/docs/agent-guide.md" in names and (base + "/mcp") in z.read("agent-kit/AGENTS.md").decode()) with open(os.path.join(work, "data", "audit.log")) as f: lines = [json.loads(l) for l in f if l.strip()] check("the audit log names the agent", any(l["user"] == "agent:ci" and l["event"] == "commit+push" for l in lines)) diff --git a/bin/smoke-hosted.sh b/bin/smoke-hosted.sh index d4680ba..c38bb67 100755 --- a/bin/smoke-hosted.sh +++ b/bin/smoke-hosted.sh @@ -42,7 +42,7 @@ agents=ci:idm-deployers;reader: agent.ci.token=ci-agent-token-0123456789 agent.reader.token=reader-agent-token-0123456789 ENV -IDM_ENVIRONMENTS="$WORK/data/environments.properties" "$HERE/bin/idmweb" serve --hosted "$WORK/hosted.properties" > "$WORK/log" 2>&1 & +IDMWEB_AGENT_KIT="$HERE/hosted/agent-kit" IDM_ENVIRONMENTS="$WORK/data/environments.properties" "$HERE/bin/idmweb" serve --hosted "$WORK/hosted.properties" > "$WORK/log" 2>&1 & PID=$! i=0 while ! grep -q 'hosted:' "$WORK/log" 2>/dev/null; do @@ -256,6 +256,55 @@ check("a wrong token is refused", ci.call("GET", "/api/session", headers={"Autho s, _, _ = ci.call("POST", "/api/workspaces/open", {"branch": "feature/x"}, headers={"Authorization": "Bearer reader-agent-token-0123456789"}, csrf=False) check("an agent without a role reads only", s == 403) +# personal agent tokens and the MCP endpoint (docs/agents.md): alice's agent acts as alice +s, made, _ = alice.call("POST", "/api/agent-tokens", {"label": "claude", "days": 30}) +check("a person makes a token for her agent; it is shown once", s == 200 and str(made.get("token", "")).startswith("idmw_") and made["label"] == "claude") +mine = {"Authorization": "Bearer " + made["token"]} +s, sess, _ = ci.call("GET", "/api/session", headers=mine) +check("her agent signs in as her, with her roles", s == 200 and sess["user"]["subject"] == "alice" and sess["user"]["editor"] and not sess["user"]["deployer"]) +check("an agent cannot make tokens", ci.call("POST", "/api/agent-tokens", {"label": "x"}, headers=mine, csrf=False)[0] == 403) +s, lst, _ = alice.call("GET", "/api/agent-tokens") +check("her token list shows the label, never the token", s == 200 and [t["label"] for t in lst["tokens"]] == ["claude"] and made["token"] not in json.dumps(lst)) +def mcp(body, headers=None): + h = dict(mine) + h.update(headers or {}) + return ci.call("POST", "/mcp", body, headers=h, csrf=False) +s, init, _ = mcp({"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {"protocolVersion": "2025-06-18", "capabilities": {}, "clientInfo": {"name": "smoke", "version": "1"}}}) +check("MCP: initialize answers with the protocol and the rules", s == 200 and init["result"]["protocolVersion"] == "2025-06-18" and "plan_deploy" in init["result"]["instructions"]) +s, tl, _ = mcp({"jsonrpc": "2.0", "id": 2, "method": "tools/list"}) +names = [t["name"] for t in tl["result"]["tools"]] +check("MCP: the tools are listed, the vault writes marked destructive", s == 200 and "run_operations" in names and "deploy" in names + and [t for t in tl["result"]["tools"] if t["name"] == "deploy"][0]["annotations"]["destructiveHint"]) +s, r, _ = mcp({"jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": {"name": "session", "arguments": {}}}) +text = r["result"]["content"][0]["text"] +check("MCP: a tool call acts as her, and the page's CSRF token stays out", s == 200 and not r["result"]["isError"] and '"subject":"alice"' in text and "csrf" not in text) +s, r, _ = mcp({"jsonrpc": "2.0", "id": 4, "method": "tools/call", "params": {"name": "outline", "arguments": {}}}) +check("MCP: her branch's tree is read through the API", s == 200 and not r["result"]["isError"] and "Loop" in r["result"]["content"][0]["text"]) +s, r, _ = mcp({"jsonrpc": "2.0", "id": 5, "method": "tools/call", "params": {"name": "deploy", "arguments": {"env": "lab", "confirm": "lab"}}}) +check("MCP: a vault write is the API's to refuse (an editor, not a deployer)", s == 200 and r["result"]["isError"] and "deployer" in r["result"]["content"][0]["text"]) +s, _, _ = mcp({"jsonrpc": "2.0", "id": 6, "method": "tools/list"}, headers={"Host": "evil.example"}) +check("MCP: another host is refused", s == 403) +s, _, _ = anon.call("POST", "/mcp", {"jsonrpc": "2.0", "id": 7, "method": "tools/list"}, csrf=False) +check("MCP: without a token it says to sign in", s == 401) +check("MCP: GET has no stream (405)", ci.call("GET", "/mcp", headers=mine)[0] == 405) +s, rl, _ = mcp({"jsonrpc": "2.0", "id": 8, "method": "resources/list"}) +uris = [x["uri"] for x in rl["result"]["resources"]] +check("MCP: the agent kit is listed as resources", s == 200 and "dirxmldev://kit/AGENTS.md" in uris) +s, rr, _ = mcp({"jsonrpc": "2.0", "id": 9, "method": "resources/read", "params": {"uri": "dirxmldev://kit/AGENTS.md"}}) +check("MCP: a resource reads with this server's MCP address filled in", s == 200 and (base + "/mcp") in rr["result"]["contents"][0]["text"] and "{{" not in rr["result"]["contents"][0]["text"]) +s, bad, _ = mcp({"jsonrpc": "2.0", "id": 10, "method": "resources/read", "params": {"uri": "dirxmldev://kit/../../hosted.properties"}}) +check("MCP: a resource outside the kit is refused", s == 200 and "error" in bad) +s, pl, _ = mcp({"jsonrpc": "2.0", "id": 11, "method": "prompts/list"}) +check("MCP: the setup and orient prompts are listed", s == 200 and [p["name"] for p in pl["result"]["prompts"]] == ["setup", "orient"]) +s, pg, _ = mcp({"jsonrpc": "2.0", "id": 12, "method": "prompts/get", "params": {"name": "setup", "arguments": {"skills_dir": ".claude/skills"}}}) +st = pg["result"]["messages"][0]["content"]["text"] +check("MCP: the setup prompt reads the rules, installs the skills where asked, then orients", s == 200 and "dirxmldev://kit/AGENTS.md" in st and ".claude/skills//agent-tokens.json` in the server's data folder, +owner-only), with who you were when you last used the page, so a change of your roles reaches your +tokens. On a team server, an administrator can turn personal tokens off (`agent.tokens=false`) or +shorten their longest life (`agent.tokens.maxDays`, default 365) in `hosted.properties`. + +Agents that act for no one (CI) keep their own `agents=` entries in `hosted.properties` +([hosted-mode.md](hosted-mode.md)). + +## 2. MCP + +The server is an MCP server at `/mcp` (Streamable HTTP, stateless). With Claude Code: + +```bash +claude mcp add --transport http dirxmldev http://localhost:8080/mcp --header "Authorization: Bearer " +``` + +Use your server's address for a team server (`https://idm.example.com/mcp`). Keep the token in an +environment variable rather than in a file you commit. + +Every tool is a call to the server's own API, made with your token: the same sign-in, origin check, +roles, audit, per-environment write lock and production gate as the page. + +| Tool | What it does | +|---|---| +| `session` | who you act as, your roles, the project and branch open, the versions | +| `outline` | the tree: drivers, policies, resources, objects, and its revision | +| `read_artifact`, `read_file` | an artifact's content by tree path; a file of the tree | +| `validate` | the engine's own checks over the tree | +| `fishbone` | a driver's policy flow | +| `git_status`, `git_diff` | the workspace against the last commit or the base branch | +| `list_operations` | every edit operation and its arguments | +| `run_operations` | edit the tree: preview (the default), then apply | +| `commit` | commit, and push | +| `environments` | the environments, their tier, whether you are connected | +| `compare` | the tree against an environment's vault | +| `compare_environments` | two environments' vaults side by side | +| `plan_deploy` | a deploy step by step, and what stands in its way | +| `driver_status`, `trace`, `snapshots` | drivers' state, a driver's trace, the snapshots | +| `simulate` | run the project's simulator cases | +| `deploy`, `rollback`, `driver_lifecycle` | vault writes: need the deployer role, and the environment's name typed by the person as `confirm` | + +The server tells the agent the rules when it connects: preview before applying, the plan before any +vault write, the person types the environment's name, production only from `main`, never a secret. + +It also serves this version's [agent kit](#4-the-agent-kit) as MCP **resources** +(`dirxmldev://kit/AGENTS.md`, `dirxmldev://kit/.claude/skills/…`, `dirxmldev://kit/docs/…`), and two +**prompts**: + +- `setup`: read the rules, install the skills (into `~/.claude/skills` unless you give `skills_dir`; + it asks before overwriting), read the agent docs, then orient. Nothing is changed in the tree or a + vault. +- `orient`: who you act as, the project and branch, what `validate` says, what is uncommitted. + +### Set up your agent + +After `claude mcp add`, in Claude Code: + +``` +/mcp__dirxmldev__setup +``` + +Agents that don't offer MCP prompts can be given the same text: *agent tokens…* → *Set up your +agent* → *Copy*, and paste it into the chat. It holds no token. + +## 3. The CLIs + +On the machine that runs the container (the local install), the CLIs run in your workspace of the +first project's branch `main`: + +```bash +docker exec idmweb idm validate tree +``` + +```bash +docker exec idmweb sim +``` + +```bash +docker exec idmweb apps --env lab tasks +``` + +`-e IDMWEB_BRANCH=feature/x` picks another branch, `-e IDMWEB_PROJECT=` another project. +Paths are the container's: use paths relative to the workspace (`tree`, `cases`). On Windows, Git +Bash rewrites arguments that look like Unix paths; prefix `MSYS_NO_PATHCONV=1` for an absolute one. + +## 4. The agent kit + +The rules, skills and docs of this version, for any agent that reads `AGENTS.md` or skills: + +- `AGENTS.md` (and `CLAUDE.md`, which imports it): how to connect, and the rules; +- `.claude/skills/dirxml-dev/`: editing, validating, simulating and deploying a driver set; +- `.claude/skills/dirxml-policy-testing/`: testing and debugging policies with the simulator; +- `docs/`: the agent guide, this page, and how work moves between branches and environments. + +The local install's `setup` writes it to `agent/` in your `idmweb` folder, and refreshes it on every +upgrade; start Claude Code there, or copy `agent/.claude/skills` to `~/.claude/skills` to have the +skills everywhere. On any server, *agent tokens…* → *Download the agent kit* gives it as a zip with +that server's MCP address. + +## What stays on a workstation + +A container can't hold what lives on the desktop: Designer, the vendor form builder, the VS Code +extension's views of local files, and macOS Keychain references for secrets (the container keeps +secrets in its own files, environment variables or command references). Install DirXMLDev on the +workstation for those; it works on the same repository. diff --git a/docs/hosted-mode.md b/docs/hosted-mode.md index dae3718..8c4da56 100644 --- a/docs/hosted-mode.md +++ b/docs/hosted-mode.md @@ -98,6 +98,10 @@ are mounted, never shipped. `hosted/docker/compose.yml` runs it behind Caddy (TL **Agents** (CI, an MCP server) use the same server with `Authorization: Bearer ` from `agents=`, or the CLI with `docker exec idmweb idm …`; `bin/smoke-container.sh` proves the image. +**People's agents** (Claude Code, Cursor) use a token the person makes on the page and act as that +person ([agents.md](agents.md)): the server's MCP endpoint is `/mcp`. `agent.tokens=false` turns +personal tokens off; `agent.tokens.maxDays` (default 365) caps how long one may last. + ## A project's repository: moving it to GitHub or GitLab A project made on the page pushes to a repository this server created, `data/projects/.git`. diff --git a/docs/quickstart.md b/docs/quickstart.md index a54cc98..4061f78 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -132,10 +132,18 @@ docker exec idmweb idm validate tree docker exec idmweb idm simulate tree --cases cases ``` -An agent on the same machine (Claude Code, a script, an MCP server) uses the same two doors: -`docker exec idmweb idm …` as a command, or the API at `http://localhost:8080/api/…` with -`Authorization: Bearer ` (the second line of `idmweb.env`). That is every -endpoint the page uses, with the agent's name on each audit line. +The simulator's CLI is `docker exec idmweb sim …` and the Identity Applications helper +`docker exec idmweb apps …`, in the same workspace. + +**Agents** (Claude Code, Cursor, a script) get everything a DirXMLDev install gives them +([agents.md](agents.md)): + +- **MCP:** the server is an MCP server at `http://localhost:8080/mcp`. Make a token on the page + (your name at the top right → *agent tokens…*), which shows the command that connects Claude + Code. The agent acts as you, under your roles. +- **The kit:** `setup` writes this version's rules, skills and docs to `agent/` in the folder: start + Claude Code there, or copy `agent/.claude/skills` into your own. +- **The CLIs** above, and the API at `http://localhost:8080/api/…` with the same token. ## 6. Vaults diff --git a/hosted/agent-kit/AGENTS.md b/hosted/agent-kit/AGENTS.md new file mode 100644 index 0000000..3af92d6 --- /dev/null +++ b/hosted/agent-kit/AGENTS.md @@ -0,0 +1,52 @@ +# Working on this IDM project with an agent + +This folder is the agent kit of a DirXMLDevWeb {{VERSION}} server: the rules, the skills and the docs +for this version, for Claude Code, Cursor, Codex or any agent that reads `AGENTS.md`. An agent works +through the same server people use, as one of them, under the same rules. + +## Connect + +**MCP (preferred).** The server is an MCP server at `{{MCP_URL}}`. Make a token on the page (your +name at the top right → *Agent tokens…*); the agent then acts as you: your roles, your connections, +your workspace of the branch you have open. For Claude Code: + +``` +claude mcp add --transport http dirxmldev {{MCP_URL}} --header "Authorization: Bearer " +``` + +Keep the token out of chats and files you commit: put it in an environment variable and refer to it. + +Then run the server's `setup` prompt (`/mcp__dirxmldev__setup` in Claude Code): it installs the +skills below from the server's resources (`dirxmldev://kit/…`) and orients the agent. An agent +without MCP prompts can be given the same text from *Agent tokens…* → *Set up your agent*. + +**The CLIs, on the machine that runs the container:** `docker exec idmweb idm …` (DirXMLDev), +`docker exec idmweb sim …` (the policy simulator), `docker exec idmweb apps …` (Identity +Applications). They run in your workspace of the first project's branch `main`; +`docker exec -e IDMWEB_BRANCH=feature/x idmweb idm …` picks another branch, +`-e IDMWEB_PROJECT=` another project. Paths are the container's: use paths relative to the +workspace (`tree`, `cases`). Wherever a skill or doc below says `bin/idm`, `bin/sim` or `bin/apps`, +run these. + +## The rules + +1. Orient first: `session`, `outline`, `validate` (or `idm validate tree`). `idm` with no arguments, + or `list_operations`, is the list of what exists; never invent a flag or an operation. +2. Edit the tree with operations (`run_operations`, or `idm tree …`); preview before + applying; validate after. +3. Before any vault write, show the person the plan (`plan_deploy`, or `idm vault.deploy tree --env E + --dry-run`) and wait for a yes. A deploy, rollback or driver start/stop needs the environment's name, + typed by the person in chat. Never fill it in yourself. +4. Production deploys come from `main`, pushed and current. Never work around a refusal: report it. +5. Never print a secret. Tools and the CLI never show one; don't look for them. +6. Commit with a message that says why; never commit secrets, exports or client data. + +## What's here + +- `.claude/skills/dirxml-dev/`: editing, validating, simulating and deploying a driver set + (DirXMLDev). +- `.claude/skills/dirxml-policy-testing/`: testing and debugging policies with the simulator. +- `docs/agent-guide.md`: the edit loop and its refusals, in full. +- `docs/agents.md`: this server for agents: the MCP tools, tokens, what each may do. +- `docs/environment-practices.md` and `docs/git-workflow.md`: how work moves between branches and + environments. diff --git a/hosted/agent-kit/CLAUDE.md b/hosted/agent-kit/CLAUDE.md new file mode 100644 index 0000000..43c994c --- /dev/null +++ b/hosted/agent-kit/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/hosted/docker/Dockerfile b/hosted/docker/Dockerfile index bab072d..7ce0e60 100644 --- a/hosted/docker/Dockerfile +++ b/hosted/docker/Dockerfile @@ -17,9 +17,12 @@ COPY artifacts/dist /opt/idmweb/web/dist COPY artifacts/secrets /opt/idmweb/bin/secrets COPY artifacts/terraform /opt/idmweb/terraform COPY artifacts/THIRD-PARTY.md artifacts/LICENSE /opt/idmweb/ -COPY bin/idm bin/idmweb bin/entrypoint.sh /opt/idmweb/bin/ +COPY artifacts/apps.py /opt/idmweb/bin/apps.py +COPY artifacts/agent-kit /opt/idmweb/agent-kit +COPY bin/idm bin/sim bin/apps bin/inworkspace bin/idmweb bin/entrypoint.sh /opt/idmweb/bin/ RUN chmod +x /opt/idmweb/bin/* /opt/idmweb/bin/secrets/* && mkdir -p /var/lib/idmweb /etc/idmweb /opt/idm/lib \ && chown -R idmweb:idmweb /var/lib/idmweb /etc/idmweb && ln -s /opt/idmweb/bin/idm /usr/local/bin/idm \ + && ln -s /opt/idmweb/bin/sim /usr/local/bin/sim && ln -s /opt/idmweb/bin/apps /usr/local/bin/apps \ # a repository mounted into the container is owned by another uid, and the container may run as # the host's user (the single-user layout): git's rules live in the system config, not a home && git config --system --add safe.directory '*' && git config --system init.defaultBranch main diff --git a/hosted/docker/bin/apps b/hosted/docker/bin/apps new file mode 100755 index 0000000..362c33c --- /dev/null +++ b/hosted/docker/bin/apps @@ -0,0 +1,6 @@ +#!/bin/sh +# The Identity Applications helper inside the container (bin/apps on a workstation): DirXMLDev's +# bin/apps, with the project's environments and your own credentials. `docker exec idmweb apps …`. +set -e +. /opt/idmweb/bin/inworkspace +exec python3 /opt/idmweb/bin/apps.py "$@" diff --git a/hosted/docker/bin/idm b/hosted/docker/bin/idm index 226f809..40292de 100755 --- a/hosted/docker/bin/idm +++ b/hosted/docker/bin/idm @@ -3,28 +3,8 @@ # customer mounted at $IDM_LIB. `docker exec idmweb idm …` is the same command an agent runs # on a workstation. set -e -# The local install (docs/simple-install.md): `docker exec idmweb idm validate tree` runs in your -# workspace of the first project's branch main (IDMWEB_PROJECT and IDMWEB_BRANCH pick another), with -# your own credentials file, when it starts in the image's working directory. Anywhere else (a -# `-w` into a tree, a team server) it runs where it is. -H=/etc/idmweb/hosted.properties -if [ -f "$H" ] && [ "$(pwd)" = /opt/idmweb ]; then - san() { printf '%s' "$1" | sed 's#[^A-Za-z0-9._-]#_#g'; } - U=$(sed -n 's/^token\.user=//p' "$H" | head -1) - if [ -n "$U" ]; then - S=$(san "$U") - MINE="/var/lib/idmweb/users/$S/environments.properties" - if [ -z "${IDM_CREDENTIALS:-}" ] && [ -f "$MINE" ]; then export IDM_CREDENTIALS="$MINE"; fi - P=${IDMWEB_PROJECT:-$(sed -n 's/^projects=//p' "$H" | head -1 | cut -d, -f1)} - B=${IDMWEB_BRANCH:-main} - if [ -n "$P" ]; then - W="/var/lib/idmweb/workspaces/$(san "$P")/$S/$(san "$B")" - if [ -d "$W" ]; then cd "$W"; else echo "note: no workspace for $P/$B yet (open the branch on the page); running outside one" >&2; cd /var/lib/idmweb; fi - else - cd /var/lib/idmweb - fi - fi -fi +# In your workspace (inworkspace says how), then the CLI. +. /opt/idmweb/bin/inworkspace IDM_HOME=${IDM_HOME:-/opt/idmweb} IDM_LIB=${IDM_LIB:-/opt/idm/lib} CP="$IDM_HOME/lib/dirxml-dev.jar:$IDM_HOME/lib/dirxml-simulator.jar:$IDM_HOME/lib/jldap.jar:$IDM_HOME/lib/dirxml-trace-core.jar:$IDM_HOME/lib/postgresql.jar" diff --git a/hosted/docker/bin/inworkspace b/hosted/docker/bin/inworkspace new file mode 100644 index 0000000..2d93c5b --- /dev/null +++ b/hosted/docker/bin/inworkspace @@ -0,0 +1,22 @@ +# Sourced by idm, sim and apps: the local install (docs/simple-install.md) runs them in your workspace +# of the first project's branch main (IDMWEB_PROJECT and IDMWEB_BRANCH pick another), with your own +# credentials file, when they start in the image's working directory. Anywhere else (a `-w` into a +# tree, a team server) they run where they are. +H=/etc/idmweb/hosted.properties +if [ -f "$H" ] && [ "$(pwd)" = /opt/idmweb ]; then + san() { printf '%s' "$1" | sed 's#[^A-Za-z0-9._-]#_#g'; } + U=$(sed -n 's/^token\.user=//p' "$H" | head -1) + if [ -n "$U" ]; then + S=$(san "$U") + MINE="/var/lib/idmweb/users/$S/environments.properties" + if [ -z "${IDM_CREDENTIALS:-}" ] && [ -f "$MINE" ]; then export IDM_CREDENTIALS="$MINE"; fi + P=${IDMWEB_PROJECT:-$(sed -n 's/^projects=//p' "$H" | head -1 | cut -d, -f1)} + B=${IDMWEB_BRANCH:-main} + if [ -n "$P" ]; then + W="/var/lib/idmweb/workspaces/$(san "$P")/$S/$(san "$B")" + if [ -d "$W" ]; then cd "$W"; else echo "note: no workspace for $P/$B yet (open the branch on the page); running outside one" >&2; cd /var/lib/idmweb; fi + else + cd /var/lib/idmweb + fi + fi +fi diff --git a/hosted/docker/bin/sim b/hosted/docker/bin/sim new file mode 100755 index 0000000..7f983c8 --- /dev/null +++ b/hosted/docker/bin/sim @@ -0,0 +1,10 @@ +#!/bin/sh +# The DirXML Policy Simulator's CLI inside the container (bin/sim on a workstation): the simulator's jar +# and the engine jars the customer mounted at $IDM_LIB. `docker exec idmweb sim …`. +set -e +. /opt/idmweb/bin/inworkspace +IDM_HOME=${IDM_HOME:-/opt/idmweb} +IDM_LIB=${IDM_LIB:-/opt/idm/lib} +CP="$IDM_HOME/lib/dirxml-simulator.jar" +for j in "$IDM_LIB"/*.jar; do [ -f "$j" ] && CP="$CP:$j"; done +exec java ${SIM_JAVA_OPTS:-} -cp "$CP" com.pointblue.dirxml.sim.Cli "$@" diff --git a/hosted/docker/build.sh b/hosted/docker/build.sh index 0ffc9ff..a64c321 100755 --- a/hosted/docker/build.sh +++ b/hosted/docker/build.sh @@ -42,6 +42,38 @@ cp -R "$IDM_HOME/bin/secrets" "$CTX/artifacts/secrets" cp -R "$IDM_HOME/terraform" "$CTX/artifacts/terraform" find "$CTX/artifacts/terraform" -name .terraform -prune -exec rm -rf {} + 2>/dev/null || true find "$CTX/artifacts/terraform" \( -name '*.tfstate*' -o -name '.terraform.lock.hcl' \) -delete 2>/dev/null || true +# bin/apps (the Identity Applications helper), behind the image's apps launcher +cp "$IDM_HOME/bin/apps" "$CTX/artifacts/apps.py" +# the agent kit (docs/agents.md): this version's rules, skills and docs for an agent working with the server +SIM_HOME=${SIM_HOME:-"$HERE/../DirXMLSimulator"} +KIT="$CTX/artifacts/agent-kit" +mkdir -p "$KIT/.claude/skills" "$KIT/docs" +cp "$HERE/hosted/agent-kit/AGENTS.md" "$HERE/hosted/agent-kit/CLAUDE.md" "$KIT/" +cp -R "$IDM_HOME/.claude/skills/dirxml-dev" "$KIT/.claude/skills/" +if [ -d "$SIM_HOME/.claude/skills/dirxml-policy-testing" ]; then + cp -R "$SIM_HOME/.claude/skills/dirxml-policy-testing" "$KIT/.claude/skills/" +else + echo "note: no DirXMLSimulator checkout at $SIM_HOME: the agent kit has no simulator skill" >&2 +fi +cp "$IDM_HOME/docs/agent-guide.md" "$KIT/docs/" +cp "$HERE/docs/agents.md" "$HERE/docs/environment-practices.md" "$HERE/docs/git-workflow.md" "$KIT/docs/" +# each skill says, under its front matter, where its commands are in this install +for s in "$KIT"/.claude/skills/*/SKILL.md; do + python3 - "$s" <<'PY' +import sys +p = sys.argv[1] +t = open(p, encoding="utf-8").read() +note = ("\n> **In a DirXMLDevWeb install:** on the machine that runs the container, `bin/idm` is `docker exec idmweb idm`, " + "`bin/sim` is `docker exec idmweb sim` and `bin/apps` is `docker exec idmweb apps`, run in your workspace; " + "or use the `dirxmldev` MCP tools. The kit's AGENTS.md has the rules and how to connect.\n") +if t.startswith("---"): + end = t.index("\n---", 3) + 4 + t = t[:end] + "\n" + note + t[end:] +else: + t = note + "\n" + t +open(p, "w", encoding="utf-8").write(t) +PY +done # the engine's jars are NetIQ's: refuse to build if any reached the context (a class from one would be # com/novell/nds/dirxml/… or com/netiq/…; DirXMLDev's own jar is com/pointblue/…) for j in "$CTX"/artifacts/*.jar; do diff --git a/pom.xml b/pom.xml index 98a280d..8c92c9d 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.pointblue.dirxml dirxml-dev-web - 0.71.0 + 0.72.0 MIT License @@ -23,7 +23,7 @@ - 0.49.0 + 0.50.0 1.7.0