源码快照 @ 2026-09-02 (source-only, 剔除非源码素材)
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
---
|
||||
title: "Enterprise"
|
||||
description: "Deploy OpenWork on your own infrastructure"
|
||||
---
|
||||
|
||||
Want to run OpenWork on your own infra? We'll help you get there.
|
||||
|
||||
OpenWork can be deployed inside your VPC or on-prem, with SSO, audit logs, and controls over which models and tools your team can use. If you're exploring a self-hosted or private-cloud rollout, talk to us.
|
||||
|
||||
<Card title="Book a call with our team" icon="calendar" href="https://calendar.app.google/86QpCENvhfEzDFLu5">
|
||||
Schedule a 30-minute intro at [calendar.app.google/86QpCENvhfEzDFLu5](https://calendar.app.google/86QpCENvhfEzDFLu5) to scope your deployment.
|
||||
</Card>
|
||||
|
||||
## What we'll cover
|
||||
|
||||
- Your target environment (cloud provider, VPC, on-prem)
|
||||
- Identity, access, and audit requirements
|
||||
- Model and tool allowlists
|
||||
- Rollout plan and support model
|
||||
|
||||
Prefer email? Reach us at [team@openworklabs.com](mailto:team@openworklabs.com?subject=Enterprise%20deployment).
|
||||
|
||||
## How desktop deployment works
|
||||
|
||||
Rolling OpenWork out to a fleet does not involve a custom installer build. You deploy the standard **OpenWork Enterprise binary** — a dedicated distribution that requires sign-in and administrator activation, on its own update channel — and optionally push a single `desktop-bootstrap.json` file through your MDM so every machine targets your Den server on first launch. Certificates come from the OS trust stores you already manage, and desktop policies are delivered by the server after sign-in, so no MDM scripting is needed beyond that one file.
|
||||
|
||||
See [Enterprise desktop deployment](/start-here/enterprise-desktop-deployment) for the full reference.
|
||||
@@ -0,0 +1,74 @@
|
||||
---
|
||||
title: "Get started with OpenWork Cloud"
|
||||
description: "Connect the desktop app to your organization for Collections, managed connections, models, policies, and hosted workspaces"
|
||||
---
|
||||
|
||||
<Note>
|
||||
OpenWork Cloud is optional. The local desktop app stays free, requires no Cloud sign-in, and supports your own providers, skills, plugins, and custom MCP servers. Use Cloud when an organization needs to assign and manage those resources for a team.
|
||||
</Note>
|
||||
|
||||
OpenWork Cloud is available at [app.openworklabs.com](https://app.openworklabs.com/). It is for teams that want shared identity, Collections, managed connections and models, desktop policies, or hosted workspaces.
|
||||
|
||||
First, create or join an organization and connect the desktop app to it.
|
||||
|
||||
<Frame>
|
||||

|
||||
</Frame>
|
||||
|
||||
1. Open [app.openworklabs.com](https://app.openworklabs.com/) in your browser.
|
||||
2. Click `Create organization`.
|
||||
3. Enter your org name.
|
||||
4. Finish checkout if OpenWork Cloud asks for billing.
|
||||
5. Continue to the desktop sign-in steps below.
|
||||
|
||||
Once you're there, you can configure the settings for the organization.
|
||||
|
||||
## Configure org settings
|
||||
|
||||
<Note>Only the organization owner can change these settings.</Note>
|
||||
|
||||
1. Open the `dashboard/org_settings` [page](https://app.openworklabs.com/dashboard/org-settings) in the cloud app.
|
||||
2. Set the name for your organization.
|
||||
3. Optional: enable `Restrict allowed email domains` and add the approved domains.
|
||||
4. Optional: change any `Desktop restrictions` that should apply after people sign in from the desktop app.
|
||||
|
||||
These settings apply across the organization. You can also create teams, publish plugins through Collections, configure Connectors and shared models, and deploy hosted workspaces.
|
||||
|
||||
Next, you want to make sure you can sign in the desktop app.
|
||||
|
||||
## Sign in from the desktop app
|
||||
|
||||
1. Open `Settings > Account`.
|
||||
2. Click `Sign in`.
|
||||
3. Finish sign-in in your browser.
|
||||
4. If the browser does not return to OpenWork automatically, click `Paste sign-in code`, paste the `openwork://den-auth?...` link or one-time code from OpenWork Cloud, then click `Finish sign-in`.
|
||||
|
||||
## Cloud URL used by the desktop app
|
||||
|
||||
The desktop app uses a single Cloud URL: the Den web `baseUrl`. The hosted
|
||||
default is `https://app.openworklabs.com`.
|
||||
|
||||
All Cloud API and MCP traffic is derived from that one URL through the web app
|
||||
proxy:
|
||||
|
||||
- Cloud API requests go to `<baseUrl>/api/den/v1/...`.
|
||||
- OpenWork Cloud MCP requests go to `<baseUrl>/api/den/mcp/...`.
|
||||
|
||||
The desktop app does not store or read a separate API URL. On startup it reads
|
||||
the Cloud URL from `desktop-bootstrap.json`; if that file is missing, it falls
|
||||
back to `https://app.openworklabs.com`. When you change the Cloud URL in
|
||||
`Settings > Account`, OpenWork writes the new `baseUrl` back to the bootstrap
|
||||
file so the next launch uses the same source of truth.
|
||||
|
||||
## Choose your organization
|
||||
|
||||
After sign-in, pick your organization in `Settings > Account`. Organization-assigned plugins and skills appear in `Settings > Library`.
|
||||
|
||||
## What to do next
|
||||
|
||||
- Send members to [Connect your services](/start-here/connect-your-stack/connect-services) to sign in to team-approved accounts from the desktop app.
|
||||
- Use [Connectors](/cloud/share-with-your-team/shared-mcp-connections) to publish a managed service and grant member or team access.
|
||||
- Use [Publish and copy a skill](/start-here/do-work-with-it/publish-and-copy-a-skill) for the primary skill-sharing flow, or [Collections](/cloud/share-with-your-team/collections) to group and assign several plugins.
|
||||
- Use [Shared workspaces](/cloud/run-in-the-cloud/shared-workspace) to deploy hosted OpenWork runtimes that teammates can open from the desktop app.
|
||||
- Use [LLM providers](/cloud/share-with-your-team/managed-llm-provider) or [custom LLM providers](/cloud/share-with-your-team/custom-llm-provider) to manage shared model access.
|
||||
- Use [Members and RBAC](/cloud/members-and-rbac) to invite people, create teams, and control access.
|
||||
@@ -0,0 +1,93 @@
|
||||
---
|
||||
title: "Managing members and RBAC"
|
||||
description: "Invite teammates, create teams, and control Cloud access with owners, admins, members, and custom roles"
|
||||
---
|
||||
|
||||
OpenWork Cloud separates org membership from team assignment. Teams are used for Collection and provider access; roles control what a person can manage across the org.
|
||||
|
||||
## Default roles
|
||||
|
||||
OpenWork Cloud comes with three default roles:
|
||||
|
||||
- `Owner`: full org control, including member roles and custom roles.
|
||||
- `Admin`: can invite people, manage teams, and manage most shared Cloud resources.
|
||||
- `Member`: can use resources shared with them but cannot manage org administration.
|
||||
|
||||
Only `Owner` can change member roles, remove members, or create, edit, and delete custom roles.
|
||||
|
||||
## Invite members
|
||||
|
||||
1. Open `Members`.
|
||||
2. On the `Members` or `Invitations` tab, click `Add member` or `Invite member`.
|
||||
3. Enter the teammate `Email`.
|
||||
4. Choose the initial `Role`.
|
||||
5. Click `Send invite`.
|
||||
|
||||
<Frame>
|
||||

|
||||
</Frame>
|
||||
|
||||
Invites are tied to the invited email address.
|
||||
|
||||
## SSO just-in-time provisioning
|
||||
|
||||
When SSO just-in-time provisioning adds someone to an organization on first sign-in, OpenWork assigns the baseline `Member` role. IdP attributes such as `role`, `groups`, or `admin` are not used to grant OpenWork organization roles.
|
||||
|
||||
Treat `Admin`, `Owner`, and custom-role elevation as explicit access changes made inside OpenWork. Review those changes in the member list and audit trail instead of relying on unvalidated IdP attributes for authorization.
|
||||
|
||||
## Restrict who can join
|
||||
|
||||
If you only want teammates from specific companies or domains to join:
|
||||
|
||||
1. Open `Org settings`.
|
||||
2. Turn on `Restrict allowed email domains`.
|
||||
3. Add each approved domain to the `Domain allowlist`.
|
||||
4. Click `Save settings`.
|
||||
|
||||
When this is enabled, OpenWork Cloud only lets people join with email addresses from the approved domains. Existing invites still target a specific email address, so both checks apply: the invitee must use the invited email address and that address must match the allowlist.
|
||||
|
||||
## Create teams
|
||||
|
||||
1. Open `Members -> Teams`.
|
||||
2. Click `Create Team`.
|
||||
3. Set `Team name`.
|
||||
4. Choose `Team members`.
|
||||
5. Click `Create team`.
|
||||
|
||||
You can use teams to control access to Collections and LLM providers without assigning people one by one.
|
||||
|
||||
### Manage teams with SCIM Groups
|
||||
|
||||
Organizations using SCIM can open `SCIM` and enable **Create teams from SCIM groups**. OpenWork then creates a team for each provisioned Group and keeps its membership synchronized from the identity provider. These teams are labeled **Managed by SCIM** and must be changed in the identity provider; manually created teams and memberships remain independent.
|
||||
|
||||
To connect an identity provider, follow
|
||||
[Okta SCIM provisioning](/cloud/scim-okta) or
|
||||
[Microsoft Entra SCIM provisioning](/cloud/scim-microsoft-entra).
|
||||
|
||||
When SCIM deprovisions a user, OpenWork retains a disconnected member record for organization history. The global user is deleted only when they have no other active organization memberships. Reactivate the user through SCIM before they can regain SAML access.
|
||||
|
||||
## Use custom roles when needed
|
||||
|
||||
1. Open `Members -> Roles`.
|
||||
2. Click `Create role`.
|
||||
3. Enter `Role name`.
|
||||
4. Choose the permissions that role should have.
|
||||
5. Click `Create role`.
|
||||
|
||||
Use `security_configuration.manage` for the identity/security operator role that manages SSO, SCIM, and organization API keys. Keep it separate from day-to-day `Admin` membership operations when your organization needs separation of duties.
|
||||
|
||||
## Practical access pattern
|
||||
|
||||
A simple setup that works well for most orgs:
|
||||
|
||||
- keep 1-2 `Owner` users
|
||||
- give day-to-day operators `Admin`
|
||||
- keep most people as `Member`
|
||||
- use `Teams` to decide who can see specific Collections or providers
|
||||
|
||||
## Notes
|
||||
|
||||
- You cannot change the org owner's role or remove the owner.
|
||||
- Owners and admins can manage teams and invitations, but only owners can handle the sensitive RBAC changes.
|
||||
- Only owners can change `Org settings`, including allowed email domains and desktop restrictions.
|
||||
- You cannot delete a custom role while members or pending invitations still use it.
|
||||
@@ -0,0 +1,223 @@
|
||||
---
|
||||
title: "OpenWork MCP Gateway"
|
||||
sidebarTitle: "Overview"
|
||||
description: "The OpenWork MCP Gateway is one hosted endpoint that brings your organization's skills, plugins, and connections into Claude Code, Codex, Claude Desktop, Cursor, VS Code, or any MCP client."
|
||||
---
|
||||
|
||||
import { OpenWorkConnectInstaller } from "/snippets/openwork-connect-installer.jsx";
|
||||
|
||||
The OpenWork MCP Gateway is one MCP endpoint for everything your organization has made available to you: skills, plugins, workflows, and connections. Instead of configuring one MCP server per service, your agent connects to the gateway once and discovers capabilities on demand. The public hosted endpoint is:
|
||||
|
||||
```text
|
||||
https://api.openworklabs.com/mcp/agent
|
||||
```
|
||||
|
||||
This guide is for connecting an external MCP client to OpenWork. To connect
|
||||
Gmail, calendar, Drive, Slack, Notion, Linear, or another service inside the
|
||||
OpenWork desktop app, use
|
||||
[Connect your services](/start-here/connect-your-stack/connect-services).
|
||||
|
||||
Your client opens a browser, you sign in to OpenWork, choose the organization to authorize, and the client stores the OAuth token for that organization.
|
||||
|
||||
`app.openworklabs.com/api/den` is an internal same-origin desktop proxy used by OpenWork first-party flows. Do not paste it into external MCP clients.
|
||||
|
||||
<OpenWorkConnectInstaller />
|
||||
|
||||
## Set up your client
|
||||
|
||||
Step-by-step guides for every client:
|
||||
|
||||
<CardGroup cols={3}>
|
||||
<Card title="OpenCode" href="/model-context-protocol/opencode" />
|
||||
<Card title="Claude Code" href="/model-context-protocol/claude-code" />
|
||||
<Card title="Codex" href="/model-context-protocol/codex" />
|
||||
<Card title="Claude Desktop" href="/model-context-protocol/claude-desktop" />
|
||||
<Card title="ChatGPT" href="/model-context-protocol/chatgpt" />
|
||||
<Card title="Cursor" href="/model-context-protocol/cursor" />
|
||||
<Card title="VS Code" href="/model-context-protocol/vs-code" />
|
||||
<Card title="Windsurf" href="/model-context-protocol/windsurf" />
|
||||
<Card title="Zed" href="/model-context-protocol/zed" />
|
||||
<Card title="Gemini CLI" href="/model-context-protocol/gemini-cli" />
|
||||
</CardGroup>
|
||||
|
||||
## In the OpenWork desktop app
|
||||
|
||||
If you use the OpenWork desktop app, there is no MCP endpoint to configure.
|
||||
When you sign in, the app injects OpenWork Connect with access scoped to your
|
||||
active organization and refreshes that first-party access automatically. Use
|
||||
`Settings` > `Connect` to sign in to the services available to you.
|
||||
|
||||
## Client support status
|
||||
|
||||
| Client | Status | Notes | Guide |
|
||||
| --- | --- | --- | --- |
|
||||
| OpenCode | Verified | Native remote MCP OAuth passed end-to-end implementation testing. | [OpenWork MCP for OpenCode](/model-context-protocol/opencode) |
|
||||
| Codex | Setup only | Add/login/reconnect steps are below; native remote MCP OAuth must be rerun on this exact branch before Codex is marked verified. | [OpenWork MCP for Codex](/model-context-protocol/codex) |
|
||||
| Cursor | Setup only | Cursor Web/Agents use an HTTPS OAuth callback. Cursor Desktop OAuth uses `cursor://anysphere.cursor-mcp/oauth/callback`, which OpenWork accepts through an exact private-use allowlist with PKCE S256 enforced. Native proof is not complete. | [OpenWork MCP for Cursor](/model-context-protocol/cursor) |
|
||||
| ChatGPT Desktop | Setup only | Setup guide only; use ChatGPT `Settings > MCP servers`. Native proof is not complete. | [OpenWork MCP for ChatGPT](/model-context-protocol/chatgpt) |
|
||||
| Claude Code | Setup only | Setup guide only; native proof is not complete. | [OpenWork MCP for Claude Code](/model-context-protocol/claude-code) |
|
||||
| Claude Desktop | Setup only | Setup guide only; native proof is not complete. | [OpenWork MCP for Claude Desktop](/model-context-protocol/claude-desktop) |
|
||||
| VS Code | Setup only | Setup guide only; native proof is not complete. | [OpenWork MCP for VS Code](/model-context-protocol/vs-code) |
|
||||
| Windsurf | Setup only | Setup guide only via mcp-remote; native proof is not complete. | [OpenWork MCP for Windsurf](/model-context-protocol/windsurf) |
|
||||
| Zed | Setup only | Setup guide only via mcp-remote; native proof is not complete. | [OpenWork MCP for Zed](/model-context-protocol/zed) |
|
||||
| Gemini CLI | Setup only | Setup guide only; native proof is not complete. | [OpenWork MCP for Gemini CLI](/model-context-protocol/gemini-cli) |
|
||||
| Any client | Setup only | Use only with clients that support remote Streamable HTTP MCP servers and OAuth. | — |
|
||||
|
||||
## OpenCode setup
|
||||
|
||||
Add this MCP entry to your OpenCode config:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcp": {
|
||||
"openwork": {
|
||||
"type": "remote",
|
||||
"enabled": true,
|
||||
"url": "https://api.openworklabs.com/mcp/agent",
|
||||
"oauth": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Then authenticate:
|
||||
|
||||
```sh
|
||||
opencode mcp auth openwork
|
||||
```
|
||||
|
||||
If OpenCode runs on a remote machine but the browser opens locally, follow
|
||||
[Connect OpenWork MCP from a remote machine](/cloud/run-in-the-cloud/connect-openwork-mcp-remote-machine).
|
||||
|
||||
To reconnect or switch organizations, remove the old token and authenticate again:
|
||||
|
||||
```sh
|
||||
opencode mcp logout openwork
|
||||
opencode mcp auth openwork
|
||||
```
|
||||
|
||||
Full guide: [OpenWork MCP for OpenCode](/model-context-protocol/opencode).
|
||||
|
||||
## Codex setup
|
||||
|
||||
Codex is setup-only for this PR until its native OAuth flow is rerun on this exact branch.
|
||||
|
||||
Add OpenWork, then log in:
|
||||
|
||||
```sh
|
||||
codex mcp add openwork --url https://api.openworklabs.com/mcp/agent
|
||||
codex mcp login openwork
|
||||
```
|
||||
|
||||
To reconnect or switch organizations, log out and log in again:
|
||||
|
||||
```sh
|
||||
codex mcp logout openwork
|
||||
codex mcp login openwork
|
||||
```
|
||||
|
||||
Full guide: [OpenWork MCP for Codex](/model-context-protocol/codex).
|
||||
|
||||
## Cursor setup-only guide
|
||||
|
||||
Paste the OpenWork Connect endpoint where Cursor asks for the remote MCP server URL, then start OAuth from Cursor's connection prompt:
|
||||
|
||||
```text
|
||||
https://api.openworklabs.com/mcp/agent
|
||||
```
|
||||
|
||||
Cursor Web/Agents use an HTTPS OAuth callback. Cursor Desktop OAuth uses the private-use callback `cursor://anysphere.cursor-mcp/oauth/callback`, which OpenWork accepts through an exact allowlist with PKCE S256 enforced, so intercepted authorization codes cannot be redeemed without the code verifier.
|
||||
|
||||
Full guide: [OpenWork MCP for Cursor](/model-context-protocol/cursor).
|
||||
|
||||
## ChatGPT setup-only guide
|
||||
|
||||
Open ChatGPT `Settings > MCP servers` ([open settings](https://chatgpt.com/#settings/Connectors)), paste the OpenWork Connect endpoint, and start OAuth from ChatGPT's connection prompt:
|
||||
|
||||
```text
|
||||
https://api.openworklabs.com/mcp/agent
|
||||
```
|
||||
|
||||
Full guide: [OpenWork MCP for ChatGPT](/model-context-protocol/chatgpt).
|
||||
|
||||
## Organization selection
|
||||
|
||||
The organization you choose in the browser is pinned into the token. Changing the active organization in OpenWork later does not change an external client's existing token. Use the logout/auth or logout/login reconnect commands above when you need a different organization.
|
||||
|
||||
## What you can ask the agent
|
||||
|
||||
Once connected, your agent can use OpenWork capabilities from plain English in the composer. These flows are exercised end to end against a live OpenWork Cloud deployment:
|
||||
|
||||
- **Check your cloud identity** — "Which OpenWork Cloud organization am I
|
||||
connected to?" The agent reports the organization, your account, and your
|
||||
role.
|
||||
- **Invite teammates** — "Add omar@example.com to my organization." The agent
|
||||
sends the invitation, updates the allowed email domains when needed, and
|
||||
surfaces seat-limit billing rules instead of failing silently.
|
||||
- **Manage teams** — "Assign Priya to the Sales team." Works for active
|
||||
members; the agent explains when someone still has a pending invitation.
|
||||
- **Share skills with your org** — "Create a skill that writes weekly status
|
||||
reports and share it with my whole organization." The agent writes the
|
||||
skill inside a plugin, adds the plugin to a Collection, and grants the
|
||||
Collection org-wide — teammates find the assigned skill in `Settings > Library`.
|
||||
|
||||
Org policy still applies: invitations respect seat limits and domain rules, and resource access follows organization membership, roles, policies, and exposure allowlists.
|
||||
|
||||
## OAuth and protocol details
|
||||
|
||||
OpenWork Connect is a remote Streamable HTTP MCP server with OAuth:
|
||||
|
||||
- The protected resource is exactly `https://api.openworklabs.com/mcp/agent`.
|
||||
- Clients discover OAuth metadata with RFC9728 protected-resource discovery.
|
||||
- The authorization server and browser sign-in origin is `https://app.openworklabs.com/api/auth`.
|
||||
- OAuth authorize and token requests must include exactly one `resource` value: `https://api.openworklabs.com/mcp/agent`.
|
||||
- Clients that need registration use dynamic client registration as a fallback during discovery.
|
||||
- PKCE is required for public/dynamic clients, and only the `S256` code challenge method is supported.
|
||||
- Redirect URIs must be HTTPS callbacks or HTTP loopback callbacks such as `http://127.0.0.1:<port>/callback`. Private-use callback schemes are not accepted for public OAuth clients, except exact allowlisted callbacks for known native clients — currently Cursor Desktop's `cursor://anysphere.cursor-mcp/oauth/callback` — with PKCE S256 enforced.
|
||||
|
||||
## Token lifetime and refresh
|
||||
|
||||
Clients normally refresh automatically. The values matter when diagnosing reconnects:
|
||||
|
||||
- Public OAuth access tokens are JWTs signed and validated with EdDSA. The issuer is exactly `https://app.openworklabs.com/api/auth`, the audience is exactly `https://api.openworklabs.com/mcp/agent`, and access tokens expire after 45 minutes.
|
||||
- Refresh tokens are opaque rotating grants with a 30-day inactivity window and a 30-second rotation overlap for near-simultaneous refreshes.
|
||||
- Each refresh revokes the previous refresh grant; because OpenWork stores only token hashes, a replay during the overlap can issue another successor instead of returning prior plaintext.
|
||||
- Successor grants remain normal rotating grants. If an old refresh grant is replayed after the overlap, expired, or revoked, the client may receive `invalid_grant` and the client/user refresh-token family is revoked. Reconnect by logging out of the MCP entry and authenticating again.
|
||||
- Access is rechecked against the active OpenWork session and organization membership, so removing a member or revoking the session cuts off MCP access.
|
||||
|
||||
## What the gateway exposes
|
||||
|
||||
Instead of flooding your client with hundreds of tool definitions, the gateway keeps the tool list small and constant. `/mcp/agent` exposes two discovery tools:
|
||||
|
||||
- `search_capabilities` searches the capabilities available to the signed-in member.
|
||||
- `execute_capability` runs an exact capability name returned by `search_capabilities`.
|
||||
|
||||
Search can include OpenWork Cloud resources such as skills, plugins, connectors, Collections, workers, members, roles, teams, model providers, and org-connected external MCP tools when those capabilities are enabled and shared with the member.
|
||||
|
||||
Availability is governed by organization membership, role, policy, and exposure allowlists. It intentionally does not expose authentication internals, admin-only system routes, webhooks, API-key creation or deletion, or credential-returning endpoints.
|
||||
|
||||
## MCP Apps
|
||||
|
||||
Some gateway results come back as MCP Apps: rich, interactive UI rendered from a tool result instead of plain text — great for dynamic interfaces like a created-skill confirmation card or a workflow result view. OpenWork implements the standard [MCP Apps extension](https://modelcontextprotocol.io/docs/extensions/apps) (`ui://` resources declared on tools), so first-party OpenWork apps render in any MCP client that supports the extension, and fall back to readable text everywhere else.
|
||||
|
||||
<Frame>
|
||||

|
||||
</Frame>
|
||||
|
||||
MCP Apps from third-party providers — apps shipped by an MCP server your organization added as a connector — currently render in the OpenWork desktop app, where connectors are enabled as a native MCP Apps preview per organization. Rendering third-party apps in external clients requires exposing that connector as a full MCP server, an option that is planned but not available yet.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **401 missing or invalid token**: run the client's auth/login command again. Confirm the client points to `https://api.openworklabs.com/mcp/agent`.
|
||||
- **403 membership or scope error**: confirm you selected the right organization and still have membership and required permissions there.
|
||||
- **`invalid_grant`**: the refresh grant was expired, revoked, or replayed after the 30-second rotation overlap. Run logout, then auth/login again.
|
||||
- **429 rate limit**: wait for the response's `Retry-After` value before retrying.
|
||||
- **Support requests**: include the response `X-Request-Id` header plus any JSON body MCP `referenceId` or OAuth `reference_id` field so OpenWork support can find the server-side trace.
|
||||
|
||||
## Requirements
|
||||
|
||||
- An OpenWork Cloud account and organization.
|
||||
- An MCP client that supports remote Streamable HTTP MCP servers.
|
||||
- OAuth support in the MCP client, including browser-based authorization.
|
||||
|
||||
If your client does not support OAuth for remote MCP servers, it cannot connect to OpenWork Connect yet.
|
||||
@@ -0,0 +1,54 @@
|
||||
---
|
||||
title: "Connect OpenWork MCP from a remote machine"
|
||||
description: "Authenticate OpenWork Connect for OpenCode on a remote host while using a browser on your local machine"
|
||||
---
|
||||
|
||||
Use this when OpenCode runs on a remote machine, SSH host, or cloud VM, but the OAuth browser opens on your laptop or another local machine. In the commands below, `<server-name>` is the OpenWork MCP name in your OpenCode config, often `openwork`.
|
||||
|
||||
## Why remote OAuth times out
|
||||
|
||||
During `opencode mcp auth <server-name>`, OpenCode listens for the OAuth redirect on the remote host at:
|
||||
|
||||
```text
|
||||
http://127.0.0.1:19876/mcp/oauth/callback
|
||||
```
|
||||
|
||||
If you open the printed authorization URL in a browser on another machine, that browser resolves `127.0.0.1` to the browser's own machine, not the remote host where OpenCode is listening. The OAuth approval succeeds in the browser, but the callback never reaches OpenCode, so the auth command eventually times out.
|
||||
|
||||
## Recommended: open SSH with a local port forward
|
||||
|
||||
From the local machine where you will open the browser, start SSH with a loopback-only local port forward and a remote shell:
|
||||
|
||||
```sh
|
||||
ssh -o ExitOnForwardFailure=yes -L 127.0.0.1:19876:127.0.0.1:19876 user@remote-host
|
||||
```
|
||||
|
||||
In that SSH shell on the remote host, run:
|
||||
|
||||
```sh
|
||||
opencode mcp auth <server-name>
|
||||
```
|
||||
|
||||
Open the printed authorization URL in your local browser. Keep the SSH session open until OpenCode reports that authentication succeeded.
|
||||
|
||||
The `-L` option makes the local browser's `http://127.0.0.1:19876/...` callback travel through SSH to `127.0.0.1:19876` on the remote host. The callback stays bound to loopback on both machines.
|
||||
|
||||
## Optional: add a tunnel from a second terminal
|
||||
|
||||
If you are already connected to the remote host in one terminal, keep that terminal for OpenCode and open a second local terminal for the tunnel:
|
||||
|
||||
```sh
|
||||
ssh -N -o ExitOnForwardFailure=yes -L 127.0.0.1:19876:127.0.0.1:19876 user@remote-host
|
||||
```
|
||||
|
||||
Then run `opencode mcp auth <server-name>` in your existing remote shell, open the printed URL locally, and keep both terminals open until authentication succeeds.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **Timeout or stale URL**: keep the SSH tunnel open, stop the timed-out auth command, run `opencode mcp auth <server-name>` again, and open the new printed URL. OAuth URLs can expire after a failed attempt.
|
||||
- **Port already occupied**: something on the local or remote machine is already using port `19876`. Stop that process, or configure a different OpenCode `oauth.callbackPort` and update both sides of the SSH forward to use the same port.
|
||||
- **Custom callback port**: if your OpenCode MCP config sets `oauth.callbackPort`, both forwarding endpoints must match it. For example, with port `34567`, use `-L 127.0.0.1:34567:127.0.0.1:34567`.
|
||||
- **Cannot reach the remote host**: SSH must be able to reach `user@remote-host` from the local/browser machine. Use the same hostname, bastion, VPN, or SSH config entry you normally use to open a remote shell.
|
||||
- **No device-code fallback**: OpenCode currently does not have an OAuth device-code flow for MCP, so browser-based auth still needs the loopback callback to reach OpenCode.
|
||||
|
||||
Keep the callback on loopback. Do not expose the callback listener on `0.0.0.0`, change it to a public redirect URL, or paste authorization codes into chats or terminals.
|
||||
@@ -0,0 +1,43 @@
|
||||
---
|
||||
title: "Open OpenWork Web in your browser"
|
||||
description: "Open a full OpenWork instance in your browser with nothing to install"
|
||||
---
|
||||
|
||||
<Warning>
|
||||
**Hosted access.** OpenWork Web is available to every organization when an OpenWork-hosted deployment explicitly enables it. A platform admin in Den `/admin` can also grant complimentary access to one specific organization while deployment-wide availability is off. Web billing must be configured before an owner or admin can purchase it. It costs **$50 per joined organization member per month**; pending invitations are not billed until they are accepted.
|
||||
</Warning>
|
||||
|
||||
Open `/dashboard/web` to see the price, billable member quantity, expected monthly total, and purchase action. Access opens only after the organization's subscription is confirmed.
|
||||
|
||||
## Who can use it
|
||||
|
||||
Every joined member can see OpenWork Web when the hosted deployment offers it. An organization owner or admin purchases the subscription; all joined members receive access after it becomes active.
|
||||
|
||||
Self-hosted deployments do not show OpenWork Web by default: a missing or false `DEN_OPENWORK_WEB_ENABLED` value fails closed unless a platform admin in Den `/admin` has explicitly granted complimentary access to that organization. If you use an OpenWork-hosted organization and do not see it in the sidebar, email [team@openworklabs.com](mailto:team@openworklabs.com) so the deployment configuration or organization grant can be checked.
|
||||
|
||||
## Open OpenWork Web
|
||||
|
||||
1. Sign in to your organization dashboard.
|
||||
2. Click `OpenWork Web` in the sidebar.
|
||||
3. If prompted, purchase OpenWork Web and wait for the subscription to be confirmed.
|
||||
4. Click `Open OpenWork Web`.
|
||||
|
||||
<Frame>
|
||||

|
||||
</Frame>
|
||||
|
||||
You are now looking at the full OpenWork interface in your browser.
|
||||
|
||||
<Frame>
|
||||

|
||||
</Frame>
|
||||
|
||||
## Sleep and persistence
|
||||
|
||||
The hosted instance sleeps when it is idle and wakes when you open it again. Files in the workspace persist across sleep, so you can close the tab and come back later without losing your work.
|
||||
|
||||
## Early-access limitation
|
||||
|
||||
During early access, the instance URL is itself the credential. Do not share it, paste it into public chats, or publish it anywhere public.
|
||||
|
||||
If you accidentally share the URL, ask an OpenWork admin for help.
|
||||
@@ -0,0 +1,37 @@
|
||||
---
|
||||
title: "OpenWork Web"
|
||||
description: "Run a full OpenWork workspace in the browser, hosted by OpenWork"
|
||||
---
|
||||
|
||||
<Warning>
|
||||
**Hosted access.** OpenWork Web is available to every organization when an OpenWork-hosted deployment explicitly enables it. A platform admin in Den `/admin` can also grant complimentary access to one specific organization while deployment-wide availability is off. Web billing must be configured before an owner or admin can purchase it. It costs **$50 per joined organization member per month**; pending invitations are not billed until they are accepted.
|
||||
</Warning>
|
||||
|
||||
OpenWork Web gives your team a full OpenWork workspace in the browser, hosted and managed for you — nothing to install. An organization owner or admin can purchase it directly from the organization dashboard.
|
||||
|
||||
## Deploy a hosted workspace
|
||||
|
||||
1. Open your org dashboard and go to `Shared Workspace`.
|
||||
2. Click `Add workspace`.
|
||||
3. Wait until the status becomes `Ready`.
|
||||
4. Click `Connect`.
|
||||
|
||||
From the connection drawer you can choose `Open in web` to use OpenWork Web directly in the browser, `Open in desktop`, or copy the `Connection URL` plus `Owner token` / `Client token`.
|
||||
|
||||
## Open it from the desktop app
|
||||
|
||||
The same hosted workspace also opens in the desktop app. From the connection
|
||||
drawer, click `Connect` and copy the `Connection URL` plus `Owner token` /
|
||||
`Client token`. In the desktop app, choose `Add workspace -> Connect custom
|
||||
remote`, paste the connection URL and token, then click `Connect remote`.
|
||||
|
||||
## When to use OpenWork Web
|
||||
|
||||
Use OpenWork Web when your team needs a hosted OpenWork runtime, a stable place for long-running tasks or messaging connectors, or one workspace that several people can open from the same org — especially for teammates who should not have to install anything.
|
||||
|
||||
## Notes
|
||||
|
||||
- `DEN_OPENWORK_WEB_ENABLED=true` makes OpenWork Web available to every organization. Missing or false remains fail-closed except for an organization with an explicit complimentary grant from a platform admin in Den `/admin`; organization mode, Stripe-variable presence, and ordinary organization metadata do not act as rollout signals.
|
||||
- A workspace must be `Ready` before it can be opened.
|
||||
- Hosted workspace limits follow your OpenWork plan.
|
||||
- If you prefer to self-host a runtime instead, run `openwork-server` and connect the desktop app to that server URL.
|
||||
@@ -0,0 +1,175 @@
|
||||
---
|
||||
title: "Microsoft Entra SCIM provisioning"
|
||||
description: "Provision OpenWork members and teams from Microsoft Entra ID"
|
||||
---
|
||||
|
||||
Use Microsoft Entra ID provisioning to create, update, and deactivate OpenWork
|
||||
members automatically. Entra security groups can also become OpenWork teams
|
||||
whose membership is managed by Entra.
|
||||
|
||||
## Before you start
|
||||
|
||||
You need:
|
||||
|
||||
- An OpenWork organization with Enterprise SSO and SCIM.
|
||||
- An OpenWork owner, or a member who can manage security configuration.
|
||||
- A working OpenWork SAML connection. Complete and test
|
||||
[Microsoft Entra SAML SSO](/cloud/sso-microsoft-entra) first.
|
||||
- A Microsoft Entra role that can manage enterprise applications and
|
||||
provisioning.
|
||||
- An Entra plan that supports assigning groups to enterprise applications if
|
||||
you want to synchronize groups.
|
||||
|
||||
Keep SSO enforcement off until you have tested both SAML sign-in and SCIM. Make
|
||||
sure at least one OpenWork owner can sign in through SAML before relying on
|
||||
Entra as the identity source.
|
||||
|
||||
## 1. Get the OpenWork connection values
|
||||
|
||||
In OpenWork, select the organization you want to connect, then open
|
||||
**Settings → SCIM**.
|
||||
|
||||
Confirm that the page shows **SAML/SSO active**, then:
|
||||
|
||||
1. Select **Create connector**. If a connector already exists, select
|
||||
**Rotate token** only when you intend to replace its current secret.
|
||||
2. Copy the **SCIM base URL**.
|
||||
3. Copy the **SCIM bearer token** immediately. OpenWork shows the full token
|
||||
only after creating or rotating it.
|
||||
4. To synchronize Entra groups as OpenWork teams, select **Enable team sync**
|
||||
under **Create teams from SCIM groups**.
|
||||
|
||||
You will enter these values in Entra:
|
||||
|
||||
| Entra field | Where to get the value |
|
||||
| --- | --- |
|
||||
| Tenant URL | OpenWork **Settings → SCIM → SCIM base URL**. It normally ends in `/api/auth/scim/v2`. |
|
||||
| Secret Token | The one-time **SCIM bearer token** shown after you create or rotate the connector. |
|
||||
|
||||
Treat the bearer token like a password. Do not put it in tickets, screenshots,
|
||||
or shared setup notes. Rotating it immediately invalidates the previous token,
|
||||
so update Entra at the same time.
|
||||
|
||||
## 2. Configure provisioning in Entra
|
||||
|
||||
In the [Microsoft Entra admin center](https://entra.microsoft.com):
|
||||
|
||||
1. Open **Entra ID → Enterprise apps**.
|
||||
2. Select the same enterprise application used for OpenWork SAML SSO.
|
||||
3. Open **Provisioning**.
|
||||
4. Select **Get started**, then choose **Automatic** provisioning.
|
||||
5. Open **Connectivity** or **Admin Credentials**, depending on the Entra
|
||||
interface shown for your tenant.
|
||||
6. Paste the OpenWork **SCIM base URL** into **Tenant URL**.
|
||||
7. Paste the OpenWork **SCIM bearer token** into **Secret Token**.
|
||||
8. Select **Test Connection** and wait for a successful result.
|
||||
9. Save the configuration.
|
||||
|
||||
If **Test Connection** fails, confirm that the URL has no extra path or trailing
|
||||
text and that the token has not been rotated since it was copied.
|
||||
|
||||
## 3. Review mappings and scope
|
||||
|
||||
Open **Attribute mapping** in the provisioning configuration. Entra should show
|
||||
mappings for both **Users** and **Groups**.
|
||||
|
||||
For a controlled rollout, set **Scope** to **Sync only assigned users and
|
||||
groups**. This limits provisioning to identities assigned under the enterprise
|
||||
application's **Users and groups** page.
|
||||
|
||||
Use a stable, unique email address for the SCIM user name. If your Entra user
|
||||
principal names use an `onmicrosoft.com` domain but users sign in to OpenWork
|
||||
with another email domain, verify which Entra attribute is mapped to
|
||||
`userName`. Keep the SAML NameID and SCIM identity consistent so Entra does not
|
||||
create a second OpenWork member for the same person.
|
||||
|
||||
## 4. Assign test users and groups
|
||||
|
||||
In the enterprise application, open **Users and groups** and select **Add
|
||||
user/group**.
|
||||
|
||||
Start with:
|
||||
|
||||
- One or two test users who can sign in through the configured SAML app.
|
||||
- One assigned security group containing those users.
|
||||
|
||||
Assignments control which objects are in scope when provisioning is set to
|
||||
**Sync only assigned users and groups**. Creating a group elsewhere in Entra is
|
||||
not enough; assign it to the OpenWork enterprise application as well.
|
||||
|
||||
## 5. Test with Provision on demand
|
||||
|
||||
Open **Provisioning → Provision on demand**.
|
||||
|
||||
Test a user first:
|
||||
|
||||
1. Search for and select an assigned test user.
|
||||
2. Select **Provision**.
|
||||
3. Confirm that import, scope evaluation, matching, and the final action all
|
||||
succeed.
|
||||
4. In OpenWork, open **Members** and confirm that the member appears with the
|
||||
expected email.
|
||||
|
||||
Then test a group:
|
||||
|
||||
1. Make sure the test group already contains at least one assigned test user.
|
||||
2. Search for and select the assigned group.
|
||||
3. Under **Selected users**, explicitly select up to five members to include in
|
||||
the on-demand test.
|
||||
4. Select **Provision**.
|
||||
5. Open the result's **Group membership operations** tab and confirm each member
|
||||
shows **Add member — Success**.
|
||||
6. In OpenWork, open **Members → Teams** and confirm that the group appears as a
|
||||
SCIM-managed team with the expected members.
|
||||
|
||||
<Note>
|
||||
Entra's on-demand group workflow can return a generic internal server error for
|
||||
an empty group, or when no members are selected. This can happen before Entra
|
||||
sends a group-create request to OpenWork. Add members and select them explicitly
|
||||
for the on-demand test. Entra's regular background provisioning cycle can still
|
||||
synchronize empty assigned groups.
|
||||
</Note>
|
||||
|
||||
## 6. Start scheduled provisioning
|
||||
|
||||
After the on-demand tests pass:
|
||||
|
||||
1. Return to **Provisioning → Overview**.
|
||||
2. Set **Provisioning Status** to **On**, or select **Start provisioning** in
|
||||
the newer Entra interface.
|
||||
3. Save the change.
|
||||
4. Check **Provisioning logs** after the first cycle completes.
|
||||
5. Refresh OpenWork **Members** and **Teams** to verify the final state.
|
||||
|
||||
Regular provisioning is incremental. A newly assigned object might not appear
|
||||
until the next cycle; use the Entra provisioning logs to distinguish a pending
|
||||
cycle from a failed operation.
|
||||
|
||||
## Verify lifecycle changes
|
||||
|
||||
Before expanding the assignment scope, verify the complete lifecycle:
|
||||
|
||||
1. Remove a test user from the Entra group and confirm that OpenWork removes the
|
||||
user from the SCIM-managed team.
|
||||
2. Disable or unassign a test user and confirm the expected OpenWork
|
||||
deprovisioning behavior.
|
||||
3. Add another assigned group and confirm that OpenWork creates a second
|
||||
SCIM-managed team.
|
||||
|
||||
Change SCIM-managed teams in Entra, not in OpenWork. OpenWork keeps manually
|
||||
created teams separate from identity-provider-managed teams.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Symptom | Likely cause | Fix |
|
||||
| --- | --- | --- |
|
||||
| **Test Connection** fails | Incorrect Tenant URL, expired or rotated token, or copied whitespace | Copy the base URL and a current token again from OpenWork, then retest |
|
||||
| User or group is not found in **Provision on demand** | The object is outside the provisioning scope | Assign it under the enterprise application's **Users and groups** page |
|
||||
| Entra will not allow a group assignment | The tenant plan does not support assigning groups to enterprise applications | Enable a suitable Entra plan or test with directly assigned users |
|
||||
| On-demand group provisioning reports an internal server error without an OpenWork request | The group is empty or no members were selected for the on-demand run | Add members, select up to five under **Selected users**, and retry |
|
||||
| A group provisions but no OpenWork team appears | OpenWork team synchronization is disabled | Open **Settings → SCIM** and enable **Create teams from SCIM groups** |
|
||||
| Entra creates a duplicate OpenWork member | SAML and SCIM use different email identifiers | Align the SAML NameID and the SCIM `userName` mapping |
|
||||
| Entra reports success but OpenWork still shows the old state | The UI or incremental cycle has not refreshed | Refresh OpenWork, check Entra **Provisioning logs**, and review the SCIM health status in OpenWork |
|
||||
|
||||
For member and team behavior after provisioning, see
|
||||
[Members and RBAC](/cloud/members-and-rbac).
|
||||
@@ -0,0 +1,186 @@
|
||||
---
|
||||
title: "Okta SCIM provisioning"
|
||||
description: "Provision OpenWork members and teams from Okta"
|
||||
---
|
||||
|
||||
Use Okta Lifecycle Management to create, update, deactivate, and reactivate
|
||||
OpenWork members. Okta groups can also become SCIM-managed OpenWork teams.
|
||||
|
||||
## Before you start
|
||||
|
||||
You need:
|
||||
|
||||
- A working and domain-verified [Okta SAML SSO](/cloud/sso-okta) connection.
|
||||
- An OpenWork owner, or a member who can manage security configuration.
|
||||
- An Okta administrator who can configure provisioning for the SAML app.
|
||||
- An Okta plan that supports the provisioning and group-push features you
|
||||
intend to use.
|
||||
|
||||
Keep OpenWork SSO enforcement off until both SAML and a narrowly scoped SCIM
|
||||
test pass. Make sure at least one OpenWork owner can sign in through Okta.
|
||||
|
||||
## 1. Create the OpenWork SCIM connector
|
||||
|
||||
In OpenWork, select the organization, then open **Settings → SCIM**. Confirm
|
||||
the page reports that SAML/SSO is active, then:
|
||||
|
||||
1. Select **Create connector**.
|
||||
2. Copy the **SCIM base URL**.
|
||||
3. Copy the bearer token immediately. OpenWork displays the full token only
|
||||
after creating or rotating the connector.
|
||||
4. Leave **Create teams from SCIM groups** off until user provisioning works.
|
||||
|
||||
Treat the bearer token like a password. Do not put it in tickets, screenshots,
|
||||
logs, or documentation. Rotating it invalidates the previous token, so update
|
||||
Okta during the same maintenance window.
|
||||
|
||||
<Note>
|
||||
Use the base URL OpenWork displays. In a split-host Helm deployment it may use
|
||||
the web auth origin even when a separate Den API origin also exists. Do not
|
||||
rewrite the hostname.
|
||||
</Note>
|
||||
|
||||
## 2. Enable SCIM on the Okta application
|
||||
|
||||
Use the same Okta application as SAML SSO:
|
||||
|
||||
1. Open **Applications → Applications → OpenWork**.
|
||||
2. Open **General** and edit **App Settings**.
|
||||
3. Enable **SCIM** under **Provisioning**, then save.
|
||||
4. Open the new **Provisioning** tab.
|
||||
5. Select **Configure API Integration**.
|
||||
6. Enable **API Integration**.
|
||||
|
||||
Enter:
|
||||
|
||||
| Okta field | Value |
|
||||
| --- | --- |
|
||||
| SCIM connector base URL | OpenWork **SCIM base URL** |
|
||||
| Unique identifier field for users | `userName` |
|
||||
| Supported provisioning actions | Push new users and push profile updates. Add push groups after user provisioning works |
|
||||
| Authentication mode | HTTP Header |
|
||||
| Authorization | OpenWork SCIM bearer token |
|
||||
|
||||
Select **Test API Credentials** or **Test Connector Configuration**, depending
|
||||
on your Okta interface. Save only after Okta reports **Connector configured
|
||||
successfully**. For the initial user-only setup, Okta should detect **Create
|
||||
Users** and **Update User Attributes**. It is expected for import and group
|
||||
features to remain undetected when you have not selected them.
|
||||
|
||||
## 3. Configure provisioning to OpenWork
|
||||
|
||||
Open **Provisioning → To App**, then enable:
|
||||
|
||||
- **Create Users**
|
||||
- **Update User Attributes**
|
||||
- **Deactivate Users**
|
||||
|
||||
Do not enable password synchronization. SCIM creates federated membership; it
|
||||
does not create an OpenWork password credential.
|
||||
|
||||
Review the attribute mappings. At minimum, keep these identity values aligned:
|
||||
|
||||
| Okta value | SCIM attribute |
|
||||
| --- | --- |
|
||||
| Stable work email or Okta username | `userName` |
|
||||
| Primary work email | `emails[type eq "work"].value` |
|
||||
| First name | `name.givenName` |
|
||||
| Last name | `name.familyName` |
|
||||
| Display name | `displayName` |
|
||||
|
||||
The SAML NameID and SCIM `userName` must identify the same person. A mismatch
|
||||
can create a duplicate OpenWork member instead of linking SSO and SCIM.
|
||||
|
||||
## 4. Provision one test user
|
||||
|
||||
For the cleanest first test, use a user who has never been assigned or
|
||||
provisioned to this Okta application:
|
||||
|
||||
1. Open the Okta application's **Assignments** tab.
|
||||
2. Assign one test user and confirm their application username is the same
|
||||
email used for SAML NameID and SCIM `userName`.
|
||||
3. Wait for Okta's automatic provisioning job to finish. Do not repeatedly
|
||||
edit the assignment or trigger provisioning while the job is running.
|
||||
4. Open Okta **Reports → System Log** and confirm a single **Push new user to
|
||||
external application** event succeeds.
|
||||
5. In OpenWork, open **Members** and verify the same member appears without a
|
||||
second password-backed account.
|
||||
|
||||
If the user was already assigned for the earlier SAML test, Okta may show a
|
||||
**Provision User** action after you enable provisioning. Select it once, then
|
||||
wait and inspect the System Log before taking another action. A second request
|
||||
can race the first successful create and return `409 User already exists`.
|
||||
|
||||
Do not broaden assignments until the test user's create, update, deactivate,
|
||||
and reactivate lifecycle works.
|
||||
|
||||
## 5. Verify the user lifecycle
|
||||
|
||||
For the assigned test user, verify:
|
||||
|
||||
1. Changing the profile name in Okta updates the OpenWork member.
|
||||
2. Unassigning or deactivating the user removes active organization access.
|
||||
3. The user cannot regain access through password sign-up while SCIM marks the
|
||||
identity inactive.
|
||||
4. Reactivating and reassigning the user restores the same OpenWork identity.
|
||||
5. An unrelated OpenWork member remains unchanged throughout the test.
|
||||
|
||||
OpenWork retains a disconnected member record for organization history. The
|
||||
global user is deleted only after their final active organization membership
|
||||
is removed.
|
||||
|
||||
## 6. Push Okta groups as OpenWork teams
|
||||
|
||||
After user provisioning works:
|
||||
|
||||
1. In OpenWork **Settings → SCIM**, enable
|
||||
**Create teams from SCIM groups**.
|
||||
2. In Okta, open the application's **Push Groups** tab.
|
||||
3. Select **Push Groups** and choose a small test group by name or rule.
|
||||
4. Confirm Okta creates the group and pushes its current membership.
|
||||
5. In OpenWork, open **Members → Teams** and verify a team labeled
|
||||
**Managed by SCIM** appears with the expected members.
|
||||
6. Add and remove one member in Okta and verify the OpenWork team follows.
|
||||
|
||||
Manage pushed groups in Okta, not in OpenWork. Manually created OpenWork teams
|
||||
remain independent.
|
||||
|
||||
## 7. Expand scope and enforce SSO
|
||||
|
||||
After the lifecycle and group tests pass:
|
||||
|
||||
1. Assign the intended Okta users and groups to the application.
|
||||
2. Review Okta provisioning events for failures.
|
||||
3. Review OpenWork's SCIM health and unresolved-failure count.
|
||||
4. Confirm at least one owner has working Okta SAML access.
|
||||
5. Only then consider enabling **Require SSO for this organization**.
|
||||
|
||||
## When to run reconciliation
|
||||
|
||||
Use **Settings → SCIM → Run reconciliation** to check OpenWork's local
|
||||
SCIM-managed identities for inconsistent organization membership or missing
|
||||
provider-account state. OpenWork records unresolved drift for retry or manual
|
||||
review.
|
||||
|
||||
Reconciliation does not import users from Okta, force Okta to provision an
|
||||
assigned user, or clear an Okta application-assignment error. Use the Okta
|
||||
Assignments page and System Log for outbound provisioning. When OpenWork has no
|
||||
local drift to process, reconciliation may complete without a visible member
|
||||
change.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Symptom | Likely cause | Fix |
|
||||
| --- | --- | --- |
|
||||
| **Create connector** is disabled | OpenWork does not see an enabled SSO provider | Complete and test Okta SAML first; if SAML returns `invalid_saml_configuration`, upgrade and save the connection again |
|
||||
| **Test API Credentials** fails | Incorrect base URL, stale token, whitespace, or a rewritten hostname | Copy both values again from OpenWork and use the advertised base URL unchanged |
|
||||
| Okta reports `409 User already exists` | The user existed before SCIM, or more than one create job ran for the same assignment | Inspect the System Log first. If a successful create immediately precedes the conflict and the exact member exists in OpenWork, stop triggering **Provision User**. Use a never-before-provisioned user for the clean lifecycle test |
|
||||
| Okta creates a duplicate member | SAML NameID and SCIM `userName` differ | Map both to the same stable work email |
|
||||
| Assignment succeeds but no member appears | To App provisioning is disabled or the user is outside assignment scope | Enable **Create Users** and confirm the person is assigned to the application |
|
||||
| **Run reconciliation** does not provision an assigned user | Reconciliation only checks OpenWork's local SCIM identity state | Trigger and inspect outbound provisioning in Okta instead |
|
||||
| Deactivation does not remove access | **Deactivate Users** is disabled or Okta has not sent the event | Enable it and inspect the Okta System Log and OpenWork SCIM health |
|
||||
| A pushed group does not create a team | OpenWork team synchronization is off | Enable **Create teams from SCIM groups**, then retry the Okta group push |
|
||||
| A group is pushed without members | Its members are not assigned or provisioned to the application | Assign and provision the users before validating group membership |
|
||||
|
||||
For member and team behavior after provisioning, see
|
||||
[Members and RBAC](/cloud/members-and-rbac).
|
||||
@@ -0,0 +1,135 @@
|
||||
---
|
||||
title: "Security and operations"
|
||||
description: "Review OpenWork Cloud security posture, access governance, and availability expectations"
|
||||
---
|
||||
|
||||
This page summarizes the security and operational controls that matter when a team runs OpenWork Cloud or a private OpenWork deployment.
|
||||
|
||||
OpenWork is local-first, but Cloud adds shared identity, members, RBAC, hosted workers, shared providers, and organization policy. Treat Cloud as the control plane for team access and shared runtime state.
|
||||
|
||||
## Encryption and transport
|
||||
|
||||
Production deployments should enforce encrypted transport and encrypted storage:
|
||||
|
||||
- Use HTTPS for the Cloud app, Den API, OAuth callbacks, webhook ingress, and worker-control traffic.
|
||||
- Terminate TLS at the edge or load balancer and forward only through trusted private networking.
|
||||
- Enable encryption at rest for the production database, object storage, backups, and logs that may contain operational metadata.
|
||||
- Keep local development defaults local-only. Example development database URLs and local secrets in repository scripts are not production settings.
|
||||
|
||||
OpenWork Cloud's hosted Den database runs on PlanetScale. PlanetScale encrypts database storage and backups at rest, and PlanetScale database connection strings require TLS before SQL commands can be issued. OpenWork's production migration workflow also configures strict TLS verification for PlanetScale MySQL-protocol connections.
|
||||
|
||||
OpenWork also encrypts selected sensitive database columns, such as provider secrets and SSO configuration, with application-level AES-256-GCM encryption using `DEN_DB_ENCRYPTION_KEY`. This is an additional control and does not replace provider-managed storage encryption at rest.
|
||||
|
||||
For private or self-hosted deployments, the operator is responsible for enabling TLS certificates, database storage encryption, backup encryption, and private network policy in the target environment.
|
||||
|
||||
## Secrets and keys
|
||||
|
||||
Production secrets should live in a managed secrets system or KMS-backed environment, not in committed files or shared shell history.
|
||||
|
||||
Use separate secrets per environment for:
|
||||
|
||||
- Better Auth signing and session secrets
|
||||
- database encryption material
|
||||
- OAuth, SSO, SCIM, and webhook secrets
|
||||
- provider API keys and worker provisioning tokens
|
||||
|
||||
OpenWork stores organization API keys and SCIM bearer tokens as hashes. SSO provider configuration is stored encrypted. Rotate production secrets on a defined schedule and immediately after suspected exposure or emergency access use. SCIM token rotation invalidates the previous bearer token; update the identity provider immediately after rotating.
|
||||
|
||||
## Identity and access governance
|
||||
|
||||
Use the Cloud member model as the source of truth for organization access:
|
||||
|
||||
- Keep at least two `Owner` members.
|
||||
- Use `Admin` for day-to-day operators.
|
||||
- Keep most people as `Member`.
|
||||
- Use custom roles only when the built-in roles are too broad.
|
||||
- Use teams for resource access to Collections and providers.
|
||||
|
||||
SSO just-in-time provisioning uses `Member` as the safe default for first-login users. OpenWork does not convert IdP attributes such as `role`, `groups`, or `admin` into organization roles during SSO sign-in. Elevation to `Admin`, `Owner`, or a custom role should be an explicit, reviewed, and audited change in OpenWork.
|
||||
|
||||
For access reviews, export the current member list, roles, teams, pending invitations, API keys, SSO configuration, and SCIM configuration on a regular cadence. Reconfirm that each member still needs their current role and team access.
|
||||
|
||||
## MFA and step-up controls
|
||||
|
||||
For production organizations, use SSO with identity-provider MFA as the primary step-up control:
|
||||
|
||||
- Enable enforced SSO for organizations that have owners, admins, SCIM, SSO, API keys, or shared production providers.
|
||||
- Require MFA, phishing-resistant factors where available, and conditional-access policy in the identity provider before users reach OpenWork Cloud.
|
||||
- Keep SSO, SCIM, and API-key management behind the `security_configuration.manage` permission. Owners receive that permission by default and can delegate it through a custom role; default admins do not receive it automatically.
|
||||
- Invitation role assignment is bounded by the inviter's own permissions, so default admins can invite standard admins or members but cannot grant delegated security-configuration access unless they already hold that permission.
|
||||
- The server requires a session created in the last 15 minutes before privileged routes can change SSO, SCIM, API keys, organization settings, custom roles, member roles, billing, or inference settings.
|
||||
- Do not rely on standalone email/password accounts for production owner access unless the deployment has an equivalent MFA and recovery control outside OpenWork.
|
||||
|
||||
Native factor enrollment and in-product reauthentication should be treated as product work before replacing SSO-enforced MFA as the production control.
|
||||
|
||||
## Standard email/password protections
|
||||
|
||||
The standard OpenWork Cloud email/password path is protected server-side against high-volume guessing and known-compromised passwords:
|
||||
|
||||
- Email/password sign-in is rate limited. The default sign-in rule allows 5 attempts in a 5-minute window, in addition to the global Better Auth rate limit.
|
||||
- Failed email/password sign-ins are tracked by a hashed account identifier. After 5 recent failures, the account enters progressive lockout: 5 minutes first, then longer lockouts for continued failures, capped at 1 hour. Successful sign-in clears the failure counter.
|
||||
- Password creation and password reset requests screen the proposed password against the Have I Been Pwned Pwned Passwords range API. OpenWork sends only the first 5 characters of the SHA-1 password hash, uses the k-anonymity response to compare suffixes locally, and rejects known-compromised passwords.
|
||||
- If password screening is temporarily unavailable, password creation or reset fails closed and asks the user to try again later.
|
||||
|
||||
OpenWork does not call the breached-password service during normal sign-in. Existing-password sign-in should be protected by rate limiting, lockout, identity-provider controls, monitoring, and a forced reset process if a credential is later suspected to be compromised.
|
||||
|
||||
Private deployments without external egress can disable breached-password screening with `DEN_PASSWORD_BREACH_SCREENING_ENABLED=false`. The Helm chart sets `config.auth.passwordBreachScreeningEnabled: "false"` by default for isolated self-hosted installs. This removes the external range API dependency and the known-compromised password rejection.
|
||||
|
||||
For mover and leaver handling:
|
||||
|
||||
- Change roles when responsibilities change.
|
||||
- Remove members when access is no longer needed.
|
||||
- Cancel stale invitations.
|
||||
- Rotate or revoke organization API keys that were issued for the affected member.
|
||||
- Prefer SCIM deprovisioning where an identity provider is the system of record.
|
||||
|
||||
SCIM lifecycle failures should not be treated as successful offboarding. OpenWork records failed SCIM provisioning and deprovisioning work for retry, runs periodic local SCIM drift reconciliation, and surfaces unresolved SCIM health issues in the organization SCIM screen. Operators should monitor those health indicators and route Den API logs that include `[scim][sync_failure_recorded]` or `[scim][maintenance_failed]` to their alerting system.
|
||||
|
||||
## Emergency access
|
||||
|
||||
Plan emergency access before it is needed.
|
||||
|
||||
Recommended pattern:
|
||||
|
||||
- Maintain a break-glass `Owner` account or equivalent recovery path.
|
||||
- Protect it with the strongest authentication available in the deployment.
|
||||
- Store credentials in a controlled emergency vault.
|
||||
- Alert on any use.
|
||||
- Review the audit trail after use.
|
||||
- Rotate credentials or signing material if emergency access exposed them.
|
||||
|
||||
Do not use break-glass accounts for routine administration.
|
||||
|
||||
## Audit events
|
||||
|
||||
OpenWork records structured organization audit events for privileged Cloud actions, including:
|
||||
|
||||
- API key creation and deletion
|
||||
- member role changes and removals
|
||||
- invitation creation, refresh, and cancellation
|
||||
- custom role creation, updates, and deletion
|
||||
- SCIM token rotation and connection deletion
|
||||
- SSO registration and deletion
|
||||
|
||||
Audit payloads avoid bearer tokens, API key secrets, SCIM tokens, SAML certificates, and OIDC client secrets. Sensitive access changes also emit structured `[audit-alert]` log lines so operators can route SSO, SCIM, API-key, role, invitation, and member-change events into alerting. For private deployments, export audit data and audit-alert logs through the deployment's database or logging pipeline into the organization's retention and SIEM process.
|
||||
|
||||
## Availability and failover
|
||||
|
||||
OpenWork Cloud treats Den API as the logical control plane for identity, RBAC, hosted workers, shared providers, and organization policy. The hosted deployment avoids making that logical control plane a single physical chokepoint:
|
||||
|
||||
- Render runs the Cloud app and Den API as scaled services with a minimum of two instances for production traffic.
|
||||
- Render load balances requests across healthy instances, stops routing to instances that fail health checks, and restarts failed instances.
|
||||
- Den API instances are stateless application replicas; shared state lives in PlanetScale and external managed services.
|
||||
- PlanetScale production clusters provide one primary and at least two replicas across three availability zones, with automated failover managed by PlanetScale.
|
||||
- PlanetScale automated backups run at least every 12 hours on the Base plan; operators should also define retention, restore testing, and point-in-time recovery expectations for their plan.
|
||||
|
||||
Production and private deployments should avoid a single-node control plane:
|
||||
|
||||
- Run the Cloud app and Den API behind health checks.
|
||||
- Run more than one API instance where the platform supports it.
|
||||
- Use a managed database with automated backups and failover, such as PlanetScale production branches or an equivalent MySQL-compatible HA service.
|
||||
- Monitor API health, worker provisioning, queue depth, webhook failures, sign-in failures, and database connectivity.
|
||||
- Define recovery objectives for the database, object storage, and worker state.
|
||||
- Test restore and failover procedures before an incident.
|
||||
|
||||
For self-hosted deployments, document the exact topology: ingress, app, API, worker provisioning, database, backups, failover owner, restore owner, and the person or team responsible for each part.
|
||||
@@ -0,0 +1,37 @@
|
||||
---
|
||||
title: "Collections"
|
||||
description: "Group plugins into reusable Cloud assignments for teams and members"
|
||||
---
|
||||
|
||||
Collections are the advanced OpenWork Cloud tool for grouping multiple plugins and assigning them as one managed set. Use a Collection when the same team or members need several plugins, when you expect the set to change, or when individual plugin grants would become difficult to maintain.
|
||||
|
||||
For the simplest one-skill publishing flow, start with [Publish and copy a skill](/start-here/do-work-with-it/publish-and-copy-a-skill).
|
||||
|
||||
## How Collections work
|
||||
|
||||
- A plugin contains skills and can also contain other extension resources.
|
||||
- A Collection contains one or more plugins.
|
||||
- A Collection can be granted to everyone in the organization, selected teams, or individual members.
|
||||
- The plugin creator keeps access automatically.
|
||||
- Adding, updating, or removing a plugin changes the managed set for the Collection's audience. Members can use `Refresh` in `Settings > Library` if a recent change has not appeared yet.
|
||||
|
||||
## Create and assign a Collection
|
||||
|
||||
1. Open `Collections` in the OpenWork Cloud dashboard.
|
||||
2. Click `New collection`, enter a clear name and optional description, then click `Create collection`.
|
||||
3. Open `Plugin Directory` and create or edit each plugin that belongs in the Collection.
|
||||
4. In each plugin's `Share` section, choose the Collection.
|
||||
5. Return to the Collection and use `Who can access this` to add teams or individual members, or turn on access for everyone in the organization.
|
||||
6. Ask a member to open `Settings > Library` in the desktop app and confirm the assigned plugins and skills are present.
|
||||
|
||||
Only owners and admins can create Collections and publish skills for other people by default.
|
||||
|
||||
## Keep complex setups manageable
|
||||
|
||||
- Create Collections around a stable audience or purpose, such as `Sales Enablement` or `Engineering Operations`.
|
||||
- Put plugins used by the same audience in one Collection so one access change covers the whole set.
|
||||
- Prefer team grants over many individual grants when membership changes often.
|
||||
- Use separate Collections when audiences or update schedules differ.
|
||||
- Review Collection access before adding a plugin that reaches sensitive systems.
|
||||
|
||||
For a complete organization walkthrough, see [Team quickstart](/cloud/team-quickstart). For managed service accounts and MCP servers, use [Connectors](/cloud/share-with-your-team/shared-mcp-connections) rather than placing credentials in a skill.
|
||||
@@ -0,0 +1,137 @@
|
||||
---
|
||||
title: "Adding a custom LLM provider"
|
||||
description: "Create a custom provider definition in OpenWork Cloud and import it into the desktop app"
|
||||
---
|
||||
|
||||
Use this when the provider is not in the catalog. The Cloud dashboard stores the shared credential once and then lets desktop workspaces import it.
|
||||
|
||||
<Frame>
|
||||

|
||||
</Frame>
|
||||
|
||||
## Create the custom provider
|
||||
|
||||
1. Open `LLM Providers`.
|
||||
2. Click `Add Provider`.
|
||||
3. Switch to `Custom provider`.
|
||||
4. Give it a `Name` (the `Provider ID` fills in automatically).
|
||||
5. Enter the `Base URL` of the OpenAI-compatible endpoint (for Azure AI Foundry this looks like `https://my-resource.openai.azure.com/openai/v1`).
|
||||
6. List the `Model IDs` the endpoint serves — on Azure AI Foundry these are your deployment names.
|
||||
7. Paste the shared `API key / credential`.
|
||||
8. Choose `People access` and/or `Team access`.
|
||||
9. Click `Create Provider`.
|
||||
|
||||
That's it — no JSON required for OpenAI-compatible endpoints.
|
||||
|
||||
### Advanced: raw JSON config
|
||||
|
||||
If the provider needs more than the form covers (a different AI SDK package, per-model limits or modalities, custom options), click `Advanced: edit as JSON` and provide a models.dev-style definition. The JSON must include `id`, `name`, `npm`, `env`, and `models`; `api` is optional, but most OpenAI-compatible providers use it. The editor requires valid JSON, at least one environment variable, and at least one model.
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "custom-provider",
|
||||
"name": "Custom Provider",
|
||||
"npm": "@ai-sdk/openai-compatible",
|
||||
"env": [
|
||||
"CUSTOM_PROVIDER_API_KEY"
|
||||
],
|
||||
"doc": "https://example.com/docs/models",
|
||||
"api": "https://api.example.com/v1",
|
||||
"models": [
|
||||
{
|
||||
"id": "custom-provider/example-model",
|
||||
"name": "Example Model",
|
||||
"attachment": false,
|
||||
"reasoning": false,
|
||||
"tool_call": true,
|
||||
"structured_output": true,
|
||||
"temperature": true,
|
||||
"release_date": "2026-01-01",
|
||||
"last_updated": "2026-01-01",
|
||||
"open_weights": false,
|
||||
"limit": {
|
||||
"context": 128000,
|
||||
"input": 128000,
|
||||
"output": 8192
|
||||
},
|
||||
"modalities": {
|
||||
"input": [
|
||||
"text"
|
||||
],
|
||||
"output": [
|
||||
"text"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Import it into the desktop app
|
||||
|
||||
1. Open `Settings > Cloud Providers`.
|
||||
2. Choose the correct `Active org`.
|
||||
3. Under `Cloud providers`, click `Import`.
|
||||
4. Reload the workspace when OpenWork asks.
|
||||
|
||||
### Functional example: an LLM gateway (Infron)
|
||||
|
||||
An LLM gateway is one OpenAI-compatible endpoint that fans out to many model providers. If you want to run the routing yourself, [LiteLLM](https://infron.ai/docs/frameworks-and-integrations/litellm) is a good starting point. [Infron](https://infron.ai) is the hosted option: one API key gets you every model in its [marketplace](https://infron.ai/models) with automatic provider fallbacks and a single invoice, so adding a new LLM to OpenWork is just another entry under `models`.
|
||||
|
||||
Grab a key from the [API Keys dashboard](https://infron.ai/dashboard/apiKeys). If you don't have an account yet, sign up at [infron.ai/login](https://infron.ai/login) and their [quickstart](https://infron.ai/docs/overview/quickstart/text) walks you through your first request.
|
||||
|
||||
Paste the key into `API key / credential` and use this JSON:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "infron",
|
||||
"name": "Infron",
|
||||
"npm": "@ai-sdk/openai-compatible",
|
||||
"env": [
|
||||
"INFRON_API_KEY"
|
||||
],
|
||||
"doc": "https://infron.ai/docs/frameworks-and-integrations/openwork",
|
||||
"api": "https://llm.onerouter.pro/v1",
|
||||
"models": [
|
||||
{
|
||||
"id": "deepseek/deepseek-v3.2",
|
||||
"name": "DeepSeek V3.2",
|
||||
"attachment": false,
|
||||
"reasoning": true,
|
||||
"tool_call": true,
|
||||
"structured_output": true,
|
||||
"temperature": true,
|
||||
"release_date": "2025-09-29",
|
||||
"last_updated": "2025-09-29",
|
||||
"open_weights": true,
|
||||
"limit": {
|
||||
"context": 128000,
|
||||
"input": 128000,
|
||||
"output": 8192
|
||||
},
|
||||
"modalities": {
|
||||
"input": ["text"],
|
||||
"output": ["text"]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Once imported, Infron models show up in the Chat model picker alongside everything else:
|
||||
|
||||
<Frame>
|
||||

|
||||
</Frame>
|
||||
|
||||
Pick the model and it's live in the session footer:
|
||||
|
||||
<Frame>
|
||||

|
||||
</Frame>
|
||||
|
||||
Add more entries under `models` to expose other routes the gateway supports (e.g. `openai/gpt-5.4`, `google/gemini-2.5-flash`).
|
||||
|
||||
## When to use a cloud provider
|
||||
|
||||
Use a Cloud provider when the setup is meant to be shared across an org or team. For solo use, configuring it directly in the desktop app is simpler.
|
||||
@@ -0,0 +1,29 @@
|
||||
---
|
||||
title: "Desktop policies"
|
||||
description: "Use OpenWork Cloud to control desktop app capabilities for members and teams."
|
||||
---
|
||||
|
||||
Desktop policies let organization owners and admins manage what the OpenWork desktop app allows after a member signs in to Cloud. Policies can apply to the whole organization through the default policy, or to specific members and teams through assigned policies. Policies can also provide [team prompt cards](/cloud/share-with-your-team/team-prompt-cards). The desktop app caches the effective policy, applies it on reload, and refreshes it when the active Cloud organization changes, the Cloud account changes, or the hourly desktop-config refresh runs.
|
||||
|
||||
## What policies control
|
||||
|
||||
Current desktop policy keys map to concrete product capabilities:
|
||||
|
||||
- `Custom providers`: allow or block locally added model providers that were not deployed through OpenWork Cloud.
|
||||
- `Enable OpenCode Zen Models`: allow or block built-in OpenCode models.
|
||||
- `Multiple workspaces`: allow or block creating or configuring more than one local workspace.
|
||||
- `Control Settings`: allow or block changing desktop settings.
|
||||
- `Manage Extensions`: allow or block local extension management.
|
||||
- `Built-in Extensions`: allow or block OpenWork-provided built-in extensions, including browser, image, and local-provider extensions.
|
||||
- `Welcome Page`: show or hide the getting-started page for new users.
|
||||
|
||||
## Configure a policy
|
||||
|
||||
1. Open [app.openworklabs.com](https://app.openworklabs.com/) and choose your organization.
|
||||
2. Go to `Desktop policies`.
|
||||
3. Create a new policy or edit the default policy.
|
||||
4. Turn capabilities on or off.
|
||||
5. Assign the policy to members or teams when it should not apply to everyone.
|
||||
6. Save the policy, then have members reload, refresh their Cloud account, switch the active organization, or wait for the hourly desktop-config refresh.
|
||||
|
||||
The desktop app explains blocked capabilities as organization-controlled. For example, a built-in extension disabled by policy is hidden from the normal catalog and can appear in hidden views with a `Disabled by organization` explanation.
|
||||
@@ -0,0 +1,40 @@
|
||||
---
|
||||
title: "Adding a managed LLM provider"
|
||||
description: "Create an org-managed provider in OpenWork Cloud and import it into the desktop app"
|
||||
---
|
||||
|
||||
Use `Models > Bring your Own Keys` when you want the organization to manage the credential, model list, and access rules once, then make the provider available to desktop workspaces.
|
||||
|
||||
## Create the provider in OpenWork Cloud
|
||||
|
||||
1. Open `Models > Bring your Own Keys`.
|
||||
2. Click `Add Provider`.
|
||||
3. Stay on `Catalog provider`.
|
||||
4. Choose the provider in `Provider`.
|
||||
5. Select the models the org should expose.
|
||||
6. Paste the shared `API key / credential`.
|
||||
7. Choose `People access` and/or `Team access`.
|
||||
8. Click `Create Provider`.
|
||||
|
||||
<Frame>
|
||||

|
||||
</Frame>
|
||||
|
||||
|
||||
## Import it into the desktop app
|
||||
|
||||
1. Open `Settings > Cloud Providers`.
|
||||
2. Choose the correct `Active org`.
|
||||
3. Under `Cloud providers`, click `Import`.
|
||||
4. Reload the workspace when OpenWork asks.
|
||||
|
||||
OpenWork writes the imported provider into the workspace `opencode.jsonc` and stores the org credential for that workspace.
|
||||
|
||||
## Update or remove it later
|
||||
|
||||
- Use `Sync` after changing models or access in OpenWork Cloud.
|
||||
- Use `Remove` to stop managing the imported provider from this workspace.
|
||||
- If the provider was deleted in Cloud, the desktop app shows `Removed from cloud` and lets you `Uninstall` the local config.
|
||||
- The provider must have a stored org credential before the desktop app can import it.
|
||||
|
||||
If you only need a one-off local setup, use the desktop guide at [Adding a custom LLM provider/model](/start-here/connect-your-stack/add-a-custom-llm).
|
||||
@@ -0,0 +1,128 @@
|
||||
---
|
||||
title: "Per-member LLM credentials"
|
||||
description: "Bind a separate write-only provider credential to each organization member"
|
||||
---
|
||||
|
||||
Per-member credential bindings let an organization grant one managed LLM provider to many people without sharing one upstream key. Use this when an external gateway, such as LiteLLM, issues a distinct virtual key for each member.
|
||||
|
||||
This page describes an API integration pattern. LiteLLM provisioning and metadata discovery are not built into Den; run your own provisioner against Den's generic provider and credential-binding routes plus the LiteLLM API.
|
||||
|
||||
## Choose a credential mode
|
||||
|
||||
Set `credentialMode` when you create or update an organization LLM provider:
|
||||
|
||||
- `shared` stores one provider credential. Every granted member receives that credential from the connect route.
|
||||
- `per_member` resolves a separate binding for the calling organization member. The provider can be granted before a binding exists.
|
||||
|
||||
Create a per-member custom provider with `POST /v1/llm-providers`:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Per-member gateway",
|
||||
"source": "custom",
|
||||
"customConfig": {
|
||||
"id": "per-member-gateway",
|
||||
"name": "Per-member gateway",
|
||||
"npm": "@ai-sdk/openai-compatible",
|
||||
"env": ["GATEWAY_API_KEY"],
|
||||
"api": "https://gateway.example.com/v1",
|
||||
"models": [{ "id": "team-model", "name": "Team model" }]
|
||||
},
|
||||
"credentialMode": "per_member",
|
||||
"allMembers": true
|
||||
}
|
||||
```
|
||||
|
||||
Send the organization bearer token and `x-openwork-org-id` header on these requests.
|
||||
|
||||
## Member flow
|
||||
|
||||
A granted member can manage only their own write-only material:
|
||||
|
||||
- `PUT /v1/llm-providers/:id/my-credential` with exactly one of `{ "apiKey": "..." }` or `{ "apiKeys": { "ENV_NAME": "..." } }`
|
||||
- `DELETE /v1/llm-providers/:id/my-credential`
|
||||
- `GET /v1/llm-providers/:id/connect` to obtain the provider configuration and their resolved credential
|
||||
|
||||
The connect route always returns HTTP `200` for a granted member. When the member has no active binding, it keeps the standard provider payload shape, leaves both credential fields null, and reports the member-specific state:
|
||||
|
||||
```json
|
||||
{
|
||||
"llmProvider": {
|
||||
"apiKey": null,
|
||||
"apiKeys": null,
|
||||
"memberCredential": { "state": "missing" }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`memberCredential.state` is one of `missing`, `active`, `blocked`, `stale`, or `error`. An active response carries that member's resolved `apiKey` or `apiKeys`; every other state carries null credentials. The missing or blocked state is payload data rather than an error status because published desktop clients fail the whole provider sync on any non-OK connect response. Older clients already degrade safely by skipping a provider whose connect payload has no usable credential.
|
||||
|
||||
## Admin and provisioner flow
|
||||
|
||||
Organization owners and admins can operate a central provisioner with these routes:
|
||||
|
||||
- `GET /v1/llm-providers/:id/member-credentials` lists every granted membership's state, version, and external identifiers. It never returns credential material.
|
||||
- `PUT /v1/llm-providers/:id/member-credentials/:orgMembershipId` stores one member's credential. The body accepts `apiKey` or `apiKeys`, plus optional `externalPrincipalId`, `externalCredentialId`, and `expectedVersion`.
|
||||
- `POST /v1/llm-providers/:id/member-credentials/:orgMembershipId/block` marks an existing binding blocked.
|
||||
|
||||
Use `expectedVersion` when multiple provisioner workers may update the same binding. A mismatch returns HTTP `409` with `{ "error": "version_conflict" }`.
|
||||
|
||||
A blocked binding is admin-owned. A member cannot overwrite or delete it: member writes and deletes return HTTP `409` with `{ "error": "credential_blocked" }`. An admin `PUT` is the explicit unblock and replacement path.
|
||||
|
||||
## LiteLLM example
|
||||
|
||||
The runnable example at [`examples/litellm-per-member-keys`](https://github.com/different-ai/openwork/tree/dev/examples/litellm-per-member-keys) uses LiteLLM virtual keys. Configure the Den and LiteLLM URLs, admin tokens, provider ID, and model IDs listed in its README, then run:
|
||||
|
||||
```bash
|
||||
node provision.mjs reconcile
|
||||
```
|
||||
|
||||
The example first reads the exact provider from `GET /v1/llm-providers?scope=manageable`, verifies that it is a custom `per_member` provider, then queries LiteLLM `GET /model_group/info` with the master key. Every configured model must have an exact `model_group` match and finite, positive `max_input_tokens` and `max_output_tokens`. The provisioner uses those facts to update each Den model's context, input, and output limits. When present, it maps LiteLLM's function-calling, reasoning, vision, response-schema, and temperature facts to `tool_call`, `reasoning`, `attachment`, `structured_output`, and `temperature`. The replacement preserves the complete provider config, existing model fields, credential mode, and all member/team access.
|
||||
|
||||
This step fails closed before key creation if metadata or limits are missing. It never guesses a token limit or falls back to a generic value. The full-replacement Den PATCH omits `apiKey` and `apiKeys`, so Den preserves the write-only stored credential, and the example skips the PATCH entirely when the provider is already synchronized.
|
||||
|
||||
It then reconciles missing member credentials:
|
||||
|
||||
```js
|
||||
const { memberCredentials } = await den.get(
|
||||
`/v1/llm-providers/${providerId}/member-credentials`,
|
||||
)
|
||||
|
||||
for (const binding of memberCredentials) {
|
||||
if (binding.state !== "missing") continue
|
||||
|
||||
const keyAlias = `openwork-${binding.orgMembershipId}`
|
||||
const minted = await liteLlm.post("/key/generate", {
|
||||
models,
|
||||
key_alias: keyAlias,
|
||||
metadata: {
|
||||
openwork_org_membership_id: binding.orgMembershipId,
|
||||
},
|
||||
})
|
||||
|
||||
await den.put(
|
||||
`/v1/llm-providers/${providerId}/member-credentials/${binding.orgMembershipId}`,
|
||||
{
|
||||
apiKey: minted.key,
|
||||
externalCredentialId: minted.token_id,
|
||||
},
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
LiteLLM v1.97 returns a `token_id` that can address the virtual key without retaining its plaintext value. The example stores that identifier in `externalCredentialId`; Den's admin list can return it safely to the provisioner later.
|
||||
|
||||
The `/model_group/info` call and field mapping are deliberately implemented in this LiteLLM-specific example. Den core remains vendor-neutral: its provider PATCH and per-member credential APIs accept the resulting generic model configuration without hardcoding LiteLLM behavior.
|
||||
|
||||
### Offboard in the safe order
|
||||
|
||||
Always revoke or block the upstream credential **before** removing its local materialization. If the upstream call fails, leave the Den binding active so the failure remains visible and retryable rather than reporting a false local block.
|
||||
|
||||
For the example:
|
||||
|
||||
1. Read the member's `externalCredentialId` from `GET /v1/llm-providers/:id/member-credentials`.
|
||||
2. Call LiteLLM `POST /key/block` with `{ "key": "<token_id>" }` and verify success.
|
||||
3. Call Den `POST /v1/llm-providers/:id/member-credentials/:orgMembershipId/block`.
|
||||
4. Verify the member's connect request returns HTTP `200`, null credentials, and `memberCredential.state: "blocked"`.
|
||||
|
||||
Run `node provision.mjs offboard <orgMembershipId>` to perform that sequence. The integration is proved against a real LiteLLM database and a cold Den by `evals/specs/litellm-per-member-credentials.e2e.test.ts`.
|
||||
@@ -0,0 +1,147 @@
|
||||
---
|
||||
title: "Sharing MCP connections with your team"
|
||||
description: "Share Notion, Linear, Stripe, Exa, or any MCP server in OpenWork Cloud with Individual accounts by default, or One org account when everyone should act as the same account"
|
||||
---
|
||||
|
||||
Use `Connectors` when you want the organization to manage an MCP integration once
|
||||
— the server URL, who can use it, and how people sign in — instead of every
|
||||
member configuring Notion or Linear by hand on every machine.
|
||||
|
||||
Two ways to handle whose account the AI uses, chosen per connection:
|
||||
|
||||
- **Individual accounts** — each person signs in as themselves. This is now
|
||||
the default for OAuth servers, and it fits tools like Notion or Linear where
|
||||
people have their own pages, projects, and permissions. The agent acts as
|
||||
*you*, and the provider's own access rules apply.
|
||||
- **One org account** — an admin signs in once (a bot, service account, or API
|
||||
key). Every granted member's AI acts as that identity.
|
||||
|
||||
## Publish the connection in OpenWork Cloud
|
||||
|
||||
1. Open `Connectors` in the OpenWork Cloud dashboard.
|
||||
2. Click `Add connection`.
|
||||
3. Pick a preset (Notion, Linear, Stripe, Sentry, Exa, Context7) or enter any
|
||||
MCP server URL.
|
||||
4. Choose the account mode: `Individual accounts` or `One org account`.
|
||||
5. Choose who can use it: the whole workspace, specific teams, or specific
|
||||
people.
|
||||
6. Click `Create`.
|
||||
|
||||
<Frame>
|
||||

|
||||
</Frame>
|
||||
|
||||
For a One org account connection, finish by clicking `Connect` and completing
|
||||
the provider's sign-in as the org account. Individual accounts connections are
|
||||
ready to publish immediately — each person connects themselves.
|
||||
|
||||
On `Your Connections`, an unconnected One org account connection shows
|
||||
`Waiting for an admin to connect` to members. Workspace owners and admins get a
|
||||
`Connect` button on that row so they can finish the org account sign-in there.
|
||||
|
||||
## OAuth redirect URL
|
||||
|
||||
An OAuth redirect URL, also called a callback URL, is the exact address where
|
||||
the provider sends the browser after a person approves access. It belongs to
|
||||
your specific Den deployment; it is not the MCP server URL or the client
|
||||
metadata URL.
|
||||
|
||||
New MCP connections on one Den instance share this redirect URL:
|
||||
|
||||
```text
|
||||
<DEN_API_PUBLIC_URL>/v1/mcp-connections/oauth/callback
|
||||
```
|
||||
|
||||
For the hosted OpenWork Cloud instance, the dashboard is
|
||||
`https://app.openworklabs.com` and its public Den API origin is
|
||||
`https://api.app.openworklabs.com`. Register this exact redirect URL with the OAuth
|
||||
provider:
|
||||
|
||||
```text
|
||||
https://api.app.openworklabs.com/v1/mcp-connections/oauth/callback
|
||||
```
|
||||
|
||||
For a self-hosted instance whose public Den API is
|
||||
`https://api.openwork.example.com`, register:
|
||||
|
||||
```text
|
||||
https://api.openwork.example.com/v1/mcp-connections/oauth/callback
|
||||
```
|
||||
|
||||
Use the exact scheme, hostname, optional path prefix, and port configured in
|
||||
`DEN_API_PUBLIC_URL`. Providers compare redirect URLs exactly. Existing
|
||||
connections created with an older per-connection callback keep that registered
|
||||
URL automatically; do not change it just to reconnect.
|
||||
|
||||
## Slack
|
||||
|
||||
Slack's MCP server requires a pre-registered Slack OAuth app.
|
||||
|
||||
1. Create or open a Slack app in [Slack API apps](https://api.slack.com/apps).
|
||||
2. Add this Den instance's OAuth redirect URL to the Slack app's
|
||||
`OAuth & Permissions` redirect URLs.
|
||||
3. In OpenWork Cloud, add the Slack connection and paste the app's client ID
|
||||
and client secret.
|
||||
4. Members open `Your Connections` and connect their own Slack account.
|
||||
|
||||
For the advanced local desktop-only setup, see
|
||||
[Connect Slack as a custom MCP](/start-here/connect-your-stack/connect-slack-mcp).
|
||||
|
||||
## What members see in the desktop app
|
||||
|
||||
Granted members find the connection in `Settings` > `OpenWork Connect`, grouped by what
|
||||
they need to do:
|
||||
|
||||
- **Needs your sign-in** — individual-account connections that require the
|
||||
member to authenticate or reconnect.
|
||||
- **Needs admin setup** — connections that an organization admin still needs
|
||||
to configure or authenticate.
|
||||
- **Ready to use** — connections ready for the agent, including accounts
|
||||
managed by the organization.
|
||||
|
||||
## Connect your account
|
||||
|
||||
1. Sign in to OpenWork, then open `Settings` > `OpenWork Connect`.
|
||||
2. Find the service under `Needs your sign-in`, then click `Connect`.
|
||||
3. Your browser opens the provider's own sign-in page. Approve access.
|
||||
4. The browser shows `Connected` — return to OpenWork.
|
||||
5. The connection updates by itself, no reload needed, and moves to
|
||||
`Ready to use`.
|
||||
|
||||
Your sign-in is stored in OpenWork Cloud, not on this machine — connect once
|
||||
and every device you use is connected.
|
||||
|
||||
## Your agent can use it immediately
|
||||
|
||||
No restart, no resync. Ask for something the tool can do:
|
||||
|
||||
> Find the "Q3 launch" page in Notion and summarize it.
|
||||
|
||||
The agent searches the live capabilities available through OpenWork Connect,
|
||||
then executes an exact match with **your** account — your permissions, your
|
||||
audit trail on the provider's side.
|
||||
|
||||
<Frame>
|
||||

|
||||
</Frame>
|
||||
|
||||
If a connection needs attention, the agent should tell you which account needs
|
||||
action and direct you to `Settings` > `OpenWork Connect`.
|
||||
|
||||
## Notes for admins
|
||||
|
||||
- Members only see connections they've been granted — access is explicit
|
||||
(workspace-wide, team, or per-person), and it's enforced by the server on
|
||||
every request, not by the app.
|
||||
- Removing a connection (or a member's access) takes effect immediately for
|
||||
every device.
|
||||
- Members who already connected a tool directly in their desktop app keep
|
||||
their existing setup — publishing an org connection never removes or
|
||||
changes anyone's local configuration.
|
||||
|
||||
## Relationship to local MCP servers
|
||||
|
||||
[Add a custom MCP server](/start-here/connect-your-stack/add-an-mcp-server)
|
||||
still works without a cloud account. Use that advanced path for local tools or
|
||||
servers your organization does not provide. Use Connect for accounts and
|
||||
services managed through OpenWork Cloud.
|
||||
@@ -0,0 +1,27 @@
|
||||
---
|
||||
title: "Team prompt cards"
|
||||
description: "Use desktop policies to show different suggested prompts to teams and members."
|
||||
---
|
||||
|
||||
Organization prompt cards are an OpenWork Cloud desktop-policy feature. Admins manage them in Cloud, and assigned members see them in the desktop composer as task suggestions. Managing desktop policies requires an Enterprise plan; the API returns `402` when the org is not entitled.
|
||||
|
||||
## Create team prompts
|
||||
|
||||
1. Open the Cloud dashboard and go to `Settings > Desktop policies`.
|
||||
2. Create a non-default policy for the audience. The default policy is org-wide only and cannot be assigned to members or teams.
|
||||
3. Turn on `Organization prompt suggestions`.
|
||||
4. Add 2 required prompts, and optionally a third. Each `Prompt` is limited to 500 characters; each `Description` is limited to 120 characters.
|
||||
5. Assign the policy to `Members` or `Teams`, then save. The API stores those choices as `memberIds` and `teamIds`, so different departments can use separate non-default policies.
|
||||
|
||||
## Know what members see
|
||||
|
||||
<Frame>
|
||||

|
||||
</Frame>
|
||||
|
||||
1. The admin's `Description` becomes the card title in the desktop app.
|
||||
2. The admin's `Prompt` becomes the card body and the text inserted into the composer.
|
||||
3. Clicking a card fills the composer; it does not auto-send the task.
|
||||
4. Boolean policy toggles combine across every matching policy: any `true` wins.
|
||||
5. Prompt cards do not combine. The desktop uses one prompt set from the matching policy with the highest `Priority`; ties go to the earliest created policy, then the lowest policy id.
|
||||
6. Members receive changes from `GET /v1/me/desktop-config`. The desktop loads its cached config first, refreshes on Cloud session/settings changes, and polls every hour, so reload or switch the active Cloud organization if a change is not visible.
|
||||
@@ -0,0 +1,98 @@
|
||||
---
|
||||
title: "Microsoft Entra SAML SSO"
|
||||
description: "Connect Microsoft Entra ID to OpenWork Cloud with SAML"
|
||||
---
|
||||
|
||||
Use this guide when an organization wants members to sign in to OpenWork with
|
||||
Microsoft Entra ID.
|
||||
|
||||
After SAML sign-in works, use
|
||||
[Microsoft Entra SCIM provisioning](/cloud/scim-microsoft-entra) to synchronize
|
||||
members and groups automatically.
|
||||
|
||||
## Before you start
|
||||
|
||||
You need:
|
||||
|
||||
- An OpenWork organization with the Enterprise SSO entitlement.
|
||||
- Owner or admin access in that OpenWork organization.
|
||||
- Admin access to a Microsoft Entra enterprise application.
|
||||
- The final OpenWork auth origin, for example `https://app.openworklabs.com`.
|
||||
|
||||
## 1. Create the Entra enterprise app
|
||||
|
||||
In the Microsoft Azure portal, open **Microsoft Entra ID** and create a new
|
||||
enterprise application for OpenWork. Choose **SAML** as the single sign-on
|
||||
method.
|
||||
|
||||
## 2. Configure Basic SAML Configuration
|
||||
|
||||
In Entra, open **Single sign-on** and edit **Basic SAML Configuration**.
|
||||
|
||||
Set:
|
||||
|
||||
| Entra field | Value |
|
||||
| --- | --- |
|
||||
| Identifier (Entity ID) | The OpenWork auth origin, for example `https://app.openworklabs.com` |
|
||||
| Reply URL (Assertion Consumer Service URL) | The ACS URL OpenWork shows after saving SAML, for example `https://app.openworklabs.com/api/auth/sso/saml2/sp/acs/openwork-sso-<org-id>` |
|
||||
| Sign on URL | The org-specific OpenWork **Sign-in URL** shown after saving SAML, for example `https://app.openworklabs.com/sso/<org-slug>` |
|
||||
| Relay State | Leave blank |
|
||||
|
||||
The Entity ID must be the OpenWork auth origin. Do not use the Entra tenant
|
||||
identifier, `https://sts.windows.net/<tenant-id>/`, as the Entity ID.
|
||||
|
||||
OpenWork SAML connections are organization-scoped. If a user belongs to
|
||||
multiple OpenWork organizations, the Entra app, ACS URL, and Sign-in URL select
|
||||
which organization they are entering.
|
||||
|
||||
## 3. Configure SAML signing
|
||||
|
||||
Open **SAML Certificates** and edit the token signing certificate settings.
|
||||
|
||||
Set:
|
||||
|
||||
- **Signing Option**: `Sign SAML assertion`
|
||||
- **Signing Algorithm**: `SHA-256`
|
||||
|
||||
OpenWork requires signed SAML assertions. If Entra signs only the response, the
|
||||
callback fails with `saml_error` and `Invalid SAML response`.
|
||||
|
||||
After any certificate change, copy the active certificate again. Entra can
|
||||
create or activate a new signing certificate while you are editing SAML
|
||||
settings, and OpenWork must store the certificate that is currently active.
|
||||
|
||||
## 4. Copy Entra values into OpenWork
|
||||
|
||||
In OpenWork, open the organization dashboard, then **SSO**. Choose **SAML** and
|
||||
enter:
|
||||
|
||||
| OpenWork field | Entra value |
|
||||
| --- | --- |
|
||||
| IdP Issuer URL | **Microsoft Entra Identifier**, for example `https://sts.windows.net/<tenant-id>/` |
|
||||
| Domain | The Entra-managed email domain, for example `example.onmicrosoft.com` |
|
||||
| SAML Entry Point | **Login URL**, for example `https://login.microsoftonline.com/<tenant-id>/saml2` |
|
||||
| Audience URL | Leave blank so OpenWork uses the auth origin, unless support gives you a different audience |
|
||||
| IdP Certificate | The active **Certificate (Base64)** from Entra |
|
||||
|
||||
Save the SSO connection. OpenWork then shows the generated ACS URL. Copy that
|
||||
ACS URL back into Entra's **Reply URL** if it was not available before the first
|
||||
save. Copy the OpenWork **Sign-in URL** into Entra's **Sign on URL** so the
|
||||
Entra tile can launch the same organization-scoped flow.
|
||||
|
||||
## 5. Assign test users
|
||||
|
||||
In Entra, open the enterprise application's **Users and groups** page and assign
|
||||
the users who should be allowed to sign in.
|
||||
|
||||
For external users, assign the guest identity shown by Entra, not only the
|
||||
original external email address.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Symptom | Likely cause | Fix |
|
||||
| --- | --- | --- |
|
||||
| `AADSTS700016: Application with identifier 'https://app.openworklabs.com' was not found` | Entra's Entity ID does not match OpenWork's SAML issuer | Set **Identifier (Entity ID)** to the OpenWork auth origin |
|
||||
| `/?error=saml_error&error_description=Invalid%20SAML%20response` | Entra is signing with a different certificate than the one saved in OpenWork, or assertions are not signed | Confirm **Signing Option** is `Sign SAML assertion`, then paste Entra's active **Certificate (Base64)** into OpenWork and save |
|
||||
| `/?error=unsolicited_response&error_description=IdP-initiated+SSO+not+allowed` | The deployment is running an older OpenWork version that rejects IdP-initiated SAML | Upgrade OpenWork, or launch from the org-specific OpenWork **Sign-in URL** |
|
||||
| User reaches Microsoft but cannot continue | The user is not assigned to the enterprise application | Add the user under **Users and groups** |
|
||||
| Reply URL mismatch | Entra's Reply URL does not match the OpenWork ACS URL exactly | Copy the ACS URL from OpenWork and paste it into Entra |
|
||||
@@ -0,0 +1,169 @@
|
||||
---
|
||||
title: "Okta SAML SSO"
|
||||
description: "Connect Okta to OpenWork Cloud or a self-hosted OpenWork deployment with SAML"
|
||||
---
|
||||
|
||||
Use this guide to let organization members sign in to OpenWork through an
|
||||
Okta application integration. It works for OpenWork Cloud and for a Helm
|
||||
deployment with a public HTTPS web origin.
|
||||
|
||||
After SAML sign-in works, use [Okta SCIM provisioning](/cloud/scim-okta) to
|
||||
synchronize members and groups.
|
||||
|
||||
## Before you start
|
||||
|
||||
You need:
|
||||
|
||||
- An OpenWork owner, or a member who can manage security configuration.
|
||||
- An Okta administrator who can create and assign application integrations.
|
||||
- Control of DNS for the email domain you will associate with the connection.
|
||||
- The final HTTPS OpenWork web origin, for example
|
||||
`https://openwork.example.com`.
|
||||
|
||||
Keep **Require SSO for this organization** off until an owner has completed a
|
||||
fresh SAML sign-in. Keep password access available as a recovery path during
|
||||
setup.
|
||||
|
||||
<Note>
|
||||
For a Helm deployment with separate web and API hosts, use the URLs OpenWork
|
||||
shows in **Settings → SSO**. Do not replace their host with the separate API
|
||||
origin. SAML uses the deployment's configured auth origin, which is normally
|
||||
the public web origin when `/api/auth` is proxied through the web host.
|
||||
</Note>
|
||||
|
||||
## 1. Get the organization ID
|
||||
|
||||
In OpenWork, select the organization and open **Settings → General**. Copy the
|
||||
organization ID. OpenWork creates the SAML provider ID as:
|
||||
|
||||
```text
|
||||
openwork-sso-<organization-id>
|
||||
```
|
||||
|
||||
The corresponding assertion consumer service URL is:
|
||||
|
||||
```text
|
||||
https://<openwork-web-origin>/api/auth/sso/saml2/sp/acs/openwork-sso-<organization-id>
|
||||
```
|
||||
|
||||
You can confirm the exact generated URL after saving the OpenWork connection.
|
||||
|
||||
## 2. Create the Okta SAML application
|
||||
|
||||
In the Okta Admin Console:
|
||||
|
||||
1. Open **Applications → Applications**.
|
||||
2. Select **Create App Integration**.
|
||||
3. Choose **SAML 2.0**, then select **Next**.
|
||||
4. Set **App name** to `OpenWork` or another recognizable workspace name.
|
||||
5. Select **Next**.
|
||||
|
||||
## 3. Configure SAML in Okta
|
||||
|
||||
Under **General**, enter:
|
||||
|
||||
| Okta field | Value |
|
||||
| --- | --- |
|
||||
| Single sign-on URL | The OpenWork ACS URL from step 1 |
|
||||
| Use this for Recipient URL and Destination URL | Enabled |
|
||||
| Audience URI (SP Entity ID) | The OpenWork web origin, for example `https://openwork.example.com` |
|
||||
| Default RelayState | Leave blank |
|
||||
| Name ID format | `EmailAddress` |
|
||||
| Application username | `Okta username` |
|
||||
|
||||
Open **Show Advanced Settings** and confirm:
|
||||
|
||||
- **Response**: `Signed`
|
||||
- **Assertion Signature**: `Signed`
|
||||
- **Signature Algorithm**: `RSA-SHA256`
|
||||
- **Digest Algorithm**: `SHA256`
|
||||
- **Assertion Encryption**: `Unencrypted`
|
||||
|
||||
OpenWork requires a signed assertion. Keep the Okta username and NameID equal
|
||||
to the stable work email that SCIM will later send as `userName`.
|
||||
|
||||
Select **Next**, mark the integration as an internal application when
|
||||
appropriate, then select **Finish**.
|
||||
|
||||
## 4. Copy Okta values into OpenWork
|
||||
|
||||
In the Okta application's **Sign On** tab, select
|
||||
**View SAML setup instructions**. Copy:
|
||||
|
||||
- **Identity Provider Single Sign-On URL**
|
||||
- **Identity Provider Issuer**
|
||||
- **X.509 Certificate**, including the `BEGIN CERTIFICATE` and
|
||||
`END CERTIFICATE` lines
|
||||
|
||||
In OpenWork, open **Settings → SSO**, select **SAML**, and enter:
|
||||
|
||||
| OpenWork field | Okta value |
|
||||
| --- | --- |
|
||||
| IdP Issuer URL | **Identity Provider Issuer** |
|
||||
| Domain | The members' email domain, for example `example.com` |
|
||||
| SAML Entry Point | **Identity Provider Single Sign-On URL** |
|
||||
| Audience URL | Leave blank to use the configured OpenWork auth origin |
|
||||
| IdP Certificate | **X.509 Certificate** |
|
||||
|
||||
Save the connection. Compare the generated **ACS URL** with Okta's
|
||||
**Single sign-on URL** character for character. Also copy the generated
|
||||
OpenWork **Sign-in URL**; use this URL for the first test.
|
||||
|
||||
## 5. Verify the email domain
|
||||
|
||||
Select **Request token** under **Domain verification**. Create a DNS TXT
|
||||
record for the email domain:
|
||||
|
||||
| DNS field | Value |
|
||||
| --- | --- |
|
||||
| Type | `TXT` |
|
||||
| Name | `_better-auth-token-<provider-id>` |
|
||||
| Value | The token OpenWork displays |
|
||||
|
||||
For example, provider `openwork-sso-org_123` for `example.com` produces:
|
||||
|
||||
```text
|
||||
_better-auth-token-openwork-sso-org_123.example.com
|
||||
```
|
||||
|
||||
In a DNS console that automatically appends the zone, enter only
|
||||
`_better-auth-token-<provider-id>` in the name field. The token is valid for
|
||||
seven days. After the TXT record resolves publicly, select **Verify domain**
|
||||
in OpenWork and confirm the connection shows **Domain verified: Yes**.
|
||||
|
||||
## 6. Assign a test user in Okta
|
||||
|
||||
In the Okta application:
|
||||
|
||||
1. Open **Assignments**.
|
||||
2. Select **Assign → Assign to People**.
|
||||
3. Assign one OpenWork owner or another controlled test user.
|
||||
4. Confirm the application username is that user's stable work email.
|
||||
|
||||
Do not assign broad groups until the test user can complete the full flow.
|
||||
|
||||
## 7. Test SP-initiated sign-in
|
||||
|
||||
Open the organization-specific OpenWork **Sign-in URL** in a fresh browser
|
||||
session. Confirm that:
|
||||
|
||||
1. OpenWork redirects to the expected Okta tenant.
|
||||
2. Okta authenticates the assigned user.
|
||||
3. Okta posts the assertion to the generated OpenWork ACS URL.
|
||||
4. OpenWork returns the user to the intended organization.
|
||||
5. The member identity in OpenWork uses the same email that Okta sent as
|
||||
NameID.
|
||||
|
||||
Only after this succeeds should you configure SCIM or enable
|
||||
**Require SSO for this organization**.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Symptom | Likely cause | Fix |
|
||||
| --- | --- | --- |
|
||||
| `Provider domain has not been verified` | The TXT record is missing, attached to the zone apex, or has not propagated | Put the token at `_better-auth-token-<provider-id>.<domain>`, wait for public DNS, then select **Verify domain** |
|
||||
| `invalid_saml_configuration` | The OpenWork build did not persist the generated ACS callback URL in the SAML provider | Upgrade to a build containing the SAML callback configuration fix, then edit and save the connection again |
|
||||
| `invalid_audience` | Okta's Audience URI does not match OpenWork's configured auth origin | Use the OpenWork web/auth origin shown by the deployment; do not substitute a separate API host |
|
||||
| `invalid_destination` or `invalid_recipient` | Okta is posting to a different URL than OpenWork expects | Copy the generated OpenWork ACS URL into Okta and keep **Use this for Recipient URL and Destination URL** enabled |
|
||||
| `Invalid SAML response` | The assertion is unsigned, uses a deprecated algorithm, or the active Okta certificate differs from OpenWork | Sign the assertion with RSA-SHA256 and paste Okta's current active X.509 certificate into OpenWork |
|
||||
| Okta refuses access | The person is not assigned to the application | Assign the test user under **Assignments** |
|
||||
@@ -0,0 +1,151 @@
|
||||
---
|
||||
title: "Team quickstart: from new org to a team skill"
|
||||
description: "Create an OpenWork Cloud organization, invite teammates, publish a skill plugin through a Collection, and use it from the desktop app"
|
||||
---
|
||||
|
||||
Set up a team on [app.openworklabs.com](https://app.openworklabs.com), publish your first skill, and trigger it from the desktop app — in about 15 minutes.
|
||||
|
||||
By the end of this walkthrough you will have:
|
||||
|
||||
1. Created an organization on OpenWork Cloud.
|
||||
2. Invited a few teammates and organized them into teams.
|
||||
3. Created a plugin with a simple test skill in `Plugin Directory` and added it to a Collection.
|
||||
4. Granted the Collection to the relevant team.
|
||||
5. Signed in to the org from the OpenWork desktop app and triggered the skill with a plain-language prompt.
|
||||
|
||||
<Note>
|
||||
**Seats:** every workspace includes **5 free seats**. If you need more seats, SSO, or early access to analytics, [reach out](mailto:founders@openworklabs.com).
|
||||
</Note>
|
||||
|
||||
---
|
||||
|
||||
## 1. Create your organization
|
||||
|
||||
Head to [app.openworklabs.com](https://app.openworklabs.com). Enter your work email and press **Next** — OpenWork routes you to the right sign-in step automatically.
|
||||
|
||||
<Frame>
|
||||

|
||||
</Frame>
|
||||
|
||||
New emails get the **Create your account.** step. Pick a name and password, then **Sign up**.
|
||||
|
||||
<Frame>
|
||||

|
||||
</Frame>
|
||||
|
||||
Right after sign-up, OpenWork asks you to **name your team**. This creates your organization — you can rename it later, and no credit card is required.
|
||||
|
||||
<Frame>
|
||||

|
||||
</Frame>
|
||||
|
||||
You land in the admin dashboard with a short setup checklist. You can follow it now or come back later — this tutorial covers the important parts.
|
||||
|
||||
<Frame>
|
||||

|
||||
</Frame>
|
||||
|
||||
## 2. Invite teammates and create teams
|
||||
|
||||
Open **Members** in the sidebar. Click **Add member**, enter your teammate's email, pick a role (Member or Admin), and hit **Send invite**.
|
||||
|
||||
<Frame>
|
||||

|
||||
</Frame>
|
||||
|
||||
Repeat for a few teammates. Invited people show up as **Pending** until they accept. From the row menu (⋯) you can **Copy invite link**, **Resend invite**, or **Cancel invite**.
|
||||
|
||||
<Frame>
|
||||

|
||||
</Frame>
|
||||
|
||||
Your teammate opens the invite link, sets a password, and joins with one click:
|
||||
|
||||
<Frame>
|
||||

|
||||
</Frame>
|
||||
|
||||
<Frame>
|
||||

|
||||
</Frame>
|
||||
|
||||
Now organize people into teams. Switch to the **Teams** tab, click **Create Team**, name it (for example `Sales`), and tap the members who belong in it.
|
||||
|
||||
<Frame>
|
||||

|
||||
</Frame>
|
||||
|
||||
Teams show up with their members at a glance:
|
||||
|
||||
<Frame>
|
||||

|
||||
</Frame>
|
||||
|
||||
## 3. Create a plugin with a test skill
|
||||
|
||||
Skills are step-by-step instructions the agent loads when a task matches — think of them as runbooks your whole team can share. A skill lives inside a **plugin**, and a **Collection** groups one or more plugins for assignment.
|
||||
|
||||
First open **Collections**, click **New collection**, and create one for your team — for example `Sales Enablement`.
|
||||
|
||||
Next open **Plugin Directory → Create plugin**. Fill in the plugin name and description, then click **+ Skill**.
|
||||
|
||||
Write the skill like a great runbook:
|
||||
|
||||
- **Name** — e.g. `Prep a sales call`
|
||||
- **Description** — when should the agent use this? e.g. *"Use when someone asks to prepare for a sales call or customer meeting."*
|
||||
- **Instructions** — plain markdown with the steps you want the agent to follow.
|
||||
|
||||
In the **Share** section, choose the **Sales Enablement** Collection. If only selected teams should receive it, leave **Share with everyone in the organization** off. Click **Create plugin**. The creator keeps access automatically.
|
||||
|
||||
## 4. Give teams access to the Collection
|
||||
|
||||
Open **Collections → Sales Enablement**. Under **Who can access this**, add the `Sales` team. You can instead grant the Collection to everyone in the organization or to individual members.
|
||||
|
||||
That's the whole admin loop: **skill inside a plugin → Collection → team or member**. Anyone added to the team later inherits access to the Collection.
|
||||
|
||||
## 5. Trigger the skill from the desktop app
|
||||
|
||||
Now switch to the [OpenWork desktop app](https://openworklabs.com). On the welcome screen, click **Joining a team? Sign in** and sign in with the same account.
|
||||
|
||||
<Frame>
|
||||

|
||||
</Frame>
|
||||
|
||||
Pick your organization:
|
||||
|
||||
<Frame>
|
||||

|
||||
</Frame>
|
||||
|
||||
You can confirm the connection any time under **Settings → Account**:
|
||||
|
||||
<Frame>
|
||||

|
||||
</Frame>
|
||||
|
||||
Open **Settings → Library** and filter to **Skills**. The skill assigned through `Sales Enablement` appears there automatically; click **Refresh** if the admin changed access moments ago.
|
||||
|
||||
Now just ask for the thing the skill knows how to do. Type a prompt that matches the skill's description and hit **Run task**:
|
||||
|
||||
> *Prep me for tomorrow's sales call with Northwind Traders. 30 minutes with their operations lead, goal is a paid pilot.*
|
||||
|
||||
<Frame>
|
||||

|
||||
</Frame>
|
||||
|
||||
The agent picks up the skill and follows your runbook — research, a timed agenda, discovery questions, objection handling, and a clear next step, saved as a doc in your workspace:
|
||||
|
||||
<Frame>
|
||||

|
||||
</Frame>
|
||||
|
||||
## What's next
|
||||
|
||||
- Add more skills to the plugin as your team's playbooks grow — everyone gets updates automatically.
|
||||
- Import existing plugins from GitHub (**Plugin Directory → Create plugin → Import from GitHub**), including Anthropic-compatible plugin repositories.
|
||||
- Wire up **Sources** to keep Collections in sync with a repository.
|
||||
- Read [Collections](/cloud/share-with-your-team/collections) when you need several plugins, reusable audiences, or more complex assignments.
|
||||
|
||||
<Note>
|
||||
**Need more?** The first 5 users in your organization are free; additional users are $10 per user per month. For SSO or early analytics access, [contact us](mailto:founders@openworklabs.com).
|
||||
</Note>
|
||||
Reference in New Issue
Block a user