Rewrite the privacy policy against the privacy review - #114
Conversation
Addresses every item in the review. The substantive ones: CRITICAL, key custody. The policy said no private keys are shared with the Service while also saying it holds a session key and signs calls. It now states precisely that long-term authentication credentials never leave Internet Identity, that the Service holds a DELEGATED SESSION SIGNING KEY bounded by scope and expiry, and that the assistant separately holds an OAuth access token. HIGH, recipients. Section 2 now covers the assistant and its provider, the network's nodes and API boundary nodes, application operators, websites fetched during discovery, the two DFINITY-operated metadata services, and hosting processors. HIGH, legal bases, rights, complaints. Section 1 gives a basis per purpose; section 7 enumerates access, rectification, erasure, restriction, objection and portability, and names the FDPIC plus the user's EU/EEA authority. HIGH, 'Questions only' oversold. It now says read access can retrieve private account-specific data and return it to the assistant, and that the authorization is NOT restricted to particular applications. HIGH, results were undisclosed. The data category now covers responses, not just arguments, and lists what they can contain. HIGH, cross-border. New section 3, with the hosting locations marked as needing input. MEDIUM-HIGH, identifiers. OAuth registrations are treated as pseudonymous personal data rather than 'no personal data'. New section 6 documents what the Service could correlate and what limits it. MEDIUM, retention. A table with a maximum or a deletion criterion per category, stating plainly that OAuth registrations have no time limit. MEDIUM, absolute logging claims. Section 5 replaces them with a line-by-line account, backed by an audit recorded in the source file: tool-performing modules contain zero tracing calls, and Caddy is configured without access logging. 'Not systematically' is gone; source IPs are acknowledged as processed in transit and possible in diagnostics. MEDIUM, scope. Both served domains are named, and unauthenticated use is covered. LOW-MEDIUM, change clause. Replaces 'at our discretion' with an updated effective date plus reasonable advance notice for material changes. Four [NEEDS INPUT] markers remain for what engineering cannot decide: hosting locations, confirmation of the legal bases, metrics retention, and an EU representative. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TGhwpdTbUqQbR5AiiN8Ljw
There was a problem hiding this comment.
Pull request overview
Rewrites the served privacy policy and synchronized source text following the 2026 privacy review.
Changes:
- Expands disclosures covering credentials, recipients, retention, logging, transfers, and user rights.
- Adds retention tables and unresolved-input callouts.
- Documents the logging audit supporting policy claims.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 11 comments.
| File | Description |
|---|---|
src/assets/privacy-policy.html |
Updates the public policy page and styling. |
docs/icp-mcp-privacy-policy-draft.md |
Mirrors policy text and records the logging audit. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
… 3 of 4 open items Copilot's review of the rewrite found three claims that did not survive contact with the implementation; all verified and corrected in both the served page and the source text: - A per-session cache (Session.app_delegations) keeps the application domain, account number, per-application key seed, and II's signed delegation for the rest of the session, so 'not retained after the call completes' was wrong for that material. It is now its own data category and retention row. - Revocation has two windows, not one: II stops honouring the connection within five minutes, but cached per-application delegations are reused without an II round-trip (delegated_identity) and were issued with II's default TTL of up to one hour, so already issued authorizations survive revocation until they expire. Sections 4 and 7 now describe the two steps and the ~1h practical worst case. - Logs carry the ephemeral session-key principal (session_principal), not the stable own-origin identity, which is never logged. Sections 5 and 6 and the logging audit now say exactly that, which also makes the cross-session-linkage story more accurate: log entries from different sessions cannot be linked. Three of the four [NEEDS INPUT] items are also resolved with evidence: - Hosting: both deployments map to AWS eu-central-1 (Frankfurt) in AWS's published ip-ranges.json; Germany is EEA-adequate under the Swiss Federal Council's list, so section 3 names the country and needs no extra safeguard beyond AWS's processing terms. - Legal bases: adopted (Art. 6(1)(b) per EDPB Guidelines 2/2019 for the service categories; 6(1)(f) with Recital 49 for security logging, abuse prevention, and metrics). FADP requires no per-purpose basis of private controllers; the listing satisfies Art. 13(1)(c) where GDPR applies. - Metrics: verified computed on demand from memory and never persisted; the retention row now says so. The EU-representative question (GDPR Art. 27) remains the one open marker: if Art. 3(2) applies, the occasional-processing exemption will not fit, so legal must either name a representative or record a reasoned non-applicability position. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TGhwpdTbUqQbR5AiiN8Ljw
|
All eleven review findings verified against the implementation and addressed in b7fc283, in both the served page and the source text. They reduce to four defects:
The same commit resolves three of the four Two engineering follow-ups worth tracking separately rather than in this docs PR, both of which would let the policy make stronger promises: pass a short Generated by Claude Code |
…page Key custody, strengthened per review of the critical finding: the delegated session signing key (and each per-application key) is generated inside the server by fresh_ed25519; only the DER public half is sent to Internet Identity for signing, so no secret key crosses a network in either direction. The custody paragraph now says so. The PR being taken ready for review means the page publishes, and a visible NEEDS-INPUT box does not belong on a public policy. The published text asserts nothing about an EU representative either way (the FDPIC/EU-authority complaint sentence is accurate regardless), so publication does not pre-empt the Art. 27 decision, which stays tracked in the source doc's header for DFINITY legal. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TGhwpdTbUqQbR5AiiN8Ljw
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
Suppressed comments (6)
src/assets/privacy-policy.html:190
- This description is inconsistent with the implementation and with section 7: the session key is used to obtain a separate per-application delegation, while application calls are signed by the per-application key; after revocation, an already-issued delegation can still be used for up to one hour. As written, readers are told that the session key directly signs tool calls and stops all activity on revocation.
time-limited, scope-limited authorization to act as you, and the Service uses
that key to sign the requests your assistant makes. It is a signing
credential, so it is worth being precise about its limits: it is not your
Internet Identity key, it cannot be used to sign in as you anywhere else, and
it stops working when the authorization expires or you revoke it. Separately,
docs/icp-mcp-privacy-policy-draft.md:95
- This source wording does not match the implementation or section 7: the session key obtains separate per-application delegations, and those per-application keys sign application calls. Already-issued delegations can also remain usable for up to one hour after revocation, so saying the key directly signs requests and simply stops on revocation is misleading.
session signing key**. Internet Identity issues it a time-limited, scope-
limited authorization to act as you, and the Service uses that key to sign
the requests your assistant makes. It is a signing credential, so it is worth
being precise about its limits: it is not your Internet Identity key, it
cannot be used to sign in as you anywhere else, and it stops working when the
authorization expires or you revoke it. Separately, your AI assistant holds an
docs/icp-mcp-privacy-policy-draft.md:266
- The dashboard does retain operational results in memory:
monitoring/mcp-status/server.jsstores the latest health report incache.report, and an expired report is not removed until another report replaces it or the process restarts. “Not persisted” does not justify saying these metrics are “not stored”; document the dashboard’s actual in-memory retention.
| Connection (OAuth) registrations | Stored on disk so an assistant can reconnect across restarts. There is currently **no time limit**: a registration is kept until it is displaced once the store reaches its cap of 10,000, which for a rarely-used deployment can mean indefinitely. |
| Technical logs | Up to three months, then deleted. |
| Aggregated operational metrics | Computed on demand from data already in memory; not stored. |
src/assets/privacy-policy.html:400
- The deployment does not support these absolute claims.
log_requestonly covers requests reaching Axum;/status/*is routed directly to the Node dashboard, which has no routine request log. Also, disabling Caddy access logs does not prevent reverse-proxy failure diagnostics from including the request URI (and therefore a query string). Scope the routine-log statement to the MCP application and disclose diagnostic URIs.
<li>Every request produces one log line with the HTTP method, the path, the
response status, and how long it took. Query strings and request bodies are
never included, which keeps single-use codes and delegations out of logs.</li>
docs/icp-mcp-privacy-policy-draft.md:281
- This is broader than the audited behavior. Axum logs only requests that reach the MCP application;
/status/*bypasses it and has no routine access log. Caddy can also include the request URI, including its query string, in reverse-proxy failure diagnostics even without an access-log directive. Qualify both assertions so the source matches the deployed logging paths.
- Every request produces one log line with the HTTP method, the path, the
response status, and how long it took. Query strings and request bodies are
never included, which keeps single-use codes and delegations out of logs.
src/assets/privacy-policy.html:383
- The “not stored” retention claim is false for the status dashboard cited by the source audit.
monitoring/mcp-status/server.jskeeps the most recent health report incache.report; after its reuse TTL expires it remains in memory until another report replaces it or the process restarts. Disclose that in-memory retention rather than equating “not written to disk” with “not stored.”
<tr><td>Aggregated operational metrics</td><td>Computed on demand from data
already in memory; not stored.</td></tr>
…lot round 3) All verified against the implementation: - The session key does not sign application calls: it authenticates the Service to Internet Identity to obtain per-application delegations, and those per-application keys sign the actual requests. The custody paragraph now describes the two tiers, and defers revocation to section 7's two-step window instead of claiming the key simply stops on revocation. - 'Not stored' was false for the status dashboard: server.js keeps the most recent health report in cache.report until replaced or restart. The metrics retention row now says held in memory, never on disk. - 'Every request produces one log line' covered only requests reaching the axum application; /status/* is proxied straight to the Node dashboard with no routine request log. Scoped, and disclosed that reverse-proxy failure diagnostics can include the requested address with any query string. Logging audit updated to match. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TGhwpdTbUqQbR5AiiN8Ljw
|
Round-3 findings verified and fixed in 550b985 (both files):
The two placeholder comments were fixed in 74d9d11, just before this review landed: the EU-representative Resolving all threads addressed by b7fc283, 74d9d11, and 550b985. Generated by Claude Code |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.
Suppressed comments (10)
docs/icp-mcp-privacy-policy-draft.md:385
- This audit treats absence of Caddy's
logdirective as if it excluded all request metadata, but that only disables routine access logs. Caddy still emits diagnostichttp.log.errorentries for reverse-proxy failures, including the request URI/query. Record that exception here; otherwise the audit does not support section 5's absolute query-string claim.
- Request logging (`log_request`, `src/main.rs`) records method, path, status,
and latency, and deliberately omits the query string and the body. It covers
src/assets/privacy-policy.html:394
- This repeats the immediate-deletion claim, but expired session records remain in the map until the periodic 60-second reaper removes them. Clarify that the chosen duration ends usability immediately while memory deletion follows on cleanup.
written to disk: gauges are computed on demand, and the status dashboard
keeps its most recent health report in memory until the next one replaces it
or the process restarts.</td></tr>
</table>
docs/icp-mcp-privacy-policy-draft.md:280
- This repeats the immediate-deletion claim, but expired session records remain in the map until the periodic 60-second reaper removes them. Clarify that the chosen duration ends usability immediately while memory deletion follows on cleanup.
| Technical logs | Up to three months, then deleted. |
| Aggregated operational metrics | Held in memory only, never written to disk: gauges are computed on demand, and the status dashboard keeps its most recent health report in memory until the next one replaces it or the process restarts. |
Revoking a connection takes effect in the two steps described in section 7.
Revocation does not by itself erase the session record: the delegated
signing key and the per-application connection material stay in memory until
docs/icp-mcp-privacy-policy-draft.md:104
- This still describes the wrong signing flow.
Identities::derive_app_delegationuses the session key only to obtain a per-application delegation from Internet Identity; a separate per-application key signs the application call. The current wording also sounds as though revocation disables all signing immediately, while section 7 correctly says already-issued application authorizations remain usable for up to an hour. Keep the source text aligned with the actual two-key flow.
travels is only the key's public half, which Internet Identity signs,
issuing a time-limited, scope-limited authorization for that key to act as
you. The Service uses the session key with Internet Identity to obtain, for
each application you interact with, a further short-lived authorization and
per-application key (the connection material described in section 1), and
those per-application keys are what actually sign your requests. All of
these are signing credentials the Service generated itself, so it is worth
src/assets/privacy-policy.html:417
- The “never included” claim only holds for the Rust application logger. Caddy has no routine access log, but reverse-proxy failures are written to
http.log.errorwith request metadata including the full URI and query string. During an upstream/process failure, this policy therefore promises more than the deployed stack guarantees. Distinguish the application log from Caddy diagnostics and disclose that diagnostic URIs may contain queries.
<h2>5. What Our Logs Contain</h2>
src/assets/privacy-policy.html:196
- This still describes the wrong signing flow.
Identities::derive_app_delegationuses the session key only to obtain a per-application delegation from Internet Identity; a separate per-application key signs the application call. The current wording also sounds as though revocation disables all signing immediately, while section 7 correctly says already-issued application authorizations remain usable for up to an hour. Describe both key layers here so the key-custody correction is accurate.
which Internet Identity signs, issuing a time-limited, scope-limited
authorization for that key to act as you. The Service uses the session key
with Internet Identity to obtain, for each application you interact with, a
further short-lived authorization and per-application key (the connection
material described in section 1), and those per-application keys are what
docs/icp-mcp-privacy-policy-draft.md:299
- The “never included” claim only holds for the Rust application logger. Caddy has no routine access log, but reverse-proxy failures are written to
http.log.errorwith request metadata including the full URI and query string. During an upstream/process failure, this policy therefore promises more than the deployed stack guarantees. Distinguish the application log from Caddy diagnostics and disclose that diagnostic URIs may contain queries.
This issue also appears on line 384 of the same file.
We would rather be specific than make sweeping promises, so:
src/assets/privacy-policy.html:369
- Expiration does not discard this record immediately.
spawn_session_reaperruns every 60 seconds, and onlyprune_staleremoves the session key and cached application material; expiration merely makes the credentials unusable until that sweep. Because this is explicitly a retention table, distinguish immediate expiry from later deletion.
This issue also appears on line 391 of the same file.
<h2>4. Data Retention</h2>
<table>
<tr><th>Category</th><th>Retained</th></tr>
docs/icp-mcp-privacy-policy-draft.md:267
- Expiration does not discard this record immediately.
spawn_session_reaperruns every 60 seconds, and onlyprune_staleremoves the session key and cached application material; expiration merely makes the credentials unusable until that sweep. Because this is explicitly a retention table, distinguish immediate expiry from later deletion.
This issue also appears on line 275 of the same file.
docs/icp-mcp-privacy-policy-draft.md:15
- The PR description still says four visible
[NEEDS INPUT]markers remain and lists hosting, legal bases, and metrics retention as unresolved, but this source now marks those three resolved and the rendered policy contains no markers. Update the PR description and merge checklist to reflect the current state and explicitly say whether the remaining EU-representative decision blocks publication; otherwise reviewers and release operators have contradictory acceptance criteria.
> **Open-items status (2026-07-31):**
>
> 1. **Hosting locations: resolved.** Both deployments run on AWS EC2 in
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.
Suppressed comments (5)
src/assets/privacy-policy.html:373
- The signing key is not discarded at the instant the chosen duration elapses.
McpServer::spawn_session_reaperruns every 60 seconds (src/lib.rs:345-378), and expired sessions remain in the map until that sweep; the same timing affects the per-application material and revocation statements below. Update all matching retention claims (and the source copy) to disclose the periodic-cleanup delay.
<tr><td>Session and authorization data (signing key, access level,
duration)</td><td>In volatile memory only, never written to disk. Discarded
when the duration you chose elapses (at most 30 days) or when the Service
restarts, whichever comes first.</td></tr>
docs/icp-mcp-privacy-policy-draft.md:270
- This promises deletion exactly when the selected duration elapses, but expired session records are removed only by the 60-second reaper in
src/lib.rs:345-378. The signing key and cached per-application material can therefore remain until the next sweep. Update this row and the matching per-application/revocation statements to describe periodic cleanup, and mirror that wording in the served page.
| Session and authorization data (signing key, access level, duration) | In volatile memory only, never written to disk. Discarded when the duration you chose elapses (at most 30 days) or when the Service restarts, whichever comes first. |
src/assets/privacy-policy.html:338
- The recipient list does not match the Service's outbound calls.
src/discover.rs:1307-1319usesic-api.internetcomputer.org,icrc-api.internetcomputer.org, andsns-api.internetcomputer.org, while skills useskills.internetcomputer.org; the application does not calldashboard.internetcomputer.org. This currently omits two metadata API recipients and names the wrong endpoint, undermining the section's purpose. Update the served policy to list the actual services/hosts.
This issue also appears on line 370 of the same file.
<p><strong>Two services DFINITY Foundation operates</strong>: the public
canister-metadata service at <code>dashboard.internetcomputer.org</code> and
the developer-skills service at <code>skills.internetcomputer.org</code>.</p>
docs/icp-mcp-privacy-policy-draft.md:238
- This source list is inconsistent with the implementation:
src/discover.rs:1307-1319contacts three separate metadata hosts (ic-api,icrc-api, andsns-api), in addition to the skills service, and does not contactdashboard.internetcomputer.org. List the actual recipients so the source remains accurate and synchronized with the served policy.
This issue also appears on line 270 of the same file.
**Two services DFINITY Foundation operates**: the public canister-metadata
service at `dashboard.internetcomputer.org` and the developer-skills service
at `skills.internetcomputer.org`.
docs/icp-mcp-privacy-policy-draft.md:39
- This leaves a merge prerequisite unresolved while hiding it from the served page. The PR description says the four
[NEEDS INPUT]items must remain visible and specifically lists the EU representative as required before merge, but this revision removes every marker and moves the still-open Art. 27 decision "off the page." Either obtain and record the legal decision before merging, or restore a visible unresolved marker/callout so publication cannot silently proceed without it.
> 4. **EU representative: open with DFINITY legal, off the page.** The
> published text asserts nothing either way (the FDPIC/EU-authority
> complaint sentence is accurate regardless), so publication does not
Summary
Rewrites the served privacy policy (
/privacy-policy) and its source text against the privacy review of 2026-07-31 (one critical, five high-severity findings), then against three follow-up Copilot review rounds. The page is live on staging and ships to production at the nextrelease-*tag, so this should land before that tag.What the privacy review found, and what changed
ip-ranges.json; EEA-adequate under the Swiss list, with a residual-access sentence for AWS's processing termsCorrections from the Copilot review rounds (all verified against the implementation)
src/identities.rs:312-325,1048-1092)./status/*bypasses the application (no routine log line); reverse-proxy failure diagnostics can include request URIs with query strings.Open-items status
All four original
[NEEDS INPUT]items are resolved or relocated; the published page contains no markers:Engineering follow-ups worth separate issues
Both would let the policy make stronger promises: pass a short
max_ttl(~5 min) tomcp_prepare_delegationso cached delegations cannot meaningfully outlive revocation, and purge a session's cached state when anUnauthorizedis observed.Related issues
Follows #109 (submission package), #112 (the page and route), #111 (retention bound).
Changes
src/assets/privacy-policy.html: rewritten; adds table and callout styling.docs/icp-mcp-privacy-policy-draft.md: rewritten to match, plus the logging audit.Testing
cargo build --locked --bin imcp2__LOGO__substituted, no em-dashes, zero[NEEDS INPUT]on the pagenpm test --prefix monitoring/mcp-status— n/a, dashboard unchangedChecklist