personal memory agent
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

docs/code: scrub internal management-repo references

Drop references to internal sol pbc paths (`extro/cmo/...`,
`vpe/workspace/...`, `cpo/specs/...`, `cto/playbooks/...`) and to
internal tooling skill names from comments, docstrings, and design
docs. Where the substance is load-bearing, keep it and rephrase as
"sol pbc internal engineering notes" / "sol pbc internal brand canon."

Largest piece: docs/design/pairing.md drops ~13 parenthetical citations
to /home/jer/projects/extro/vpe/workspace/hop-solstone-pairing-wave-5-*
scope docs that public readers can't reach anyway. The decisions and
their rationales remain; only the unreachable internal-path citations
are removed. Local repo refs (`convey/...:NN`, `apps/...:NN`) are
preserved so the design doc stays useful in the public repo.

Public-repo housekeeping; no behavioral change.

+118 -122
+2 -2
AGENTS.md
··· 314 314 ## 13. Owner-facing copy: the system-anatomy canon 315 315 316 316 - **The trinity.** In owner-facing copy, name the system in canonical order: `solstone = observers + sol agent + journal`. 317 - - **The canon lives elsewhere.** The source of truth is `~/projects/extro/cmo/brand/system-anatomy.md` in the extro repo. The companion is `~/projects/extro/cmo/brand/voice-terminology.md` for owner / keeper / partner voices. 317 + - **The canon lives elsewhere.** The source of truth is sol pbc's internal brand canon (system anatomy + voice terminology guides). This repo's branded prose follows it; the canon itself is not vendored here. 318 318 - **Ban surveillance verbs in branded surfaces.** Never use "capture", "watch", "record", "monitor", "track", or "collect" in template copy, settings labels, error messages, onboarding text, or README / INSTALL prose. Prefer "observe alongside", "experience along with", or "take in what you take in". 319 319 - **`capture` is code-only.** Keep it in module names such as `observe/`, function names, OS subsystem identifiers such as `com.solstone.capture`, and internal architecture diagrams. That is intentional and aligned with the canon. 320 320 - **Name artifacts for owners, not pipelines.** In branded prose, say "raw media", "the originals", or "observations". Never say "raw captures" or "screen captures" in owner-facing strings. Code-side artifact names stay as-is. ··· 327 327 | Code surfaces | `capture` is fine in code, module names, function names, subsystem ids, and internal architecture docs. | 328 328 | Branded surfaces | `capture` is banned. Use owner-facing phrasing such as "observe alongside", "experience along with", "take in what you take in", "raw media", "the originals", or "observations". | 329 329 330 - Canon source of truth: `~/projects/extro/cmo/brand/system-anatomy.md`. 330 + Canon source of truth: sol pbc's internal brand canon (system-anatomy guide).
+1 -1
apps/support/portal.py
··· 4 4 """Welcome-mat client for support.solpbc.org. 5 5 6 6 Implements the full DPoP + self-signed access token auth flow per the 7 - welcome-mat spec and the extro-support SKILL.md interface contract. 7 + welcome-mat spec. 8 8 9 9 All cryptographic operations use the ``cryptography`` library (already a 10 10 solstone dependency). The keypair, access token, and cached TOS are
+1 -1
docs/APPS.md
··· 352 352 - **Natural-key dedup.** Read the existing output, compute a natural key per row (e.g., `(facet, event_day, title, start, end)` for facet events), skip rows already present, and append only the new ones. Use this when the output is append-only history and you want to preserve prior writes from other agents. 353 353 - **Atomic replace.** Recompute the full output, write it to a temp file, and rename into place. `atomic_write()` in `think/entities/core.py` is the established helper for text outputs; for JSONL, write the full set of lines to a tempfile and `os.replace()`. Use this when the hook owns the file end-to-end. 354 354 355 - (Retired 2026-04-18 Sprint 4.) An earlier `write_events_jsonl` hook in `think/hooks.py` opened facet-event logs in `"a"` mode with no dedup and doubled row counts on every `sol think --refresh` — see the 2026-04-17 layer-violations audit (V6) in the sol pbc internal extro repo (`vpe/workspace/solstone-layer-violations-audit.md`) for the full write-up. 355 + (Retired 2026-04-18 Sprint 4.) An earlier `write_events_jsonl` hook in `think/hooks.py` opened facet-event logs in `"a"` mode with no dedup and doubled row counts on every `sol think --refresh` — see the 2026-04-17 layer-violations audit (V6) tracked in sol pbc's internal engineering notes for the full write-up. 356 356 357 357 See `docs/coding-standards.md` L8/L9 for the broader principles. 358 358
+101 -103
docs/design/pairing.md
··· 2 2 3 3 ## 1. Summary 4 4 5 - Wave 5 adds a self-hosted iOS pairing flow to the existing Convey process: one JSON API blueprint at `/api/pairing/*` plus one owner-facing HTML blueprint at `/app/pairing/*`, both defined in `convey/pairing.py`. This follows the Wave 2 / Wave 3 root-blueprint pattern rather than adding an `apps/pairing/` package (`convey/voice.py:27-197`, `convey/push.py:24-127`, `convey/__init__.py:110-169`, `/home/jer/projects/extro/vpe/workspace/hop-solstone-pairing-wave-5-server.md:20-78`). 5 + Wave 5 adds a self-hosted iOS pairing flow to the existing Convey process: one JSON API blueprint at `/api/pairing/*` plus one owner-facing HTML blueprint at `/app/pairing/*`, both defined in `convey/pairing.py`. This follows the Wave 2 / Wave 3 root-blueprint pattern rather than adding an `apps/pairing/` package (`convey/voice.py:27-197`, `convey/push.py:24-127`, `convey/__init__.py:110-169`). 6 6 7 - The server mints one-time `ptk_...` pairing tokens in memory, accepts ssh-ed25519 public keys from the iOS client, stores paired devices in `journal/config/paired_devices.json`, and returns a one-time `dsk_...` bearer session key whose hash is the only value persisted at rest. The companion iOS contract is fixed: `solstone://pair?token=...&host=...`, `POST {host}/api/pairing/confirm`, then `Authorization: Bearer <session_key>` on subsequent API calls (`/home/jer/projects/extro/vpe/workspace/hop-solstone-pairing-wave-5-server.md:26-56`, `/home/jer/projects/extro/vpe/workspace/hop-solstone-swift-wave-5-client.md:115-137`). 7 + The server mints one-time `ptk_...` pairing tokens in memory, accepts ssh-ed25519 public keys from the iOS client, stores paired devices in `journal/config/paired_devices.json`, and returns a one-time `dsk_...` bearer session key whose hash is the only value persisted at rest. The companion iOS contract is fixed: `solstone://pair?token=...&host=...`, `POST {host}/api/pairing/confirm`, then `Authorization: Bearer <session_key>` on subsequent API calls. 8 8 9 9 The design keeps config defaults compatible with existing journals by using both a new `pairing` block in `think/journal_default.json` and in-code defaults in `think/pairing/config.py`, because `get_config()` does not merge defaults once `config/journal.json` exists (`think/journal_default.json:1-53`, `think/utils.py:557-588`, `tests/conftest.py:77-84`). It also promotes `cryptography` to a direct dependency: the repo already imports it directly in the link pairing surface, but `pyproject.toml` does not currently declare it (`apps/link/routes.py:33-35`, `pyproject.toml:32-93`). 10 10 ··· 35 35 #### `think/pairing/config.py` 36 36 37 37 - `def get_host_url() -> str:` 38 - Return the configured pairing host URL, or synthesize `http://localhost:<convey-port>` when `pairing.host_url` is null by reading the recorded Convey port and falling back to the installed default port `5015` (`think/utils.py:922-935`, `think/service.py:32-34`). 38 + Return the configured pairing host URL, or synthesize `http://localhost:<convey-port>` when `pairing.host_url` is null by reading the recorded Convey port and falling back to the installed default port `5015` (`think/utils.py:922-935`, `think/service.py:32-34`). 39 39 - `def get_token_ttl_seconds() -> int:` 40 - Return the configured token TTL, clamped to `60..3600`, defaulting to `600`. 40 + Return the configured token TTL, clamped to `60..3600`, defaulting to `600`. 41 41 - `def get_owner_identity() -> str:` 42 - Return `config.identity.preferred`, else `config.identity.name`, else `""` (`think/journal_default.json:2-15`). 42 + Return `config.identity.preferred`, else `config.identity.name`, else `""` (`think/journal_default.json:2-15`). 43 43 44 44 #### `think/pairing/tokens.py` 45 45 46 46 - `def create_token(*, ttl_seconds: int | None = None, now: int | None = None) -> PairingToken:` 47 - Mint a `ptk_...` token, insert it into the in-memory store, and return its issued/expires metadata. 47 + Mint a `ptk_...` token, insert it into the in-memory store, and return its issued/expires metadata. 48 48 - `def consume_token(token: str, *, now: int | None = None) -> PairingToken | None:` 49 - Atomically validate and consume a token; return `None` for missing, expired, or already-consumed tokens. 49 + Atomically validate and consume a token; return `None` for missing, expired, or already-consumed tokens. 50 50 - `def peek_token(token: str, *, now: int | None = None) -> PairingToken | None:` 51 - Return current token metadata without consuming it while still pruning expired entries. 51 + Return current token metadata without consuming it while still pruning expired entries. 52 52 - `def purge_expired_tokens(*, now: int | None = None) -> int:` 53 - Remove expired tokens and return the number purged. 53 + Remove expired tokens and return the number purged. 54 54 55 55 #### `think/pairing/keys.py` 56 56 57 57 - `def validate_public_key(public_key: str) -> str:` 58 - Parse and validate an ssh-ed25519 public key, reject any non-Ed25519 algorithm, and return the normalized string. 58 + Parse and validate an ssh-ed25519 public key, reject any non-Ed25519 algorithm, and return the normalized string. 59 59 - `def generate_session_key() -> str:` 60 - Mint a one-time `dsk_...` bearer session key for the paired device. 60 + Mint a one-time `dsk_...` bearer session key for the paired device. 61 61 - `def hash_session_key(session_key: str) -> str:` 62 - Return `sha256:<hex>` for storage and lookup. 62 + Return `sha256:<hex>` for storage and lookup. 63 63 - `def mask_session_key(session_key: str) -> str:` 64 - Return a log-safe mask showing only the last four characters and the total length. 64 + Return a log-safe mask showing only the last four characters and the total length. 65 65 66 66 #### `think/pairing/devices.py` 67 67 68 68 - `def load_devices() -> list[Device]:` 69 - Load and validate `paired_devices.json`, returning `[]` on missing or malformed stores with a warning. 69 + Load and validate `paired_devices.json`, returning `[]` on missing or malformed stores with a warning. 70 70 - `def find_device_by_id(device_id: str) -> Device | None:` 71 - Return one paired device by `id`, or `None`. 71 + Return one paired device by `id`, or `None`. 72 72 - `def find_device_by_session_key_hash(session_key_hash: str) -> Device | None:` 73 - Return one paired device matching a stored `session_key_hash`, or `None`. 73 + Return one paired device matching a stored `session_key_hash`, or `None`. 74 74 - `def register_device(*, name: str, platform: str, public_key: str, session_key_hash: str, bundle_id: str, app_version: str, paired_at: str | None = None) -> Device:` 75 - Create or update a device row keyed by `public_key`, generating a stable `dev_...` id on first registration and rotating the stored `session_key_hash` on re-pair. 75 + Create or update a device row keyed by `public_key`, generating a stable `dev_...` id on first registration and rotating the stored `session_key_hash` on re-pair. 76 76 - `def touch_last_seen(device_id: str, *, last_seen_at: str | None = None) -> bool:` 77 - Update `last_seen_at` for an existing paired device. 77 + Update `last_seen_at` for an existing paired device. 78 78 - `def remove_device(device_id: str) -> bool:` 79 - Remove one paired device by `id`. 79 + Remove one paired device by `id`. 80 80 - `def status_view(device: Device) -> dict[str, Any]:` 81 - Return the non-secret JSON view exposed by `GET /api/pairing/devices`. 81 + Return the non-secret JSON view exposed by `GET /api/pairing/devices`. 82 82 83 83 #### `convey/auth.py` 84 84 85 85 - `def extract_bearer_token() -> str | None:` 86 - Return the trimmed `Authorization: Bearer ...` token if present, else `None`. 86 + Return the trimmed `Authorization: Bearer ...` token if present, else `None`. 87 87 - `def resolve_paired_device() -> Device | None:` 88 - Hash the presented bearer token, load the matching paired device, and return it when valid. 88 + Hash the presented bearer token, load the matching paired device, and return it when valid. 89 89 - `def is_owner_authed() -> bool:` 90 - Return `True` when the current request already satisfies the owner checks used by `require_login()` without triggering redirects: session cookie, Basic Auth, or the completed-setup localhost bypass (`convey/root.py:49-57`, `81-139`). 90 + Return `True` when the current request already satisfies the owner checks used by `require_login()` without triggering redirects: session cookie, Basic Auth, or the completed-setup localhost bypass (`convey/root.py:49-57`, `81-139`). 91 91 - `def require_paired_device(f):` 92 - Decorator that resolves a paired-device bearer, stores it on `g.paired_device`, and returns `401` JSON when no valid paired-device bearer is present. 92 + Decorator that resolves a paired-device bearer, stores it on `g.paired_device`, and returns `401` JSON when no valid paired-device bearer is present. 93 93 94 94 ## 3. Flow diagrams 95 95 ··· 97 97 98 98 ```text 99 99 owner browser on /app/pairing/ 100 - -> POST /api/pairing/create (owner-auth via require_login) 101 - -> pairing config resolves host URL + TTL 102 - -> think.pairing.tokens.create_token(...) 103 - -> response includes token, expires_at, pairing_url, qr_data 104 - -> pairing.js renders QR client-side and starts countdown 100 + -> POST /api/pairing/create (owner-auth via require_login) 101 + -> pairing config resolves host URL + TTL 102 + -> think.pairing.tokens.create_token(...) 103 + -> response includes token, expires_at, pairing_url, qr_data 104 + -> pairing.js renders QR client-side and starts countdown 105 105 ``` 106 106 107 107 This mirrors the local-request-validation shape of `convey/voice.py` and `convey/push.py`, but the write target is an in-memory store rather than journal state (`convey/voice.py:30-53`, `convey/push.py:27-50`, `think/link/nonces.py:45-103`). ··· 110 110 111 111 ```text 112 112 iOS client 113 - -> POST /api/pairing/confirm 114 - -> allowlist bypasses require_login 115 - -> route validates JSON object + token/public_key/device metadata 116 - -> think.pairing.tokens.consume_token(token) 117 - -> think.pairing.keys.validate_public_key(public_key) 118 - -> think.pairing.keys.generate_session_key() + hash_session_key(...) 119 - -> think.pairing.devices.register_device(...) 120 - -> response returns session_key once, plus device_id/journal_root/owner_identity/server_version 113 + -> POST /api/pairing/confirm 114 + -> allowlist bypasses require_login 115 + -> route validates JSON object + token/public_key/device metadata 116 + -> think.pairing.tokens.consume_token(token) 117 + -> think.pairing.keys.validate_public_key(public_key) 118 + -> think.pairing.keys.generate_session_key() + hash_session_key(...) 119 + -> think.pairing.devices.register_device(...) 120 + -> response returns session_key once, plus device_id/journal_root/owner_identity/server_version 121 121 ``` 122 122 123 - The confirm flow deliberately follows the existing “token in header/body, then validate against a feature-owned store” pattern from observer ingest and journal-source ingest, but pairing consumes its own in-memory token and persists only the device ledger (`apps/observer/routes.py:524-538`, `apps/import/journal_sources.py:108-128`, `/home/jer/projects/extro/vpe/workspace/hop-solstone-swift-wave-5-client.md:115-124`). 123 + The confirm flow deliberately follows the existing “token in header/body, then validate against a feature-owned store” pattern from observer ingest and journal-source ingest, but pairing consumes its own in-memory token and persists only the device ledger (`apps/observer/routes.py:524-538`, `apps/import/journal_sources.py:108-128`). 124 124 125 125 ### 3.3 List / heartbeat (`GET /api/pairing/devices`, `POST /api/pairing/heartbeat`) 126 126 127 127 ```text 128 128 paired device 129 - -> Authorization: Bearer dsk_... 130 - -> list: resolve paired device or owner path, return non-secret device rows 131 - -> heartbeat: @require_paired_device resolves bearer and stores g.paired_device 132 - -> think.pairing.devices.touch_last_seen(g.paired_device["id"]) 129 + -> Authorization: Bearer dsk_... 130 + -> list: resolve paired device or owner path, return non-secret device rows 131 + -> heartbeat: @require_paired_device resolves bearer and stores g.paired_device 132 + -> think.pairing.devices.touch_last_seen(g.paired_device["id"]) 133 133 ``` 134 134 135 135 Heartbeat is bearer-only. List is mixed-auth: allowlist bypass at `require_login()`, then explicit handler-level acceptance of either a paired-device bearer or an already-owner-authenticated request. ··· 138 138 139 139 ```text 140 140 paired device OR owner browser 141 - -> allowlist bypasses require_login 142 - -> route resolves either bearer device or owner auth 143 - -> think.pairing.devices.remove_device(device_id) 144 - -> 200 {"unpaired": true} on success 141 + -> allowlist bypasses require_login 142 + -> route resolves either bearer device or owner auth 143 + -> think.pairing.devices.remove_device(device_id) 144 + -> 200 {"unpaired": true} on success 145 145 ``` 146 146 147 147 The storage rule is simple: unpair removes the row from `paired_devices.json` rather than soft-deleting it. That keeps the store authoritative for current pairings only, matching the existing push-device store style (`think/push/devices.py:76-124`). ··· 150 150 151 151 ```text 152 152 process restart 153 - -> in-memory token store is empty 154 - -> pending QR codes immediately stop working 155 - -> paired_devices.json persists 156 - -> existing dsk_... bearer tokens continue to resolve 153 + -> in-memory token store is empty 154 + -> pending QR codes immediately stop working 155 + -> paired_devices.json persists 156 + -> existing dsk_... bearer tokens continue to resolve 157 157 ``` 158 158 159 - This split is deliberate. The token store is ephemeral by scope; the device ledger is durable config (`/home/jer/projects/extro/vpe/workspace/hop-solstone-pairing-wave-5-server.md:32`, `147-148`). 159 + This split is deliberate. The token store is ephemeral by scope; the device ledger is durable config. 160 160 161 161 ## 4. Endpoint specs 162 162 ··· 200 200 201 201 ```json 202 202 "pairing": { 203 - "host_url": null, 204 - "token_ttl_seconds": 600 203 + "host_url": null, 204 + "token_ttl_seconds": 600 205 205 } 206 206 ``` 207 207 208 208 Key rules: 209 209 210 210 - `pairing.host_url` 211 - - Default in `journal_default.json`: `null`. 212 - - Runtime behavior: when null, synthesize `http://localhost:<convey-port>` using `read_service_port("convey")` and fall back to `DEFAULT_SERVICE_PORT = 5015` (`think/utils.py:922-935`, `think/service.py:32-34`). 213 - - Operator behavior: set this explicitly when Convey is exposed through a tunnel, reverse proxy, or non-localhost origin. 211 + - Default in `journal_default.json`: `null`. 212 + - Runtime behavior: when null, synthesize `http://localhost:<convey-port>` using `read_service_port("convey")` and fall back to `DEFAULT_SERVICE_PORT = 5015` (`think/utils.py:922-935`, `think/service.py:32-34`). 213 + - Operator behavior: set this explicitly when Convey is exposed through a tunnel, reverse proxy, or non-localhost origin. 214 214 - `pairing.token_ttl_seconds` 215 - - Default: `600`. 216 - - Runtime clamp: `60..3600`. 215 + - Default: `600`. 216 + - Runtime clamp: `60..3600`. 217 217 218 218 `owner_identity` does not need its own config key; it resolves from existing identity fields: `identity.preferred`, then `identity.name`, then `""` (`think/journal_default.json:2-15`). 219 219 ··· 221 221 222 222 ### 6.1 Token store 223 223 224 - Decision: use a module-level singleton store backed by `threading.Lock` and a plain dict. Rationale: the scope explicitly accepts restart-invalidated tokens, and the existing link nonce store shows the TTL + single-use semantics we need without implying journal persistence (`think/link/nonces.py:45-103`, `/home/jer/projects/extro/vpe/workspace/hop-solstone-pairing-wave-5-server.md:32`, `147-148`). 224 + Decision: use a module-level singleton store backed by `threading.Lock` and a plain dict. Rationale: the scope explicitly accepts restart-invalidated tokens, and the existing link nonce store shows the TTL + single-use semantics we need without implying journal persistence (`think/link/nonces.py:45-103`). 225 225 226 226 Token rules: 227 227 ··· 233 233 234 234 ### 6.2 Session-key crypto 235 235 236 - Decision: bearer session keys are `dsk_<urlsafe-base64-32-bytes>` and are stored only as `sha256:<hex>`. Rationale: this matches the scope’s one-time-return contract and keeps the journal ledger non-secret at rest (`/home/jer/projects/extro/vpe/workspace/hop-solstone-pairing-wave-5-server.md:41-54`, `147-148`). 236 + Decision: bearer session keys are `dsk_<urlsafe-base64-32-bytes>` and are stored only as `sha256:<hex>`. Rationale: this matches the scope’s one-time-return contract and keeps the journal ledger non-secret at rest. 237 237 238 238 ### 6.3 Public-key validation 239 239 240 - Decision: accept ssh-ed25519 only, with a `2048`-character cap on the incoming public-key string and a `128`-character cap on `device_name`. Rationale: the client contract already generates Ed25519 keys, and tight bounds keep the parser and logs safe (`/home/jer/projects/extro/vpe/workspace/hop-solstone-swift-wave-5-client.md:116-122`). 240 + Decision: accept ssh-ed25519 only, with a `2048`-character cap on the incoming public-key string and a `128`-character cap on `device_name`. Rationale: the client contract already generates Ed25519 keys, and tight bounds keep the parser and logs safe. 241 241 242 242 ### 6.4 `paired_devices.json` schema 243 243 ··· 245 245 246 246 ```json 247 247 { 248 - "devices": [ 249 - { 250 - "id": "dev_...", 251 - "name": "jer's iPhone 15 Pro", 252 - "platform": "ios", 253 - "public_key": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI...", 254 - "session_key_hash": "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", 255 - "bundle_id": "org.solpbc.solstone-swift", 256 - "app_version": "0.1.0", 257 - "paired_at": "2026-04-20T15:31:02Z", 258 - "last_seen_at": null 259 - } 260 - ] 248 + "devices": [ 249 + { 250 + "id": "dev_...", 251 + "name": "jer's iPhone 15 Pro", 252 + "platform": "ios", 253 + "public_key": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI...", 254 + "session_key_hash": "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", 255 + "bundle_id": "org.solpbc.solstone-swift", 256 + "app_version": "0.1.0", 257 + "paired_at": "2026-04-20T15:31:02Z", 258 + "last_seen_at": null 259 + } 260 + ] 261 261 } 262 262 ``` 263 263 ··· 270 270 271 271 ### 6.5 Auth chain 272 272 273 - Decision: ship a pairing-focused `convey/auth.py` now, but do not thread it into existing voice/push/observer routes. Rationale: Wave 5 needs the helper surface immediately, but the scope explicitly defers cross-route enforcement to Wave 5.1 (`/home/jer/projects/extro/vpe/workspace/hop-solstone-pairing-wave-5-server.md:66-72`, `134-148`). 273 + Decision: ship a pairing-focused `convey/auth.py` now, but do not thread it into existing voice/push/observer routes. Rationale: Wave 5 needs the helper surface immediately, but the scope explicitly defers cross-route enforcement to Wave 5.1. 274 274 275 275 Helper contract: 276 276 ··· 285 285 286 286 ### 6.7 QR generation 287 287 288 - Decision: vendor `qrcode-generator` version `1.4.4` under `convey/static/pairing-qr.js` and generate the SVG client-side from the `qr_data` field returned by `POST /api/pairing/create`. Rationale: the repo packaging only includes flat `static/*` assets, and this keeps the QR dependency browser-only, MIT-licensed, and separate from the page logic in `convey/static/pairing.js` (`pyproject.toml:110-118`, `convey/__init__.py:118-133`, `/home/jer/projects/extro/vpe/workspace/hop-solstone-pairing-wave-5-server.md:60-64`). 288 + Decision: vendor `qrcode-generator` version `1.4.4` under `convey/static/pairing-qr.js` and generate the SVG client-side from the `qr_data` field returned by `POST /api/pairing/create`. Rationale: the repo packaging only includes flat `static/*` assets, and this keeps the QR dependency browser-only, MIT-licensed, and separate from the page logic in `convey/static/pairing.js` (`pyproject.toml:110-118`, `convey/__init__.py:118-133`). 289 289 290 290 ### 6.8 Logging 291 291 292 - Decision: use `logging` only; never log raw `session_key`; log masked session keys as last four chars + total length; log public keys only at `DEBUG` with truncation. Rationale: the scope forbids raw secret leakage and pairing is explicitly bearer-token based (`/home/jer/projects/extro/vpe/workspace/hop-solstone-pairing-wave-5-server.md:147-148`). 292 + Decision: use `logging` only; never log raw `session_key`; log masked session keys as last four chars + total length; log public keys only at `DEBUG` with truncation. Rationale: the scope forbids raw secret leakage and pairing is explicitly bearer-token based. 293 293 294 294 ## 7. Domain write-ownership (L1-L9 declarations) 295 295 ··· 339 339 340 340 ### Token reuse and replay 341 341 342 - `consume_token()` is the only mutating read path and it enforces single-use + TTL. Tokens are not written to disk, so process restart invalidates all outstanding QR codes, which the scope explicitly accepts (`/home/jer/projects/extro/vpe/workspace/hop-solstone-pairing-wave-5-server.md:32`, `147-148`, `think/link/nonces.py:66-85`). 342 + `consume_token()` is the only mutating read path and it enforces single-use + TTL. Tokens are not written to disk, so process restart invalidates all outstanding QR codes, which the scope explicitly accepts (`think/link/nonces.py:66-85`). 343 343 344 344 ### Key validation and bounded input 345 345 346 - Only ssh-ed25519 public keys are accepted. Rejecting all other SSH algorithms aligns the server with the iOS client contract and keeps the parser surface narrow (`/home/jer/projects/extro/vpe/workspace/hop-solstone-swift-wave-5-client.md:116-122`). 346 + Only ssh-ed25519 public keys are accepted. Rejecting all other SSH algorithms aligns the server with the iOS client contract and keeps the parser surface narrow. 347 347 348 348 ### Log masking 349 349 350 - No raw `session_key` appears in logs, error messages, or `paired_devices.json`. Public keys are truncated when logged at `DEBUG`. This is stricter than the existing link pair route, which still echoes CSR parse failures in the JSON error body; pairing should not repeat that pattern (`apps/link/routes.py:230-235`, `/home/jer/projects/extro/vpe/workspace/hop-solstone-pairing-wave-5-server.md:147-148`). 350 + No raw `session_key` appears in logs, error messages, or `paired_devices.json`. Public keys are truncated when logged at `DEBUG`. This is stricter than the existing link pair route, which still echoes CSR parse failures in the JSON error body; pairing should not repeat that pattern (`apps/link/routes.py:230-235`). 351 351 352 352 ### Restart semantics 353 353 354 - Ephemeral pairing tokens disappear on restart; paired-device bearers remain valid because only their hashes are persisted. This split is acceptable at MVP scale and keeps the durable store strictly journal-config scoped (`/home/jer/projects/extro/vpe/workspace/hop-solstone-pairing-wave-5-server.md:32`, `41-54`, `147-148`). 354 + Ephemeral pairing tokens disappear on restart; paired-device bearers remain valid because only their hashes are persisted. This split is acceptable at MVP scale and keeps the durable store strictly journal-config scoped. 355 355 356 356 ### Wave 5.1 enforcement gap 357 357 ··· 375 375 PUBLIC_KEY=$(tr -d '\n' < "$KEY_PREFIX.pub") 376 376 377 377 CREATE_JSON=$(curl -u "$AUTH" \ 378 - -H 'Content-Type: application/json' \ 379 - -X POST "$BASE_URL/api/pairing/create" \ 380 - -d '{}') 378 + -H 'Content-Type: application/json' \ 379 + -X POST "$BASE_URL/api/pairing/create" \ 380 + -d '{}') 381 381 printf '%s\n' "$CREATE_JSON" 382 382 383 383 TOKEN=$(CREATE_JSON="$CREATE_JSON" python - <<'PY' ··· 387 387 ) 388 388 389 389 CONFIRM_JSON=$(curl \ 390 - -H 'Content-Type: application/json' \ 391 - -X POST "$BASE_URL/api/pairing/confirm" \ 392 - -d '{ 393 - "token": "'"$TOKEN"'", 394 - "public_key": "'"$PUBLIC_KEY"'", 395 - "device_name": "Pairing Smoke iPhone", 396 - "platform": "ios", 397 - "bundle_id": "org.solpbc.solstone-swift", 398 - "app_version": "0.1.0" 399 - }' \ 400 - "$BASE_URL/api/pairing/confirm") 390 + -H 'Content-Type: application/json' \ 391 + -X POST "$BASE_URL/api/pairing/confirm" \ 392 + -d '{ 393 + "token": "'"$TOKEN"'", 394 + "public_key": "'"$PUBLIC_KEY"'", 395 + "device_name": "Pairing Smoke iPhone", 396 + "platform": "ios", 397 + "bundle_id": "org.solpbc.solstone-swift", 398 + "app_version": "0.1.0" 399 + }' \ 400 + "$BASE_URL/api/pairing/confirm") 401 401 printf '%s\n' "$CONFIRM_JSON" 402 402 403 403 SESSION_KEY=$(CONFIRM_JSON="$CONFIRM_JSON" python - <<'PY' ··· 412 412 ) 413 413 414 414 curl -H "Authorization: Bearer $SESSION_KEY" \ 415 - "$BASE_URL/api/pairing/devices" 415 + "$BASE_URL/api/pairing/devices" 416 416 417 417 curl -H "Authorization: Bearer $SESSION_KEY" \ 418 - -H 'Content-Type: application/json' \ 419 - -X POST "$BASE_URL/api/pairing/heartbeat" \ 420 - -d '{}' 418 + -H 'Content-Type: application/json' \ 419 + -X POST "$BASE_URL/api/pairing/heartbeat" \ 420 + -d '{}' 421 421 422 422 curl -u "$AUTH" \ 423 - "$BASE_URL/api/pairing/devices" 423 + "$BASE_URL/api/pairing/devices" 424 424 425 425 curl -H "Authorization: Bearer $SESSION_KEY" \ 426 - -X DELETE "$BASE_URL/api/pairing/devices/$DEVICE_ID" 426 + -X DELETE "$BASE_URL/api/pairing/devices/$DEVICE_ID" 427 427 ``` 428 428 429 429 Basic Auth uses only the password component, so `-u ":$SOL_PASSWORD"` is the portable form for owner-auth pairing routes (`convey/root.py:49-57`, `docs/design/push.md:609-646`). ··· 446 446 447 447 ## 13. Sources 448 448 449 - - Wave 5 server scope: `/home/jer/projects/extro/vpe/workspace/hop-solstone-pairing-wave-5-server.md:1-161` 450 - - Wave 5 iOS companion scope: `/home/jer/projects/extro/vpe/workspace/hop-solstone-swift-wave-5-client.md:112-137` 451 449 - Root blueprint pattern: `convey/voice.py:27-197`, `convey/push.py:24-127`, `convey/__init__.py:110-169` 452 450 - Current auth gate and owner auth semantics: `convey/root.py:30-57`, `81-139` 453 451 - Existing Bearer-auth prior art: `apps/observer/routes.py:63-70`, `503-538`, `apps/import/journal_sources.py:108-128`
+2 -3
scripts/check_layer_hygiene.py
··· 16 16 17 17 By design this is a grep-level check with known false-positive surface. Known 18 18 audit-tracked violations are allowlisted below with a TODO and an audit 19 - reference. An allowlist entry is expected to disappear once its bundle ships — 20 - see ``vpe/workspace/solstone-layer-violations-audit.md`` in the sol pbc 21 - internal extro repo for the canonical list (V1-V14). 19 + reference (V1-V14, tracked in sol pbc's internal engineering notes). An 20 + allowlist entry is expected to disappear once its bundle ships. 22 21 23 22 Exit codes: 24 23 0 — no un-tracked violations
-1
tests/test_journal_index.py
··· 1766 1766 1767 1767 assert snap_before == snap_between == snap_after, ( 1768 1768 "scan_journal() mutated journal/entities/ — see " 1769 - "vpe/workspace/plan-bundle-a-entity-write-ownership.md and " 1770 1769 "docs/coding-standards.md § L6" 1771 1770 )
+2 -2
tests/verify_browser.py
··· 243 243 profile_dir = Path.home() / ".pinchtab" / "profiles" / "default" 244 244 if profile_dir.exists(): 245 245 # Clear cached default profile for deterministic runs — pinchtab persists 246 - # cookies/storage across sessions. extro-linkedin uses a separate profile 247 - # (~/.pinchtab/profiles/linkedin/) so this nuke is isolated to test state. 246 + # cookies/storage across sessions. Other tools that share pinchtab use 247 + # their own named profiles, so this nuke is isolated to test state. 248 248 try: 249 249 shutil.rmtree(profile_dir) 250 250 except OSError as exc:
+4 -4
think/policies/cogitate.toml
··· 10 10 # run_shell_command deny (priority 100). User-tier rules here override the 11 11 # engine's built-in yolo catch-all. 12 12 # 13 - # Rationale: vpe/workspace/gemini-cli-tool-hallucination-research.md — plan 14 - # mode strips run_shell_command from the tool registry, which caused the 15 - # tool-name hallucination loop we saw in cortex. Scoped yolo keeps the 16 - # registry intact without widening the blast radius to direct writes. 13 + # Rationale: plan mode strips run_shell_command from the tool registry, 14 + # which caused a tool-name hallucination loop we saw in earlier cortex 15 + # prototypes. Scoped yolo keeps the registry intact without widening the 16 + # blast radius to direct writes. 17 17 18 18 [[rule]] 19 19 toolName = ["write_file", "replace"]
+5 -5
think/providers/google.py
··· 760 760 # - Read-only cogitate talents run yolo + a scoped policy: full tool 761 761 # registry (no plan-mode stripping), but write_file / replace denied 762 762 # and run_shell_command narrowed to `sol` invocations. 763 - # Plan mode strips run_shell_command from the registry, which drove the 764 - # tool-name hallucination loop documented in 765 - # vpe/workspace/gemini-cli-tool-hallucination-research.md. Deprecated 766 - # --allowed-tools controls auto-approval, not availability, so it can't 767 - # replace the policy file for this purpose. 763 + # Plan mode strips run_shell_command from the registry, which drove a 764 + # tool-name hallucination loop in earlier prototypes (documented in sol 765 + # pbc's internal engineering notes). The deprecated --allowed-tools 766 + # flag controls auto-approval, not availability, so it can't replace 767 + # the policy file for this purpose. 768 768 cmd = [ 769 769 "gemini", 770 770 "-p",