frozen: true step: KN-DOCS-SYNC-a model: "Thinking (thinking-high)" date: 2026-08-17 branch: feat/kn-docs-sync-a status: thinking-freeze-2026-08-17 supersedes: "Freezes Knowtation-owned live Google Drive (then Notion) connectors. Reuses Calendar 1D OAuth primitives (PKCE S256, AES-256-GCM vault, hosted blob strong-consistency). Does not invent a Scooling vault, collect a provider key in Scooling, flip Scooling /connect Drive to Ready, or change the existing Hub folder gdrive importer. This freeze does not authorize KN-DOCS-SYNC-b Auto until freeze-review pass." evidence: "Scooling PRIMARY 2026-08-16 (ROADMAP F3i-kn; OVERSEER-HANDOVER NEXT). CONNECT-CATALOG-b BV pass sha256:6372219bce3ac96a10fe76db176ecf78cd0ffb6165178bb1d2265796845ef2e9. MEDIA-LIVE-READS smoke PASS. Hub lib/importers/gdrive.mjs is a Markdown-folder importer. Hub lib/importers/notion.mjs reads a Hub-only env name. Calendar 1D is the OAuth pattern."
KN-DOCS-SYNC — Knowtation-owned live Drive (then Notion) connector
Ground truth for KN-DOCS-SYNC-b Auto. Downstream Auto may treat this document as ground truth without re-deriving. It does not flip Scooling /connect Drive or Notion to Ready, collect a provider key in Scooling, invent a second vault, send Hub source_type gdrive or notion from Scooling, flip SCOOLING_MEDIA_*, invent Live on Patterns / Models / Studio / threads, open presence or pairing, or work Parentier.org.
phase: KN-DOCS-SYNC-a
outputs:
- id: kn-docs-sync
path: docs/KN-DOCS-SYNC-FREEZE.md
frozen: true
notes: Drive readonly OAuth + encrypted refresh-material vault in Knowtation only; list/import as notes through Review-before-write; optional sync_cursor; Notion stays Hub-key. No Scooling Ready flip in this tip.
frozen_inputs:
- id: connect-and-open-range
path: ~/scooling/docs/CONNECT-AND-OPEN-RANGE.md
notes: KN-DOCS-SYNC steps; Scooling never collects the key; Drive honestly disabled until this lands
- id: connect-catalog
path: ~/scooling/docs/reviews/2026-08-16-connect-catalog.md
notes: Wave 2 freeze; Drive/Notion stay not_wired; denied Hub types include gdrive and notion
- id: connect-catalog-bv
path: ~/scooling/docs/reviews/2026-08-16-connect-catalog-bv-round1-pass.md
notes: CONNECT-CATALOG-b BV pass; digest sha256:6372219bce3ac96a10fe76db176ecf78cd0ffb6165178bb1d2265796845ef2e9
- id: bring-in-connect
path: ~/scooling/docs/reviews/2026-08-16-bring-in-connect.md
notes: docs_drive_live and docs_notion not_wired; calendar-1D pattern named for KN-DOCS-SYNC
- id: calendar-1d
path: docs/CALENDAR-OAUTH-CONNECTOR-1D-SPEC.md
notes: Confidential web client + PKCE S256; encrypted vault; redirect allowlist; Scooling never holds material
- id: gdrive-folder-importer
path: lib/importers/gdrive.mjs
notes: Folder of Markdown only — not Drive API. Unchanged by this Auto.
- id: notion-hub-importer
path: lib/importers/notion.mjs
notes: Hub-only env name; CLI / POST import path. Live hosted path is new Review-before-write routes.
- id: oauth-pkce
path: lib/companion-oauth-pkce.mjs
notes: S256-only; constant-time state; fail-closed. Reuse; do not fork a second PKCE core.
- id: calendar-token-vault
path: lib/calendar/oauth-token-vault.mjs
notes: AES-256-GCM + scrypt primitive to copy. Do not write Drive blobs into calendar_oauth.
- id: calendar-blob
path: hub/bridge/calendar-blob-store.mjs
notes: Hosted strong-consistency hydrate for pending OAuth state. Copy the pattern into a docs blob store.
- id: proposals-store
path: hub/proposals-store.mjs
notes: createProposal is the Review-before-write write path for imported notes.
- id: import-source-types
path: lib/import-source-types.mjs
notes: gdrive and notion remain import types. Live Drive does not POST those types from Scooling.
- id: scooling-roadmap-f3i-kn
path: ~/scooling/docs/ROADMAP.md
notes: F3i-kn owns this build; CONNECT-DRIVE-READY is a later Scooling Auto
review_stamp:
reviewed_at: '2026-08-17T11:13:54Z'
verdict: pass
reviewer_mode: agent
reviewer_model: thinking-high
reviewer_provider: local
kit_version: 0.1.0
artifact_digest: sha256:cebd3d9d53f1c7dadcad2a5c7a09fbd280678f957fb66b1d364997622d623d96
downstream:
- id: KN-DOCS-SYNC-b
model: Auto
consumes_as_ground_truth: true
notes: Implement Drive OAuth + vault + list + Review import + optional sync_cursor + Notion Hub-key list/import (inert gates). Starts only after freeze-review pass. Auto does not edit Scooling. Auto does not flip compile-time gates to true.
- id: CONNECT-DRIVE-READY
model: Auto
consumes_as_ground_truth: true
notes: Later Scooling tip. Flip /connect Drive (then Notion) only after this Auto lands and Drive gate is operator-authorized. Not this Knowtation tip.
tier3_gates:
- T1 Muse main or muse-mirror to GitHub main (SD-14) outside SD-21 land hygiene
- T2 Setting DOCS_OAUTH_GOOGLE_AUTHORIZED or DOCS_NOTION_HUB_KEY_AUTHORIZED to the enabled literal in source or live env
- T3 Writing Google Drive client credentials or the docs vault wrapping secret into git
- T4 Pasting a Notion integration value into Scooling, a Scooling env, or a Scooling adapter
- T5 Flipping Scooling /connect docs_drive_live or docs_notion off not_wired (CONNECT-DRIVE-READY)
- T6 Any SCOOLING_MEDIA, SCOOLING_STUDIO, or SCOOLING_TASK_LOOP enabled assign
- T7 Live Google or Notion network from CI
- T8 Feature to GitHub main / non-muse-mirror head
Auto must not build until freeze review pass. This Thinking tip does not implement routes. This Thinking tip does not flip any env.
Review record
| Round | Reviewer | Verdict | Resolution |
|---|---|---|---|
| 0 | Thinking (this session) | draft | Freeze authored from CONNECT-AND-OPEN-RANGE + CONNECT-CATALOG-b BV pass + Calendar 1D + Hub gdrive/notion importers |
| 1 | Freeze-review loop (thinking) | findings | R1-F1–F5 fixed below. CLI dry-run was already pass. |
| 2 | Freeze-review loop (thinking) + ok review --freeze |
pass | R1 holds. No new cited findings. CLI C checklist clean. Cleared for KN-DOCS-SYNC-b Auto. Auto must not edit Scooling, flip gates, or writeNote on live routes. No human escalation. |
Round 1 findings (cited — file+line)
| ID | Sev | Cat | Citation | Finding | Fix |
|---|---|---|---|---|---|
| R1-F1 | MAJOR | completeness | docs/KN-DOCS-SYNC-FREEZE.md:160 (prior D10) | D10 said Hub-key only but did not say the existing env is process-wide. Auto could invent per-vault Notion material. | D10 now locks process-wide env, no per-connector copy, connected = env present. |
| R1-F2 | MAJOR | security | docs/KN-DOCS-SYNC-FREEZE.md:191 (prior q name-contains) |
List q was “name contains, ≤ 128” with no charset lock — Drive query injection. |
q MUST match ^[A-Za-z0-9 ._-]{1,128}$ or 400; no raw q= build. Security tier covers it. |
| R1-F3 | MINOR | consistency | docs/KN-DOCS-SYNC-FREEZE.md:227 (prior) | auto_approvable is not a hub/proposals-store.mjs field (createProposal at :204-215 has no such column). |
Omitted. Admission lock is no T5 fingerprint + human approve. |
| R1-F4 | MINOR | completeness | docs/KN-DOCS-SYNC-FREEZE.md:166 (prior D16) | Per-file cap only; CONNECT-CATALOG batch cap 80_000_000 unbound. |
D16 adds 80_000_000 bytes per import batch. |
| R1-F5 | MINOR | completeness | docs/KN-DOCS-SYNC-FREEZE.md §12 (prior) | “Hub UI or Scooling” in §4.3 left Hub chrome in Auto scope. | §12 excludes Hub wizard chrome; REST + tests only. |
Citation discipline
Every freeze-review finding MUST cite file+line (OVERSEER-KIT-SPEC §6). Do not
trust uncited review output. HTTP routes in this doc omit the leading slash
(api/v1/…) so the freeze mechanical gate does not treat them as absolute machine
paths. Cross-repo paths use ~/scooling/…. Never leading-slash absolute paths.
0. Job (lock)
People can already bring downloaded files into Knowtation from Scooling /connect after Review. Live Google Drive and Notion are honestly off on that page. This freeze writes the Knowtation-owned connector so a later Scooling tip can turn the Drive card on without ever holding a key.
Knowtation owns the Google consent, the stored refresh material, the file list, and the note proposals. Scooling may later start consent and show status only. Scooling never collects the key.
1. What stays frozen (do not reopen)
| Lock | Source | This Auto |
|---|---|---|
| Knowtation is the only canonical store | CROSS-REPO-COORDINATION; ADAPTER-CONTRACTS | Unchanged |
| Review-before-write for durable notes | PROPOSAL-LIFECYCLE; BRING-IN-CONNECT §5.4 | Holds — live import creates proposals; apply writes notes |
Hub folder gdrive importer |
lib/importers/gdrive.mjs |
Unchanged — still a folder of Markdown, not Drive API |
Hub notion CLI / POST import |
lib/importers/notion.mjs |
Unchanged for admin CLI / POST import. Live hosted path is new routes |
| Scooling denied Hub types | CONNECT-CATALOG §6.2 | Holds — Scooling still never sends gdrive or notion |
Scooling /connect Drive / Notion cards |
BRING-IN-CONNECT §2.3 | Stay not_wired — CONNECT-DRIVE-READY is a later Scooling Auto |
| Calendar 1D OAuth | docs/CALENDAR-OAUTH-CONNECTOR-1D-SPEC.md |
Reuse primitives — do not share the calendar vault directory or calendar client id |
Media connector id gdrive |
MEDIA-WRITE-SURFACES (opaque ref, no fetch) | Unrelated — do not reuse media import-consent for this path |
| MEDIA hosted reads | MEDIA-LIVE-READS smoke PASS | Do not flip SCOOLING_MEDIA_* |
| Sheets / chat inbox / Slack workplace | CONNECT-AND-OPEN-RANGE | Out of scope |
| Eight venue words / Home chat | SITE-FINISH / BRING-IN-CONNECT | Unchanged |
2. Hub ground truth (verified this session — do not re-derive)
| Fact | Citation | Consequence |
|---|---|---|
Hub gdrive is a folder of .md files |
lib/importers/gdrive.mjs:1-24 |
Live Drive is a new connector, not an extension of POST import gdrive |
Hub notion reads a Hub-only env name and writes notes directly |
lib/importers/notion.mjs:20-66 |
Live Notion uses the same Hub-only env name and a new Review path |
IMPORT_SOURCE_TYPES includes gdrive and notion |
lib/import-source-types.mjs:7-30 |
Those strings stay for folder / CLI import. Scooling still must not send them |
Calendar OAuth is confidential web + PKCE S256, vault under calendar_oauth |
docs/CALENDAR-OAUTH-CONNECTOR-1D-SPEC.md:39-44; lib/calendar/oauth-token-vault.mjs:79-83 |
Copy the primitive; new directory docs_oauth |
| Calendar hosted callback needs strong blob consistency | hub/bridge/calendar-blob-store.mjs:7-11 |
Docs OAuth pending state uses the same strong-consistency rule |
Scooling Drive / Notion cards are not_wired |
~/scooling/docs/reviews/2026-08-16-bring-in-connect.md:193-201 |
This tip does not flip them |
| CONNECT-CATALOG-b BV pass | ~/scooling/docs/reviews/2026-08-16-connect-catalog-bv-round1-pass.md:13 |
Wave 2 file cards are done; live Drive was out of that Auto |
| Scooling never stores a sync cursor | ~/scooling/docs/reviews/2026-08-16-bring-in-connect.md:213 |
sync_cursor lives in Knowtation only |
3. Design decisions (frozen)
| # | Decision | Frozen value | Rationale |
|---|---|---|---|
| D1 | OAuth client type (Drive) | Confidential web application + PKCE S256 | Same as Calendar 1D D1. Client credential stays on the Knowtation server. |
| D2 | Drive scopes | openid and https://www.googleapis.com/auth/drive.readonly only |
Read-only Drive + stable sub. No email / profile. No write scope. No drive.file (too narrow for folder list). No Sheets scope. |
| D3 | Refresh-material acquisition | access_type=offline and prompt=consent |
Background list / sync without re-login. |
| D4 | Vault | AES-256-GCM + scrypt, copied from lib/calendar/oauth-token-vault.mjs, stored under data/docs_oauth/{connector_id}.enc |
Do not write into calendar_oauth. Separate wrapping secret. Access material is in-memory only during a list / import / sync run. |
| D5 | Client registration | Separate Google Cloud client from Calendar | Drive scopes are Restricted; blast radius and verification stay separate. |
| D6 | Note write path | createProposal then approve / apply |
Review-before-write. Live path MUST NOT call writeNote or POST import. |
| D7 | Frontmatter source | Live Drive notes use source: google-drive and source_id = Drive file id |
Distinct from folder-import source: gdrive. Dedup by (source, source_id). |
| D8 | Revoke vs notes | Revoke deletes vault blob + Google revoke (best-effort). Vault notes stay | Notes are reviewed canonical content. Calendar events were copies in a calendar store; that purge rule does not apply. |
| D9 | Sync cursor | Optional opaque sync_cursor on the connector (Drive Changes startPageToken / newStartPageToken) |
Knowtation-only. Sync creates proposals for new or changed allowlisted files; never silent note writes. 410 → full re-list. |
| D10 | Notion auth | Hub-key only (existing process-wide env name). No Notion OAuth. No per-vault or per-connector copy of that value | CONNECT-AND-OPEN-RANGE: Notion stays Hub-key; never pasted into Scooling. Hosted Hub already has one integration value. connected means the env is present. List/import sees only pages that integration can access. Per-user Notion login is a later freeze. |
| D11 | Google verification posture | v0 in Google testing mode with allowlisted test users | drive.readonly is Restricted. Public launch is a later operator / legal gate. |
| D12 | Compile-time gates | DOCS_OAUTH_GOOGLE_AUTHORIZED and DOCS_NOTION_HUB_KEY_AUTHORIZED hard-coded false in source |
Operator Tier 3 to enable. Tests inject authorizedOverride. |
| D13 | Scooling role | Initiate + status only (later CONNECT-DRIVE-READY) | Scooling never holds refresh material, never calls Google or Notion, never stores sync_cursor. |
| D14 | Auto b DONE bar | Drive connector + Notion Hub-key connector both land inert | Drive is the first live flip. Notion code ships gated false so Scooling does not need a second Knowtation freeze. |
| D15 | MIME allowlist (Drive) | Google Docs, text/markdown, text/plain, application/pdf, Word docx |
Sheets, Slides, folders-as-notes, audio, video, and other MIME → unsupported_mime. |
| D16 | Caps | 20 file ids per import; 25_000_000 bytes per file; 80_000_000 bytes per import batch; list page size 50; sync rate-limit 60s per connector | Aligns with CONNECT-CATALOG / BRING-IN-CONNECT file caps. |
| D17 | Hosted persistence | New docs blob store, strong consistency for pending OAuth state | Same failure that calendar fixed (callback on a different lambda). |
4. WHAT — surfaces
4.1 Providers
provider |
Auth | This Auto |
|---|---|---|
google-drive |
Google OAuth (D1–D5) | Required |
notion |
Hub-key (D10) | Required, inert until T2 |
No Microsoft, Slack, Discord, Telegram, WhatsApp, Sheets, or chat-inbox provider.
4.2 Routes (self-hosted Hub + hosted gateway → bridge)
All routes except the Google callback sit behind existing JWT + vault access (same middleware class as api/v1/calendar/...). Gate off → 501 { code: 'NOT_AUTHORIZED' } and no network, no vault I/O.
| Method / route | Role | Request | Response (no secrets) |
|---|---|---|---|
POST api/v1/docs/connectors |
editor, admin | { provider, display_name?, return_url? } |
Drive: { connector_id, authorization_url, expires_at }. Notion: { connector_id, status } (connected if Hub-key present, else needs_reauth) |
GET api/v1/docs/connectors/callback |
state-authenticated | code + state query |
302 to allowlisted return_url. Failure 302 return_url with connect=error and reason enum only |
GET api/v1/docs/connectors |
viewer+ | — | { schema: 'knowtation.docs_connectors/v0', connectors:[{ connector_id, provider, display_name, status, last_sync_at, last_sync_error, file_count }] } |
GET api/v1/docs/connectors/:id/files |
viewer+ | page_token?, q? |
{ files:[{ file_id, name, mime, modified, size, importable }], next_page_token? } — no bodies, no emails, no owners. q MUST match ^[A-Za-z0-9 ._-]{1,128}$ or the route returns 400 BAD_REQUEST (no Drive q= string built from raw input). |
POST api/v1/docs/connectors/:id/import |
editor, admin | { file_ids: string[] } |
{ proposed, skipped, proposal_ids } — creates proposals only |
POST api/v1/docs/connectors/:id/sync |
editor, admin | — | { proposed, skipped, last_sync_at } — optional cursor; rate-limited ≥60s |
DELETE api/v1/docs/connectors/:id |
editor, admin | — | { revoked: true } — blob gone; notes kept |
status enum: pending, connected, needs_reauth, revoked.
last_sync_error enum only: auth_expired, rate_limited, provider_error, network_error, none. No provider text.
Notion POST ignore return_url. Notion has no callback route. Notion sync uses Notion next_cursor stored as sync_cursor.
4.3 OAuth flow (Drive only)
Later Scooling (CONNECT-DRIVE-READY) or Hub UI
POST api/v1/docs/connectors { provider: 'google-drive', return_url }
Knowtation: PENDING connector; PKCE S256 + state (CSPRNG, TTL 10m,
bound to vault_id + connector_id + return_url); persist verifier+state server-side
→ { connector_id, authorization_url, expires_at }
Browser opens authorization_url
Google consent (readonly + offline)
GET api/v1/docs/connectors/callback (code + state)
validate state (constant-time, single-use, not expired)
exchange code + code_verifier + client credential at Google (server TLS)
encrypt refresh material → oauth_ref; discard plaintext; access material in memory
store account_sub (OpenID sub, never email)
302 allowlisted return_url (no code, no material in the redirect)
Redirect URI and return_url are exact-match allowlisted (env names in §8). Open redirects fail closed.
4.4 List → Review import (both providers)
- List returns metadata only (
importabletrue iff MIME / Notion type is allowlisted). - Import accepts 1–20 ids. Unknown / not importable / oversize / empty convert →
skippedwith bounded reason (unsupported_mime,too_large,not_found,already_pending,empty_extract). - Server fetches content (Drive export / download, or Notion markdown). Converts to one Markdown note per id.
- Server calls
createProposalwithsource: import,review_queue: docs-sync, andintenta fixed prefix plus the untrusted file name (truncated ≤ 128). Do not invent anauto_approvablefield on the note store (hub/proposals-store.mjshas no such column). The admission lock is §4.4 step 9. - Path:
imports/google-drive/{safe_id}.mdorimports/notion/{safe_id}.md.safe_idis the provider id with non[A-Za-z0-9_-]stripped, ≤ 64 chars. - Frontmatter (Drive):
source: google-drive,source_id,connector_id,imported_at. Notion:source: notion,source_id(page id),connector_id,imported_at. - If a live note already exists with the same (
source,source_id), create an edit proposal withbase_state_id— do not mint a second path. - Approve / apply uses the existing note-proposal apply path (self-hosted
createProposal+ apply; hosted gateway / canister propose + apply-approved). Auto MUST NOT add a new after-approve hook. docs-syncproposals are not admitted to personal self-apply. Do not add a T5 fingerprint. Human approve only.
File / page name and body are untrusted. Existing proposal injection labels apply. Never interpret name as a command.
4.5 Optional sync
POST …/sync is optional. When sync_cursor is absent, perform a first page list and store the new cursor. When present, Drive uses Changes list; Notion uses search / database iterate with next_cursor. New or changed allowlisted items become proposals (same as import). Unchanged ids are no-ops. Sync never writes notes directly. Concurrent sync on the same connector → 429 RATE_LIMITED.
4.6 Revoke
- Drive: decrypt
oauth_ref; POST Google revoke (best-effort; network failure does not block local purge). Notion: no remote revoke API required. - Delete the encrypted blob (Drive) / clear Hub-key handle from the connector record (Notion never copies the Hub env into the record).
- Set
revoked_at,status: revoked; keep a material-free tombstone. - Do not delete imported notes.
invalid_grant on Drive refresh → needs_reauth, last_sync_error: auth_expired, stop sync. Notes stay. Hard DELETE is required to drop the connector.
5. HOW — modules and data
5.1 New modules (suggested paths)
| Path | Role |
|---|---|
lib/docs/oauth-token-vault.mjs |
Copy calendar vault primitive; path root docs_oauth; connector id ^conn_[A-Za-z0-9_-]{8,64}$ |
lib/docs/google-drive-connector.mjs |
PKCE begin, callback, list, export/download, sync, revoke. Injected Google client in tests |
lib/docs/google-drive-normalizer.mjs |
Pure MIME → markdown / skip. No network |
lib/docs/notion-hub-connector.mjs |
List + fetch using Hub env name; no OAuth |
lib/docs/docs-connector-store.mjs |
Connector records (no secrets); sync_cursor; tombstones |
lib/docs/docs-import-propose.mjs |
Build proposal input; dedup; caps |
hub/bridge/docs-blob-store.mjs |
Hosted hydrate/persist; strong consistency for pending OAuth |
hub/bridge/docs-routes.mjs |
Bridge route registration (mirror calendar) |
Gateway mounts the same path prefixes and proxies to the bridge (mirror hub/gateway/server.mjs calendar block). Self-hosted hub/server.mjs mounts the same routes.
5.2 Connector record (client-visible)
| Field | Client? | Notes |
|---|---|---|
connector_id |
yes | conn_… |
provider |
yes | google-drive | notion |
display_name |
yes | Untrusted; ≤ 128 |
status |
yes | enum §4.2 |
account_sub |
no | Drive OpenID sub only; never email |
oauth_ref |
no | Opaque handle to the enc blob |
sync_cursor |
no | Opaque provider cursor |
last_sync_at |
yes | ISO8601 or null |
last_sync_error |
yes | enum |
file_count |
yes | Last successful list count (0 if none) |
revoked_at |
yes | ISO8601 or null |
Vault payload (Drive, never returned): refresh material, granted scope string, token type, obtained_at, account_sub. Client id / client credential are env only, never written to the blob.
5.3 Drive conversion
| MIME | Action |
|---|---|
application/vnd.google-apps.document |
files.export as text/markdown; if empty, text/plain |
text/markdown |
download bytes as UTF-8 body |
text/plain |
download bytes as UTF-8 body |
application/pdf |
download bytes; convert with a new exported pdfBytesToMarkdown(buf) extracted from lib/importers/pdf.mjs (unpdf + normalizeExtractedText). CLI importPdf keeps writeNote. Live path MUST NOT call importPdf / writeNote. Empty or image-only extract → skip empty_extract |
application/vnd.openxmlformats-officedocument.wordprocessingml.document |
download bytes; convert with a new exported docxBytesToMarkdown(buf) extracted from lib/importers/docx.mjs (mammoth + normalizeMarkdownBody). CLI importDocx keeps writeNote. Live path MUST NOT call importDocx / writeNote. Empty convert → skip empty_extract |
application/vnd.google-apps.folder |
list children only; not importable as a note |
| other | importable: false, skip unsupported_mime |
Google Docs export failure → skip provider_error for that id; do not fail the whole batch.
5.4 Notion conversion
Reuse the Hub markdown fetch already in lib/importers/notion.mjs as a called helper or extracted function, but the live route MUST go through docs-import-propose (proposals), not writeNote. List uses Notion search (pages the integration can see). Databases are listed as containers; import targets pages. Skip unsupported block types by omitting them (do not fail the page).
The Hub env name stays server-side. Connector records store no copy of that value.
6. Security invariants (enforced, not documented-only)
- No secret egress. Refresh material, client credential, authorization codes,
code_verifier,state, Hub Notion value, andsync_cursornever appear in responses, redirects, logs, error messages, provenance, projections, MCP resources, or Scooling adapters. - State / CSRF.
stateis CSPRNG, single-use, TTL 10m, bound tovault_id+connector_id+return_url, compared constant-time vialib/companion-oauth-pkce.mjs. Replay, tamper, expiry → deny. - PKCE S256 only.
plainrejected everywhere. - Redirect allowlist. Exact match; no open redirect.
- Read-only Drive scopes (D2). Any write scope is a later freeze.
- Scooling never collects the key. No Scooling env, form field, adapter argument, or localStorage for Google or Notion material.
- Untrusted content. Names and bodies are untrusted prompt content.
- Least data. List omits owners, permissions, emails, sharing ACLs.
- Server-only provider calls. Agents never call Google or Notion. MCP may list connector status (token-free) but MUST NOT expose file bodies until a note exists via Review.
- Backoff. Honor
429/Retry-After. Bounded retries (max 3) thenrate_limited. - Gate off = inert. False gates → 501, no network, no vault I/O.
- Namespace isolation. Docs vault directory ≠
calendar_oauth. Docs blob keys ≠calendar/oauth/…. - Id injection.
file_ids/ page ids must match^[A-Za-z0-9_-]{1,128}$(Drive) or Notion UUID-with-optional-hyphens ≤ 64. Else 400BAD_REQUEST.
7. Fail-closed rules
| Code | When |
|---|---|
NOT_AUTHORIZED |
Gate false |
NOT_CONFIGURED |
Drive env incomplete on a begin/callback while gate is true |
STATE_INVALID |
Missing / replay / tamper / expiry / vault mismatch |
RETURN_URL_DENIED |
return_url not exact-allowlisted |
PROVIDER_DENIED |
Unknown provider |
CONNECTOR_NOT_FOUND |
Unknown or revoked id on mutating routes |
NEEDS_REAUTH |
Import / sync / list while needs_reauth |
RATE_LIMITED |
Sync < 60s or provider 429 exhausted |
BAD_REQUEST |
Cap exceeded, bad ids, unknown fields |
SOURCE_TYPE_DENIED |
Any attempt to treat this path as POST import gdrive / notion from a Scooling adapter in this tip (there is no Scooling adapter in this tip; regression test still asserts Hub does not accept a Scooling-shaped import body on these routes) |
Callback failures redirect with reason in { state_invalid, denied, provider_error, not_configured } only.
8. Environment (never committed)
Names only — values are operator-set, never written in this artifact.
| Name | Purpose |
|---|---|
DOCS_OAUTH_GOOGLE_AUTHORIZED |
Tier 3 Drive gate; source default false |
DOCS_NOTION_HUB_KEY_AUTHORIZED |
Tier 3 Notion gate; source default false |
GOOGLE_DRIVE_OAUTH_CLIENT_ID |
Drive OAuth client id (server) |
GOOGLE_DRIVE_OAUTH_CLIENT_SECRET |
Drive OAuth client credential (server-only) |
KNOWTATION_DOCS_OAUTH_SECRET |
Vault wrapping secret (≥ 32 chars) |
DOCS_OAUTH_REDIRECT_URI |
Exact Google-registered callback URL |
SCOOLING_RETURN_URL_ALLOWLIST |
Reuse the existing Calendar allowlist (exact URLs) |
NOTION_API_KEY |
Existing Hub-only Notion value; never copied to Scooling |
.gitignore already excludes data/. .museignore must keep data/ excluded. Auto adds docs_oauth/ to any ignore file that currently names calendar_oauth/ explicitly; if only data/ is ignored, no ignore edit is required.
.env.example may list the names above with empty placeholders. Auto MUST NOT commit a real value.
9. Scooling consumer contract (not implemented here)
Frozen so CONNECT-DRIVE-READY cannot invent a second design. This Knowtation Auto must not edit the Scooling repo.
| Later Scooling may | Later Scooling must not |
|---|---|
POST api/v1/docs/connectors with provider: 'google-drive' and an allowlisted return_url |
Collect, store, or log refresh material |
Open authorization_url in the browser |
Call Google or Notion |
| GET connector status (token-free) | Send Hub source_type gdrive or notion |
After Ready flip, let a person pick listed file_ids and POST import |
Store sync_cursor |
Flip docs_drive_live to Ready only after this Auto lands and Drive gate is operator-authorized |
Flip Ready in the Knowtation tip |
Notion Ready on /connect is after Drive Ready (CONNECT-AND-OPEN-RANGE “then Notion”). Same rule: no key in Scooling.
10. Test matrix (seven-tier) — KN-DOCS-SYNC-b consumes this
All provider network is faked via an injected Google client and Notion client. No live Google or Notion call in CI. Live smoke is operator-run behind T2.
| Tier | File | Coverage |
|---|---|---|
| unit | test/docs-oauth-connector-unit.test.mjs |
PKCE/state build+validate; docs vault encrypt/decrypt + tamper fail; MIME allow/deny; id regex; path safe_id; gate false short-circuit |
| integration | test/docs-oauth-connector-integration.test.mjs |
Drive begin → callback → list → import proposals → sync cursor → DELETE. Notion Hub-key list → import proposals. Dedup edit proposal |
| e2e | test/docs-oauth-connector-e2e.test.mjs |
Hub/bridge route walkthrough with fakes: connect, list metadata only, import creates docs-sync proposals, approve apply writes one note, revoke keeps the note |
| stress | test/docs-oauth-connector-stress.test.mjs |
Paginated list; 20-id cap; concurrent sync 429; Changes 410 full re-list without duplicate proposals |
| data-integrity | test/docs-oauth-connector-data-integrity.test.mjs |
Dedup by source+source_id; vault blob survives restart; revoke deletes blob; notes remain; cursor opaque and not in GET connectors |
| performance | test/docs-oauth-connector-performance.test.mjs |
List 50 + import 20 within local budget; vault decrypt overhead bounded |
| security | test/docs-oauth-connector-security.test.mjs |
No material in responses/redirects/logs; state replay/tamper/expiry deny; PKCE plain rejected; redirect allowlist; gate off → 501 no network; Scooling-shaped POST import gdrive/notion not used; file_ids injection fixtures; list q reject quotes/operators; Notion value absent from connector JSON; calendar_oauth namespace unused; no T5 fingerprint for docs-sync |
Test command: node --test test/docs-oauth-connector-*.test.mjs.
Security tier MUST fail against a pre-fix stub that returns refresh material on GET connectors or writes notes from import without a proposal.
11. Auto file allowlist (KN-DOCS-SYNC-b)
| Path | Why |
|---|---|
lib/docs/** |
New connector / vault / normalizer / propose |
hub/bridge/docs-blob-store.mjs |
Hosted persistence |
hub/bridge/docs-routes.mjs |
Bridge routes |
hub/bridge/server.mjs |
Register docs routes + blob wrap |
hub/gateway/server.mjs |
Proxy the §4.2 prefixes |
hub/server.mjs |
Self-hosted mounts |
lib/importers/notion.mjs |
Extract fetch helper only; keep CLI writeNote behavior |
lib/importers/pdf.mjs |
Export pdfBytesToMarkdown only; CLI importPdf behavior unchanged |
lib/importers/docx.mjs |
Export docxBytesToMarkdown only; CLI importDocx behavior unchanged |
docs/HUB-API.md |
Honesty — live routes + “folder gdrive is not Drive OAuth” |
docs/openapi.yaml |
Route shapes |
docs/PROPOSAL-LIFECYCLE.md |
docs-sync queue note |
docs/ROADMAP.md |
Status |
docs/OVERSEER-HANDOVER.md |
NEXT |
test/docs-oauth-connector-*.test.mjs |
Seven-tier |
.env.example |
Names only, if the file exists on the tip |
.gitignore / .museignore |
Only if calendar_oauth is named and docs_oauth must join it |
Forbidden in this Auto
- Any path under
~/scooling/ lib/importers/gdrive.mjsbehavior changelib/calendar/**writes (read/reuse only)- Compile-time gate flipped to true
SCOOLING_*orMEDIA_*env assigns- New Hub
source_typestring - Sheets / chat-inbox / Slack connectors
- Notion OAuth
- Direct
writeNoteon the live Drive / live Notion routes
12. Out of scope
- Scooling CONNECT-DRIVE-READY Ready flip
- Google Sheets live (
docs_sheetsstaysnot_wired) - Chat inbox / Slack / Discord / Telegram / WhatsApp as workplace
- Drive write-back (create/edit Google files)
- Notion OAuth
- Google app public verification
- MCP/CLI parity for the new routes (after Hub REST is proven)
- Deleting vault notes on revoke
- OpenClaw Hub importer
- Hub UI chrome / new Hub wizard (REST + seven-tier only; learner UI is later Scooling CONNECT-DRIVE-READY)
- Presence, pairing, Parentier.org, Home chat
13. Definition of Done (KN-DOCS-SYNC-b)
- [ ] Freeze-review pass before Auto starts
- [ ] D1–D17 implemented; gates hard-coded false
- [ ] Seven-tier
test/docs-oauth-connector-*.test.mjsgreen locally - [ ] Build verification pass before ROADMAP → DONE
- [ ] Folder
gdriveimporter unchanged - [ ] No Scooling file edits
- [ ] No secrets committed
- [ ] Both governance docs updated together
- [ ] Feature-branch hygiene; merge remains Tier 3 (or SD-21 land with no live flip)
13.1 Before Auto may start
- This artifact
frozen: true /freeze-review-loopor/freeze-reviewverdict passok review --freeze docs/KN-DOCS-SYNC-FREEZE.mdpass
13.2 Auto must not
- Flip Scooling Drive / Notion cards
- Collect a key in Scooling
- Enable the compile-time gates
- Call live Google or Notion from CI
- Treat Hub POST
importgdriveas this feature