From 2a1db2f73d9517e39ffd902ec545449579a5a6e8 Mon Sep 17 00:00:00 2001 From: Phillip Moore Date: Tue, 2 Jun 2026 07:30:09 -0400 Subject: [PATCH 1/3] chore(tooling): refresh managed config to current Vergil tooling/actions Items 1/3/6/7 of the fleet refresh (epic mq-rest-admin-project/.github#14): marketplace -> vergil-claude-plugin; embed canonical CLAUDE.md template (+ vrg-docker-run -> vrg-container-run); ignore .vergil/; cd.yml release uses secrets: inherit (fixes CD startup_failure). Hook guard (item 2) already landed via #509. audit COMPLIANT; vrg-validate passes. Refs #510. --- .claude/settings.json | 2 +- .github/workflows/cd.yml | 4 +--- .gitignore | 3 +++ CLAUDE.md | 42 ++++++++++++++++++++++++++++++---------- 4 files changed, 37 insertions(+), 14 deletions(-) diff --git a/.claude/settings.json b/.claude/settings.json index b39e710..513546b 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -21,7 +21,7 @@ "vergil-marketplace": { "source": { "source": "github", - "repo": "vergil-project/vergil-plugin" + "repo": "vergil-project/vergil-claude-plugin" } } }, diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index d657394..0146e94 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -31,6 +31,4 @@ jobs: language: python container-tag: "3.14" registry-publish: true - secrets: - APP_CLIENT_ID: ${{ secrets.APP_CLIENT_ID }} - APP_PRIVATE_KEY: ${{ secrets.APP_PRIVATE_KEY }} + secrets: inherit diff --git a/.gitignore b/.gitignore index 82c51d4..89b587b 100644 --- a/.gitignore +++ b/.gitignore @@ -207,3 +207,6 @@ marimo/_static/ marimo/_lsp/ __marimo__/ .worktrees/ + +# Vergil tooling scratch (PR/session working dir) +.vergil/ diff --git a/CLAUDE.md b/CLAUDE.md index aefca2a..5b6c5d1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -36,19 +36,19 @@ on-ramp. ### Structure ```text -~/dev/github/mq-rest-admin-python/ ← sessions ALWAYS start here +/ ← sessions ALWAYS start here .git/ - CLAUDE.md, src/, tests/, … ← main worktree (usually `develop`) - .worktrees/ ← container for parallel worktrees - issue-454-adopt-worktree-convention/ ← worktree on feature/454-... + CLAUDE.md, … ← main worktree (usually `develop`) + .worktrees/ ← container for parallel worktrees + issue--/ ← worktree on feature/- … ``` ### Rules 1. **Sessions always start at the project root.** - `cd ~/dev/github/mq-rest-admin-python && claude` — never from inside - `.worktrees//`. This keeps the memory-path slug stable and shared. + Never start Claude from inside `.worktrees//`. This keeps the + memory-path slug stable and shared. 2. **Each parallel agent is assigned exactly one worktree.** The session prompt names the worktree (see Agent prompt contract below). - For Read / Edit / Write tools: use the worktree's absolute path. @@ -56,7 +56,7 @@ on-ramp. or use absolute paths. 3. **The main worktree is read-only.** All edits flow through a worktree on a feature branch — the logical endpoint of the standing - "no direct commits to `develop`" policy. + "no direct commits to develop" policy. 4. **One worktree per issue.** Don't stack in-flight issues. When a branch lands, remove the worktree before starting the next. 5. **Naming: `issue--`.** `` is the GitHub issue @@ -70,22 +70,44 @@ placeholders): ```text You are working on issue #: . -Your worktree is: /Users/pmoore/dev/github/mq-rest-admin-python/.worktrees/issue--/ +Your worktree is: /.worktrees/issue--/ Your branch is: feature/- Rules for this session: - Do all git operations from inside your worktree: - cd && git + cd && vrg-git - For Read / Edit / Write tools, use the absolute worktree path. - For Bash commands that touch files, cd into the worktree first or use absolute paths. - Do not edit files at the project root. The main worktree is read-only — all changes flow through your worktree on your feature branch. +- When you need to run validation, run it from inside your worktree + (vrg-container-run mounts the current directory). ``` All fields are required. +## Shell command policy + +Use `vrg-git` instead of `git` for all git operations. Use `vrg-gh` +instead of `gh` for all GitHub CLI operations. These wrappers enforce +subcommand allowlists, flag deny lists, and credential selection. + +Raw `git` and `gh` are denied by the permission model. If a command +is not available through the wrappers, explain the situation to the +human who can run it directly via `! ` in the prompt. + +## Validation + +```bash +vrg-container-run -- vrg-validate +``` + +This is the **only** validation command. Do not run individual linters, +formatters, or other tools outside of `vrg-validate`. If a tool is not +invoked by `vrg-validate`, it is not part of the validation pipeline. + ## Project Overview `pymqrest` is a Python wrapper for the IBM MQ administrative REST API. The project provides a Python mapping layer for MQ REST API attribute translations and command metadata experiments. The current focus is on attribute mapping and metadata modeling. @@ -123,7 +145,7 @@ gates. ### Validation ```bash -vrg-docker-run -- vrg-validate # Full validation (runs in dev container) +vrg-container-run -- vrg-validate # Full validation (runs in dev container) ``` - Lock file verification - Security audit (pip-audit) From ffa0b7b4c00c28d403d326c69e5710885a92b6cf Mon Sep 17 00:00:00 2001 From: Phillip Moore Date: Tue, 2 Jun 2026 07:30:10 -0400 Subject: [PATCH 2/3] fix(security): source test and example credentials from the environment Updated CodeQL/Semgrep rulesets flag hard-coded credentials. Remove all hard-coded password literals from test and example code: - tests/pymqrest/{test_auth,test_session,test_ensure,test_sync}.py: TEST_PASSWORD now reads MQ_TEST_PASSWORD from the env (defaults empty); mock transports ignore the value and assertions compare against the same constant. - examples/*.py __main__ blocks: require MQ_ADMIN_PASSWORD from the env instead of defaulting to a hard-coded "mqadmin". vrg-validate (incl. 100% coverage) passes. Refs #510. --- examples/channel_status.py | 4 ++-- examples/dlq_inspector.py | 4 ++-- examples/health_check.py | 6 +++--- examples/provision_environment.py | 6 +++--- examples/queue_depth_monitor.py | 4 ++-- examples/queue_status.py | 4 ++-- tests/pymqrest/test_auth.py | 3 ++- tests/pymqrest/test_ensure.py | 3 ++- tests/pymqrest/test_session.py | 3 ++- tests/pymqrest/test_sync.py | 3 ++- 10 files changed, 22 insertions(+), 18 deletions(-) diff --git a/examples/channel_status.py b/examples/channel_status.py index eaecb19..0db7099 100644 --- a/examples/channel_status.py +++ b/examples/channel_status.py @@ -11,7 +11,7 @@ from __future__ import annotations from dataclasses import dataclass -from os import getenv +from os import environ, getenv from pymqrest import MQRESTError, MQRESTSession from pymqrest.auth import LTPAAuth @@ -120,7 +120,7 @@ def main(session: MQRESTSession) -> list[ChannelInfo]: session = MQRESTSession( rest_base_url=getenv("MQ_REST_BASE_URL", "https://localhost:9443/ibmmq/rest/v2"), qmgr_name=getenv("MQ_QMGR_NAME", "QM1"), - credentials=LTPAAuth(getenv("MQ_ADMIN_USER", "mqadmin"), getenv("MQ_ADMIN_PASSWORD", "mqadmin")), + credentials=LTPAAuth(getenv("MQ_ADMIN_USER", "mqadmin"), environ["MQ_ADMIN_PASSWORD"]), verify_tls=False, ) diff --git a/examples/dlq_inspector.py b/examples/dlq_inspector.py index 59e93f1..d3a7863 100644 --- a/examples/dlq_inspector.py +++ b/examples/dlq_inspector.py @@ -12,7 +12,7 @@ from __future__ import annotations from dataclasses import dataclass -from os import getenv +from os import environ, getenv from pymqrest import MQRESTSession from pymqrest.auth import LTPAAuth @@ -147,7 +147,7 @@ def _to_int(value: object) -> int: session = MQRESTSession( rest_base_url=getenv("MQ_REST_BASE_URL", "https://localhost:9443/ibmmq/rest/v2"), qmgr_name=getenv("MQ_QMGR_NAME", "QM1"), - credentials=LTPAAuth(getenv("MQ_ADMIN_USER", "mqadmin"), getenv("MQ_ADMIN_PASSWORD", "mqadmin")), + credentials=LTPAAuth(getenv("MQ_ADMIN_USER", "mqadmin"), environ["MQ_ADMIN_PASSWORD"]), verify_tls=False, ) diff --git a/examples/health_check.py b/examples/health_check.py index 1cd8f2f..8e04c27 100644 --- a/examples/health_check.py +++ b/examples/health_check.py @@ -14,7 +14,7 @@ from __future__ import annotations from dataclasses import dataclass, field -from os import getenv +from os import environ, getenv from pymqrest import MQRESTError, MQRESTSession from pymqrest.auth import LTPAAuth @@ -122,7 +122,7 @@ def main(sessions: list[MQRESTSession]) -> list[QMHealthResult]: MQRESTSession( rest_base_url=getenv("MQ_REST_BASE_URL", "https://localhost:9443/ibmmq/rest/v2"), qmgr_name=getenv("MQ_QMGR_NAME", "QM1"), - credentials=LTPAAuth(getenv("MQ_ADMIN_USER", "mqadmin"), getenv("MQ_ADMIN_PASSWORD", "mqadmin")), + credentials=LTPAAuth(getenv("MQ_ADMIN_USER", "mqadmin"), environ["MQ_ADMIN_PASSWORD"]), verify_tls=False, ) ) @@ -133,7 +133,7 @@ def main(sessions: list[MQRESTSession]) -> list[QMHealthResult]: MQRESTSession( rest_base_url=qm2_url, qmgr_name="QM2", - credentials=LTPAAuth(getenv("MQ_ADMIN_USER", "mqadmin"), getenv("MQ_ADMIN_PASSWORD", "mqadmin")), + credentials=LTPAAuth(getenv("MQ_ADMIN_USER", "mqadmin"), environ["MQ_ADMIN_PASSWORD"]), verify_tls=False, ) ) diff --git a/examples/provision_environment.py b/examples/provision_environment.py index 9ac06f6..232f1ea 100644 --- a/examples/provision_environment.py +++ b/examples/provision_environment.py @@ -15,7 +15,7 @@ from __future__ import annotations from dataclasses import dataclass, field -from os import getenv +from os import environ, getenv from pymqrest import MQRESTError, MQRESTSession from pymqrest.auth import LTPAAuth @@ -289,14 +289,14 @@ def _delete( qm1_session = MQRESTSession( rest_base_url=getenv("MQ_REST_BASE_URL", "https://localhost:9443/ibmmq/rest/v2"), qmgr_name="QM1", - credentials=LTPAAuth(getenv("MQ_ADMIN_USER", "mqadmin"), getenv("MQ_ADMIN_PASSWORD", "mqadmin")), + credentials=LTPAAuth(getenv("MQ_ADMIN_USER", "mqadmin"), environ["MQ_ADMIN_PASSWORD"]), verify_tls=False, ) qm2_session = MQRESTSession( rest_base_url=getenv("MQ_REST_BASE_URL_QM2", "https://localhost:9444/ibmmq/rest/v2"), qmgr_name="QM2", - credentials=LTPAAuth(getenv("MQ_ADMIN_USER", "mqadmin"), getenv("MQ_ADMIN_PASSWORD", "mqadmin")), + credentials=LTPAAuth(getenv("MQ_ADMIN_USER", "mqadmin"), environ["MQ_ADMIN_PASSWORD"]), verify_tls=False, ) diff --git a/examples/queue_depth_monitor.py b/examples/queue_depth_monitor.py index ac04cba..73ac1ef 100644 --- a/examples/queue_depth_monitor.py +++ b/examples/queue_depth_monitor.py @@ -14,7 +14,7 @@ from __future__ import annotations from dataclasses import dataclass -from os import getenv +from os import environ, getenv from pymqrest import MQRESTSession from pymqrest.auth import LTPAAuth @@ -126,7 +126,7 @@ def _to_int(value: object) -> int: session = MQRESTSession( rest_base_url=getenv("MQ_REST_BASE_URL", "https://localhost:9443/ibmmq/rest/v2"), qmgr_name=getenv("MQ_QMGR_NAME", "QM1"), - credentials=LTPAAuth(getenv("MQ_ADMIN_USER", "mqadmin"), getenv("MQ_ADMIN_PASSWORD", "mqadmin")), + credentials=LTPAAuth(getenv("MQ_ADMIN_USER", "mqadmin"), environ["MQ_ADMIN_PASSWORD"]), verify_tls=False, ) diff --git a/examples/queue_status.py b/examples/queue_status.py index b542fdf..e68be88 100644 --- a/examples/queue_status.py +++ b/examples/queue_status.py @@ -12,7 +12,7 @@ from __future__ import annotations from dataclasses import dataclass -from os import getenv +from os import environ, getenv from pymqrest import MQRESTError, MQRESTSession from pymqrest.auth import LTPAAuth @@ -139,7 +139,7 @@ def main(session: MQRESTSession) -> None: session = MQRESTSession( rest_base_url=getenv("MQ_REST_BASE_URL", "https://localhost:9443/ibmmq/rest/v2"), qmgr_name=getenv("MQ_QMGR_NAME", "QM1"), - credentials=LTPAAuth(getenv("MQ_ADMIN_USER", "mqadmin"), getenv("MQ_ADMIN_PASSWORD", "mqadmin")), + credentials=LTPAAuth(getenv("MQ_ADMIN_USER", "mqadmin"), environ["MQ_ADMIN_PASSWORD"]), verify_tls=False, ) diff --git a/tests/pymqrest/test_auth.py b/tests/pymqrest/test_auth.py index b11c3fe..305b425 100644 --- a/tests/pymqrest/test_auth.py +++ b/tests/pymqrest/test_auth.py @@ -3,6 +3,7 @@ from __future__ import annotations import dataclasses +from os import getenv from typing import TYPE_CHECKING import pytest @@ -20,7 +21,7 @@ if TYPE_CHECKING: from collections.abc import Mapping -TEST_PASSWORD = "secret" +TEST_PASSWORD = getenv("MQ_TEST_PASSWORD", "") STATUS_OK = 200 STATUS_UNAUTHORIZED = 401 diff --git a/tests/pymqrest/test_ensure.py b/tests/pymqrest/test_ensure.py index 0f0a64c..725d0d3 100644 --- a/tests/pymqrest/test_ensure.py +++ b/tests/pymqrest/test_ensure.py @@ -4,6 +4,7 @@ import json from dataclasses import dataclass +from os import getenv from typing import TYPE_CHECKING import pytest @@ -15,7 +16,7 @@ if TYPE_CHECKING: from collections.abc import Mapping, Sequence -TEST_PASSWORD = "pass" +TEST_PASSWORD = getenv("MQ_TEST_PASSWORD", "") EXPECT_ONE_REQUEST = 1 EXPECT_TWO_REQUESTS = 2 diff --git a/tests/pymqrest/test_session.py b/tests/pymqrest/test_session.py index 23cf959..5bb6321 100644 --- a/tests/pymqrest/test_session.py +++ b/tests/pymqrest/test_session.py @@ -4,6 +4,7 @@ import json from dataclasses import dataclass +from os import getenv from typing import TYPE_CHECKING import pytest @@ -28,7 +29,7 @@ REQUEST_EXCEPTION_MESSAGE = "boom" STATUS_INTERNAL_SERVER_ERROR = 500 STATUS_CREATED = 201 -TEST_PASSWORD = "pass" +TEST_PASSWORD = getenv("MQ_TEST_PASSWORD", "") TEST_DEPTH = 5 diff --git a/tests/pymqrest/test_sync.py b/tests/pymqrest/test_sync.py index bb5c76c..4bf2acc 100644 --- a/tests/pymqrest/test_sync.py +++ b/tests/pymqrest/test_sync.py @@ -5,6 +5,7 @@ import json import time from dataclasses import dataclass +from os import getenv from typing import TYPE_CHECKING import pytest @@ -17,7 +18,7 @@ if TYPE_CHECKING: from collections.abc import Mapping, Sequence -TEST_PASSWORD = "pass" +TEST_PASSWORD = getenv("MQ_TEST_PASSWORD", "") EXPECT_ONE_POLL = 1 EXPECT_TWO_POLLS = 2 EXPECT_THREE_POLLS = 3 From a18b26726440637301d33f6571cc310c6b3463da Mon Sep 17 00:00:00 2001 From: Phillip Moore Date: Tue, 2 Jun 2026 07:33:33 -0400 Subject: [PATCH 3/3] fix(security): use verified SSL context in archived extraction scripts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Semgrep python.lang.security.unverified-ssl-context flagged ssl._create_unverified_context() in the archived MQSC doc-extraction scripts. They fetch public IBM docs (https://www.ibm.com/docs, valid certs), so switch to ssl.create_default_context() — verification works and the insecure context is removed. Scripts are archived/not run in CI; change is static-clean for the scanner and more correct if re-run. Refs #510. --- docs/archive/extraction/scripts/build_mqsc_pcf_command_map.py | 2 +- .../archive/extraction/scripts/extract_mqsc_command_metadata.py | 2 +- docs/archive/extraction/scripts/extract_pcf_command_pages.py | 2 +- .../extraction/scripts/refresh_mqsc_output_parameters.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/archive/extraction/scripts/build_mqsc_pcf_command_map.py b/docs/archive/extraction/scripts/build_mqsc_pcf_command_map.py index 9ff6468..31e3a42 100755 --- a/docs/archive/extraction/scripts/build_mqsc_pcf_command_map.py +++ b/docs/archive/extraction/scripts/build_mqsc_pcf_command_map.py @@ -248,7 +248,7 @@ def read_group_entries() -> list[GroupEntry]: def fetch_html(href: str) -> str: url = f"{IBM_DOCS_BASE}{href}" - context = ssl._create_unverified_context() # noqa: S323, SLF001 + context = ssl.create_default_context() request = Request( # noqa: S310 url, headers={"User-Agent": "pymqrest-pcf-map/1.0", "Accept": "text/html"}, diff --git a/docs/archive/extraction/scripts/extract_mqsc_command_metadata.py b/docs/archive/extraction/scripts/extract_mqsc_command_metadata.py index a2a0163..93878c2 100755 --- a/docs/archive/extraction/scripts/extract_mqsc_command_metadata.py +++ b/docs/archive/extraction/scripts/extract_mqsc_command_metadata.py @@ -602,7 +602,7 @@ def extract_command_name(html: str) -> str | None: def fetch_html(href: str) -> str: url = f"{IBM_DOCS_BASE}{href}" - context = ssl._create_unverified_context() # noqa: S323, SLF001 + context = ssl.create_default_context() request = urllib.request.Request( # noqa: S310 url, headers={ diff --git a/docs/archive/extraction/scripts/extract_pcf_command_pages.py b/docs/archive/extraction/scripts/extract_pcf_command_pages.py index 77238f4..8b28343 100755 --- a/docs/archive/extraction/scripts/extract_pcf_command_pages.py +++ b/docs/archive/extraction/scripts/extract_pcf_command_pages.py @@ -63,7 +63,7 @@ def handle_data(self, data: str) -> None: def fetch_index(url: str) -> str: - context = ssl._create_unverified_context() # noqa: S323, SLF001 + context = ssl.create_default_context() request = Request( # noqa: S310 url, headers={"User-Agent": "pymqrest-pcf-index/1.0", "Accept": "text/html"}, diff --git a/docs/archive/extraction/scripts/refresh_mqsc_output_parameters.py b/docs/archive/extraction/scripts/refresh_mqsc_output_parameters.py index 665ae54..3bdd4c6 100755 --- a/docs/archive/extraction/scripts/refresh_mqsc_output_parameters.py +++ b/docs/archive/extraction/scripts/refresh_mqsc_output_parameters.py @@ -162,7 +162,7 @@ def fetch_html(href: str) -> str: if cache_path.exists(): return cache_path.read_text(encoding="utf-8", errors="ignore") url = f"{IBM_DOCS_BASE}{href}" - context = ssl._create_unverified_context() # noqa: S323, SLF001 + context = ssl.create_default_context() request = urllib.request.Request( # noqa: S310 url, headers={