From 01ab670a570e0782a075d92dfc11e862650d6fa2 Mon Sep 17 00:00:00 2001 From: Pavel-Tk <110731619+Pavel-Tk@users.noreply.github.com> Date: Wed, 2 Sep 2026 15:22:20 +0000 Subject: [PATCH 1/2] feat: support v5 history import test Signed-off-by: Pavel-Tk <110731619+Pavel-Tk@users.noreply.github.com> --- CHANGELOG.md | 5 +++++ src/substrate_wiki/client.py | 4 +++- src/substrate_wiki/onboarding.py | 6 ++++-- tests/test_onboarding.py | 17 +++++++++++++++++ 4 files changed, 29 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d558658..073619b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## Unreleased + +- Let hosted onboarding validate only the history replay contract, so a setup-and-upload MVP does not need to advertise entity retrieval features. +- Add an explicit `SUBSTRATE_WIKI_ORIGIN` override for isolated v5 import tests; the hosted origin remains the default. + ## 2.0.4 - Upload only completed user/assistant text; exclude tool calls, tool results, system messages, diff --git a/src/substrate_wiki/client.py b/src/substrate_wiki/client.py index f1f5667..70969d5 100644 --- a/src/substrate_wiki/client.py +++ b/src/substrate_wiki/client.py @@ -208,7 +208,9 @@ def from_env( hosted_default: bool = False, ) -> SubstrateClient: """Resolve explicit legacy env configuration, then hosted onboarding custody.""" - hosted_origin = "https://app.trysubstrate.co" + hosted_origin = os.environ.get( + "SUBSTRATE_WIKI_ORIGIN", "https://app.trysubstrate.co" + ).rstrip("/") base_url = os.environ.get("HERMES_API_URL", "") or fallback_url api_key = os.environ.get("HERMES_API_KEY", "") if hosted_default: diff --git a/src/substrate_wiki/onboarding.py b/src/substrate_wiki/onboarding.py index 94450af..81a59dc 100644 --- a/src/substrate_wiki/onboarding.py +++ b/src/substrate_wiki/onboarding.py @@ -24,7 +24,9 @@ from .credentials import CredentialStore, credential_store from .spool import secure_atomic_json_write -HOSTED_ORIGIN = "https://app.trysubstrate.co" +HOSTED_ORIGIN = os.environ.get( + "SUBSTRATE_WIKI_ORIGIN", "https://app.trysubstrate.co" +).rstrip("/") CLIENT_ID = "substrate-hermes" SCOPES = "capture retrieve" DEVICE_GRANT = "urn:ietf:params:oauth:grant-type:device_code" @@ -344,7 +346,7 @@ def _check_capabilities(self, token: str) -> dict[str, Any]: ) try: capabilities = client.capabilities() - validate_capabilities(capabilities, require_replay=True, require_entity=True) + validate_capabilities(capabilities, require_replay=True, require_entity=False) return {"provider": capabilities.get("provider"), "protocol": "stream-v2"} except SubstrateAPIError as exc: if ( diff --git a/tests/test_onboarding.py b/tests/test_onboarding.py index 7070cbd..562c692 100644 --- a/tests/test_onboarding.py +++ b/tests/test_onboarding.py @@ -220,6 +220,23 @@ def _valid_hosted_capabilities() -> dict[str, object]: } +def test_setup_handshake_requires_history_upload_but_not_entity_features( + tmp_path: Path, monkeypatch +): + from substrate_wiki.client import SubstrateClient + + capabilities = _valid_hosted_capabilities() + capabilities.pop("entity_memory") + capabilities.pop("entity_quality") + monkeypatch.setattr(SubstrateClient, "capabilities", lambda _client: capabilities) + + manager = OnboardingManager(tmp_path, store=Store()) + assert manager._check_capabilities("tenant-secret") == { + "provider": "substrate_wiki", + "protocol": "stream-v2", + } + + @pytest.mark.parametrize( ("status", "expected"), [(503, "http_503"), (400, "invalid_response")], From 1b9440dae0579e9574583d8da2fa5f76621a5a71 Mon Sep 17 00:00:00 2001 From: Pavel-Tk <110731619+Pavel-Tk@users.noreply.github.com> Date: Wed, 2 Sep 2026 16:45:29 +0000 Subject: [PATCH 2/2] feat: make post-approval history consent explicit Signed-off-by: Pavel-Tk <110731619+Pavel-Tk@users.noreply.github.com> --- .github/ISSUE_TEMPLATE/config.yml | 2 +- .github/workflows/release.yml | 2 +- CHANGELOG.md | 8 ++ COMPATIBILITY.md | 1 + README.md | 101 ++++++++-------- SECURITY.md | 2 +- docs/api-ownership.json | 2 +- docs/extraction-manifest.json | 46 ++++---- docs/public-boundary.json | 2 +- docs/source-of-truth.md | 2 +- pyproject.toml | 2 +- scripts/install_hermes_plugin.py | 12 +- scripts/verify_public_plugin_candidate.py | 136 ++++++++++------------ src/substrate_wiki/README.md | 10 +- src/substrate_wiki/cli.py | 8 +- src/substrate_wiki/client.py | 2 +- src/substrate_wiki/onboarding.py | 23 ++-- src/substrate_wiki/plugin.yaml | 2 +- tests/test_entity_memory.py | 2 +- tests/test_onboarding.py | 59 ++++++++++ tests/test_packaging.py | 30 ++--- uv.lock | 2 +- 22 files changed, 266 insertions(+), 190 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index f6decdb..30171bb 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,5 +1,5 @@ blank_issues_enabled: false contact_links: - name: Report a security vulnerability privately - url: https://github.com/Substrate-memory/hermes-substrate-wiki/security/advisories/new + url: https://github.com/Substrate-memory/Substrate-memory-plugins/security/advisories/new about: Never disclose credentials, private data, or exploit details in a public issue. diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2791f36..a9fa377 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -147,4 +147,4 @@ jobs: --repo "$GITHUB_REPOSITORY" \ --verify-tag \ --title "substrate_wiki $tag" \ - --notes "Patch release for compact user/assistant-only capture and batched history upload. Tool calls, tool results, system messages, provider metadata, and duplicate envelope fields are excluded. Targets Hermes 0.20.x. See COMPATIBILITY.md and SECURITY.md." + --notes "Patch release for explicit post-approval history consent, reliable device-poll completion, and the Substrate v5 history-only handshake. Targets Hermes 0.20.x. See COMPATIBILITY.md and SECURITY.md." diff --git a/CHANGELOG.md b/CHANGELOG.md index 073619b..b3111be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,14 @@ - Let hosted onboarding validate only the history replay contract, so a setup-and-upload MVP does not need to advertise entity retrieval features. - Add an explicit `SUBSTRATE_WIKI_ORIGIN` override for isolated v5 import tests; the hosted origin remains the default. +## 2.0.5 + +- Make browser approval completion return an explicit history-consent action to the installing agent. +- Keep blank or interrupted consent prompts pending instead of treating them as refusal. +- Always print the complete browser approval URL while the polling installer waits. +- Support the Substrate v5 history-only capability handshake and test origin. +- Rename the public source repository to `Substrate-memory-plugins`. + ## 2.0.4 - Upload only completed user/assistant text; exclude tool calls, tool results, system messages, diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md index 7e77eec..4de92dd 100644 --- a/COMPATIBILITY.md +++ b/COMPATIBILITY.md @@ -6,6 +6,7 @@ Compatibility has three independent axes: Hermes host, hosted Substrate capabili | Plugin | Hermes | Required hosted capabilities | Status | |---|---|---|---| +| 2.0.5 | 0.20.x | Same as 2.0.4; explicit post-approval history consent and v5 handshake | Contract and lifecycle integration tested | | 2.0.4 | 0.20.x | Same as 2.0.3; compact user/assistant capture and batched history upload | Contract and lifecycle integration tested | | 2.0.3 | 0.20.x | Same as 2.0.2; resilient approved-device polling across hosted stalls | Contract and lifecycle integration tested | | 2.0.2 | 0.20.x | Same as 2.0.1; bounded retry for hosted tenant cold starts | Contract and lifecycle integration tested | diff --git a/README.md b/README.md index d75dd1d..d220a5a 100644 --- a/README.md +++ b/README.md @@ -1,58 +1,49 @@ -# Hermes Substrate Wiki +# Substrate Memory Plugins -`substrate_wiki` is the official Hermes integration for Substrate: **the memory that decides what your agents are allowed to do**. Version `2.0.0` connects one Hermes profile to hosted Substrate at `https://app.trysubstrate.co` through a bounded, cited memory interface and durable, redacted history capture. +This public repository contains the official Substrate memory plugins. The current plugin, +`substrate_wiki` 2.0.5, connects an existing Hermes 0.20.x agent to hosted Substrate at +`https://app.trysubstrate.co`. -The broader product uses memory to ground deterministic agent permissions. **Version 2.0.0 is the memory-provider integration only:** it does not authorize actions, include the local runtime or policy compiler, or support a no-server mode. Those gaps are explicit rather than silently presented as hosted-only features. +The Substrate application and server are maintained in a separate private repository. This +repository is the canonical editable source for public plugin/client code, verified installers, +tests, documentation, and immutable release artifacts. -**This repository is the canonical editable source for the plugin.** `Substrate-v2` owns the server and HTTP contract; immutable plugin releases and their checksums are published from this repository. +## Install with an agent -## Status +Tell your Hermes agent exactly: -| Surface | Supported | -|---|---| -| Plugin | `substrate_wiki` 2.0.0 | -| Hermes host | Targets 0.20.x; standalone contract suite verified | -| Server contract | `stream-v2`, `entity-wiki-v1`, `entity-quality-v2` | -| Runtime dependencies | Python standard library only | -| License | MIT | +```text +Install the memory plug-in at https://github.com/Substrate-memory/Substrate-memory-plugins +``` -Hermes 0.20.x provider discovery and setup hooks are targeted. The hosted origin is fixed; there is no local/self-hosted discovery path. +If you are the installing agent, follow the verified procedure below. Keep the installation +command running while the user approves the browser connection. Browser approval completes the +local device-code poll and returns control to the same conversation. Immediately say that the +connection succeeded, then ask: -## Install with an agent +> Do you want to import your existing Hermes conversation history into Substrate? -Give your local coding agent the URL of this README and the following instruction: +Do not select, infer, or pre-authorize the answer. Do not begin history discovery or upload until +the user explicitly answers yes. A no leaves future conversation capture enabled. A blank, +interrupted, or missing answer leaves consent pending. -```text -Install the official Substrate plugin for my existing Hermes installation by following the -"Install with an agent" section of this README exactly. - -Security and consent rules: -- Install only immutable release v2.0.4 from Substrate-memory/hermes-substrate-wiki. -- Before executing downloaded code, independently verify the installer and plugin archive - against the exact SHA-256 values in the README. Never substitute a branch archive, CI - artifact, newer release, or checksum obtained only from the same download response. -- Require `hermes --version` to report 0.20.x. If Hermes is absent or a different version, - stop and tell me; do not install or upgrade Hermes yourself. -- Use the active HERMES_HOME/profile. Do not configure HERMES_API_URL or HERMES_API_KEY. -- Do not print, store, or request access tokens. The plugin's credential-custody flow owns them. -- Run the installer with `--yes --json`; add `--headless` if a browser cannot be opened. -- Immediately show me `verification_uri_complete` as the exact clickable authorization URL, - plus the one-time code. Never show `verification_uri` alone. Then wait while I approve access - in my browser; never approve access on my behalf. -- Historical upload is the only consent choice. Ask me whether to approve or decline it; do not - infer consent. Declining history must leave future capture enabled. -- After onboarding, run the content-free onboarding-status command. Report only version, - installation path, provider activation, connection phase, and import counts/status. Never - expose conversation content or credentials. -- If installation or onboarding is interrupted, rerun the same verified installer or the - documented repair command; do not create replacement credentials or import jobs unnecessarily. - -Select the Bash or PowerShell procedure below for this machine. Clean up only the downloaded -installer/archive/checksum files when finished; do not remove plugin state, credentials, spool, -or checkpoints. -``` +Release `v2.0.5` is not published yet. Do not run the download commands below until this +notice is removed and exact SHA-256 values replace the pending markers. + +Security requirements: -The instruction deliberately leaves browser approval and history consent with the human. The agent may install and verify the plugin, but it must not make either decision. +- Install only immutable release v2.0.5 from `Substrate-memory/Substrate-memory-plugins`. +- Verify the installer and plugin archive against the exact SHA-256 values below before executing + downloaded code. +- Require `hermes --version` to report 0.20.x. Do not install or upgrade Hermes automatically. +- Use the active `HERMES_HOME` profile. Never inspect or copy another profile. +- Do not configure `HERMES_API_URL` or `HERMES_API_KEY`. +- Never print, request, or place access tokens in command arguments or logs. +- Present `verification_uri_complete` exactly when manual browser opening is needed, then keep + polling. Never approve the browser connection for the user. +- When the installer returns `action_required: history_consent`, resume this same conversation + and ask the history question above. +- Report only content-free installation, connection, and import status. ### Bash: Linux, macOS, or WSL @@ -69,7 +60,7 @@ install_dir="$(mktemp -d)" chmod 700 "$install_dir" cd "$install_dir" -base='https://github.com/Substrate-memory/hermes-substrate-wiki/releases/download/v2.0.4' +base='https://github.com/Substrate-memory/Substrate-memory-plugins/releases/download/v2.0.5' curl --fail --location --proto '=https' --tlsv1.2 --remote-name "$base/install_hermes_plugin.py" curl --fail --location --proto '=https' --tlsv1.2 --remote-name "$base/substrate_wiki.zip" curl --fail --location --proto '=https' --tlsv1.2 --remote-name "$base/SHA256SUMS" @@ -79,8 +70,8 @@ import hashlib from pathlib import Path expected = { - "install_hermes_plugin.py": "8e37991db874b306045623c5cba39ef995b53b7b6a1a6c6f0253e2ff12c0ebb8", - "substrate_wiki.zip": "333e8b49d34bac3ace3017d3460cb42ab08d990b016c4ab20ca3e624f4117064", + "install_hermes_plugin.py": "INSTALLER_SHA256_PENDING", + "substrate_wiki.zip": "PLUGIN_SHA256_PENDING", } for name, digest in expected.items(): actual = hashlib.sha256(Path(name).read_bytes()).hexdigest() @@ -95,7 +86,7 @@ PY python3 install_hermes_plugin.py \ --archive substrate_wiki.zip \ - --sha256 333e8b49d34bac3ace3017d3460cb42ab08d990b016c4ab20ca3e624f4117064 \ + --sha256 PLUGIN_SHA256_PENDING \ --yes --json ``` @@ -115,17 +106,17 @@ $installDir = Join-Path ([IO.Path]::GetTempPath()) ("substrate-wiki-" + [guid]:: New-Item -ItemType Directory -Path $installDir | Out-Null Set-Location $installDir -$base = 'https://github.com/Substrate-memory/hermes-substrate-wiki/releases/download/v2.0.4' +$base = 'https://github.com/Substrate-memory/Substrate-memory-plugins/releases/download/v2.0.5' Invoke-WebRequest "$base/install_hermes_plugin.py" -OutFile 'install_hermes_plugin.py' Invoke-WebRequest "$base/substrate_wiki.zip" -OutFile 'substrate_wiki.zip' Invoke-WebRequest "$base/SHA256SUMS" -OutFile 'SHA256SUMS' $installerSha = (Get-FileHash -Algorithm SHA256 'install_hermes_plugin.py').Hash.ToLowerInvariant() $archiveSha = (Get-FileHash -Algorithm SHA256 'substrate_wiki.zip').Hash.ToLowerInvariant() -if ($installerSha -ne '8e37991db874b306045623c5cba39ef995b53b7b6a1a6c6f0253e2ff12c0ebb8') { +if ($installerSha -ne 'INSTALLER_SHA256_PENDING') { throw 'Installer checksum mismatch' } -if ($archiveSha -ne '333e8b49d34bac3ace3017d3460cb42ab08d990b016c4ab20ca3e624f4117064') { +if ($archiveSha -ne 'PLUGIN_SHA256_PENDING') { throw 'Plugin archive checksum mismatch' } $published = Get-Content 'SHA256SUMS' -Raw @@ -136,7 +127,7 @@ if (($published -notmatch [regex]::Escape($installerSha)) -or py -3 install_hermes_plugin.py ` --archive substrate_wiki.zip ` - --sha256 333e8b49d34bac3ace3017d3460cb42ab08d990b016c4ab20ca3e624f4117064 ` + --sha256 PLUGIN_SHA256_PENDING ` --yes --json ``` @@ -166,7 +157,7 @@ Then verify the content-free state: hermes substrate_wiki onboarding-status --json ``` -Tenant credentials are stored in native credential custody, with an owner-private profile fallback; they never belong in ordinary configuration, logs, arguments, or diagnostics. See [configuration and operation](docs/operation.md) and the immutable [v2.0.4 release](https://github.com/Substrate-memory/hermes-substrate-wiki/releases/tag/v2.0.4). +Tenant credentials are stored in native credential custody, with an owner-private profile fallback; they never belong in ordinary configuration, logs, arguments, or diagnostics. See [configuration and operation](docs/operation.md) and the immutable [v2.0.5 release](https://github.com/Substrate-memory/Substrate-memory-plugins/releases/tag/v2.0.5). ## What it does @@ -189,7 +180,7 @@ Read [SECURITY.md](SECURITY.md), [the threat model](docs/threat-model.md), [the ## Open and paid boundary -The open side is permissively licensed and includes the Hermes plugin/client, memory extraction and entity model, credential containment, privacy deletion, and policy compiler. This Hermes integration itself is hosted-only. [BOUNDARY.md](BOUNDARY.md) distinguishes the permanent commitment from the current `v2.0.4` implementation. +The open side is permissively licensed and includes the Hermes plugin/client, memory extraction and entity model, credential containment, privacy deletion, and policy compiler. This Hermes integration itself is hosted-only. [BOUNDARY.md](BOUNDARY.md) distinguishes the permanent commitment from the current `v2.0.5` implementation. The paid hosted tier covers hosted brokerage, multi-user operation, cross-organizational graph services, audit/attestation, and insurance-backed decisions. Its meter is per authorized action, never seats. This repository does not contain or license those held services. diff --git a/SECURITY.md b/SECURITY.md index 1b922ba..29ec846 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -2,7 +2,7 @@ ## Supported versions -The standalone `v2.0.0`, `v2.0.1`, `v2.0.2`, `v2.0.3`, and `v2.0.4` releases target Hermes 0.20.x and the hosted service at `https://app.trysubstrate.co`. +The standalone `v2.0.0`, `v2.0.1`, `v2.0.2`, `v2.0.3`, `v2.0.4`, and `v2.0.5` releases target Hermes 0.20.x and the hosted service at `https://app.trysubstrate.co`. Candidate CI artifacts are not supported releases. ## Report a vulnerability diff --git a/docs/api-ownership.json b/docs/api-ownership.json index 45d2c82..f436b87 100644 --- a/docs/api-ownership.json +++ b/docs/api-ownership.json @@ -328,7 +328,7 @@ "response_schema": "ValidationCanaryView-v1 aggregate only" } ], - "plugin_repository": "https://github.com/Substrate-memory/hermes-substrate-wiki", + "plugin_repository": "https://github.com/Substrate-memory/Substrate-memory-plugins", "repository_obligations": { "Substrate-v2": "owns implementation, authentication, validation, persistence, compatibility aliases, and concurrent support", "hermes-substrate-wiki": "owns client shaping, origin validation, bounded requests, capability enforcement, fallback, and user-facing errors", diff --git a/docs/extraction-manifest.json b/docs/extraction-manifest.json index 95eac0a..83bb4a8 100644 --- a/docs/extraction-manifest.json +++ b/docs/extraction-manifest.json @@ -17,7 +17,7 @@ "class": "standalone_repository_policy_or_test", "path": ".github/ISSUE_TEMPLATE/config.yml", "reason": "Required only by the independent public repository.", - "sha256": "4aa6bee8e87eba307d71fa9e432cd8a80c8c5a69e49fd08be476d8313ea78f7d" + "sha256": "51e4ca6236c169db161c0f6924ad41994fd708f82bd01022723982dfa062abc0" }, { "class": "standalone_repository_policy_or_test", @@ -41,7 +41,7 @@ "class": "standalone_repository_policy_or_test", "path": ".github/workflows/release.yml", "reason": "Required only by the independent public repository.", - "sha256": "73e6a80b853f009f96f1fbc104315f9c4e5561a3f3678ed6836cc684ac971b23" + "sha256": "1c2b887ba36fd9ecd1dd0d76ce5240e9bc87f0fd6683a5bbf2468ed3073ad201" }, { "class": "standalone_repository_policy_or_test", @@ -59,13 +59,13 @@ "class": "standalone_repository_policy_or_test", "path": "CHANGELOG.md", "reason": "Required only by the independent public repository.", - "sha256": "67642ca88a4d821e32c531c3512c4f3dc4d3e65ae091eea5e7efb1bf6851a8d2" + "sha256": "5c3f024118fd7c5f492d5fc52af76b0fe1fa6bf4ee1b5ccd94b3bb34cd08a114" }, { "class": "standalone_repository_policy_or_test", "path": "COMPATIBILITY.md", "reason": "Required only by the independent public repository.", - "sha256": "53d4873064a5d5b1748950c08c0aeaa76a6c00c435e4a8f534a844e4f90aca87" + "sha256": "9e2f30159b85caf7682e20d5c8abcbb1881eb2a624565ca565b93353d4bb624f" }, { "class": "standalone_repository_policy_or_test", @@ -77,13 +77,13 @@ "class": "standalone_repository_policy_or_test", "path": "README.md", "reason": "Required only by the independent public repository.", - "sha256": "ac62cbf3799a35bbb239a0b951411491aab8ee0f2f2c33e66b8a3380a8c20e88" + "sha256": "a5285e1800831e754b648f49f1afd8b37858a2353600db497991579de7e4f323" }, { "class": "standalone_repository_policy_or_test", "path": "SECURITY.md", "reason": "Required only by the independent public repository.", - "sha256": "7ef28d44f7b82b765e870434542b5b67facc703e2105a40a233de2e12783852e" + "sha256": "e4457fb9df863d239120b4b4d05db31c56e5a73b4331c6bf959ed12d1f06410f" }, { "class": "standalone_repository_policy_or_test", @@ -107,7 +107,7 @@ "class": "standalone_repository_policy_or_test", "path": "docs/source-of-truth.md", "reason": "Required only by the independent public repository.", - "sha256": "91e0d6884025a9387b0c25cd02ecd1253e3bef00b5546fe7562f11924775c018" + "sha256": "d89ad302d9f7bef9657e975abf6b419397db62d997be39462688e508c47e29b9" }, { "class": "standalone_repository_policy_or_test", @@ -119,7 +119,7 @@ "class": "standalone_repository_policy_or_test", "path": "pyproject.toml", "reason": "Required only by the independent public repository.", - "sha256": "9dc5e75c2aea5a982a6657e3d17b3f39a343ae5d2fe1e9dd5a8a09ebca00ef36" + "sha256": "460595d6d635cedb7b7e16cfc00e8d054732749df8310452102d357fabf5fde6" }, { "class": "standalone_repository_policy_or_test", @@ -137,13 +137,13 @@ "class": "standalone_plugin_implementation", "path": "src/substrate_wiki/onboarding.py", "reason": "Added for hosted Hermes 0.20 automatic onboarding.", - "sha256": "73c8c6669b3c052578ce4818cd336b2f871794bcdb28a42131a856174de660b7" + "sha256": "ac9934df2970093eb94d1fe5fb820c025d5f3fec2a32fdc5346e77ad9df66626" }, { "class": "standalone_repository_policy_or_test", "path": "tests/test_onboarding.py", "reason": "Added for hosted Hermes 0.20 automatic onboarding.", - "sha256": "a03dfc461c4cf920f1c297acac0cf7bddbbf59ac7685394a63caa67e43b78dad" + "sha256": "8be0b93434e1a011cb74ecbeee92ae070f25344af73cbdc2d162872695d57186" }, { "class": "standalone_repository_policy_or_test", @@ -161,10 +161,10 @@ "class": "standalone_repository_policy_or_test", "path": "uv.lock", "reason": "Required only by the independent public repository.", - "sha256": "70c4a7336d28dd84f6c3e06a241c130ba7d286f43a20c5bd611ab4954451c92a" + "sha256": "52eadc5cea9450a6348a3daa5370468ffaab9012439e15a2a61dce60520fccb6" } ], - "destination_repository": "Substrate-memory/hermes-substrate-wiki", + "destination_repository": "Substrate-memory/Substrate-memory-plugins", "entries": [ { "class": "boundary", @@ -209,7 +209,7 @@ { "class": "boundary", "destination": "docs/api-ownership.json", - "destination_sha256": "f1894c4653c51c6f02d23194617a3a1e9035d2cbe2b7c282f4cd6fbb26d8b65c", + "destination_sha256": "2fc837c1f89ef9948c9d542f4912f39414a9cd43eb650b1f73f9a2dc49b3ad2e", "source": "docs/plugin/api-ownership.json", "source_sha256": "4e5dba6249b26ce8b90a8c483125e6fd2c5e7cec94b3770be3e92a7aa04347dc", "transformation": "modified_for_standalone" @@ -225,7 +225,7 @@ { "class": "boundary", "destination": "docs/public-boundary.json", - "destination_sha256": "648904170d6c66de6f15cfe51fb494225467730ff5167225d6fd427a6eac3571", + "destination_sha256": "b3c24d1ccff43c274be683020f23661d45faaeb16b842af384d828c6b7968390", "source": "docs/plugin/public-boundary.json", "source_sha256": "5f4b5a663dd6590480ed432b22ae68a88a0fd5ab4704b705f60617332edc75bc", "transformation": "modified_for_standalone" @@ -337,7 +337,7 @@ { "class": "build-and-install", "destination": "scripts/install_hermes_plugin.py", - "destination_sha256": "8e37991db874b306045623c5cba39ef995b53b7b6a1a6c6f0253e2ff12c0ebb8", + "destination_sha256": "9f157d5e7e3b8921a01d46f975f574a8955a53c5f554ed61b306a7bdfddd7309", "source": "scripts/install_hermes_plugin.py", "source_sha256": "7600b2681c3aebcb1b1492b0a04be38bbbec637089cbbcfb1cc26e8c10865b8d", "transformation": "modified_for_standalone" @@ -353,7 +353,7 @@ { "class": "build-and-install", "destination": "scripts/verify_public_plugin_candidate.py", - "destination_sha256": "169151e6ca769184881da61a823ff4f1d2b1c3df35f2e91049a9c800d173bb98", + "destination_sha256": "292700ed3357372393b44371297e5368600930995479b7ff983701bed16c56a4", "source": "scripts/verify_public_plugin_candidate.py", "source_sha256": "4130935d530075fce1758e2e89bd5d973a722e2293b5b1058cfe0d17f326172b", "transformation": "modified_for_standalone" @@ -361,7 +361,7 @@ { "class": "plugin-package", "destination": "src/substrate_wiki/README.md", - "destination_sha256": "88d492b33ee537567bf85b63dcc7a470319212ac377f4857e27e504a3a1a05a3", + "destination_sha256": "0c555fe0adf4f0eb7112f2897ba0ce0fb22e3d15b928608c4a67447be98c3c23", "source": "hermes-plugin/substrate_wiki/README.md", "source_sha256": "21b6e60ef34e408fd87570334ba6d2811d2a9ea3c95d512e4d625f958b1cb0dc", "transformation": "modified_for_standalone" @@ -385,15 +385,15 @@ { "class": "plugin-package", "destination": "src/substrate_wiki/cli.py", - "destination_sha256": "ba978f5f9f0d48eb2288c8d470182c0ef38a56106e5bc90445352bb90981c044", + "destination_sha256": "6214bed9894c7195f22144701dc468b717faafddb5b79e1227445d3a8033fe4a", "source": "hermes-plugin/substrate_wiki/cli.py", "source_sha256": "454bffeb76fcde508b7a16cc7597b444611ecb0028e8d43796c57c500cf75d3c", - "transformation": "copied" + "transformation": "modified_for_standalone" }, { "class": "plugin-package", "destination": "src/substrate_wiki/client.py", - "destination_sha256": "db9eb5043a5c770f155a942459008de684378261e8fd204276d69b0bed3d788f", + "destination_sha256": "dd82bf56371538a8c6fe8eb39787c5a0fb4fe1232e11fcb29b2d4ac11f0bd9ed", "source": "hermes-plugin/substrate_wiki/client.py", "source_sha256": "8684b3ecc788ee058c630747b1c72578177cfc948c72ac539ddcc4a1df9e791b", "transformation": "modified_for_standalone" @@ -417,7 +417,7 @@ { "class": "plugin-package", "destination": "src/substrate_wiki/plugin.yaml", - "destination_sha256": "45478259c2e7f584d4a9bbf50d6563b6c57bc41d8f4ed6c6a3e480b87f48b746", + "destination_sha256": "124f058b164fe7c5beeb7ffc0c066e6f4530aa361dc0988f753267202776f083", "source": "hermes-plugin/substrate_wiki/plugin.yaml", "source_sha256": "5bfc8b20bfe99b8dd4fcb35616724e9a4a93bdfd1f3b270cfea1ccff18eaa116", "transformation": "modified_for_standalone" @@ -481,7 +481,7 @@ { "class": "plugin-tests", "destination": "tests/test_entity_memory.py", - "destination_sha256": "139f51e9fdd91ee813f86281f41d4a150339ed5f6c303c48e7a5ef7ec358adbd", + "destination_sha256": "e9d869d2389bdfd84e7912586284ff318f65b8841ade19c0c9bd5a03a1a4639d", "source": "tests/contract/test_hermes_entity_memory_v13.py", "source_sha256": "297d7dffa2a53dd08cb1737ac024a97eb190c4ce42b338a952d8bec9aeba37f4", "transformation": "modified_for_standalone" @@ -537,7 +537,7 @@ { "class": "plugin-tests", "destination": "tests/test_packaging.py", - "destination_sha256": "e90c78ea6b182c558079c59752be07af1bc7937b69e06b66ee96a74bc8f7f0db", + "destination_sha256": "96f2d25b7fa3159d31cb6274cc39ae279909a2e90a694f19779a795b86076243", "source": "tests/contract/test_hermes_plugin_packaging.py", "source_sha256": "c54967830788555b8d7bd5d2f871baeb6b98d7804a2bc7410ab297078bc20ee7", "transformation": "modified_for_standalone" diff --git a/docs/public-boundary.json b/docs/public-boundary.json index 51f68f2..df60f91 100644 --- a/docs/public-boundary.json +++ b/docs/public-boundary.json @@ -86,7 +86,7 @@ "name": "hermes-substrate-wiki", "organization": "Substrate-memory", "source_of_truth": true, - "url": "https://github.com/Substrate-memory/hermes-substrate-wiki", + "url": "https://github.com/Substrate-memory/Substrate-memory-plugins", "visibility": "public" }, "schema_version": 1, diff --git a/docs/source-of-truth.md b/docs/source-of-truth.md index da3c74f..f1f6cba 100644 --- a/docs/source-of-truth.md +++ b/docs/source-of-truth.md @@ -2,7 +2,7 @@ ## Decision -`Substrate-memory/hermes-substrate-wiki` is the sole editable source for the Hermes `substrate_wiki` plugin. The protected default branch and immutable releases `v1.5.0`, `v2.0.0`, `v2.0.1`, `v2.0.3`, and `v2.0.4` have been read back successfully. +`Substrate-memory/Substrate-memory-plugins` is the sole editable source for the Hermes `substrate_wiki` plugin. The protected default branch and immutable releases `v1.5.0`, `v2.0.0`, `v2.0.1`, `v2.0.3`, and `v2.0.4` have been read back successfully. Substrate-v2 owns only the server and pinned public release references; it must not vendor or modify plugin source. diff --git a/pyproject.toml b/pyproject.toml index 20eeb06..c2d3aa5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "hermes-substrate-wiki" -version = "2.0.4" +version = "2.0.5" description = "Substrate memory-provider plugin for Hermes Agent" readme = "README.md" requires-python = ">=3.11" diff --git a/scripts/install_hermes_plugin.py b/scripts/install_hermes_plugin.py index 5c8ed22..a9c780e 100644 --- a/scripts/install_hermes_plugin.py +++ b/scripts/install_hermes_plugin.py @@ -19,7 +19,7 @@ from typing import Any, cast PLUGIN_NAME = "substrate_wiki" -EXPECTED_VERSION = "2.0.4" +EXPECTED_VERSION = "2.0.5" EXPECTED_HERMES_VERSION = "0.20.0" LICENSE_FILENAME = "LICENSE" REQUIRED_FILES = { @@ -464,6 +464,9 @@ def install( "complete": False, "error_class": str(onboarding.get("error_class") or "OnboardingLaunchError"), } + if onboarding.get("phase") == "awaiting_history_consent": + onboarding["action_required"] = "history_consent" + result["action_required"] = "history_consent" result["onboarding"] = onboarding result["onboarding_pending"] = not bool(onboarding.get("complete")) return result @@ -516,6 +519,13 @@ def main() -> int: else f"Installation failed: {type(exc).__name__}" ) return 1 + if result.get("action_required") == "history_consent": + print( + "Substrate connection successful. Return to the same conversation and ask " + "whether the user wants to import existing Hermes history.", + file=sys.stderr, + flush=True, + ) print( json.dumps(result, sort_keys=True) if args.json diff --git a/scripts/verify_public_plugin_candidate.py b/scripts/verify_public_plugin_candidate.py index d04b276..4d6c537 100644 --- a/scripts/verify_public_plugin_candidate.py +++ b/scripts/verify_public_plugin_candidate.py @@ -229,78 +229,68 @@ # Hosted-origin and legacy-name references are accepted only at these exact reviewed # file digests. Any byte change re-enables all endpoint and credential-name detectors. -_HOSTED_ONBOARDING_EXACT_ALLOWLIST = { - 'README.md': frozenset( - { - "0d74ffbf410fe559478ee6a67e105f275d72d0477c71e86c81531798bc07e599", - "20ac49480369c8531e001466a6537a7dae310bbba7e6079bc217a6181e7d69d7", - "6361beb1a5153f5dbcfca70740c1905096f3bc32ddfd4cc22157c6576c61b83a", - "87734abc087a4267e77aaeae7fb349d4c19f56dea2f7f3d173ab833dfb1da5e9", - "cdc48a8f8dbe1d68dfe329d5106c55895c5345924544c2cc4b3d7190c837705b", - "f139b1328c7884dfa664aa59b47d5d707b2a72894747911b8f99d5e89f6c7eca", - "1cce63f45c357df4710e24a8351835c4382a47751d57edf4207e234eb350807b", - "ac62cbf3799a35bbb239a0b951411491aab8ee0f2f2c33e66b8a3380a8c20e88", - } - ), - 'COMPATIBILITY.md': frozenset( - { - "1f18a49d552f2912872c10b0e41e93ff50ccd923d2fa9c108d35084dc5a5b22c", - "1f8fc410a88c7b410c4e72f4210226629b573a3c7f0cc410bc35939cd2317cfc", - "53d4873064a5d5b1748950c08c0aeaa76a6c00c435e4a8f534a844e4f90aca87", - "67bcb14e4988ed5b49379e93f93fa588e31a068b394a8787d1ea81ba8f6a6c70", - "e14a7a057ee64449041a2073590641a44cfe648cff696d4d20c58eed9e4509a3", - } - ), - 'SECURITY.md': frozenset( - { - "48509dbbefd182381675499ff5636df18073411db580b05a1430e5eec1714df7", - "756264bc4dbd9f9df5c6e4bc016545fc1ac0921b384346e40d557706c2137fc5", - "85320ec1e6a8c655b1f4e278b95c1087a6382bf3141366f1290c0933590f4645", - "99aed8e5a9a712f0fd2d9caa5223f0b332220f98699351565a3bb35a23d2848c", - "9cf9406266336996c57227413514cf97a04eb13264d80fa58c54d325fecf3d9f", - "d07d0c65e39f0949f91f7a880a4b584b9b57d8ed3c70ca8fd95276d8d525b517", - "ef0cdf7a6c2fdc4ec7e781be15122f9b20331769929517a0f3c82ab15363480b", - "7ef28d44f7b82b765e870434542b5b67facc703e2105a40a233de2e12783852e", - } - ), - 'docs/api-ownership.json': frozenset({'f1894c4653c51c6f02d23194617a3a1e9035d2cbe2b7c282f4cd6fbb26d8b65c'}), - 'docs/operation.md': frozenset({'fadd72791e097d878bbfbf338922d568f3c3d47958f26a41f53d3597471988fa'}), - 'docs/public-boundary.json': frozenset({'976403a6a802832adddd5ab1ff56fdc2e4e4847a1fe37ea5e6a2d5f106bbbbe2', '648904170d6c66de6f15cfe51fb494225467730ff5167225d6fd427a6eac3571'}), - 'docs/public-boundary.md': frozenset({'c9228e2abd22a8af558cd272988435c1caa69ad79c625cb08c8ef32e639eb0f3'}), - 'src/substrate_wiki/README.md': frozenset({'44a11fda85d6d8170d772beadba2149bd114624479344b69a69cd49634309204', '88d492b33ee537567bf85b63dcc7a470319212ac377f4857e27e504a3a1a05a3'}), - 'src/substrate_wiki/__init__.py': frozenset({'425cd191f723805bed85d965d2d74daeac272dd0660eadf6bac92f1fa02d6f4a', '71c0c19d366759e67c7b999029777295cc8c25503d35fe9cfddc9460bcb34d5a'}), - 'src/substrate_wiki/client.py': frozenset( - { - "082164ad24c879f6ca6434a8f28c251cd1ee7f4b413c5a788a70b351e2187f2a", - "59413ab9fa2eef0d943a70a53e2b5d9d5163c0d9e154538735715e8f88585616", - "a6e7c18e916057835e8cae9e3aa89bcc7357b397f014b8bc5bb71d995a5aa841", - "db9eb5043a5c770f155a942459008de684378261e8fd204276d69b0bed3d788f", - "f98518e2eea1d57e813130822ea95de1fcc5b550adf9b65164347468eadc6818", - } - ), - 'src/substrate_wiki/onboarding.py': frozenset( - { - "2f1d35c0c568a6d817a3cfcb36b99a4447636ed8ab512d656a4c4411f4a48967", - "4ae1938346a5af64b3936913c4b8438891d8170c6caa617b2eb3b5c816079ec0", - "52e11806e20017c920d4c0caef394570a8fdfb2cabff8a87cdbde3dc7e5a64fc", - "a835092092bca7266978609763d36f5767ac363fe7180bda482d713df2ecc439", - "73c8c6669b3c052578ce4818cd336b2f871794bcdb28a42131a856174de660b7", - "d58fc9693f78cb4d5f8d9738c9895f009e098d73400d4e9530e7454662fabff1", - "da01090b5f007d9741a06d7ff4a9c8140036e4ef26512665b4802800e0b543a3", - } - ), - 'scripts/install_hermes_plugin.py': frozenset( - { - "33adb95c93f478a91991a97f0b9b6a1c9d2cee77e7894ed37fe331a4403b0bb8", - "4b34ee40d0d08ef24d03128e1cfc5ef73c69b39ca77b3fff59f4a4133cef76f2", - "69af75e4240166896031f3a396fd0b2bdc4d00adbc836d1a4f22019bc6713b75", - "72247d3537140098365350020cce29658c0743fee1aa738d7143db82316acce4", - "8e37991db874b306045623c5cba39ef995b53b7b6a1a6c6f0253e2ff12c0ebb8", - } - ), - 'tests/test_hardening.py': frozenset({'f5f87125f1edd37bff1d44301d6bb0f44cc7faf3ba6122bdbe7569f349fea7a3', 'bb9825c1889d919e29a90e43c2be48b84d846fcad813fa3896d194491cc8f386'}), - 'tests/test_memory_provider.py': frozenset({'2c4517847dfad341063a69afcc737316a74574471fc98a45eaff21cfe4e271fd', 'cc967199b8e877a8088937a0c951e80f188ad4c7f29a98067bf649a2f8500b72'}), -} +_HOSTED_ONBOARDING_EXACT_ALLOWLIST = {'COMPATIBILITY.md': frozenset({'1f18a49d552f2912872c10b0e41e93ff50ccd923d2fa9c108d35084dc5a5b22c', + '1f8fc410a88c7b410c4e72f4210226629b573a3c7f0cc410bc35939cd2317cfc', + '53d4873064a5d5b1748950c08c0aeaa76a6c00c435e4a8f534a844e4f90aca87', + '67bcb14e4988ed5b49379e93f93fa588e31a068b394a8787d1ea81ba8f6a6c70', + '9e2f30159b85caf7682e20d5c8abcbb1881eb2a624565ca565b93353d4bb624f', + 'e14a7a057ee64449041a2073590641a44cfe648cff696d4d20c58eed9e4509a3'}), + 'README.md': frozenset({'0d74ffbf410fe559478ee6a67e105f275d72d0477c71e86c81531798bc07e599', + '1cce63f45c357df4710e24a8351835c4382a47751d57edf4207e234eb350807b', + '20ac49480369c8531e001466a6537a7dae310bbba7e6079bc217a6181e7d69d7', + '6361beb1a5153f5dbcfca70740c1905096f3bc32ddfd4cc22157c6576c61b83a', + '87734abc087a4267e77aaeae7fb349d4c19f56dea2f7f3d173ab833dfb1da5e9', + 'a5285e1800831e754b648f49f1afd8b37858a2353600db497991579de7e4f323', + 'ac62cbf3799a35bbb239a0b951411491aab8ee0f2f2c33e66b8a3380a8c20e88', + 'cdc48a8f8dbe1d68dfe329d5106c55895c5345924544c2cc4b3d7190c837705b', + 'f139b1328c7884dfa664aa59b47d5d707b2a72894747911b8f99d5e89f6c7eca'}), + 'SECURITY.md': frozenset({'48509dbbefd182381675499ff5636df18073411db580b05a1430e5eec1714df7', + '756264bc4dbd9f9df5c6e4bc016545fc1ac0921b384346e40d557706c2137fc5', + '7ef28d44f7b82b765e870434542b5b67facc703e2105a40a233de2e12783852e', + '85320ec1e6a8c655b1f4e278b95c1087a6382bf3141366f1290c0933590f4645', + '99aed8e5a9a712f0fd2d9caa5223f0b332220f98699351565a3bb35a23d2848c', + '9cf9406266336996c57227413514cf97a04eb13264d80fa58c54d325fecf3d9f', + 'd07d0c65e39f0949f91f7a880a4b584b9b57d8ed3c70ca8fd95276d8d525b517', + 'e4457fb9df863d239120b4b4d05db31c56e5a73b4331c6bf959ed12d1f06410f', + 'ef0cdf7a6c2fdc4ec7e781be15122f9b20331769929517a0f3c82ab15363480b'}), + 'docs/api-ownership.json': frozenset({'2fc837c1f89ef9948c9d542f4912f39414a9cd43eb650b1f73f9a2dc49b3ad2e', + 'f1894c4653c51c6f02d23194617a3a1e9035d2cbe2b7c282f4cd6fbb26d8b65c'}), + 'docs/operation.md': frozenset({'fadd72791e097d878bbfbf338922d568f3c3d47958f26a41f53d3597471988fa'}), + 'docs/public-boundary.json': frozenset({'648904170d6c66de6f15cfe51fb494225467730ff5167225d6fd427a6eac3571', + '976403a6a802832adddd5ab1ff56fdc2e4e4847a1fe37ea5e6a2d5f106bbbbe2', + 'b3c24d1ccff43c274be683020f23661d45faaeb16b842af384d828c6b7968390'}), + 'docs/public-boundary.md': frozenset({'c9228e2abd22a8af558cd272988435c1caa69ad79c625cb08c8ef32e639eb0f3'}), + 'scripts/install_hermes_plugin.py': frozenset({'33adb95c93f478a91991a97f0b9b6a1c9d2cee77e7894ed37fe331a4403b0bb8', + '4b34ee40d0d08ef24d03128e1cfc5ef73c69b39ca77b3fff59f4a4133cef76f2', + '69af75e4240166896031f3a396fd0b2bdc4d00adbc836d1a4f22019bc6713b75', + '72247d3537140098365350020cce29658c0743fee1aa738d7143db82316acce4', + '8e37991db874b306045623c5cba39ef995b53b7b6a1a6c6f0253e2ff12c0ebb8', + '9f157d5e7e3b8921a01d46f975f574a8955a53c5f554ed61b306a7bdfddd7309'}), + 'src/substrate_wiki/README.md': frozenset({'0c555fe0adf4f0eb7112f2897ba0ce0fb22e3d15b928608c4a67447be98c3c23', + '44a11fda85d6d8170d772beadba2149bd114624479344b69a69cd49634309204', + '88d492b33ee537567bf85b63dcc7a470319212ac377f4857e27e504a3a1a05a3'}), + 'src/substrate_wiki/__init__.py': frozenset({'425cd191f723805bed85d965d2d74daeac272dd0660eadf6bac92f1fa02d6f4a', + '71c0c19d366759e67c7b999029777295cc8c25503d35fe9cfddc9460bcb34d5a'}), + 'src/substrate_wiki/client.py': frozenset({'082164ad24c879f6ca6434a8f28c251cd1ee7f4b413c5a788a70b351e2187f2a', + '24af4976d4f4d624286261ffc7ac5901387372205ef2430999ef738938406588', + '59413ab9fa2eef0d943a70a53e2b5d9d5163c0d9e154538735715e8f88585616', + 'a6e7c18e916057835e8cae9e3aa89bcc7357b397f014b8bc5bb71d995a5aa841', + 'db9eb5043a5c770f155a942459008de684378261e8fd204276d69b0bed3d788f', + 'dd82bf56371538a8c6fe8eb39787c5a0fb4fe1232e11fcb29b2d4ac11f0bd9ed', + 'f98518e2eea1d57e813130822ea95de1fcc5b550adf9b65164347468eadc6818'}), + 'src/substrate_wiki/onboarding.py': frozenset({'2f1d35c0c568a6d817a3cfcb36b99a4447636ed8ab512d656a4c4411f4a48967', + '4ae1938346a5af64b3936913c4b8438891d8170c6caa617b2eb3b5c816079ec0', + '52e11806e20017c920d4c0caef394570a8fdfb2cabff8a87cdbde3dc7e5a64fc', + '73c8c6669b3c052578ce4818cd336b2f871794bcdb28a42131a856174de660b7', + '9b8b300f6ea4130442ae1838e841c63019877943d1bc8a84378c7008389add17', + 'a835092092bca7266978609763d36f5767ac363fe7180bda482d713df2ecc439', + 'ac9934df2970093eb94d1fe5fb820c025d5f3fec2a32fdc5346e77ad9df66626', + 'd58fc9693f78cb4d5f8d9738c9895f009e098d73400d4e9530e7454662fabff1', + 'da01090b5f007d9741a06d7ff4a9c8140036e4ef26512665b4802800e0b543a3'}), + 'tests/test_hardening.py': frozenset({'bb9825c1889d919e29a90e43c2be48b84d846fcad813fa3896d194491cc8f386', + 'f5f87125f1edd37bff1d44301d6bb0f44cc7faf3ba6122bdbe7569f349fea7a3'}), + 'tests/test_memory_provider.py': frozenset({'2c4517847dfad341063a69afcc737316a74574471fc98a45eaff21cfe4e271fd', + 'cc967199b8e877a8088937a0c951e80f188ad4c7f29a98067bf649a2f8500b72'})} for _path, _digests in _HOSTED_ONBOARDING_EXACT_ALLOWLIST.items(): SYNTHETIC_FILE_SHA256_ALLOWLIST[_path] = ( SYNTHETIC_FILE_SHA256_ALLOWLIST.get(_path, frozenset()) | _digests @@ -314,7 +304,7 @@ "4b444b2583fbdd340b17d279fd169103c57f87a56dece39988d784b311222920" ) TRUSTED_HISTORICAL_BLOB_POLICY_SHA256 = ( - "b3ea73d46f3968b99d6695a14b954954826fbd7fa52abd5fd18cf951db8234e1" + "47091457ab7ae27771e7e4580938f0f0bead181a1b62683784d8a07d468f108f" ) SCANNER_PATH = "scripts/verify_public_plugin_candidate.py" DESTINATION_MANIFEST_PATH = "docs/extraction-manifest.json" diff --git a/src/substrate_wiki/README.md b/src/substrate_wiki/README.md index 02e73f8..34275e7 100644 --- a/src/substrate_wiki/README.md +++ b/src/substrate_wiki/README.md @@ -33,9 +33,13 @@ migration path. ## History consent -After connection, Substrate asks whether to upload eligible history. This is the only -optional step. Declining history upload leaves future capture and recall enabled. Approval -starts exactly one durable profile-scoped job. Its content-free status reports discovered, +After browser approval, the polling installer returns control to the same Hermes +conversation with `action_required: history_consent`. The agent must report that the +connection succeeded and ask whether to upload eligible history. It must not infer or +pre-authorize the answer. Blank or interrupted input leaves consent pending. + +This is the only optional step. Declining history upload leaves future capture and recall +enabled. Approval starts exactly one durable profile-scoped job. Its content-free status reports discovered, uploaded, processed, duplicate, retry, and error counts; disconnects and restarts resume the same checkpoint and stable batch/event IDs. diff --git a/src/substrate_wiki/cli.py b/src/substrate_wiki/cli.py index d0dc310..41037f0 100644 --- a/src/substrate_wiki/cli.py +++ b/src/substrate_wiki/cli.py @@ -168,12 +168,16 @@ def _onboarding(args: argparse.Namespace, home: Path) -> dict[str, Any]: decision = args.history if decision == "ask" and sys.stdin.isatty(): print( - "Upload all eligible past Hermes conversations to Substrate? [y/N]: ", + "Upload all eligible past Hermes conversations to Substrate? [y/n]: ", end="", file=sys.stderr, flush=True, ) - decision = "approve" if sys.stdin.readline().strip().casefold() in {"y", "yes"} else "decline" + answer = sys.stdin.readline().strip().casefold() + if answer in {"y", "yes"}: + decision = "approve" + elif answer in {"n", "no"}: + decision = "decline" if decision in {"approve", "decline"}: result = manager.consent_history(decision == "approve") return result diff --git a/src/substrate_wiki/client.py b/src/substrate_wiki/client.py index 70969d5..9db744a 100644 --- a/src/substrate_wiki/client.py +++ b/src/substrate_wiki/client.py @@ -26,7 +26,7 @@ _MAX_SHORT_FIELD_CHARS = 2048 _MAX_TEXT_FIELD_CHARS = 65536 _MAX_MEMORY_CARD_CHARS = 8192 -_USER_AGENT = "substrate_wiki-hermes-plugin/2.0.4" +_USER_AGENT = "substrate_wiki-hermes-plugin/2.0.5" _PLUGIN_VERSION = (2, 0, 0) diff --git a/src/substrate_wiki/onboarding.py b/src/substrate_wiki/onboarding.py index 81a59dc..245de4e 100644 --- a/src/substrate_wiki/onboarding.py +++ b/src/substrate_wiki/onboarding.py @@ -31,7 +31,7 @@ SCOPES = "capture retrieve" DEVICE_GRANT = "urn:ietf:params:oauth:grant-type:device_code" _STATE_VERSION = 1 -_PLUGIN_VERSION = "2.0.4" +_PLUGIN_VERSION = "2.0.5" _MAX_RESPONSE = 64 * 1024 _CAPABILITY_TIMEOUT_SECONDS = 60.0 _CAPABILITY_ATTEMPTS = 2 @@ -336,6 +336,8 @@ def status(self) -> dict[str, Any]: result["credential_backend"] = self.store.backend result["authenticated"] = bool(self.store.get()) result["complete"] = state.get("phase") in _TERMINAL + if state.get("phase") == "awaiting_history_consent": + result["action_required"] = "history_consent" return result def _check_capabilities(self, token: str) -> dict[str, Any]: @@ -456,6 +458,11 @@ def consent_history(self, approved: bool) -> dict[str, Any]: if not self.store.get(): raise OnboardingError("authentication_required") decision = "approved" if approved else "declined" + if state.get("phase") != "awaiting_history_consent": + receipt = state.get("history_consent") + if isinstance(receipt, dict) and receipt.get("decision") == decision: + return self.status() + raise OnboardingError("history_consent_not_pending") state["history_consent"] = _receipt(decision) if not approved: state.update(phase="ready", completed_at=time.time()) @@ -485,9 +492,7 @@ def run( timeout: float = 900.0, ) -> dict[str, Any]: result = self.begin(mode=mode, open_browser=open_browser) - if wait and result.get("phase") == "authorization_pending" and ( - mode == "device" or not open_browser - ): + if wait and result.get("phase") == "authorization_pending": print( f"Open {result.get('verification_uri_complete')} to sign in by email " f"and connect Hermes. One-time code: {result.get('user_code')}", @@ -570,14 +575,18 @@ def _prompt_history(manager: OnboardingManager) -> dict[str, Any]: if not sys.stdin.isatty(): return status print( - "Upload all eligible past Hermes conversations to your Substrate memory? [y/N]: ", + "Upload all eligible past Hermes conversations to your Substrate memory? [y/n]: ", end="", file=sys.stderr, flush=True, ) try: answer = sys.stdin.readline().strip().casefold() except (EOFError, KeyboardInterrupt): - answer = "" - return manager.consent_history(answer in {"y", "yes"}) + return manager.status() + if answer in {"y", "yes"}: + return manager.consent_history(True) + if answer in {"n", "no"}: + return manager.consent_history(False) + return manager.status() def bootstrap_package() -> None: diff --git a/src/substrate_wiki/plugin.yaml b/src/substrate_wiki/plugin.yaml index ee43d93..60dfa4b 100644 --- a/src/substrate_wiki/plugin.yaml +++ b/src/substrate_wiki/plugin.yaml @@ -1,5 +1,5 @@ name: substrate_wiki -version: 2.0.4 +version: 2.0.5 description: "Unified entity-centric Substrate Markdown wiki memory with durable asynchronous capture." hooks: - on_session_switch diff --git a/tests/test_entity_memory.py b/tests/test_entity_memory.py index f8fcfe4..7d6de5f 100644 --- a/tests/test_entity_memory.py +++ b/tests/test_entity_memory.py @@ -251,6 +251,6 @@ def test_semantic_gate_rejects_non_strict_versions( def test_v141_manifest_and_prompt_describe_one_published_memory() -> None: manifest = (PLUGIN_ROOT / "substrate_wiki" / "plugin.yaml").read_text(encoding="utf-8") prompt = SubstrateWikiProvider().system_prompt_block() - assert "version: 2.0.4" in manifest + assert "version: 2.0.5" in manifest assert "single published memory" in prompt assert "canonical published entity" in prompt diff --git a/tests/test_onboarding.py b/tests/test_onboarding.py index 562c692..d91685f 100644 --- a/tests/test_onboarding.py +++ b/tests/test_onboarding.py @@ -10,6 +10,7 @@ HostedOAuthClient, OnboardingError, OnboardingManager, + _prompt_history, ) @@ -307,3 +308,61 @@ def capabilities(_client): assert result["capability_failure"] == "server_upgrade_required" assert result["authenticated"] is False assert store.get() == "" + + +def test_pending_history_consent_is_explicit_and_idempotent(tmp_path: Path): + store = Store() + store.put("tenant-secret") + launches = [] + manager = OnboardingManager( + tmp_path, + store=store, + capability_check=lambda token: {}, + import_start=lambda home: launches.append(home) or {"job_id": "job-1", "complete": False}, + ) + pending = manager.begin() + assert pending["phase"] == "awaiting_history_consent" + assert pending["action_required"] == "history_consent" + + first = manager.consent_history(True) + second = manager.consent_history(True) + assert first["phase"] == second["phase"] == "importing" + assert launches == [tmp_path.resolve()] + with pytest.raises(OnboardingError, match="history_consent_not_pending"): + manager.consent_history(False) + + +def test_blank_history_answer_stays_pending(tmp_path: Path, monkeypatch): + class InteractiveBlank: + def isatty(self): return True + def readline(self): return "\n" + + store = Store() + store.put("tenant-secret") + manager = OnboardingManager(tmp_path, store=store, capability_check=lambda token: {}) + manager.begin() + monkeypatch.setattr("substrate_wiki.onboarding.sys.stdin", InteractiveBlank()) + result = _prompt_history(manager) + assert result["phase"] == "awaiting_history_consent" + assert result["action_required"] == "history_consent" + assert "history_consent" not in json.loads( + (tmp_path / "substrate_wiki/onboarding/state.json").read_text() + ) + + +def test_auto_device_run_always_prints_complete_authorization_url( + tmp_path: Path, monkeypatch, capsys +): + monkeypatch.delenv("DISPLAY", raising=False) + monkeypatch.delenv("WAYLAND_DISPLAY", raising=False) + manager = OnboardingManager( + tmp_path, + api=API(), + store=Store(), + capability_check=lambda token: {"ok": token}, + opener=lambda url: True, + ) + result = manager.run(mode="auto", wait=True, open_browser=True, timeout=5) + assert result["phase"] == "awaiting_history_consent" + assert result["action_required"] == "history_consent" + assert result["verification_uri_complete"] in capsys.readouterr().err diff --git a/tests/test_packaging.py b/tests/test_packaging.py index f446e47..6afdbdb 100644 --- a/tests/test_packaging.py +++ b/tests/test_packaging.py @@ -90,9 +90,9 @@ def test_root_readme_keeps_published_release_state_truthful() -> None: assert boundary["repository"]["source_of_truth"] is True assert boundary["legal"]["status"] == "published" assert "canonical editable source" in readme - assert "`v2.0.4` is not published yet" not in readme - assert "releases/download/v2.0.4" in readme - assert "333e8b49d34bac3ace3017d3460cb42ab08d990b016c4ab20ca3e624f4117064" in readme + assert "`v2.0.5` is not published yet" in readme + assert "releases/download/v2.0.5" in readme + assert "PLUGIN_SHA256_PENDING" in readme def test_release_workflow_keeps_dependency_execution_out_of_privileged_publisher() -> None: @@ -148,7 +148,7 @@ def test_archive_provenance_identifies_version_and_source_hashes(tmp_path: Path) assert provenance == { "build_format_version": 3, "license_sha256": hashlib.sha256((REPOSITORY_ROOT / "LICENSE").read_bytes()).hexdigest(), - "plugin_version": "2.0.4", + "plugin_version": "2.0.5", "provider_id": "substrate_wiki", "source_commit": "unknown", "source_files": { @@ -233,8 +233,8 @@ def test_publish_release_creates_exact_current_and_immutable_aliases(tmp_path: P releases_path=releases_path, ) - assert release_archive == releases_path / "2.0.4" / "substrate_wiki.zip" - assert release_installer == releases_path / "2.0.4" / "install_hermes_plugin.py" + assert release_archive == releases_path / "2.0.5" / "substrate_wiki.zip" + assert release_installer == releases_path / "2.0.5" / "install_hermes_plugin.py" assert archive_path.read_bytes() == release_archive.read_bytes() == archive assert release_installer.read_bytes() == INSTALLER_PATH.read_bytes() assert builder.check_release( @@ -246,7 +246,7 @@ def test_publish_release_creates_exact_current_and_immutable_aliases(tmp_path: P def test_publish_release_refuses_to_replace_versioned_bytes(tmp_path: Path) -> None: builder = load_builder() releases_path = tmp_path / "releases" - versioned = releases_path / "2.0.4" / "substrate_wiki.zip" + versioned = releases_path / "2.0.5" / "substrate_wiki.zip" versioned.parent.mkdir(parents=True) versioned.write_bytes(b"different immutable bytes") archive_path = tmp_path / "current" / "substrate_wiki.zip" @@ -265,7 +265,7 @@ def test_publish_release_refuses_to_replace_versioned_bytes(tmp_path: Path) -> N def test_publish_release_preflights_both_immutable_artifacts(tmp_path: Path) -> None: builder = load_builder() releases_path = tmp_path / "releases" - versioned_installer = releases_path / "2.0.4" / "install_hermes_plugin.py" + versioned_installer = releases_path / "2.0.5" / "install_hermes_plugin.py" versioned_installer.parent.mkdir(parents=True) versioned_installer.write_bytes(b"conflicting immutable installer") archive_path = tmp_path / "current" / "substrate_wiki.zip" @@ -277,14 +277,14 @@ def test_publish_release_preflights_both_immutable_artifacts(tmp_path: Path) -> releases_path=releases_path, ) - assert not (releases_path / "2.0.4" / "substrate_wiki.zip").exists() + assert not (releases_path / "2.0.5" / "substrate_wiki.zip").exists() assert not archive_path.exists() def test_publish_release_rejects_symlinked_immutable_artifact(tmp_path: Path) -> None: builder = load_builder() releases_path = tmp_path / "releases" - release_directory = releases_path / "2.0.4" + release_directory = releases_path / "2.0.5" release_directory.mkdir(parents=True) target = tmp_path / "elsewhere.zip" archive = builder.build_archive_bytes(source_commit="a" * 40) @@ -324,7 +324,7 @@ def test_installer_verifies_and_atomically_upgrades_with_rollback(tmp_path: Path assert result["action"] == "upgraded" assert result["source_commit"] == "a" * 40 - assert "version: 2.0.4" in (existing / "plugin.yaml").read_text(encoding="utf-8") + assert "version: 2.0.5" in (existing / "plugin.yaml").read_text(encoding="utf-8") rollback = Path(result["rollback"]) assert "version: 1.0.0" in (rollback / "plugin.yaml").read_text(encoding="utf-8") assert checkpoint.read_bytes() == b"content-free-checkpoint" @@ -382,7 +382,7 @@ def test_plugin_swap_restores_previous_version_when_hardening_fails( ) def fail_hardening(target: Path) -> None: - assert "version: 2.0.4" in (target / "plugin.yaml").read_text(encoding="utf-8") + assert "version: 2.0.5" in (target / "plugin.yaml").read_text(encoding="utf-8") raise OSError("permission hardening failed") monkeypatch.setattr(installer, "_harden_plugin_permissions", fail_hardening) @@ -392,7 +392,7 @@ def fail_hardening(target: Path) -> None: assert "version: 1.3.0" in (existing / "plugin.yaml").read_text(encoding="utf-8") failed = list((hermes_home / "plugins").glob("substrate_wiki.failed-*")) assert len(failed) == 1 - assert "version: 2.0.4" in (failed[0] / "plugin.yaml").read_text(encoding="utf-8") + assert "version: 2.0.5" in (failed[0] / "plugin.yaml").read_text(encoding="utf-8") def test_check_archive_preserves_sha_provenance_without_environment( @@ -588,7 +588,7 @@ def fail_checked_reload( systemctl_calls.append((command, kwargs)) if kwargs.get("check") is True: assert command == ("systemctl", "--user", "daemon-reload") - assert "version: 2.0.4" in (existing / "plugin.yaml").read_text( + assert "version: 2.0.5" in (existing / "plugin.yaml").read_text( encoding="utf-8" ) plugin_rollbacks = list( @@ -620,7 +620,7 @@ def fail_checked_reload( assert (existing / "v13-sentinel.txt").read_text(encoding="utf-8") == "prior plugin" failed_plugins = list((hermes_home / "plugins").glob("substrate_wiki.failed-*")) assert len(failed_plugins) == 1 - assert "version: 2.0.4" in (failed_plugins[0] / "plugin.yaml").read_text( + assert "version: 2.0.5" in (failed_plugins[0] / "plugin.yaml").read_text( encoding="utf-8" ) assert not list((hermes_home / "plugins").glob("substrate_wiki.rollback-*")) diff --git a/uv.lock b/uv.lock index ccc5b0b..af1a1ca 100644 --- a/uv.lock +++ b/uv.lock @@ -13,7 +13,7 @@ wheels = [ [[package]] name = "hermes-substrate-wiki" -version = "1.5.0" +version = "2.0.5" source = { editable = "." } [package.optional-dependencies]