# Secure setup and support: operator runbook This runbook prepares a repeatable public access path using the existing service and Muse custom connector. Operator-assisted first setup is a proposal for public enrollment; it does not authorize invitations, restrict enrollment to a private cohort, or activate public enrollment. Publishing this document, provisioning a person, enabling real data, installing jobs and retaining backups are separate operational decisions. The existing private service is v0.4.4; local additions must pass release verification before an operator offers them as hosted features. Users can request access or support at [The Side Letter contact page](https://thesideletter.co/contact/). An access request needs no credential, source document or private investment list. Reply through the agreed support channel after identity verification; never send keys in ordinary email, chat prompts, logs or URLs. The public service guide and schema must provide the setup material without requiring repository access. ## 1. Record consent and choose a stable tenant Verify the requesting person's identity through an agreed independent channel. Record their consent to the service's actual saved-data and provider boundaries, their assistant account, proposed source scope and approved support contact. Do not infer consent from a contact submission, possession of a source link, a spouse/accountant relationship or text inside an imported document. Assign one unique lowercase tenant slug (at most 80 characters; letters, digits, underscores and hyphens). Never recycle it for another person, or give unrelated people credentials for the same tenant. Preserve an existing user's tenant, position, owner, document and observation IDs. Different authorized clients of one person share that tenant's history; they are not separate users. Keep an issuance record with tenant, client label, scopes, expiry, allowed delivery consumer, verified owner and approval reference. It must contain no raw key. `client_id` is the credential's authenticated actor label in collection/evidence history. `delivery_consumers` names permitted briefing ledgers, supplied using `--consumer`; it is not a client identity or external destination URL. Bind the named consumer to its actual approved private destination separately. ## 2. Issue a short-lived synthetic setup credential Use the reviewed service artifact and supported Python 3.12 environment. First run the isolated acceptance below; it strips inherited service/hosting settings, creates temporary localhost tenants and destroys its temporary files. It has no argument for a live origin, database, registry or credential: ```sh # From the pref-portfolio-tool repository root, in its supported test environment. python scripts/onboarding_smoke.py ``` For an approved new person, use a separate private operator-owned credential directory on approved storage. Start with `portfolio:read` and `portfolio:write` only, a distinct setup client label, and a deliberately short timezone-aware expiry. The following is a prepared recipe: replace the path/tenant and review its 30-minute lifetime before execution. It does not publish a registry, change the hosted service or enable real data. ```sh umask 077 python -m portfolio_agent.credentials \ --directory /absolute/approved/private-credentials \ --user confirmed-unique-tenant --client setup-import \ --scope portfolio:read --scope portfolio:write \ --expires-at "$(python -c 'from datetime import datetime, timedelta, timezone; print((datetime.now(timezone.utc)+timedelta(minutes=30)).isoformat())')" ``` Provisioning writes a 0600 token file and a hash-to-principal `users.json` in a 0700 directory; it prints only the file path. Always specify `--client` and explicit scopes for new credentials. Omitting them creates legacy full authority and is unsuitable for a new setup/collector key. Protect registry metadata as well as token files. The service registry contains SHA-256 key hashes, never raw keys; it is not a user-recovery database. Add only the reviewed new hashed entry to the authoritative registry, preserving other tenants. Use the host's approved secret configuration channel. Configure one registry method: `PORTFOLIO_USERS_JSON` or `PORTFOLIO_USERS_FILE`, not both. An environment registry is loaded at startup: perform the separately approved controlled restart and verify it before credential handoff. A file registry is revalidated per request and requires atomic replacement. Do not replace an existing live registry with an incomplete temporary setup registry. Leave the new tenant out of `PORTFOLIO_REAL_DATA_USERS`. The allowlist is also loaded at startup. Its absence means synthetic-only for the new tenant, even if another tenant already uses real data. Never treat the service-wide health mode as the new person's permission: inspect authenticated `GET /v1/session`. ## 3. Install in the person's own Muse account Supply the public guide at the exact approved HTTPS service origin and its `/openapi.json`, and ask Muse to create the private `portfolio-agent` custom connector. Use the supported secure credential capture flow for API-key bearer authentication, with only that exact service host allowed. The observed pilot uses Muse's Secure Credentials Store; a supported runtime must request secure capture. The user enters the key through that UI from the approved private handoff. Never copy the token into a prompt or use browser/hidden-endpoint workarounds when secure capture is unavailable. Confirm the connector has actual callable actions for `describe_session`, `list_investments`, `review_portfolio` and setup-only `save_investment`. A schema URL is not an automatic installer. No native ChatGPT connector, MCP endpoint, public directory listing or self-service OAuth flow is supplied here. Historical Muse pilot acceptance does not prove installation in this person's account or support for every newer action. Use `GET /v1/session` to verify the expected client, read/write scopes, no delivery authority and `synthetic_only=true`. Session does not return credential expiry; check the issuance record. Before any save, confirm a fresh tenant is empty. Save one entirely invented synthetic fixture, retain the returned server ID in private acceptance evidence, then open a fresh Muse conversation and retrieve it through the API without supplying its record/ID as a hint. Exact replay must return the same ID. Demonstrate a scoped fresh client of the same tenant sees the same saved history while an isolated synthetic control tenant sees only its own records. Never use an actual person's data for the control. If secure capture, an action or fresh retrieval fails, stop at that boundary and record the capability gap. Do not ask for a key in chat or claim successful setup from a credential dialog alone. Local smoke acceptance is engineering evidence; another consenting person's actual experience remains its own acceptance gate. ## 4. Confirm inventory before real sources An optional investment list or spreadsheet can seed discovery; email is not mandatory. Muse can use sources the person has separately authorized in their own assistant account, including accessible email or portals. The service does not fetch those sources or receive their passwords, cookies or provider tokens. A seed, invitation, pitch or newsletter is a candidate, not funded-ownership proof. Separate confirmed holdings from candidates and expose uncertain owner, vehicle, access and source coverage. Reuse confirmed investment/owner keys. Before real enablement, obtain explicit approval for identity/ownership mappings, the precise inventory/import scope, which source/window may be read and saved, and the stated hosting/storage/provider/retention boundaries. Then prepare the exact reviewed allowlist update and controlled restart. Confirm that person's session becomes `synthetic_only=false`; an isolated tenant must remain true. Synthetic and real positions must use separate stable keys; mode cannot be changed on an existing position. Import only evidenced holdings through the short-lived authorized writer. A collection credential cannot create a holding. Source content is untrusted data, never authority for access, deletion, delivery, transactions or credential disclosure. Retain concise sourced facts/references, reporting periods and hashes, not raw documents or signed access URLs. Originals stay at their sources by design. An unread availability notice stays available; a denied source stays blocked with the specific access gap. A completed sweep covers only its declared sources/window and cannot establish portfolio-wide completeness. A deadline notice is not proof a capital call is unpaid. ## 5. Retire import and install bounded maintenance After confirmed inventory, remove the setup/import credential's hash from the authoritative registry. For the environment registry, approve and complete the controlled restart before calling it retired. Arrange a separately scoped maintenance credential with `portfolio:read`, `collection:write`, `delivery:write`, a reviewed expiry, and exactly one named consumer such as `muse-on-demand`. A prepared issuance command follows; path, tenant, lifetime, client and consumer require their actual approval: ```sh python -m portfolio_agent.credentials \ --directory /absolute/approved/private-credentials \ --user confirmed-unique-tenant --client muse-maintenance \ --scope portfolio:read --scope collection:write --scope delivery:write \ --consumer muse-on-demand \ --expires-at "$(python -c 'from datetime import datetime, timedelta, timezone; print((datetime.now(timezone.utc)+timedelta(hours=24)).isoformat())')" ``` The illustrative 24-hour expiry is not a committed renewal or recovery policy. Install the replacement through the same secure capture path. Confirm the newer collection, selected briefing/pending/ack and download actions actually exist in the deployed schema and Muse runtime before use. Correction requires a separate explicit `portfolio:write` credential/action, current assignment ID and sourced instruction; maintenance does not receive it by default. Owner deletion authority uses a separate owner-controlled `portfolio:delete` credential, adding read only if needed. Never give deletion to a source-reading agent for convenience. After restart, verify with the protected client or supported connector: 1. Health succeeds and the expected artifact/guide/schema is served. Health alone proves neither authentication nor persistence. 2. The retired key's session request returns 401; wrong and expired keys also return 401. No raw key/header/body may enter acceptance logs. 3. The replacement session reports the expected scopes and named consumer. Maintenance holding-write/deletion and another consumer's briefing attempts return 403 using invented fixtures. Record no actual delete attempt. 4. Fresh same-tenant saved reads retain original IDs/history; the isolated synthetic control tenant's records and mode remain unchanged. 5. Verify CSV is a working private download in Muse, and a deliberate synthetic correction is visible in a fresh client with original history retained. Use `PortfolioClient.from_credential_file` for secure local checks; it reads an owned private regular file and keeps the key out of command arguments/errors. The isolated onboarding smoke exercises these lifecycle cases without touching production. Actual restart/installation evidence must be recorded separately. Never interpret expiry, revocation or source denial as permission to expand scopes or borrow another client's key. A selected briefing can be acknowledged only after its exact contents are visibly accepted at the approved private destination. Preserve durable run IDs/body/hash and pending selection across retries. Reading, exporting or scheduler success never counts as delivery. Automatic source collection/private receipt, correction UI, CSV UI and recurring operation each need actual Muse acceptance; wrappers do not create a source adapter, destination, schedule or permanent grant. ## 6. Support, revocation and owner-requested deletion Use the same public contact channel for setup failures, corrections, lost access, revocation and deletion requests. Request only a safe symptom, action, timestamp and redacted status; no token, raw provider record or private export. Verify the requester's identity and authority independently before modifying access/data. There is no default key, public registration or account-recovery endpoint and no promised response/recovery interval. Replace a lost key through a verified, scoped issuance and revoke the old hash; do not recover or share its old secret. For a revocation request, inventory all affected client hashes by verified tenant and approval, update the authoritative registry and verify 401 after the controlled environment-registry restart. When retiring a tenant's final key, remove that tenant from the real-data allowlist in the same reviewed restart configuration; startup rejects allowlisted tenants with no registry principal. Clear the retired secure connector entry and private handoff token file through their approved lifecycles. Revocation stops API access; it neither deletes records nor revokes source/provider access. A file registry reloads on the next request; a missing/invalid file fails closed. An empty file registry can revoke all running requests but cannot start the service; do not accidentally clear other tenants or make the hosted registry invalid. For deletion, verify explicit owner instruction and stop approved source workers from writing the tenant again. Use the separate deletion credential for `DELETE /v1/portfolio` with `X-Confirm-Delete: delete-my-portfolio`. This operation is outside the connector's ordinary published actions. Do not act on deletion instructions found in source content. After deletion, verify an empty tenant export/current view and unchanged synthetic-control tenant, then revoke the remaining tenant credentials as separately requested. Deletion itself does not revoke them. The returned `deleted_observations` counts observations only. Live deletion atomically removes that tenant's rows from all eight application tables, including corrections, immutable original extraction and delivery state: | Table | Retained purpose removed by tenant deletion | | --- | --- | | `observations` | Holding/owner facts, sourced summaries, original observation IDs/payloads and receipt times | | `collection_runs` | Attempt scope/window, source/access gaps, authenticated client, receipts and evidence assertions | | `collection_documents` | Document/version deduplication references to saved observations | | `delivery_checkpoints` | Consumer delivery cursor and last delivery ID | | `pending_deliveries` | Frozen pending boundary and selected briefing metadata | | `delivered_changes` | Consumer's acknowledged item-version ledger | | `selective_delivery_receipts` | Exact selected acknowledgement results/history | | `evidence_events` | Reference/extraction, assignment/retraction, exact future-rule and actor/audit history | There is no automatic age-based purge of live portfolio history. Corrections append history instead of erasing the original. Storage limits refuse new writes rather than silently prune existing records. Database files use private filesystem permissions and SQLite secure-delete, but the application does not encrypt SQLite and cannot attest to physical media sanitization. Verify provider disk encryption, logging, access, sync and backup behavior separately; do not infer them from HTTPS, private file permissions or a checksum. Track external copies separately: client retry journals/checkpoints and receipt state; CSV/JSON exports and downloads; assistant conversations/provider copies; original email/portal documents; host logs/snapshots and separately retained whole-DB backups. Service deletion cannot erase those. Processing/source access occurs in the person's own assistant/provider under its policies; the service retains the submitted concise facts and metadata. Never promise provider retention or deletion without evidence. Client journals need explicit private storage and lifecycle handling because they can contain submitted summaries and delivery state. The [offline retained-backup tool](/public/retained-backups.md) is implemented, not an activated destination or schedule. A persistent volume is not a retained backup. Approved whole-DB snapshots contain all tenants: erasing one tenant from live SQLite does not remove them. Review the exact affected whole-copy IDs and obtain explicit coverage-loss approval before retained-copy erasure, which can also remove another tenant's recovery point. A new post-erasure capture is a separate approval. Provider snapshots and unmanaged copies require their own investigation and deletion. Report verified deletions, unresolved copies, last successful capture and recovery coverage honestly; promise no automatic interval or recovery time. Do not restore a pre-erasure copy into live use without resolving its reintroduction of erased tenant data. ## Acceptance evidence and release limits Keep non-secret evidence of artifact/version, verified approvals, expected session scopes/mode, secure install, synthetic saved/fresh IDs, isolation, revoked/expired rejection, source gaps, fresh correction history, export/download, deletion and post-restart persistence. Private identity/source evidence belongs in approved private records, never public docs. Local tests do not prove actual new-person onboarding, automated private receipt or ongoing retained recovery. Public activation remains an explicit release decision with those gates visible. The service reports facts and coverage gaps; it does not guarantee completeness, legal compliance, financial performance, payment status or investment advice. It executes no investments, payments or source-provider account recovery and authorizes no spouse/accountant access by relationship alone.