Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/labeler.yml
Original file line number Diff line number Diff line change
Expand Up @@ -619,6 +619,7 @@ substrate:release:
- 'tools/release-management/**'
- 'tools/release-verify/**'
- 'tools/reproducible-archive/**'
- 'tools/secret-prompt/**'

substrate:review:
- any:
Expand Down
3 changes: 2 additions & 1 deletion docs/labels-and-capabilities.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ framework substrate:
| `substrate:action-guard` | substrate | Deterministic pre-tool-use command guards. |
| `substrate:privacy` | substrate | PII redaction / approved-LLM gating. |
| `substrate:framework-dev` | substrate | Build / validate / eval the framework itself. |
| `substrate:release` | substrate | Release-artefact helpers an adopter's release process runs: reproducible-archive build, lint and comparison; maven-artifact-verify for staged JVM artefacts; release-config loading and pre-flight checks; release-verify checks of a staged release candidate; release-management rendering of the paste-ready release blocks. |
| `substrate:release` | substrate | Release-artefact helpers an adopter's release process runs: reproducible-archive build, lint and comparison; maven-artifact-verify for staged JVM artefacts; release-config loading and pre-flight checks; release-verify checks of a staged release candidate; release-management rendering of the paste-ready release blocks; secret-prompt for the release manager's upload credentials. |
| `substrate:setup` | substrate | Adopter-side setup state an agent resolves at runtime rather than at development time: lock parsing, floor comparison, reconciliation fingerprints. |
| `substrate:review` | substrate | A second model's read of a change before it is published: other models' CLIs run read-only, findings merged. |

Expand Down Expand Up @@ -378,6 +378,7 @@ or a contract-free mix of substrates (e.g. `tools/spec-inventory` is
| [`tools/reproducible-archive`](../tools/reproducible-archive/) | `substrate:release` | `repro-archive` — build, lint and compare reproducible source archives from a git ref (`git archive` + `.gitattributes` `export-ignore`, every reproducible-builds.org archive rule applied); used by `release-rc-cut` Step 2/2b and `release-verify-rc` Step 9 |
| [`tools/maven-artifact-verify`](../tools/maven-artifact-verify/) | `substrate:release` | `maven-artifact-verify` — verify locally staged JVM RC artefacts: POM ALv2 licence / developers / scm (with parent-POM inheritance resolution), podling incubation disclaimer in `<description>`, companion `-sources.jar` / `-javadoc.jar` presence with their own signatures and checksums; used by `release-verify-rc` Step 6b |
| [`tools/release-verify`](../tools/release-verify/) | `substrate:release` | `release-verify` — deterministic, read-only checks of a staged RC (inventory, signatures against the project KEYS only, checksums, NOTICE/LICENSE, prohibited binaries, dangling symlinks, version strings) and the verdict roll-up, each as JSON; used by `release-verify-rc` Steps 1–3, 5–8 and 10; and `list-keys`, the fingerprints and UIDs of a KEYS file or fetched key, for `release-keys-sync` Steps 0–1 |
| [`tools/secret-prompt`](../tools/secret-prompt/) | `substrate:release` | `secret-prompt` — asks the release manager for a secret (terminal, macOS dialog, zenity/kdialog, pinentry) and runs one command with it in one environment variable, so an upload token never reaches the shell history, a file or an agent's transcript |
| [`tools/symlink-lint`](../tools/symlink-lint/) | `substrate:framework-dev` | Self-adoption symlink hygiene — rejects cyclic symlinks, misdirected skill relays (canonical/relay target-correctness), and incomplete self-adoption symlink sets |
| [`tools/pilot-report-validator`](../tools/pilot-report-validator/) | `substrate:framework-dev` | Adopter pilot-report validator — required frontmatter keys, no unfilled placeholders, valid profile, and required body sections; counterpart to `spec-validator` for `docs/pilot-report-template.md` |
| [`tools/skill-reconciler-diff`](../tools/skill-reconciler-diff/) | `substrate:framework-dev` | Deterministic structural diff between two skill trees — parses frontmatter, section headings, step inventory, placeholders, support files, and safety-baseline clauses into a JSON diff object for the `skill-reconciler` skill |
Expand Down
5 changes: 3 additions & 2 deletions docs/vendor-neutrality.md
Original file line number Diff line number Diff line change
Expand Up @@ -615,7 +615,7 @@ Organization scope (declared, orthogonal to vendor): ASF = 16, agnostic = 64.

**LLM / agent-integration neutrality**

**Agent harness: 34/34 substrate tools run under any harness unchanged (100%).** Substrate tools are Magpie's own machinery; each declares the agent harness it integrates with (`**Harness:**`), or `agnostic`. A tool is neutral when it is harness-agnostic or supports two or more harnesses; *coupled* when it targets a single harness.
**Agent harness: 35/35 substrate tools run under any harness unchanged (100%).** Substrate tools are Magpie's own machinery; each declares the agent harness it integrates with (`**Harness:**`), or `agnostic`. A tool is neutral when it is harness-agnostic or supports two or more harnesses; *coupled* when it targets a single harness.

| Substrate tool | Substrate | Harness support | Verdict |
|---|---|---|---|
Expand All @@ -639,6 +639,7 @@ Organization scope (declared, orthogonal to vendor): ASF = 16, agnostic = 64.
| `release-verify` | release | any | ✅ agnostic |
| `reproducible-archive` | release | any | ✅ agnostic |
| `sandbox-lint` | sandbox | Claude Code, Codex, Cursor, Gemini CLI, Grok, Kiro, OpenCode | ✅ portable |
| `secret-prompt` | release | any | ✅ agnostic |
| `security-management` | analytics | any | ✅ agnostic |
| `security-tracker-stats-dashboard` | analytics | any | ✅ agnostic |
| `setup-preflight` | setup | any | ✅ agnostic |
Expand All @@ -665,7 +666,7 @@ Harness → substrate tools it supports:
- **Junie** (1): `agent-guard`
- **Kiro** (3): `agent-guard`, `sandbox-lint`, `spec-loop`
- **OpenCode** (3): `agent-guard`, `sandbox-lint`, `spec-loop`
- **any harness** (31): `adversarial-review`, `agent-isolation`, `container-gateway`, `contributor-metrics`, `dashboard-generator`, `dev`, `egress-gateway`, `maven-artifact-verify`, `permission-audit`, `pilot-report-validator`, `pr-management`, `preflight-audit`, `privacy-llm`, `probe-templates`, `release-config`, `release-management`, `release-verify`, `reproducible-archive`, `security-management`, `security-tracker-stats-dashboard`, `setup-preflight`, `skill-and-tool-validator`, `skill-evals`, `skill-reconciler-diff`, `skill-token-count`, `spec-inventory`, `spec-status-index`, `spec-validator`, `symlink-lint`, `vendor-neutrality-score`, `vetted-ops`
- **any harness** (32): `adversarial-review`, `agent-isolation`, `container-gateway`, `contributor-metrics`, `dashboard-generator`, `dev`, `egress-gateway`, `maven-artifact-verify`, `permission-audit`, `pilot-report-validator`, `pr-management`, `preflight-audit`, `privacy-llm`, `probe-templates`, `release-config`, `release-management`, `release-verify`, `reproducible-archive`, `secret-prompt`, `security-management`, `security-tracker-stats-dashboard`, `setup-preflight`, `skill-and-tool-validator`, `skill-evals`, `skill-reconciler-diff`, `skill-token-count`, `spec-inventory`, `spec-status-index`, `spec-validator`, `symlink-lint`, `vendor-neutrality-score`, `vetted-ops`

**Model endpoint: neutral by construction — 4 default-approved endpoint classes across independent trust domains, plus adopter opt-in.** From the [`privacy-llm` registry](../tools/privacy-llm/models.md): the framework keys approval on *endpoint identity*, not on who hosts the model, so no single LLM vendor is privileged.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ The render emits it **after** the dist promotion and the final tag, as its own s
(for example `twine upload dist/apache_<project>-<version>*`, `mvn nexus-staging:release -DstagingRepositoryId=<id>`, `docker push <registry>/<image>:<version>`, `helm push …`).
These are the project's own commands; the skill never invents a channel or a command the config does not declare.
An entry with no `publish_command` emits nothing and comes back in `warnings`.
A `publish_command` that uploads with the release manager's token is best declared wrapped in
[`secret-prompt`](../../../../tools/secret-prompt/README.md) (`secret-prompt --env TWINE_PASSWORD --set TWINE_USERNAME=__token__ --prefix pypi- -- twine upload …`),
so running it asks the release manager for the token (the tool's README has the one-time `uv tool install`);
the render still emits it verbatim, and never asks for or handles the token itself.

**Reproducibility gate — the artefact must be good before it is published.**
A convenience artefact is publishable only if the `release-verify-rc` run recorded on the planning issue rebuilt it from the voted tag and it reproduced:
Expand Down
7 changes: 7 additions & 0 deletions plugins/magpie-setup/templates/release-build.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,13 @@ convenience_artefacts:
publish_command: null # run by the RM after the vote, e.g. `twine upload dist/*`, `mvn nexus-staging:release`, `docker push …`; dist-release needs none (promoted with the source)
```

A `stage_command` or `publish_command` that needs a credential the release manager holds — an upload token —
is best declared wrapped in [`secret-prompt`](https://github.com/apache/magpie/tree/main/tools/secret-prompt), so running it asks the release manager
for the token (in their terminal, or as a dialog when an agent runs it) instead of reading one from a file:
`secret-prompt --env TWINE_PASSWORD --set TWINE_USERNAME=__token__ --prefix pypi- -- twine upload dist/*`
(the release manager installs it once with
`uv tool install "git+https://github.com/apache/magpie#subdirectory=tools/secret-prompt"`).

How each skill uses the list:

| Skill | Uses |
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ members = [
"tools/cve-tool-vulnogram/oauth-api",
"tools/github-body-field",
"tools/github-rollup",
"tools/secret-prompt",
"tools/gmail/oauth-draft",
"tools/jira",
"tools/maildir",
Expand Down
177 changes: 177 additions & 0 deletions tools/secret-prompt/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
<!-- SPDX-License-Identifier: Apache-2.0
https://www.apache.org/licenses/LICENSE-2.0 -->

<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->

- [`secret-prompt`](#secret-prompt)
- [Prerequisites](#prerequisites)
- [How to use](#how-to-use)
- [How it asks](#how-it-asks)
- [What it guarantees](#what-it-guarantees)
- [Tests](#tests)

<!-- END doctoc generated TOC please keep comment here to allow auto update -->

# `secret-prompt`

**Capability:** substrate:release

**Harness:** agnostic

Asks the operator for a secret, such as a single-use PyPI token, and runs
**one command** with it in **one environment variable**. The secret goes from
the prompt into that command's environment and nowhere else: it is never
printed, written to a file, or passed on a command line, so it stays out of
the shell history, `ps`, CI logs, and an agent's transcript.

It exists for the release steps that need a credential the release manager
holds — uploading the convenience packages to PyPI being the common one —
when the step is run by an agent on the release manager's behalf, or from a
script that has no terminal to prompt on. The release manager still types
or pastes the secret themselves, after checking the command the prompt names;
the agent only ever sees that command's output.

## Prerequisites

- **Runtime** — Python 3.11+ (stdlib only), run via `uv`.
- **CLIs** — one way to ask: a terminal, or `osascript` (macOS, built in),
`zenity` or `kdialog` (Linux desktop), or `pinentry` (any OS with GnuPG,
including `pinentry-mac`).
- **Credentials / auth** — None of its own; it carries the one secret the
operator enters.
- **Network** — None; the command it runs may need some.

## How to use

```bash
secret-prompt --env TWINE_PASSWORD --set TWINE_USERNAME=__token__ --prefix pypi- \
--title "PyPI upload" --prompt "Paste the single-use PyPI token" \
-- twine upload dist/*.whl dist/*.tar.gz
```

Install it once, so the bare `secret-prompt` in a release recipe resolves:

```bash
uv tool install "git+https://github.com/apache/magpie#subdirectory=tools/secret-prompt"
```

Or run it without installing, from a checkout of this repository or straight from GitHub:

```bash
uv run --directory tools/secret-prompt --project . secret-prompt --env … -- <command>
uvx --from "git+https://github.com/apache/magpie#subdirectory=tools/secret-prompt" \
secret-prompt --env … -- <command>
```

| Option | Meaning |
| --- | --- |
| `--env NAME` | Environment variable that receives the secret (required). |
| `--set NAME=VALUE` | A non-secret variable for the command too, repeatable (`TWINE_USERNAME=__token__`). |
| `--prefix TEXT` | Refuse a secret that does not start with `TEXT` (`pypi-`), so a wrong paste runs nothing. |
| `--prompt` / `--title` | Text of the prompt and dialog title. |
| `--backend` | `auto` (default), or force `tty`, `osascript`, `zenity`, `kdialog`, `pinentry`. |

Exit status is the command's own; `1` when no usable secret arrived (cancelled,
empty, the wrong prefix, or no backend could ask), `127` when the command cannot
be started.

## How it asks

`--backend auto` uses the first that applies:

1. **A terminal** (`/dev/tty` can be opened) — a hidden prompt there, even when
stdin is redirected.
2. **macOS** — an `osascript` dialog with a hidden-answer field. The prompt text
reaches AppleScript as an argument, never spliced into the script.
3. **A Linux desktop** (`DISPLAY` or `WAYLAND_DISPLAY` set) — `zenity --password`,
else `kdialog --password`.
4. **`pinentry`** — the Assuan `GETPIN` exchange, which covers `pinentry-mac`,
`pinentry-gnome3`, `pinentry-qt` and the curses ones.

When a backend *fails* rather than being cancelled — its program is missing, or
it cannot show a dialog — `auto` moves on to the next one and, if none can ask,
reports why. A **cancel** (the Cancel button, `Esc`, or `Ctrl-C`) stops there:
nothing else is asked and nothing runs.

An agent's command has no terminal, so on the release manager's Mac it gets
the dialog; the same command in the release manager's own terminal prompts
there instead. A **sandboxed** agent cannot show the macOS dialog — the
Seatbelt sandbox turns off Apple Events — so run the step outside the sandbox
(it is the release manager's upload either way), or let it fall back to
`pinentry`.

## What it guarantees

- The secret is passed only in the environment of the one command it runs
(`exec`, so that command replaces the tool and keeps its exit status).
- Nothing runs unless a non-empty secret arrives that matches `--prefix`; a
cancelled dialog runs nothing.
- Error messages name the expected prefix, never what was entered.
- Every prompt ends with what the caller cannot change: **`It will be passed to:`**
the command, resolved to an absolute path before asking and run exactly as
shown, and **`with:`** every `--set` variable plus any inherited variable that
shares the secret's prefix (`TWINE_*` for `TWINE_PASSWORD`, so a
`TWINE_REPOSITORY_URL` pointing elsewhere is visible). Check those lines
before pasting: an agent chooses the command, and this is what stops a
look-alike prompt from handing the token to something that prints or
forwards it.
- `--set` values are shown as given. An inherited value is shown only when its
name has a routing segment (`URL`, `HOST`, `REPOSITORY`, `REGISTRY`,
`INDEX`, `ENDPOINT`, `USER`, `USERNAME`, `CERT`) and no credential-like one;
every other is listed as `NAME=(set; value not shown)`, because a dialog
receives the prompt in its argv. A name is credential-like when a segment,
with or without a trailing `S`, is `AUTH`, `CRED`, `CREDENTIAL`, `JWT`,
`PAT`, `PW`, `PWD`, `COOKIE`, `SESSION`, `BEARER`, `PRIVATE`, `WEBHOOK`,
`HOOK` or `DSN`, or ends in `TOKEN`, `PASS`, `PASSWORD`, `PASSWD`,
`PASSPHRASE`, `SECRET` or `KEY`.
- An inherited URL always shows its host, whatever its name, so where the
secret may go is never hidden: up to its path under a routing name (query and
fragment as `?***`/`#***`), otherwise only `scheme://host/***`.
- An inherited variable with one of the denied names below (`NODE_OPTIONS`
beside `NODE_AUTH_TOKEN`) is shown verbatim, as it can make the command run
something else.
- Nothing is asked when an inherited variable can't be shown safely. That
means a URL containing `@` (a user or password, or a host that could be
disguised), a `://` value too quoted or spaced to parse, or a routing value
that may carry a credential: an `@` that is not a plain e-mail address, a
`?` or `#`, or a `:` that is not a port (`user:pw@proxy`, `alice:s3cret`,
`host/simple?token=…`). It also
means a denied name that is also credential-like, a shown value with a
control character or over 200 characters, or a name that is not a plain
identifier. The error names the variable to unset. Only the secret's own
prefix is scanned: npm's registry settings (`npm_config_registry`, `.npmrc`)
are not shown.
- The command is shown in full or not at all: one longer than 1000 characters
is refused, never truncated. Every command argument and every shown value
must be printable — no newline, escape sequence or invisible bidi character
that could rewrite or reorder those lines — and a value may be at most 200
characters; otherwise nothing is asked. `--prompt` and `--title` must be a single
printable line of at most 200 characters that does not start with `-`, so
they cannot imitate the lines above; `zenity` and `kdialog`, whose toolkits may
render `<` and `&` as markup, decline a prompt containing them and `auto`
moves on to the next backend.
- `--env` and `--set` refuse names that change how the command runs or where it
reads its configuration: `PATH`, `IFS`, `HOME`, `ENV`, `BASH_ENV`,
`SHELLOPTS`, `BASHOPTS`, `PS4`, and the `LD_*`, `DYLD_*`, `PYTHON*`, `PERL5*`,
`RUBY*`, `NODE_*` and `GIT_*` families — except `NODE_AUTH_TOKEN`, npm's
publish credential.

What it cannot do is vouch for an environment the caller already controls:
an agent that sets `LD_PRELOAD` or edits `~/.pypirc` *before* calling it can
still subvert the command. The prompt shows what it can; when in doubt, run the
step yourself, in your own terminal.

It does **not** make the secret short-lived: create a single-use token for the
step and delete it straight after, as the release docs say.

## Tests

```bash
uv run --directory tools/secret-prompt --project . pytest
```

Every backend is exercised without a real dialog: `osascript` through a
patched `subprocess.run`, `pinentry` through a fake `pinentry` script on
`PATH`, which also drives an end-to-end run checking that the secret reaches
the command's environment and none of the output.
Loading
Loading