Skip to content

Commit aa5e0e5

Browse files
mpstatonclaude
andcommitted
plan(reach-edu-instance): rewritten on the identity spec's designed path — session tenancy, not sibling instances
Operator recheck was right: the id-didi-sh spec of record designs the session to carry its workspace — minimal token (didi_id + sid), /api/me org memberships, and augment-it explicitly specified to map org ↔ workspace per session. The running code built only the binary org gate and papered the mapping over with the ACTIVE_CLIENT_ID instance pin; worse, active client is instance-GLOBAL (workspace.activate switches every connected session) and capability frames carry client untrusted. v0.0.1.0 makes the designed mapping the plan: workspace org_id config, per-session allowed_clients from /api/me (superuser = the operating-team cross-client path), per-session activate with validation, and server-side client enforcement on every dispatched frame — one domain serving both tenants. The per-client-instance shape is demoted to a named fallback if Stephenie's onboarding can't wait. Build order gains a multi-tenant contamination proof script as step 1's acceptance. Files changed: - context-v/plans/Open-Augment-Didi-Sh-To-Reach-Edu.md Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RW28dw3kQAKXr2ZNefCukE
1 parent aad873d commit aa5e0e5

1 file changed

Lines changed: 88 additions & 51 deletions

File tree

context-v/plans/Open-Augment-Didi-Sh-To-Reach-Edu.md

Lines changed: 88 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ authors:
77
- Michael Staton
88
augmented_with:
99
- Claude Code on Claude Fable 5
10-
semantic_version: 0.0.0.1
10+
semantic_version: 0.0.1.0
11+
revisions:
12+
- "2026-07-28 — v0.0.1.0 — REWRITTEN after rechecking the id-didi-sh spec of record (operator: 'the idea was it create an auth token that carried the workspace'). The spec confirms the intent: the token stays minimal (didi_id + sid) but /api/me supplies org memberships and augment-it is designed to map org ↔ workspace per session. The per-client-instance recommendation (v0.0.0.1's Option A) demoted to fallback; the designed org↔workspace session binding is now the plan."
1113
tags:
1214
- Plan
1315
- Augment-It
@@ -32,39 +34,61 @@ single-tenant is the **door**: `DIDI_AUTH=required`,
3234
`REQUIRED_ORG_ID=humain.vc`, `ACTIVE_CLIENT_ID=humain-vc` on
3335
workspace-service.
3436

35-
## The load-bearing constraint
36-
37-
`ACTIVE_CLIENT_ID` pins the tenant PER-INSTANCE, not per-user-org.
38-
Relaxing `REQUIRED_ORG_ID` alone would authenticate reach.edu users into
39-
the humain-vc workspace. Safe multi-client therefore means either
40-
per-client instances (each with its own pin) or real org→client session
41-
binding (a feature in workspace-service). The house has a strong
42-
precedent for the former: every client already gets its own Twenty, its
43-
own hub, its own stack folder (`self-host-stack/client-stacks/<client>`).
44-
45-
## Recommended shape: a per-client instance (Option A)
46-
47-
`reach.augment.didi.sh` — a sibling instance in the same Railway project,
48-
sharing the client-agnostic backends, pinning its own tenant:
49-
50-
| Piece | Action |
51-
|---|---|
52-
| `nats`, `record-surrealdb-resolver`, `content-ingest`, `prompt-runner` | SHARED — already client-agnostic (client rides every request; canonical writes carry `client_access`) |
53-
| `workspace-service-reach` | NEW service from the same Dockerfile. Env: `DIDI_AUTH=required`, `REQUIRED_ORG_ID=reach.edu`, `ACTIVE_CLIENT_ID=reach-edu`, own tiny `/data` volume (sessions + clients root), `PORT=3001` |
54-
| `shell-reach` | NEW build of the shell with its WS URL baked to `wss://ws.reach.augment.didi.sh/ws` (the WS URL is a build-time constant — see DEPLOYMENT.md gotchas) |
55-
| `chat` / `strategy-curator` remotes | SHARED if the shell's remote registry allows cross-origin asset URLs per instance (they're static assets); else thin per-instance builds |
56-
| DNS | `reach.augment` + `ws.reach.augment` CNAMEs per the custom-domain-cutover skill; both must stay on `*.didi.sh` for the shared `didi_session` cookie |
57-
58-
Why A over org-mapping: zero new auth code in the hot path, blast-radius
59-
isolation (a reach-edu session cannot even express a humain-vc
60-
workspace), per-client kill switch, and it matches the per-client stack
61-
doctrine everywhere else.
62-
63-
**Option B (logged, not chosen): org→client binding in workspace-service**
64-
— map didi org → allowed client(s) at session establishment, one domain
65-
serves all tenants. Less infra, more auth surface; becomes worth it
66-
around client #4 or when cross-client operators (us) want one login.
67-
Revisit then.
37+
## What the identity spec actually designed (recheck, 2026-07-28)
38+
39+
The id-didi-sh spec of record
40+
(`ai-labs/context-v/specs/Id-Didi-Sh-Identity-Service.md`) is explicit:
41+
42+
- The **token stays minimal**`didi_id` + `session_id` only
43+
("Deliberately not in the token: orgs, roles, email").
44+
- **`GET /api/me`** supplies org memberships + roles; and the augment-it
45+
integration is specified as: *"per-capability authorization consults
46+
cached `/api/me` org-roles **mapped onto workspaces**"* — with
47+
per-service authorization state named directly: *"for augment-it:
48+
**org ↔ workspace** per Workspaces-as-Tenant-Primitive."*
49+
50+
So the session is DESIGNED to carry its workspace(s), resolved through
51+
org membership. What's running today implemented only the binary gate
52+
(`REQUIRED_ORG_ID` membership yes/no) and papered over the mapping with
53+
the `ACTIVE_CLIENT_ID` instance pin. Two gaps make the current state
54+
single-tenant:
55+
56+
1. **No org↔workspace map** — nothing says org `reach.edu` ⇒ workspace
57+
`reach-edu`.
58+
2. **Active client is INSTANCE-GLOBAL**`workspaces.ts` holds one
59+
module-level `activeClientId`; `workspace.activate` switches it for
60+
every connected session. Safe solo, catastrophic multi-user.
61+
3. (Corollary) capability args carry `client` from the UI **untrusted**
62+
with multiple orgs on one instance, the server must derive/validate
63+
the client from the session, not accept it from the frame.
64+
65+
## Recommended shape: build the designed mapping (the spec's path)
66+
67+
One domain, `augment.didi.sh`, serving both tenants:
68+
69+
1. **Workspace → org binding**: each workspace under `CLIENTS_ROOT`
70+
declares its org (`clients/<id>/workspace.json` gains
71+
`org_id: "reach.edu"` / `"humain.vc"`). The map lives with the
72+
workspace, not in env.
73+
2. **Session-scoped tenancy** in workspace-service: at WS establishment,
74+
`/api/me` memberships → allowed workspaces (superuser → all; this is
75+
the operating-team fast path that keeps US cross-client). The session
76+
record carries `allowed_clients` + its own `active_client`;
77+
`workspace.activate` becomes per-session and validates against the
78+
allowed set. `ACTIVE_CLIENT_ID` env survives only as a dev-mode
79+
default.
80+
3. **Server-side client enforcement**: dispatch overrides/validates the
81+
`client` arg in every capability frame against the session's allowed
82+
set — the tenant-aware envelope the Workspaces spec already names.
83+
This is the security-critical line of the build.
84+
4. `REQUIRED_ORG_ID` relaxes to "member of ANY org that maps to a
85+
workspace on this instance."
86+
87+
**Fallback (formerly Option A, demoted):** a per-client instance
88+
(`reach.augment.didi.sh` with its own pin) remains viable as a stopgap if
89+
Stephenie's onboarding must precede the session-tenancy build — but it
90+
contradicts the identity spec's architecture and doubles frontend
91+
plumbing (baked WS URLs). Prefer the designed path.
6892

6993
## Stephenie's onboarding (the identity half)
7094

@@ -96,29 +120,42 @@ Revisit then.
96120

97121
## Build order (when signed off)
98122

99-
1. Railway: mint `workspace-service-reach` (+ volume) and `shell-reach`
100-
with baked WS URL; wire env; deploy from `rebuild/turbo-rsbuild`.
101-
2. DNS: `reach.augment` / `ws.reach.augment` CNAMEs; wait out cert
102-
issuance per the cutover skill (including its stale-cache theater).
103-
3. Deploy the Augment-from-DB remotes (org-workbench, search-results,
104-
search-and-add, person-* ) as static-asset services; register them in
105-
shell-reach's remote registry.
106-
4. id-didi-sh: create org `reach.edu`, invite stesoro@reach.edu; verify
107-
the unlock flow end-to-end (the OAuth pilot discipline from the
108-
Twenty stacks applies: designed-but-unproven until she logs in).
109-
5. Browser drive against reach.augment.didi.sh (workbench loads, client
110-
pin is reach-edu, a humain-vc org is NOT visible), then the human
111-
walk-through: Stephenie's first login as the acceptance test.
112-
6. DEPLOYMENT.md gains the second-instance section; changelog entry.
123+
1. **workspace-service session tenancy** (the core): workspace org_id
124+
config, `/api/me`-derived `allowed_clients` per session, per-session
125+
`active_client`, `workspace.activate` validation, and server-side
126+
`client` enforcement on every dispatched frame. Proof script: two
127+
fake sessions with different orgs cannot see or write each other's
128+
client — the multi-tenant twin of prove-org-relations' contamination
129+
check.
130+
2. **Frontends follow the session**: the shell/remotes read
131+
`workspace.active` per session (already do) — verify no surface
132+
caches a global client across a session switch.
133+
3. id-didi-sh: create org `reach.edu`, invite **stesoro@reach.edu**
134+
(invite-only by design — invites carry org_id + role; there is no
135+
open signup). Verify the redeem→cookie→WS flow end-to-end.
136+
4. Deploy the Augment-from-DB remotes (org-workbench, search-results,
137+
search-and-add, person-*) as static-asset services on the existing
138+
instance; register in the shell's remote registry — this is the
139+
surface Stephenie actually needs.
140+
5. Relax the deployed env: drop `ACTIVE_CLIENT_ID` pin +
141+
`REQUIRED_ORG_ID` single-value in favor of the mapping; redeploy.
142+
6. Browser drive on augment.didi.sh: a reach.edu-org session sees ONLY
143+
reach-edu (org list, roster, corpora), a humain.vc session sees only
144+
humain-vc, a superuser sees both. Then the human walk-through:
145+
Stephenie's first login as the acceptance test.
146+
7. DEPLOYMENT.md multi-tenant section; changelog entry.
113147

114148
## Open decisions
115149

116-
1. Sign off Option A (per-client instance) vs holding for Option B.
150+
1. Sign off the session-tenancy build (the spec's designed path) — or
151+
invoke the per-client-instance fallback if her onboarding can't wait
152+
for it.
117153
2. Whether the Augment-from-DB remotes ship in this pass (recommended —
118154
they're the surface Stephenie actually needs) or the chat-first
119155
surface suffices for v1.
120-
3. Who sends Stephenie the invite + onboarding note (content drafted as
121-
part of step 4).
156+
3. Stephenie's role in org reach.edu (`editor` seems right — writes with
157+
attribution, no org admin), and who sends the invite + onboarding
158+
note.
122159

123160
## See also
124161

0 commit comments

Comments
 (0)