@opengeni/core 3.0.0-canary.1 → 3.0.1-canary.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/application/connect-operation.d.ts +3 -0
- package/dist/domain/connector-tool-permissions.d.ts +27 -0
- package/dist/domain/knowledge-files.d.ts +1 -1
- package/dist/domain/knowledge-messages.d.ts +2 -2
- package/dist/domain/knowledge-preparation.d.ts +9 -0
- package/dist/domain/knowledge-preparation.test.d.ts +1 -0
- package/dist/domain/organization-integration-catalog.d.ts +15 -0
- package/dist/domain/pr-review.d.ts +2 -2
- package/dist/index.d.ts +3 -0
- package/dist/index.js +666 -60
- package/dist/index.js.map +1 -1
- package/package.json +12 -12
- package/src/application/connect-operation.ts +32 -3
- package/src/domain/capabilities.ts +93 -1
- package/src/domain/connector-tool-permissions.ts +297 -0
- package/src/domain/knowledge-files.ts +10 -3
- package/src/domain/knowledge-messages.ts +1 -0
- package/src/domain/knowledge-preparation.test.ts +251 -0
- package/src/domain/knowledge-preparation.ts +126 -0
- package/src/domain/organization-integration-catalog.ts +83 -0
- package/src/domain/product-integration-pack.ts +1 -1
- package/src/domain/product-integration-skill.gen.ts +6 -2
- package/src/domain/resources.ts +8 -1
- package/src/domain/session-tool-policy.ts +12 -4
- package/src/domain/sessions.ts +165 -34
- package/src/index.ts +4 -0
package/dist/index.js
CHANGED
|
@@ -206,6 +206,74 @@ async function restoreSkill(db, input) {
|
|
|
206
206
|
);
|
|
207
207
|
}
|
|
208
208
|
|
|
209
|
+
// src/domain/organization-integration-catalog.ts
|
|
210
|
+
import { CORE_INTEGRATION_DEFINITIONS } from "@opengeni/capabilities";
|
|
211
|
+
import {
|
|
212
|
+
assertOrganizationIntegrationAllowed
|
|
213
|
+
} from "@opengeni/contracts";
|
|
214
|
+
function integrationSourceForOrganizationPolicy(policy, source) {
|
|
215
|
+
const snapshot = structuredClone(source);
|
|
216
|
+
if (policy.mode === "unrestricted") return snapshot;
|
|
217
|
+
if (snapshot.kind === "definition") {
|
|
218
|
+
const definition = CORE_INTEGRATION_DEFINITIONS.find(
|
|
219
|
+
(item) => item.id === snapshot.definitionId
|
|
220
|
+
);
|
|
221
|
+
assertOrganizationIntegrationAllowed(policy, definition?.id ?? null);
|
|
222
|
+
return snapshot;
|
|
223
|
+
}
|
|
224
|
+
if (snapshot.kind !== "auto") {
|
|
225
|
+
assertOrganizationIntegrationAllowed(policy, `custom:${snapshot.kind}`);
|
|
226
|
+
return snapshot;
|
|
227
|
+
}
|
|
228
|
+
const openapi = policy.allowedIntegrationKeys.includes("custom:openapi");
|
|
229
|
+
const graphql = policy.allowedIntegrationKeys.includes("custom:graphql");
|
|
230
|
+
if (openapi && graphql) return snapshot;
|
|
231
|
+
if (openapi) return { ...snapshot, kind: "openapi" };
|
|
232
|
+
if (graphql) return { kind: "graphql", endpoint: snapshot.url };
|
|
233
|
+
assertOrganizationIntegrationAllowed(policy, null);
|
|
234
|
+
return snapshot;
|
|
235
|
+
}
|
|
236
|
+
var dedicated = [
|
|
237
|
+
{ key: "gmail", label: "Gmail" },
|
|
238
|
+
{ key: "atlassian", label: "Atlassian (Jira and Confluence)" },
|
|
239
|
+
{ key: "slack-personal", label: "Slack personal accounts" },
|
|
240
|
+
{ key: "slack-bot", label: "Slack workspace bots" },
|
|
241
|
+
{ key: "github-personal", label: "GitHub personal accounts" },
|
|
242
|
+
{ key: "github-app", label: "GitHub App installations" },
|
|
243
|
+
{ key: "github-lens", label: "GitHub PR review" },
|
|
244
|
+
{ key: "fiken", label: "Fiken" },
|
|
245
|
+
{ key: "x", label: "X" },
|
|
246
|
+
{ key: "reddit", label: "Reddit" }
|
|
247
|
+
];
|
|
248
|
+
function organizationIntegrationCatalog() {
|
|
249
|
+
return {
|
|
250
|
+
integrations: [
|
|
251
|
+
...CORE_INTEGRATION_DEFINITIONS.map((definition) => ({
|
|
252
|
+
key: definition.id,
|
|
253
|
+
label: definition.name,
|
|
254
|
+
kind: "curated"
|
|
255
|
+
})),
|
|
256
|
+
...dedicated.map((item) => ({ ...item, kind: "curated" })),
|
|
257
|
+
{ key: "custom:mcp", label: "Custom MCP servers", kind: "custom" },
|
|
258
|
+
{ key: "custom:openapi", label: "Custom OpenAPI services", kind: "custom" },
|
|
259
|
+
{ key: "custom:graphql", label: "Custom GraphQL services", kind: "custom" }
|
|
260
|
+
]
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
function integrationKeyForConnectProvider(providerId) {
|
|
264
|
+
if (CORE_INTEGRATION_DEFINITIONS.some((definition) => definition.id === providerId))
|
|
265
|
+
return providerId;
|
|
266
|
+
if (dedicated.some((item) => item.key === providerId)) return providerId;
|
|
267
|
+
if (providerId === "fiken-token" || providerId === "fiken-oauth") return "fiken";
|
|
268
|
+
if (providerId === "google-drive-knowledge" || providerId === "google-drive-publish")
|
|
269
|
+
return "google-drive";
|
|
270
|
+
if (["mcp-oauth", "mcp-headers", "mcp-bearer", "mcp-install"].includes(providerId))
|
|
271
|
+
return "custom:mcp";
|
|
272
|
+
if (providerId === "openapi") return "custom:openapi";
|
|
273
|
+
if (providerId === "graphql") return "custom:graphql";
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
|
|
209
277
|
// src/access/index.ts
|
|
210
278
|
import { resolveFirstPartyDelegationSecret } from "@opengeni/config";
|
|
211
279
|
import {
|
|
@@ -4009,13 +4077,16 @@ function assertHostMcpAuthoritySourceAdmissionEnabled(settings, connectionRef) {
|
|
|
4009
4077
|
// src/domain/capabilities.ts
|
|
4010
4078
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
4011
4079
|
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
4080
|
+
import { isDeepStrictEqual } from "util";
|
|
4081
|
+
import { withLockedCapabilityInstallation } from "@opengeni/db/capability-reconciliation";
|
|
4012
4082
|
import { environmentsEncryptionKeyBytes as environmentsEncryptionKeyBytes2 } from "@opengeni/config";
|
|
4013
4083
|
import {
|
|
4014
4084
|
CapabilityCatalogItem,
|
|
4015
4085
|
capabilityCatalogItemIsTrustedForExposure,
|
|
4016
4086
|
FIKEN_PROVIDER_DOMAIN as FIKEN_PROVIDER_DOMAIN2,
|
|
4017
4087
|
FIRST_PARTY_MCP_TOOL_NAMES,
|
|
4018
|
-
OPENGENI_PERSONAL_SLACK_MCP_URL
|
|
4088
|
+
OPENGENI_PERSONAL_SLACK_MCP_URL,
|
|
4089
|
+
assertOrganizationIntegrationAllowed as assertOrganizationIntegrationAllowed2
|
|
4019
4090
|
} from "@opengeni/contracts";
|
|
4020
4091
|
import {
|
|
4021
4092
|
CODEX_APPS_MCP_SERVER_ID,
|
|
@@ -4047,6 +4118,10 @@ import {
|
|
|
4047
4118
|
upsertCapabilityCatalogItem
|
|
4048
4119
|
} from "@opengeni/db";
|
|
4049
4120
|
import { HTTPException as HTTPException14 } from "hono/http-exception";
|
|
4121
|
+
import {
|
|
4122
|
+
withOrganizationIntegrationAcquisition,
|
|
4123
|
+
withOrganizationIntegrationPolicyFence
|
|
4124
|
+
} from "@opengeni/db/organization-integration-policy";
|
|
4050
4125
|
|
|
4051
4126
|
// src/domain/fiken.ts
|
|
4052
4127
|
import {
|
|
@@ -4612,7 +4687,7 @@ var productIntegrationSkillDescription = "Design, implement, verify, and hand of
|
|
|
4612
4687
|
var productIntegrationSkillFiles = [
|
|
4613
4688
|
{
|
|
4614
4689
|
"path": "SKILL.md",
|
|
4615
|
-
"content": '---\nname: opengeni-product-integration\naudience: integration-agent\ndescription: >-\n Design, implement, verify, and hand off a tenant-safe OpenGeni product\n integration while adapting to the customer\'s architecture, UI, data APIs, and\n desired delivery autonomy. Select only for an implementation session;\n installation alone does not expose it to other agents.\n---\n\nThis implementation Skill is inactive until explicitly selected for an implementation session. Never attach it to customer-facing runtime sessions.\n\n\n# OpenGeni Client\n\nUse this skill when a customer\'s product and OpenGeni remain separate systems.\nThat is the normal integration shape: the product owns its users and business\nUI, while a standalone OpenGeni deployment owns agent sessions and execution.\n\nDo not interpret "embed" as "move OpenGeni into the product process." Advanced\nin-process router/core embedding is a separate infrastructure choice. Route that\nwork to the repo-maintainer `opengeni` skill and `docs/embedding.md`.\n\nDo not confuse two meanings of "skill": this file teaches a customer\'s coding\nagent how to integrate OpenGeni; session `skills` are runtime capabilities or\ninstructions attached to an OpenGeni agent. The former designs the integration.\nThe latter is product data sent through the installed SDK contract.\n\nCode and the live service are authoritative. Prefer `/v1/config/client`,\n`/v1/access/me`, the installed package types, and live probes over memorized\nroute, model, tool, or backend lists. When source is available, verify exact\nbehavior in `packages/sdk`, `packages/react`, contracts, and API routes.\nRead `docs/product-integration.md` when the repository is available; it is the\ncanonical product boundary for organization keys, workspace mapping, and Skill\nownership.\n\n## Work Adaptively\n\n- Inspect the customer\'s repository, authentication, tenancy, data routes,\n frontend conventions, installed packages, tests, CI, and deployment guidance\n before asking questions or choosing an integration shape.\n- Ask only for consequential product choices or external authority that cannot\n be inferred. Do not ask the customer to restate facts the system proves.\n- Use a reversible, clearly stated default when an unresolved choice is\n low-risk. Resolve privacy, tenant authority, data writes, cost exposure, and\n ambiguous external mutations before crossing those boundaries.\n- Match the requested delivery autonomy. Repository or cloud access is\n technical capability, not permission to push, deploy, merge, or change\n production.\n- This Skill guides an implementation agent. Never copy it into the runtime\n Skills of the customer-facing agent.\n\n## Fastest Path: `@opengeni/sdk/chat`\n\nWhen using `user`, first provision that user\'s approved workspace membership\nthrough the explicit onboarding flow in `references/external-users-and-connect.md`.\nThe facade uses `asUser()` and never grants or restores membership on a chat\nrequest. An existing tenant is not proof that this user belongs to it. For shared\nconversations use the same OpenGeni session ID; identity changes authority, not\nthe conversation address. Use `chatBySessionId` to reopen historical sessions\nwhose IDs were derived with the old user-namespaced helper.\n\nStart here when the product already has a chat, or wants one, and OpenGeni\nshould sit behind it. Install, keep the organization API key on the server, and\nput one handler behind the chat endpoint:\n\n```bash\nbun add @opengeni/sdk\n```\n\n```ts\nimport { OpenGeni, createChatHandler } from "@opengeni/sdk/chat";\n\nconst og = new OpenGeni({\n apiKey: process.env.OPENGENI_API_KEY!,\n organizationId: process.env.OPENGENI_ORGANIZATION_ID!,\n});\n\nexport const POST = createChatHandler(og, {\n // Your auth hook. Tenant and user come from the authenticated request, never the body.\n resolve: async (request) => {\n const me = await authenticate(request);\n return me ? { tenant: me.accountId, user: me.userId } : new Response("Unauthorized", { status: 401 });\n },\n // format: "vercel" keeps an existing useChat client; "openai-chat" / "openai-responses"\n // keep an OpenAI-shaped client. The default streams native chunks for custom clients.\n});\n\n// Server-side use without an endpoint:\nconst chat = await og.chat({ tenant: "acme", user: "u_42", conversation: "c_9" });\nconst reply = await chat.send("hello"); // reply.text; chat.stream(...) yields chunks\n```\n\nBrowser: use a custom or compatible frontend for the backend chat handler.\nFor native OpenGeni React UI, install `@opengeni/react` and use\n`SessionConversation` or compose `MessageTimeline` and `ChatComposer` with\nthe normal SDK and authenticated session routes. Reset private UI state and cancel old\nrequests when the authenticated user or tenant changes. Every customer gets one workspace (`tenant`), every\nconversation one deterministic session, and each session picks its own\nisolation. Conversation IDs are independent of the acting user; ordinary API\nauthorization decides who can use the same shared conversation. Without a `user`,\n`resolve` must return the `conversation` itself. The Vercel and OpenAI adapters\nsend only the latest user message and import earlier messages once as context\non the first message; afterwards OpenGeni owns the history.\n\n| Scenario | `agentAccess` | `memory` |\n| --- | --- | --- |\n| Support desk: agent confined to its chat tree | `"session"` (default) | `false` (default) |\n| Agents restricted to their canonical user\'s chats | `"user"` with `asUser()` | `"user"` |\n| A team collaborating across chats | `"workspace"` | `"workspace"` |\n| Any of the above with Knowledge authoring initially Off | any | `false` |\n\n`agentAccess` is enforced in the server-side session-authorization seam for\nagents as outbound task scope: own tree, same canonical user, or workspace.\nA narrow target remains reachable by an authorized broad coordinator; target\nprivate visibility and normal permissions still apply. `asUser()` establishes\ncanonical authority, not a second end-user label. Graduate to `og.client`\n(`OpenGeniClient`) on the same `chat.sessionId` when the product needs files,\ntools, approval policies, forks, or realtime voice. The\n`examples/chat-quickstart` directory provides a backend-only server example.\n\n## Choose The Integration Shape First\n\nPick the smallest surface that satisfies the product:\n\n1. **Stock OpenGeni handoff** \u2014 link or deep-link into the OpenGeni web app.\n The product keeps no agent UI.\n2. **Headless product integration (default)** \u2014 the product backend uses\n `@opengeni/sdk`; the product renders its own UI and exposes tenant-scoped,\n same-origin routes to its browser or mobile client.\n3. **React session integration** \u2014 compose `@opengeni/react/session` hooks and\n pure projections into the product\'s UI. Add styled subpaths only for the\n surfaces the product wants.\n4. **OpenGeni-rendered React experience** \u2014 mount the packaged composer,\n timeline, realtime, or session chrome and import\n `@opengeni/react/compiled.css` once. No Tailwind setup or source scan is\n required. Override `--og-*` tokens only when branding is wanted.\n5. **Workbench integration** \u2014 mount the optional Changes/Files/Terminal/Desktop\n workspace when the product genuinely exposes agent compute. It has optional\n heavy peers and is not required for ordinary chat/session integration.\n\nRead `references/product-integration-shapes.md` before designing the boundary.\nRead `references/api-workflows.md` for session, upload, retry, repository,\nmachine, and schedule patterns.\n\nFor deeper implementation decisions, read selectively:\n\n- [Discovery and autonomy](references/discovery-and-autonomy.md)\n- [Isolation and authorization](references/isolation-and-authorization.md)\n- [Product shapes and UI](references/product-shapes-and-ui.md)\n- [Data tools and credentials](references/data-tools-and-credentials.md)\n- [Integration configuration and verification](references/runtime-profile-and-verification.md)\n- [Implementation checklist](references/implementation-overview.md)\n- [External users and embedded connection setup](references/external-users-and-connect.md)\n\nThis tree is the canonical developer guide for both repository installation and\nthe generated OpenGeni Product Integration Pack. It does not define a runtime\nprofile API, schedule Skill fields, or a new registry. Any references to an\nintegration\'s "runtime profile" mean configuration owned by the customer\'s code,\nnot a new OpenGeni resource. The Pack remains inactive until explicitly selected\nfor a coding session.\n\n## Choose The Credential\n\n- Use an **organization API key** when one server-side product integration\n provisions or manages many organization workspaces in one OpenGeni\n organization.\n- Use a **workspace API key** when the integration is deliberately constrained\n to one organization workspace and should not provision others.\n- Use a **delegated token** when the host acts with short-lived, explicit\n user/workspace authority rather than one standing product credential.\n- A **deployment access key** is a coarse deployment perimeter. Never use it as\n tenant identity or infer organization/workspace authority from it.\n\n## Default Trust Boundary\n\n- Keep the organization API key and operator credentials on the product server.\n- Authenticate the product\'s user first, resolve their allowed OpenGeni\n workspace/session server-side, and expose only the routes that product needs.\n- Use `@opengeni/sdk` instead of reconstructing event streaming, upload signing,\n retries, or wire types by hand.\n- Use `proxySessionEventStream` for a same-origin browser SSE route. Structural\n React client types let a host implement only the methods its mounted hooks use.\n- Direct browser access is valid only when the deployment\'s normal browser auth\n or an explicitly accepted bearer/CORS design makes it safe. Never ship a\n privileged shared API key in a browser bundle.\n\nThe product owns external identity, tenant-to-workspace mapping, business\nentities, navigation, presentation, and product-specific admission. OpenGeni\nowns sessions, turns, durable event history, approvals, agent execution,\nselected tools/resources, files, realtime session state, and compute lifecycle.\nLink records by opaque IDs; do not copy one system\'s whole data model into the\nother.\n\n## Organization And Workspace Bootstrap\n\nUse one organization API key for the external backend. Organization key\nadministration is exposed through `listOrganizationApiKeys`,\n`createOrganizationApiKey`, and `deleteOrganizationApiKey`, corresponding to\nthe organization-scoped `/v1/organizations/:organizationId/api-keys` routes.\nThe create response shows the token once; store it only in the product\'s secret\nmanager.\n\nFor each chosen product sharing boundary, call `ensureWorkspace` /\n`PUT /v1/workspaces/external` with a stable external mapping identity and persist\nthe returned `result.workspace.id`; `result.created` distinguishes the first\ninsert from an idempotent replay. Call it an **organization workspace** in\ncustomer guidance; its exact wire kind is `"shared"`. Personal workspaces are\nexcluded and must never be selected through a default-workspace fallback.\n\nChoose the workspace from who shares documents, workspace instructions,\nConnections, and integrations: normally one workspace per customer. Chat\nhuman visibility is controlled by `visibility`, not by `agentAccess` or Knowledge.\nUse `asUser(externalId)` for the authenticated product user. The server derives\nthe canonical user; never supply an `endUser` label as authority. Separately,\n`agentAccess: "session" | "user" | "workspace"` controls cross-session agent\nreach. Compatibility `memoryScope: "workspace" | "user" | "off"` selects Knowledge\nauthoring scope, not transcript visibility. Off initializes authoring to Off;\nexisting authorized Knowledge remains retrievable. Personal Knowledge belongs\nto the verified user of the active\nturn, including when different users collaborate in one shared session. Use\nexisting task notes for temporary session-tree coordination; there is no active\nsession Memory scope. Use a separate workspace when groups need different\nConnections, integrations, or instructions.\n\nUnscoped organization-key-created top-level sessions are workspace-visible.\nFor product-user ownership, use the server-side `asUser(externalId)` client and\nexplicit workspace membership described in `references/external-users-and-connect.md`;\nverified external owners can create private sessions when the organization enables\nthat feature. Private sessions do not make workspace Files or Sites private.\nManaged-human Only-me sessions are not a backend impersonation mechanism. A live\nagent with cross-session tools can reach unrelated sessions only when its\noutbound `agentAccess` scope and ordinary resource authorization allow it.\nThe target\'s `agentAccess` never restricts inbound access; private-session\nownership and ordinary permissions still apply.\nRemoving tools is not a substitute for private human visibility.\n\nThe external backend owns product Skills. Store and version them outside\nOpenGeni, then pass the selected definitions inline in\n`CreateSessionRequest.skills` for each product-created session. There is no\norganization-wide Skill registry or Skill inheritance in this integration\ncontract.\n\nUse `CreateSessionRequest.bundledSkillIds` to narrow OpenGeni\'s bundled guidance\nindependently: omitted means defaults, `[]` means none, and explicit IDs such as\n`builtin:opengeni-documents` allow only those whose normal inclusion rules hold.\nChildren inherit and can only narrow; scheduled-task `agentConfig` and automation\n`sessionTemplate` accept the same field. This does not hide workspace or inline\nSkills, grant tools, or disable eager `skill_read`. Keep the selection stable on\nkeyed-create retries. Never try to control it through arbitrary session metadata.\n\nPack installation `manifestSnapshot` is historical JSON, not a current admission\ncontract. Preserve it alongside `manifestDigest`; do not normalize its Skill\nlabels or replay old headerless Skills as new session input. New inputs require\nvalid `SKILL.md` frontmatter, which owns the name and description.\n\n## Prompt And Context Contract\n\nUse each prompt surface for its exact authority and lifetime:\n\n- Workspace `agentInstructions`: stable workspace-wide system persona and behavior.\n- Session `instructions`: durable system-level agent refinement for one session.\n- `modelContext`: ordinary model-visible content attached to one exact user\n message as a separate history part; standard timeline rendering omits it.\n- `initialMessage` and later message text: the visible part of that user message.\n\n`modelContext` is not secret, private, or privileged; full event/audit reads may\nreturn it. Do not hide business facts in a snapshot when the agent should inspect\nthem with an authorized product MCP tool. Prefer concise message context plus\ncanonical tool access. Changing `modelContext` must not change the persistent\nagent instruction prefix.\n\n## Client Workflow\n\n1. Resolve the API base URL and load the server-held organization API key.\n2. Resolve the authenticated product tenant, call `ensureWorkspace` with its\n stable external identity, and persist or verify the opaque workspace mapping.\n3. Read client config and access context without falling back to a Personal\n workspace.\n4. Load the exact Skills selected by the external product and pass them inline.\n5. Create a session with a stable idempotency key; optionally preallocate its ID\n when the product must persist a link before the first turn can run.\n6. Attach only canonical resources and an explicit minimal tool selection the\n user may use. Omitting tool selections inherits workspace/deployment\n defaults, including first-party workspace and cross-session capabilities.\n7. Stream/replay session events through the SDK; tolerate unknown additive event\n types.\n8. Send visible text separately from `modelContext`.\n9. Use the SDK upload helper; it owns begin, signed storage PUT, and completion.\n10. Surface approvals, human-input requests, queue state, errors, credit limits,\n and reconnect state as product state rather than generic chat text.\n11. Add realtime, Connected Machines, schedules, or the workbench only when the\n product use case needs them.\n\n## Guardrails\n\n- Workspace-scoped routes are canonical; resource IDs never authorize by\n themselves.\n- Organization workspaces have wire `kind: "shared"`; Personal workspaces are\n outside the external product mapping.\n- Use one workspace per customer and private/shared visibility for human\n access. Knowledge settings and prompt instructions do not create a tenant boundary.\n `agentAccess` optionally restricts agent reach further; tool removal is\n defense in depth, not a replacement for authorization.\n- Use separate workspaces only when groups must not share documents,\n Connections, integrations, or workspace instructions.\n- Do not invent an organization-wide Skill registry or rely on Skill\n inheritance. The external backend passes selected Skills inline per session.\n- The SDK cannot accept arbitrary customer backend functions as remote tools.\n Expose an existing API through a reviewed OpenAPI/GraphQL Integration or an\n MCP server.\n- OpenGeni\'s credential broker encrypts secrets and keeps them out of model\n context, but the trusted control plane can decrypt them for the authorized\n provider request. Do not describe it as zero knowledge.\n- Do not call Temporal, NATS, Postgres, workers, sandbox providers, object\n storage APIs, or MCP transports as substitutes for the public SDK/API.\n- Do not claim auth, model, tool, billing, CORS, storage, or compute behavior\n until the live deployment or current source proves it.\n- Keep examples generic and parameterized. Skills may name non-secret origins\n and conventions, but credentials come from a secret manager or environment.\n- Generate a customer-specific skill only for stable facts their coding agents\n repeatedly need. Keep it beside their integration code, point it at the SDK,\n include a config/access smoke probe, and never paste secrets into it.\n Start from `references/customer-skill-template.md` when the OpenGeni skill\n package is available.\n'
|
|
4690
|
+
"content": '---\nname: opengeni-product-integration\naudience: integration-agent\ndescription: >-\n Design, implement, verify, and hand off a tenant-safe OpenGeni product\n integration while adapting to the customer\'s architecture, UI, data APIs, and\n desired delivery autonomy. Select only for an implementation session;\n installation alone does not expose it to other agents.\n---\n\nThis implementation Skill is inactive until explicitly selected for an implementation session. Never attach it to customer-facing runtime sessions.\n\n\n# OpenGeni Client\n\nUse this skill when a customer\'s product and OpenGeni remain separate systems.\nThat is the normal integration shape: the product owns its users and business\nUI, while a standalone OpenGeni deployment owns agent sessions and execution.\n\nDo not interpret "embed" as "move OpenGeni into the product process." Advanced\nin-process router/core embedding is a separate infrastructure choice. Route that\nwork to the repo-maintainer `opengeni` skill and `docs/embedding.md`.\n\nDo not confuse two meanings of "skill": this file teaches a customer\'s coding\nagent how to integrate OpenGeni; session `skills` are runtime capabilities or\ninstructions attached to an OpenGeni agent. The former designs the integration.\nThe latter is product data sent through the installed SDK contract.\n\nCode and the live service are authoritative. Prefer `/v1/config/client`,\n`/v1/access/me`, the installed package types, and live probes over memorized\nroute, model, tool, or backend lists. When source is available, verify exact\nbehavior in `packages/sdk`, `packages/react`, contracts, and API routes.\nRead `docs/product-integration.md` when the repository is available; it is the\ncanonical product boundary for organization keys, workspace mapping, and Skill\nownership.\n\nWithout repository access, start at https://docs.opengeni.ai/llms.txt and fetch\nthe relevant Markdown pages. Before replacing an AI provider, answering a cost\nquestion, or reporting a setup blocker, read\n[Compatibility and troubleshooting](references/compatibility-and-troubleshooting.md).\n\n## Work Adaptively\n\n- Inspect the customer\'s repository, authentication, tenancy, data routes,\n frontend conventions, installed packages, tests, CI, and deployment guidance\n before asking questions or choosing an integration shape.\n- Ask only for consequential product choices or external authority that cannot\n be inferred. Do not ask the customer to restate facts the system proves.\n- Use a reversible, clearly stated default when an unresolved choice is\n low-risk. Resolve privacy, tenant authority, data writes, cost exposure, and\n ambiguous external mutations before crossing those boundaries.\n- Match the requested delivery autonomy. Repository or cloud access is\n technical capability, not permission to push, deploy, merge, or change\n production.\n- This Skill guides an implementation agent. Never copy it into the runtime\n Skills of the customer-facing agent.\n\n## Fastest Path: `@opengeni/sdk/chat`\n\nWhen using `user`, first provision that user\'s approved workspace membership\nthrough the explicit onboarding flow in `references/external-users-and-connect.md`.\nThe facade uses `asUser()` and never grants or restores membership on a chat\nrequest. An existing tenant is not proof that this user belongs to it. For shared\nconversations use the same OpenGeni session ID; identity changes authority, not\nthe conversation address. Use `chatBySessionId` to reopen historical sessions\nwhose IDs were derived with the old user-namespaced helper.\n\nStart here when the product already has a chat, or wants one, and OpenGeni\nshould sit behind it. Install, keep the organization API key on the server, and\nput one handler behind the chat endpoint:\n\n```bash\nbun add @opengeni/sdk\n```\n\n```ts\nimport { OpenGeni, createChatHandler } from "@opengeni/sdk/chat";\n\nconst og = new OpenGeni({\n apiKey: process.env.OPENGENI_API_KEY!,\n organizationId: process.env.OPENGENI_ORGANIZATION_ID!,\n});\n\nexport const POST = createChatHandler(og, {\n // Your auth hook. Tenant and user come from the authenticated request, never the body.\n resolve: async (request) => {\n const me = await authenticate(request);\n return me ? { tenant: me.accountId, user: me.userId } : new Response("Unauthorized", { status: 401 });\n },\n // format: "vercel" keeps an existing useChat client; "openai-chat" / "openai-responses"\n // keep an OpenAI-shaped client. The default streams native chunks for custom clients.\n});\n\n// Server-side use without an endpoint:\nconst chat = await og.chat({ tenant: "acme", user: "u_42", conversation: "c_9" });\nconst reply = await chat.send("hello"); // reply.text; chat.stream(...) yields chunks\n```\n\nBrowser: use a custom or compatible frontend for the backend chat handler.\nFor native OpenGeni React UI, install `@opengeni/react` and use\n`SessionConversation` or compose `MessageTimeline` and `ChatComposer` with\nthe normal SDK and authenticated session routes. Reset private UI state and cancel old\nrequests when the authenticated user or tenant changes. Every customer gets one workspace (`tenant`), every\nconversation one deterministic session, and each session picks its own\nisolation. Conversation IDs are independent of the acting user; ordinary API\nauthorization decides who can use the same shared conversation. Without a `user`,\n`resolve` must return the `conversation` itself. The Vercel and OpenAI adapters\nsend only the latest user message and import earlier messages once as context\non the first message; afterwards OpenGeni owns the history.\n\n| Scenario | `agentAccess` | `memory` |\n| --- | --- | --- |\n| Support desk: agent confined to its chat tree | `"session"` (default) | `false` (default) |\n| Agents restricted to their canonical user\'s chats | `"user"` with `asUser()` | `"user"` |\n| A team collaborating across chats | `"workspace"` | `"workspace"` |\n| Any of the above with Knowledge authoring initially Off | any | `false` |\n\n`agentAccess` is enforced in the server-side session-authorization seam for\nagents as outbound task scope: own tree, same canonical user, or workspace.\nA narrow target remains reachable by an authorized broad coordinator; target\nprivate visibility and normal permissions still apply. `asUser()` establishes\ncanonical authority, not a second end-user label. Graduate to `og.client`\n(`OpenGeniClient`) on the same `chat.sessionId` when the product needs files,\ntools, approval policies, forks, or realtime voice. The\n`examples/chat-quickstart` directory provides a backend-only server example.\n\n## Choose The Integration Shape First\n\nPick the smallest surface that satisfies the product:\n\n1. **Stock OpenGeni handoff** \u2014 link or deep-link into the OpenGeni web app.\n The product keeps no agent UI.\n2. **Headless product integration (default)** \u2014 the product backend uses\n `@opengeni/sdk`; the product renders its own UI and exposes tenant-scoped,\n same-origin routes to its browser or mobile client.\n3. **React session integration** \u2014 compose `@opengeni/react/session` hooks and\n pure projections into the product\'s UI. Add styled subpaths only for the\n surfaces the product wants.\n4. **OpenGeni-rendered React experience** \u2014 mount the packaged composer,\n timeline, realtime, or session chrome and import\n `@opengeni/react/compiled.css` once. No Tailwind setup or source scan is\n required. Override `--og-*` tokens only when branding is wanted.\n5. **Workbench integration** \u2014 mount the optional Changes/Files/Terminal/Desktop\n workspace when the product genuinely exposes agent compute. It has optional\n heavy peers and is not required for ordinary chat/session integration.\n\nRead `references/product-integration-shapes.md` before designing the boundary.\nRead `references/api-workflows.md` for session, upload, retry, repository,\nmachine, and schedule patterns.\n\nFor deeper implementation decisions, read selectively:\n\n- [Discovery and autonomy](references/discovery-and-autonomy.md)\n- [Isolation and authorization](references/isolation-and-authorization.md)\n- [Product shapes and UI](references/product-shapes-and-ui.md)\n- [Data tools and credentials](references/data-tools-and-credentials.md)\n- [Integration configuration and verification](references/runtime-profile-and-verification.md)\n- [Implementation checklist](references/implementation-overview.md)\n- [External users and embedded connection setup](references/external-users-and-connect.md)\n\nThis tree is the canonical developer guide for both repository installation and\nthe generated OpenGeni Product Integration Pack. It does not define a runtime\nprofile API, schedule Skill fields, or a new registry. Any references to an\nintegration\'s "runtime profile" mean configuration owned by the customer\'s code,\nnot a new OpenGeni resource. The Pack remains inactive until explicitly selected\nfor a coding session.\n\n## Choose The Credential\n\n- Use an **organization API key** when one server-side product integration\n provisions or manages many organization workspaces in one OpenGeni\n organization.\n- Use a **workspace API key** when the integration is deliberately constrained\n to one organization workspace and should not provision others.\n- Use a **delegated token** when the host acts with short-lived, explicit\n user/workspace authority rather than one standing product credential.\n- A **deployment access key** is a coarse deployment perimeter. Never use it as\n tenant identity or infer organization/workspace authority from it.\n\n## Default Trust Boundary\n\n- Keep the organization API key and operator credentials on the product server.\n- Authenticate the product\'s user first, resolve their allowed OpenGeni\n workspace/session server-side, and expose only the routes that product needs.\n- Use `@opengeni/sdk` instead of reconstructing event streaming, upload signing,\n retries, or wire types by hand.\n- Use `proxySessionEventStream` for a same-origin browser SSE route. Structural\n React client types let a host implement only the methods its mounted hooks use.\n- Direct browser access is valid only when the deployment\'s normal browser auth\n or an explicitly accepted bearer/CORS design makes it safe. Never ship a\n privileged shared API key in a browser bundle.\n\nThe product owns external identity, tenant-to-workspace mapping, business\nentities, navigation, presentation, and product-specific admission. OpenGeni\nowns sessions, turns, durable event history, approvals, agent execution,\nselected tools/resources, files, realtime session state, and compute lifecycle.\nLink records by opaque IDs; do not copy one system\'s whole data model into the\nother.\n\n## Organization And Workspace Bootstrap\n\nUse one organization API key for the external backend. Organization key\nadministration is exposed through `listOrganizationApiKeys`,\n`createOrganizationApiKey`, and `deleteOrganizationApiKey`, corresponding to\nthe organization-scoped `/v1/organizations/:organizationId/api-keys` routes.\nThe create response shows the token once; store it only in the product\'s secret\nmanager.\n\nFor each chosen product sharing boundary, call `ensureWorkspace` /\n`PUT /v1/workspaces/external` with a stable external mapping identity and persist\nthe returned `result.workspace.id`; `result.created` distinguishes the first\ninsert from an idempotent replay. Call it an **organization workspace** in\ncustomer guidance; its exact wire kind is `"shared"`. Personal workspaces are\nexcluded and must never be selected through a default-workspace fallback.\n\nChoose the workspace from who shares documents, workspace instructions,\nConnections, and integrations: normally one workspace per customer. Chat\nhuman visibility is controlled by `visibility`, not by `agentAccess` or Knowledge.\nUse `asUser(externalId)` for the authenticated product user. The server derives\nthe canonical user; never supply an `endUser` label as authority. Separately,\n`agentAccess: "session" | "user" | "workspace"` controls cross-session agent\nreach. Compatibility `memoryScope: "workspace" | "user" | "off"` selects Knowledge\nauthoring scope, not transcript visibility. Off initializes authoring to Off;\nexisting authorized Knowledge remains retrievable. Personal Knowledge belongs\nto the verified user of the active\nturn, including when different users collaborate in one shared session. Use\nexisting task notes for temporary session-tree coordination; there is no active\nsession Memory scope. Use a separate workspace when groups need different\nConnections, integrations, or instructions.\n\nUnscoped organization-key-created top-level sessions are workspace-visible.\nFor product-user ownership, use the server-side `asUser(externalId)` client and\nexplicit workspace membership described in `references/external-users-and-connect.md`;\nverified external owners can create private sessions when the organization enables\nthat feature. Private sessions do not make workspace Files or Sites private.\nManaged-human Only-me sessions are not a backend impersonation mechanism. A live\nagent with cross-session tools can reach unrelated sessions only when its\noutbound `agentAccess` scope and ordinary resource authorization allow it.\nThe target\'s `agentAccess` never restricts inbound access; private-session\nownership and ordinary permissions still apply.\nRemoving tools is not a substitute for private human visibility.\n\nThe external backend owns product Skills. Store and version them outside\nOpenGeni, then pass the selected definitions inline in\n`CreateSessionRequest.skills` for each product-created session. There is no\norganization-wide Skill registry or Skill inheritance in this integration\ncontract.\n\nUse `CreateSessionRequest.bundledSkillIds` to narrow OpenGeni\'s bundled guidance\nindependently: omitted means defaults, `[]` means none, and explicit IDs such as\n`builtin:opengeni-documents` allow only those whose normal inclusion rules hold.\nChildren inherit and can only narrow; scheduled-task `agentConfig` and automation\n`sessionTemplate` accept the same field. This does not hide workspace or inline\nSkills, grant tools, or disable eager `skill_read`. Keep the selection stable on\nkeyed-create retries. Never try to control it through arbitrary session metadata.\n\nPack installation `manifestSnapshot` is historical JSON, not a current admission\ncontract. Preserve it alongside `manifestDigest`; do not normalize its Skill\nlabels or replay old headerless Skills as new session input. New inputs require\nvalid `SKILL.md` frontmatter, which owns the name and description.\n\n## Prompt And Context Contract\n\nUse each prompt surface for its exact authority and lifetime:\n\n- Workspace `agentInstructions`: stable workspace-wide system persona and behavior.\n- Session `instructions`: durable system-level agent refinement for one session.\n- `modelContext`: ordinary model-visible content attached to one exact user\n message as a separate history part; standard timeline rendering omits it.\n- `initialMessage` and later message text: the visible part of that user message.\n\n`modelContext` is not secret, private, or privileged; full event/audit reads may\nreturn it. Do not hide business facts in a snapshot when the agent should inspect\nthem with an authorized product MCP tool. Prefer concise message context plus\ncanonical tool access. Changing `modelContext` must not change the persistent\nagent instruction prefix.\n\n## Client Workflow\n\n1. Resolve the API base URL and load the server-held organization API key.\n2. Resolve the authenticated product tenant, call `ensureWorkspace` with its\n stable external identity, and persist or verify the opaque workspace mapping.\n3. Read client config and access context without falling back to a Personal\n workspace.\n4. Load the exact Skills selected by the external product and pass them inline.\n5. Create a session with a stable idempotency key; optionally preallocate its ID\n when the product must persist a link before the first turn can run.\n6. Attach only canonical resources and an explicit minimal tool selection the\n user may use. Omitting tool selections inherits workspace/deployment\n defaults, including first-party workspace and cross-session capabilities.\n7. Stream/replay session events through the SDK; tolerate unknown additive event\n types.\n8. Send visible text separately from `modelContext`.\n9. Use the SDK upload helper; it owns begin, signed storage PUT, and completion.\n10. Surface approvals, human-input requests, queue state, errors, credit limits,\n and reconnect state as product state rather than generic chat text.\n11. Add realtime, Connected Machines, schedules, or the workbench only when the\n product use case needs them.\n\n## Guardrails\n\n- Workspace-scoped routes are canonical; resource IDs never authorize by\n themselves.\n- Organization workspaces have wire `kind: "shared"`; Personal workspaces are\n outside the external product mapping.\n- Use one workspace per customer and private/shared visibility for human\n access. Knowledge settings and prompt instructions do not create a tenant boundary.\n `agentAccess` optionally restricts agent reach further; tool removal is\n defense in depth, not a replacement for authorization.\n- Use separate workspaces only when groups must not share documents,\n Connections, integrations, or workspace instructions.\n- Do not invent an organization-wide Skill registry or rely on Skill\n inheritance. The external backend passes selected Skills inline per session.\n- The SDK cannot accept arbitrary customer backend functions as remote tools.\n Expose an existing API through a reviewed OpenAPI/GraphQL Integration or an\n MCP server.\n- OpenGeni\'s credential broker encrypts secrets and keeps them out of model\n context, but the trusted control plane can decrypt them for the authorized\n provider request. Do not describe it as zero knowledge.\n- Do not call Temporal, NATS, Postgres, workers, sandbox providers, object\n storage APIs, or MCP transports as substitutes for the public SDK/API.\n- Do not claim auth, model, tool, billing, CORS, storage, or compute behavior\n until the live deployment or current source proves it.\n- Keep examples generic and parameterized. Skills may name non-secret origins\n and conventions, but credentials come from a secret manager or environment.\n- Generate a customer-specific skill only for stable facts their coding agents\n repeatedly need. Keep it beside their integration code, point it at the SDK,\n include a config/access smoke probe, and never paste secrets into it.\n Start from `references/customer-skill-template.md` when the OpenGeni skill\n package is available.\n'
|
|
4616
4691
|
},
|
|
4617
4692
|
{
|
|
4618
4693
|
"path": "agents/openai.yaml",
|
|
@@ -4622,6 +4697,10 @@ var productIntegrationSkillFiles = [
|
|
|
4622
4697
|
"path": "references/api-workflows.md",
|
|
4623
4698
|
"content": '# OpenGeni API Workflows\n\nThis reference is intentionally pattern-level. Check the live service or source\ncontracts for exact schemas before generating SDK code. When the repository is\navailable, `docs/product-integration.md` is the canonical organization-key,\nworkspace-mapping, and Skill-ownership guide.\n\n## Access Setup\n\nChoose one credential deliberately:\n\n- Managed SaaS product integration: create an organization API key through\n `POST /v1/organizations/:organizationId/api-keys` /\n `createOrganizationApiKey`, store the one-time token on the product server,\n and send `Authorization: Bearer <api-key>`.\n- One-workspace automation: use a workspace API key and do not call\n organization provisioning routes.\n- User/workspace delegation: use a short-lived delegated token with explicit\n authority rather than a standing organization key.\n- Configured/self-hosted perimeter: a deployment access key may gate the\n deployment, but it is not tenant identity.\n- Local development: the service may resolve a default dev subject/workspace without external auth.\n\nOnly add `x-opengeni-access-key` when the operator says the deployment\nshared-key boundary is enabled. It is not a replacement for organization API\nkeys in managed SaaS.\n\n## Minimal Server-Side Session Client\n\n```ts\nimport { OpenGeniClient } from "@opengeni/sdk";\n\nconst client = new OpenGeniClient({\n baseUrl: process.env.OPENGENI_API_BASE_URL!,\n apiKey: process.env.OPENGENI_ORGANIZATION_API_KEY!,\n});\n\nconst organizationId = process.env.OPENGENI_ORGANIZATION_ID!;\nconst { workspace } = await client.ensureWorkspace({\n accountId: organizationId,\n externalSource: "acme-product",\n externalId: productTenant.id,\n name: productTenant.displayName,\n});\nif (workspace.kind !== "shared") {\n throw new Error("Product integrations require an organization workspace");\n}\n\nconst skills = await productSkillStore.resolveForSession(productTenant.id);\nconst created = await client.createSession(workspace.id, {\n initialMessage: "Inspect the uploaded logs and summarize the failing deploy step.",\n idempotencyKey: crypto.randomUUID(),\n skills,\n firstPartyMcpTools: selectedFirstPartyTools,\n tools: selectedIntegrationServers,\n});\n\nfor await (const event of client.streamEvents(workspace.id, created.id)) {\n if (event.type === "agent.message.delta") {\n process.stdout.write((event.payload as { text?: string }).text ?? "");\n }\n}\n```\n\nThis code belongs on the product server, not in a browser bundle. For a browser\ntimeline, expose a tenant-scoped same-origin route and use the SDK\'s\n`proxySessionEventStream` helper. Authenticate the product user and resolve the\nallowed workspace/session before opening the upstream stream.\n\n`ensureWorkspace` maps through `PUT /v1/workspaces/external`. Use a stable\nexternal source/id pair and persist the returned opaque id. The returned\norganization workspace has wire `kind: "shared"`. Personal workspaces are\nexcluded; do not fall back to `/v1/access/me`\'s default/personal workspace.\nThe method returns `{ workspace, created }`; use `workspace.id`, and treat\n`created: false` as the normal idempotent replay result.\n\nThe product backend stores and versions Skills outside OpenGeni and passes the\nselected definitions inline in `CreateSessionRequest.skills`. There is no\norganization-wide Skill registry or Skill inheritance in this integration\ncontract.\n\nThe product must choose the workspace mapping from its sharing rule before\nrunning this flow. A tenant-shared workspace is suitable only when that tenant\nmay share workspace-scoped agent authority and resources. Use a per-user\nworkspace for cross-user chat privacy and a per-chat workspace for hard\nsame-user chat isolation. Knowledge authoring Off does not create either\nboundary.\n\nFor a headless product, send an explicit minimal `firstPartyMcpTools` and\n`tools` selection. Omission inherits deployment/workspace defaults. Removing\ncross-session tools from a shared workspace is defense in depth, not a hard\ntenant boundary.\n\n## Existing APIs As Agent Tools\n\nThe SDK cannot serialize ordinary customer backend functions into tools. Use\none of the supported network boundaries:\n\n- For an existing HTTP API, host a focused OpenAPI 3.0/3.1 document and call\n `previewApiIntegration`, then `installApiIntegration` with the exact revision,\n digest, Connection, stable instance key, and selected operations.\n- For GraphQL, use the same preview/install lifecycle with the GraphQL source.\n- For MCP, install a workspace capability or pass a session-specific\n `mcpServers` definition with an HTTPS URL, allowed tools, approval policy, and\n write-only headers or a `connectionRef`.\n\nPreview/install is deterministic backend work and can be reconciled across many\nworkspaces; a model does not need to read and approve the same API description\nfor every workspace. Persist the returned Integration instance/server IDs and\nskip unchanged desired versions rather than reinstalling on every chat.\n\nCreate API-key credentials with `createConnection`. Rotate ordinary credentials\nthrough `updateConnection` with `expectedVersion`; OAuth providers use their\ndedicated reconnect flow. For session-specific MCP headers, later message\nrequests may carry the supported MCP credential update. Responses expose\nmetadata and credential versions, never the values.\n\nOpenGeni encrypts brokered credentials at rest and keeps them out of model\ncontext. The trusted control plane can decrypt them to call the exact provider;\nthe model and sandbox receive only schemas and bounded results. The provider API\nmust still enforce tenant/user scope on every operation and must not trust a\nmodel-supplied tenant ID.\n\n## Runtime Profile And Models\n\nUse workspace `agentInstructions` for stable workspace behavior, session\n`instructions` for one role/conversation, Skills for conditional procedures,\nand `modelContext` for current dashboard or route state. Avoid duplicating one\npolicy across all four surfaces.\n\nInline Skills are transmitted once in `createSession` and fixed onto that\nsession, not sent on each turn. Version the customer-owned runtime profile and\napply new Skill content to new sessions unless the product deliberately\nmigrates old ones.\n\nWorkspace `sessionDefaults` set the default model and reasoning for new\nsessions. A session or message may override them subject to the workspace model\naccess policy. Resolve model IDs from the live client configuration rather than\nhard-coding a remembered list.\n\nOpenGeni-credit models in every organization workspace draw from the same\norganization account balance; workspace creation does not create separate\nwallets. Connected subscriptions and workspace-owned provider credentials may\ninstead use an externally billed path. Preserve the workspace and product\nboundary in usage attribution when the customer needs a per-user or per-tenant\nview over the shared organization balance.\n\nReconcile stable workspace settings, Connections, Integrations, and runtime\nprofile versions during provisioning, startup, deployment, or a controlled\nmigration. Do not PATCH the same settings or reinstall the same Integration on\nevery chat request when no desired version changed.\n\n## Session Creation Options\n\nBeyond `initialMessage`/`tools`/`resources`, the create body (`POST /v1/workspaces/:workspaceId/sessions`, the SDK\'s `createSession(workspaceId, request)`) chooses where the session runs:\n\n- `sandboxBackend` \u2014 pick the managed sandbox execution backend; omit for the deployment default.\n- `targetSandboxId` (uuid) \u2014 run the session on an enrolled **Connected Machine** (a user-owned machine) instead of a managed sandbox. It seeds the session\'s active-sandbox pointer at creation so the first turn routes to that machine; an invalid/unowned/offline target fails the create.\n- `workingDir` \u2014 the host path the machine runs the session under (the base for its agent cwd, terminal, and file dock). **Only valid together with `targetSandboxId`** \u2014 sending `workingDir` alone is a 422. Omit it to use the machine\'s default working directory.\n- `sandbox` \u2014 shared-sandbox placement for managed sandboxes, a three-way union: `"shared"` (join the creating session\'s box; a top-level `"shared"` is a 422), `"new"` (mint a fresh box), or `{ groupId }` (join a specific sibling group in the same workspace). Omitted resolves a context-dependent default server-side.\n- `idempotencyKey` (1\u2013200 chars) \u2014 a workspace-scoped CREATE idempotency key (see below).\n\n`targetSandboxId`/`workingDir` are the managed-sandbox-vs-Connected-Machine choice; `sandboxBackend` and the `sandbox` placement union only apply to managed sandboxes. To move a session onto a different machine *after* creation, use the active-sandbox swap (below) \u2014 not `updateSession`, whose only field is the session `title`.\n\n## Replay And Retry\n\n- Persist the latest event sequence seen by the client.\n- On reconnect, list events after the last known sequence before reopening the stream.\n- Retry idempotent reads and stream reconnects with bounded backoff.\n- Session creation exposes a workspace-scoped `idempotencyKey` (distinct from the per-call `clientEventId`): forward a stable value so concurrent/retried creates of the same logical session collapse to a single session. Without it every create is independent, so a blind retry can double-create \u2014 keep sending a stable key when you retry.\n- Treat unknown event types as extensible timeline entries, not client crashes.\n\n## Files\n\nThe usual flow is:\n\n1. `POST /v1/workspaces/:workspaceId/files/uploads`\n2. `PUT` bytes to the returned signed object-storage URL with the required headers.\n3. Complete the upload through the returned workspace upload endpoint.\n4. Attach the file resource to a session, follow-up turn, or scheduled task only after it is ready.\n\nNever attach a file id from another workspace. Correct behavior is no data leak: 403 when the credential has no workspace grant, 404 when the resource is not in the granted workspace.\n\n## Knowledge And Search\n\nUploaded originals remain Files. Attaching a ready file in a normal chat lets the\naccepted agent turn prepare its searchable source content when Knowledge\nauthoring is enabled; users do not need a separate document-base upload flow.\nThe attempt-bound `knowledge_retain_file` tool provides explicit preparation.\nRetained sources, findings and collections share the canonical Knowledge entries\nAPI. Use `listKnowledgeEntries` and `getKnowledgeEntry` for retrieval, or the\nfirst-party `knowledge_*` tools for agents. A finding cites a specific source\nrevision; a collection relates existing entries across sources without copying.\n\nDefault retrieval returns published content. Explicit `view: "needs_review"`\nreturns accessible pending proposals as unapproved context, so an agent can\nimprove an existing entry instead of creating duplicates. Agent learning resolves\nworkspace/personal defaults plus chat or scheduled-task overrides: Automatic\npublishes, Review first stages without pausing work, and Off disables agent\nauthoring while retaining authorized retrieval. Conversation history and\ntemporary task notes remain separate. Inspect the installed SDK and live schema\nfor exact request shapes rather than using the retired Memory writers.\n\n## GitHub Repositories\n\nFor private repos, use the workspace GitHub repository list before attaching a resource. A valid repository resource normally includes clone URL, ref, mount path, GitHub installation id, and GitHub repository id from OpenGeni\'s listing response. The worker mints short-lived GitHub App tokens for selected repositories and should not persist clone credentials in session manifests.\n\nDo not ask customers to paste GitHub App private keys into their client integration. Managed SaaS uses the OpenGeni-owned app; self-hosted operators configure their own app server-side.\n\n## Connected Machines And Enrollment\n\nA Connected Machine is a user-owned machine enrolled into a workspace and used as first-class primary compute (no cloud box behind it; it uses its own git auth; repos are not cloned onto it). `selfhosted` is the internal `sandboxBackend` enum value for such a machine.\n\nDiscover and target machines:\n\n- `GET /v1/workspaces/:workspaceId/machines` \u2014 list the workspace\'s machines (each with its derived state, latest metrics, and shared-session count) plus the active-sandbox pointer. Pass `?sessionId=` for an in-session view that also includes the session\'s own group box. SDK: `listMachines(workspaceId, { sessionId })`.\n- `GET /v1/workspaces/:workspaceId/machines/:enrollmentId/metrics/series?window=15m|1h|6h|24h` \u2014 the downsampled (~1/min) metrics history. SDK: `machineMetricsSeries(workspaceId, enrollmentId, { window })`.\n- Create a session with `targetSandboxId` (a machine\'s `sandboxId` from the list) plus an optional `workingDir` to run on it.\n- `POST /v1/workspaces/:workspaceId/sessions/:sessionId/active-sandbox` with `{ target }` \u2014 swap the session\'s active sandbox mid-conversation; `target` is a machine\'s `sandboxId`, or `"session"`/`"default"` to return to the session\'s own group box. The response echoes `swapped`, `activeSandboxId`, `activeEpoch`, and a `reason` when a target is refused. SDK: `swapActiveSandbox(workspaceId, sessionId, { target })`.\n\nEnroll a machine (the client-driven parts):\n\n- Interactive device flow: the machine\'s own agent starts and polls the flow agent-side (unauthenticated). A workspace operator resolves the pending request by user code with `POST /v1/enrollments/device/lookup` (no workspace in the path \u2014 the server resolves it from the code, then authorizes `enrollments:read`), then `POST /v1/workspaces/:workspaceId/enrollments/device/approve` (the loud consent step; `allowScreenControl` opts into screen control) or `.../device/deny`. SDK: `lookupDeviceEnrollment(userCode)`, `approveDeviceEnrollment(workspaceId, { userCode, allowScreenControl })`, `denyDeviceEnrollment(workspaceId, { userCode })`.\n- Headless / fleet: `POST /v1/workspaces/:workspaceId/enrollments/token` mints a short-TTL SECRET enroll token (surface it once with a copy-now warning); the machine\'s agent redeems it agent-side at `POST /v1/enrollments/token/exchange`. SDK: `mintEnrollToken(workspaceId, { allowScreenControl })`.\n- `POST /v1/workspaces/:workspaceId/enrollments/:enrollmentId/revoke` removes a machine. Approving, minting, and revoking all require `enrollments:manage`; listing needs `enrollments:read`.\n\nNever distribute an OpenGeni credential to a Connected Machine or try to inject git tokens into it \u2014 the machine authenticates to git with its own credentials. Device start/poll and token exchange are agent-side calls, not client SDK methods.\n\n## Billing And Limits\n\nManaged SaaS uses prepaid Stripe credits and local usage/cost accounting. Client behavior should be simple:\n\n- Show billing/credit status from `/v1/billing` when the user has billing permission.\n- Stop costly writes/runs when the API returns a credit/limit denial.\n- Preserve read/export paths when writes are blocked.\n- Surface top-up links from OpenGeni; do not call Stripe directly from a customer agent unless OpenGeni explicitly returns a Stripe URL.\n\n## Generated Customer Skills\n\nWhen generating a customer-specific agent skill that teaches their coding agents how to call OpenGeni:\n\n- Include only their non-secret base URL, organization-workspace mapping\n convention, and safe API examples.\n- Tell the agent to read API keys from the customer\'s secret manager or environment, never from the skill.\n- State that the backend uses an organization API key, organization workspaces\n have wire `kind: "shared"`, and Personal workspaces are excluded.\n- State where the external product stores Skills and that it passes selected\n definitions inline per session; never invent an organization-wide Skill\n registry or inheritance layer.\n- Keep the skill versioned with their integration code and add a quick smoke command that calls `/v1/config/client` and `/v1/access/me`.\n- State which integration shape the product chose and where its tenant-safe\n proxy/client lives; do not teach every possible shape in every customer skill.\n- Describe only primitives proven by the installed SDK and live deployment; do\n not turn roadmap assumptions into customer instructions.\n- Start from `customer-skill-template.md` in this directory so the generated\n skill records the chosen shape and smoke probes without copying credentials.\n'
|
|
4624
4699
|
},
|
|
4700
|
+
{
|
|
4701
|
+
"path": "references/compatibility-and-troubleshooting.md",
|
|
4702
|
+
"content": "# Verify compatibility and recover setup problems\n\n## Product documentation without repository access\n\nFetch https://docs.opengeni.ai/llms.txt for the official documentation index.\nRead the relevant Markdown page, especially\nhttps://docs.opengeni.ai/guides/integrate-your-product.md,\nhttps://docs.opengeni.ai/reference/authentication.md, and\nhttps://docs.opengeni.ai/reference/sdk.md. An ordinary customer integration\ndoes not require a clone of OpenGeni. If a fetch fails, report that source as\nunavailable and inspect the installed package and authorized service instead.\n\nDocumentation availability and deployed feature availability are separate.\nInspect the installed package exports/types and `/v1/config/client`; verify the\nchosen contract against the intended deployment before implementing against it.\n\n## Decide what is being replaced\n\n| Customer dependency | Evidence needed |\n| --- | --- |\n| Existing Vercel `useChat` frontend | Compatible UI message stream and the product's authenticated handler routes |\n| Server-side AI SDK `streamText` or provider constructor | Actual provider/request features in use; a UI stream adapter alone is insufficient |\n| OpenAI-shaped chat client | Supported subset of Chat Completions or Responses in the installed handler |\n| Embeddings and retrieval | Independently supported embedding contract; keep the existing provider when only generation is migrated |\n| Conversation history | Stable conversation identity, authorization, one-time imported history, later durable session history |\n| Account setup | Correct credential type, authorized organization/workspace, required onboarding/configuration, and actual usable endpoint |\n| Per-request monetary cost | Reply schema and billing semantics, not model-picker categories |\n\nThe `@opengeni/sdk/chat` adapters run in the customer's backend and talk to the\nOpenGeni session API. They do not establish `/responses`, `/chat/completions`,\nor `/embeddings` on the OpenGeni service base URL. Do not invent methods such as\n`sessions.createResponse`. Generic OpenAI-compatible client documentation proves\nclient behavior, not OpenGeni server support.\n\nResolve a missing central compatibility fact before replacing production wiring.\nIf evidence is unavailable, continue independent work and describe any scaffold\nas unvalidated. A caveat must constrain implementation when the unknown decides\nwhether the whole solution can work.\n\nIf the user says they will change secrets and wants account configuration,\nanswer that narrower request first. Inspect authorized setup tools/configuration,\nperform in-scope actions, and state the exact human step if administration is\nunavailable. Do not ask the customer to provide OpenGeni's own API specification.\n\n## Cost reporting\n\nThe current `ChatReply`, Vercel UI stream, and OpenAI response extension have no\nfirst-class monetary cost field. Check the installed version before answering.\n`getBillingUsage` is a separate accounting route requiring `billing:read` and\nappropriate access. Its bounded usage list is not a complete per-turn accounting\ninterface, and a normal integration credential must not be assumed to hold billing\nauthority. Do not promise that it adds fields to an AI SDK response.\n\nAccounting must distinguish OpenGeni credit charge, estimated provider expense,\nand external subscription/provider billing. A zero OpenGeni charge is not a zero\nprovider expense. Multiple model responses may contribute to a single turn.\nState unknown telemetry as unknown, not zero. Use installed schemas or a verified\nresponse as evidence; neither model availability nor a missing CLI answers this.\n\n## Discovery and development preflight\n\nRecover ranked tool-search misses using authorized inventory and exact-name\ndisclosure. Inspect the reviewed capability catalog where relevant. `skill_read`\ndoes not need a sandbox; a missing `ogtool` should not block reading product\nguidance. Keep command errors visible instead of treating suppressed errors as\nproof that no capability exists.\n\nFor GitHub, distinguish App configuration, workspace binding, repository access,\nand session attachment using the available GitHub tools. Confirm the real\nrepository root before patching. A missing optional notes file should not\nshort-circuit the rest of discovery.\n\nBefore coding, read the package manager declaration, lockfile, runtime-version\nfiles (including `mise.toml` when present), tests, and CI. Probe the selected\ncompute for the required versions. Repair missing tools inside the authorized\ndisposable sandbox, then rerun the intended checks. A Connected Machine's\nsystem-wide configuration has separate user ownership.\n\nAn install is successful only when its exit/result and a version or execution\nprobe establish success. Rerun the intended test after repair. If blocked,\nreport the attempted repair and exact missing requirement, rather than handing\nback an avoidable package installation. Never substitute a whitespace check for\nunit, type, or integration tests. Report code written, committed, tested,\nintegration verified, and published as distinct facts within the requested scope.\n"
|
|
4703
|
+
},
|
|
4625
4704
|
{
|
|
4626
4705
|
"path": "references/customer-skill-template.md",
|
|
4627
4706
|
"content": "---\nname: customer-opengeni-integration\ndescription: >-\n Use when editing or verifying this product's server-side OpenGeni adapter,\n tenant-to-workspace mapping, session proxy, or integration smoke tests.\n---\n\n# Customer OpenGeni integration\n\nReplace every bracketed placeholder with a non-secret project fact. Keep this\nSkill beside the product's integration code and review it whenever the installed\n`@opengeni/sdk` major version changes.\n\n## Stable configuration\n\n- OpenGeni base URL: `[non-secret HTTPS base URL]`\n- Organization ID: `[non-secret UUID]`\n- External source convention: `[stable product namespace, for example acme-support]`\n- Workspace isolation unit: `[tenant | end user | chat | another explicit sharing group]`\n- Credential environment variable: `OPENGENI_ORGANIZATION_API_KEY`\n- Base URL environment variable: `OPENGENI_API_BASE_URL`\n- Organization environment variable: `OPENGENI_ORGANIZATION_ID`\n\nNever put API keys, delegated signing secrets, provider tokens, production\nresponses, or user identifiers in this Skill. Read credentials from the\nserver-side secret manager or environment at runtime.\n\n## Chosen integration shape\n\nThis product uses `[organization API key | workspace API key | delegated token]`\nbecause `[one sentence explaining the authority boundary]`.\n\n- Server-side adapter/proxy: `[path]`\n- Tenant-to-workspace mapping persistence: `[path or table/model name]`\n- Session/event proxy: `[path]`\n- Product Skill store/loader: `[path]`\n- Runtime profile version/source: `[version and path]`\n- Explicit first-party tool allowlist: `[source of truth]`\n- External Integration/MCP server selection: `[source of truth]`\n\nIf the selected shape is an organization API key, map the smallest product\ngroup allowed to share workspace-scoped agent authority to one OpenGeni\norganization workspace with `ensureWorkspace`. The wire kind is `\"shared\"`.\nUse per-tenant mapping for collaborative chats, per-user mapping for cross-user\nprivacy, and per-chat mapping for hard same-user chat isolation. Personal\nworkspaces are excluded and must never be used as a default fallback. Persist\nthe returned opaque workspace ID and pass the exact product-selected Skills\ninline in `CreateSessionRequest.skills` for every product-created session;\nthere is no organization-wide Skill inheritance. Turning Knowledge authoring off\ndoes not isolate sessions.\nEach submitted Skill contains `files` with a valid `SKILL.md`. Its YAML\nfrontmatter owns the name and description used in the agent's initial index;\ndo not keep a second editable summary in the product adapter. Submit files\nalone; optional legacy name/description values must exactly match frontmatter.\nUse a key issued by the organization API-key control plane. Do not reuse an\nambiguous legacy null-workspace token; provenance migrations revoke those keys\nso old and new API instances both fail closed during rollout.\n\nIf the selected shape is a workspace API key, configure one pre-provisioned\nworkspace ID and never call `ensureWorkspace` or an organization API-key route.\nThe credential is valid only for that exact workspace. If the selected shape is\na delegated token, use only the account/workspace and permissions frozen into\nthe host-issued token; do not substitute organization-key behavior.\n\n## Required workflow\n\n1. Authenticate the product user and resolve the allowed product tenant.\n2. Load the server-held credential; never return it to the browser.\n3. For an organization key, resolve or ensure the tenant's workspace with the\n stable external source/id pair. For a workspace key, load and verify the\n configured pre-provisioned workspace ID instead.\n4. Apply explicit workspace settings through installed SDK methods.\n5. Create sessions with a stable idempotency key and product-owned inline\n Skills, plus explicit minimal `tools` and `firstPartyMcpTools` selections.\n6. Reject caller-supplied workspace/session IDs that do not match the product's\n persisted tenant relationship.\n7. Proxy event streaming with replay-by-sequence and duplicate suppression.\n8. Reconcile settings, Connections, API Integrations, and runtime profile only\n when their desired version changes; do not repeat control-plane installation\n on every chat request.\n\n## Smoke probes\n\nRun through the product's authenticated server-side test harness; do not paste\ncredentials into shell history or this file.\n\n```text\nGET /v1/config/client\nGET /v1/access/me\nGET /v1/workspaces\n```\n\nVerify that the live deployment and installed SDK types agree. They outrank\nremembered route, model, provider, tool, or compute lists. For an organization\nkey, an empty `workspaceGrants` array in `/v1/access/me` is expected;\n`GET /v1/workspaces` is the complete organization-workspace inventory.\n"
|
|
@@ -4636,7 +4715,7 @@ var productIntegrationSkillFiles = [
|
|
|
4636
4715
|
},
|
|
4637
4716
|
{
|
|
4638
4717
|
"path": "references/external-users-and-connect.md",
|
|
4639
|
-
"content": "# External users and embedded connection setup\n\nUse these APIs only when the installed SDK and deployment expose them. This\nguide describes implemented external identity, service lifecycle, core\nPersonal/private session access, and curated OAuth paths\u2014not completion of every\nwhite-label surface. Optional native linking is explicit delegation, not account\nmerging. Full provider coverage remains unfinished. Do not infer guarantees from\nthe presence of a contract type.\n\n## Optional use of an existing native account\n\nOrdinary embedding needs only `asUser`; never require native registration or\nlinking for a product user. For an existing OpenGeni user who deliberately wants\nthe product to use their native workspace access, begin a link through the\nexternal client with `beginIdentityLink`. Show the returned challenge only in\nthe native consent URL fragment, never its query, logs or analytics. The native\n`/identity-links/:linkId?organization=:organizationId#challenge=:challenge` page\nrequires the user's real native login, displays both identities and the requested\npermissions, and permits narrowing before confirmation. The fragment is scrubbed\nbefore the application mounts. A page reload requires reopening the original\nconsent URL. Poll `getIdentityLink` from the product backend for confirmation.\n\nAfter explicit confirmation, choose linked mode on the backend:\n\n```ts\nconst linked = serviceClient.asLinkedUser(authenticatedUser.id, {\n source: \"my-product\",\n linkId: confirmedLink.id,\n expectedLinkRevision: confirmedLink.revision,\n});\n```\n\nUse the same source and opaque ID used at initiation. A confirmed link does not\nchange `asUser`, move external sessions or credentials, or merge accounts. New\nlinked work belongs to the native user. Requests intersect the key's permissions,\nthe live native user's permissions and the approved link ceiling. Never fall back\nto service or external mode when linked authorization fails.\n\nLink expiry is optional; null means until revoked. Either confirmed participant\ncan revoke using the observed revision. Accepted linked turns, scheduled task\nrevisions, child sessions and causal continuations retain the link restriction;\nrevocation denies later execution without requiring the original API key to stay\nactive. This is an execution-time check, not a promise to undo a remote operation\nalready started. Native access to native-owned resources remains intact. Do not\nassume external-owned host MCP bindings transfer to the native owner through a\nlink: create a separate binding while explicitly acting as the native user.\nIts owner revision is the native member's revision; linked work independently\nretains the live link restriction. `listIdentityLinks(workspaceId, cursor?)`\nprovides a participant-only inventory, and native workspace settings expose the\nsame list/revoke behavior without retaining the original consent URL.\n\n## Separate service administration from user requests\n\nKeep one organization-key client on the trusted product backend. After product\nauthentication, derive the immutable external identity from the server session:\n\n```ts\nconst actor = serviceClient.asUser(authenticatedUser.id, { source: \"my-product\" });\nconst transport = actor.connectTransport();\nconst providers = await transport.catalog(authorizedWorkspaceId);\n```\n\nHere `serviceClient`, `authenticatedUser`, and `authorizedWorkspaceId` are\nhost-owned dependencies, not fields accepted from the browser. `asUser` creates\na separate client and does not mutate the service client. It requires an\norganization key; a workspace key or deployment access key is not a substitute.\nNever retry a denied user request using the unscoped service client.\n\nAn organization key is trusted to assert and lazily provision product users;\nthere is no separate provisioning permission or registration ceremony. The first\nauthenticated request may create the identity anchor even if its later workspace\noperation is denied. Workload permissions still restrict that operation. Derive\nIDs from authenticated host records and bound onboarding in the host; never\nforward arbitrary browser-supplied identities. Personal identity anchors are not\nshared product-tenant workspaces or a way to obtain workspace membership.\n\nIdentity is scoped by organization, source and opaque external ID. Source\ndefaults to `default`; use a stable source namespace when multiple identity\nsystems share an organization. IDs are case-sensitive, not emails to normalize:\nmaximum 1024 UTF-8 bytes for the ID and 200 for source; empty strings, NUL and\ninvalid Unicode are rejected. A native-looking ID does not impersonate a native\nuser. Workspace mapping identity passed to `ensureWorkspace` is a separate\nconcept from this acting-user identity.\n\nUser mode lazily establishes an external identity but does not grant access to a\nshared workspace. An explicitly authorized service onboarding operation may use:\n\n```ts\nawait serviceClient.addExternalWorkspaceMember(authorizedWorkspaceId, {\n identity: { externalId: authenticatedUser.id, source: \"my-product\" },\n permissions: [\"workspace:read\", \"connections:read\", \"connections:write\"],\n operationId: onboardingOperationId,\n});\n```\n\nDo this only after the host has approved membership, not on every arbitrary\nbrowser request. The service needs `members:manage` and may not grant authority\nbeyond its ceiling. Persist `onboardingOperationId` before the call. Exact keyed\nreplays return historical identity without restoring removed membership; different\npermissions conflict instead of overwriting a subsequently reduced grant. Installation also\nneeds `capabilities:manage`; do not add it unless installation is a product\nfeature the user may perform. User requests intersect actual membership with\nthe initiating key's permissions. Service administration remains separate.\n\n### Removal and account-wide lifecycle\n\nFor recoverable onboarding, establish and retain the identity anchor before\ngranting membership. Service `lookupExternalIdentity(organizationId, identity)`\nis non-provisioning and returns content-free identity/membership IDs, statuses and\nseparate revisions, including suspended/offboarded identities. It requires\n`members:manage` and does not reactivate an identity. A missing result is not proof\nthat an earlier unkeyed request cannot still provision one.\n\nTo withdraw this external member's workspace access while fencing a pending keyed\ngrant, call `cancelExternalWorkspaceMemberGrant(organizationId, workspaceId,\norganizationMembershipId, { operationId: cancellationOperationId,\ncancelGrantOperationId: onboardingOperationId })`. Persist both distinct UUIDs;\nretry the exact cancellation body after response loss. This reuses native\nteardown and fences the named grant even if membership is absent. It withdraws\ncurrent workspace membership, not just one session. New grant IDs are explicit\nnew onboarding, never retries. Legacy requests without `operationId` remain\nunfenced: drain old writers before claiming late-grant protection.\n\nUse the service client's existing `removeWorkspaceMember(workspaceId, subjectId)`\nto remove an external actor from a shared workspace. It requires `members:manage`,\nrechecks the live key, preserves the last-admin guard, and uses the same fenced\nsettlement/cancellation path as native removal. It does not disable the actor in\nother workspaces. Ordinary `asUser` reads never restore removed membership.\n\nFor account-wide changes, call `serviceClient.updateExternalIdentityMembership(\norganizationId, organizationMembershipId, request)`. The membership ID comes from\nthe identity returned by onboarding; the initial membership authorization\nrevision is 1. The request contains `kind` (`suspend`, `reactivate`, or `offboard`),\n`expectedAuthorizationRevision`, a UUID `operationId`, and optional `reason`.\nKeep the returned membership revision for the next transition. Reuse the exact\noperation ID and body when reconciling an uncertain response, never a different\ntransition under the old ID. Replay still requires live service authority.\n\nThis endpoint requires the organization service key's explicit `account:admin`;\nthe external-user lane and native-user targets are rejected. Suspension disables\nnew external admission and uses the canonical organization protocol to revoke\nwork and grants. Reactivation restores admission only: workspace memberships,\nscheduled work, and resource grants are not restored. Offboarding is terminal\nthrough this API and follows the existing organization retention policy; it is\nnot immediate deletion of history or upstream provider consent. Audit records\nidentify the service key separately from native administering memberships.\n\n### Personal workspaces and private sessions\n\nAn admitted external actor can access its exact provisioned Personal workspace;\n`asUser` workspace discovery includes that pointer when the key permits\n`workspace:read`. This is not a service-key fallback, and `ensureWorkspace`\ncontinues to provision shared product-tenant workspaces only. Personal permissions\nuse the same non-administrative owner set as native Personal workspaces,\nintersected with the key ceiling. No Personal member-management wildcard is added.\n\nCore session creation, private-read authorization, listing, pinning, visibility\nchanges, and same-workspace fork operations use dedicated external owning-user\nproof. The native-cookie flag remains false. Private creation still requires\nplatform readiness and, in shared workspaces, the existing organization private\nsession setting. Request-time session creation/tenancy commits recheck the live\nkey and identity generation; a failed recheck rolls back the mutation. Forking\nprivate content into workspace visibility retains the existing explicit sharing\nacknowledgment. Private sessions do not make shared-workspace Files or Sites\nprivate. Full personal-resource, worker, stream, and scheduled-execution parity\nstill needs its own integrated verification; do not promise it from these core\nsession checks alone.\n\n## Host bridge and browser ownership\n\nExpose only the Connect operations the product needs through authenticated,\nsame-origin backend routes. Every route must authenticate the host session,\nderive the actor and workspace mapping server-side, and apply the host's normal\nCSRF protection to mutations. Never forward an arbitrary upstream URL, actor\nheader, organization ID or bearer supplied by the browser. Validate request\nbodies and return credential-free projections; redact errors before display or\nlogging. Forward cancellation without assuming it rolls back server effects.\n\nThe backend transport implements catalog, accounts, pending, begin, get,\nadvance, cancel and disconnect. A browser `ConnectTransport` calls those host\nroutes; it never contains the organization-key SDK client. Use one\n`ConnectController` per authenticated actor/workspace and dispose it when either\nchanges. `@opengeni/react/connect` provides optional unstyled `ConnectChooser`,\n`ConnectSetup`, `ConnectAccounts` and `useConnect`. `ConnectPanel` composes the\nthree surfaces; import `@opengeni/react/connect.css` for its opt-in scoped styles.\nThe host still owns navigation and controller lifetime. Controller replacement\nclears pending credential forms and prior account/catalog views.\nProviding `returnUrl` to `ConnectAccounts` enables explicit reconnect bound to\nthe selected account's provider, ownership and ID; `ConnectPanel` wires this\nautomatically. Reconnect does not silently substitute a different account.\n\nWire session timeline `onReconnect` to the host's connection experience. Use\n`findConnectRecoveryAccount` from `@opengeni/connect` with fresh account metadata\nand the event's exact connection ID, then begin setup for that account. A missing\nID or deleted account requires an explicit user choice, not a provider-name match.\nHost-owned credential recovery stays in the product's account flow rather than\nbeing sent to an OpenGeni credential setup page. The native session and runnable\nhost example use this same exact-account lookup.\n\n## Durable OAuth and explicit installation\n\n1. Read catalog readiness for the actual actor. The catalog covers generic,\n curated and first-party Connect adapters. Model-account pools retain their\n dedicated SDK APIs and device flow, described below; operator configuration\n is not a user-connect action.\n2. Begin with explicit provider, ownership, a stable idempotency key and the\n exact return URL chosen by the trusted host backend. Persist the attempt ID\n in authenticated host state before navigation. Do not derive the return URL\n from an unchecked browser field.\n3. For popup mode, invoke `authorizeConnectAttempt` directly from a user gesture\n with `createBrowserConnectNavigation(window)`. Blocked popups are errors;\n full redirect is an explicit host choice, not an automatic fallback.\n4. Recover through `get` or authenticated `pending`. The callback preserves the\n stored return URL, including escaping and fragment, without adding status\n parameters. Popup messages and URL parameters never prove completion.\n5. OAuth may commit credentials while the attempt remains\n `connected_but_incomplete`. Advance with `retry` to review an integration\n preview, then submit its preview ID/content hash and explicitly selected\n operation IDs. Never assume OAuth installed all operations.\n6. Changed source requires a new preview and approval. Preserve revision and\n idempotency fields on retries. An uncertain provider effect is not permission\n to start a duplicate mutation with a fresh key.\n\nAborting polling stops observation, not setup. Explicit `cancel` stops setup\nwithout revoking credentials already committed. `disconnect` currently revokes\nlocal OpenGeni connection access, not upstream provider consent. Pass the observed\naccount `version` as `expectedVersion` to reject a stale selection. The shared\naccount component requires that version and explicit confirmation; an unknown\noutcome requires live reload, not automatic replay. Provider-specific account\nmanagement remains unfinished.\n\n## Embedded Sites\n\n`@opengeni/react/sites` exports `SiteList`, `SiteDetail` and the structural\n`SiteClient` host-proxy interface. The SDK's existing published-artifact methods\nimplement it. `asUser` retains the public client class and artifact methods.\nSites remain workspace-shared artifacts, not private session outputs.\n\nUse `SiteList.onOpen` for host navigation. `SiteDetail` reuses the existing\nopaque-origin `PublishedHtmlArtifactFrame`; never introduce a second renderer\nor put backend keys in HTML. Supply only an authenticated, filtered `toolBridge`.\nUse `createSiteToolBridge` from `@opengeni/sdk/site` with the exact artifact ID,\nversion ID and that version's `requestedTools`. Provide an authenticated catalog\ntransport and `callTool` backed by the host's `callWorkspaceSiteTool` SDK method.\nThe native console uses this same bridge. Recreate it when the actor or version\nchanges. It strips iframe-supplied authority, pins the Site context, and retries\nonly an explicit pre-execution stale-catalog response, never an uncertain effect.\nFor the Site's ordinary session SDK, optionally supply `fetchResponse` with your\nauthenticated host transport. The shared bridge applies the same bounded\n`siteSessionPath` routing as the native console and forwards only content negotiation\nand event replay headers; host authentication and tenant selection remain outside\nthe iframe. The bridge adds its pinned Site ID/version headers (never trusts\niframe-supplied ones), enabling the API's verified Site-origin attribution on\nnew conversations. `originSiteId=current` resolves to that pinned Site for\nconversation filtering. Provenance does not grant access or replace the acting\nuser/workspace authority. Keep these headers through your authenticated proxy;\ndo not synthesize provenance from caller-supplied session metadata.\nOmit this transport for tools-only Sites. Display uses\n`getWorkspaceArtifactHtml` at the observed version, not a retained-source download.\nIt checks Site read authority every 15 seconds while loaded and clears the frame\non denial, scope replacement or version/status change. This is bounded UI\nrevalidation, not instantaneous revocation of downloaded HTML; bridge calls must\nindependently enforce current backend authority.\n\n`canPublish` controls presentation only. The backend still requires\n`artifacts:publish`; rollback/archive/restore preserve the observed current\nversion and require explicit confirmation. Failed mutations clear the loaded\nstate and require refresh rather than an unsafe retry. Authoring buttons and\nprompts belong to the host: create an ordinary authorized session and navigate\nto your existing session UI. There is no dedicated SDK authoring helper or\nbranded Site component button. Native Site UI reuse\nand complete visual acceptance remain separate integration work.\n\n## Credentials and focused acceptance\n\nFor a named curated API integration account, pass `installationTarget: {\ninstanceKey, displayName, expectedInstanceVersion? }` when beginning Connect.\nReconnect uses the exact current instance version; new accounts omit that version.\nThe attempt retains this choice through OAuth, operation preview and installation.\nWithout an explicit target, setup creates an independent account rather than\noverwriting a default instance. OAuth success alone still requires operation review.\n\nFiken's `fiken-token` catalog entry is workspace-only. Submit the `apiToken` and\noptional `defaultCompanySlug` fields through the credential action; OpenGeni verifies\nthe token and accessible companies before storage. Resume/replay the same attempt\nand operation identity rather than submitting the secret to a new attempt after an\nuncertain response. The separate workspace-only `fiken-oauth` entry uses the\ndeployment's registered Fiken OAuth application. It preserves the exact host\nreturn URL and atomically commits the verified company account and completion\nreceipt. Reconnect checks the observed account version; callback replay does not\nrepeat the provider exchange. Neither adapter grants personal ownership.\n\nNative/local/configured workspace setup and organization/workspace API-key setup\nuse the same durable Connect flow where the adapter supports them. Service keys\ncannot create personal connections. Keep keys on the product backend; a callback\nuses its signed initiating principal and current authority, not a new browser login.\nAn external Connect attempt also retains its original key/link restriction.\nChanging clients does not replace it: if the initiating key or link was revoked,\nstart a new authorized setup rather than expecting a new key to revive the old\nattempt. This short-lived setup rule is separate from accepted agent/scheduled\nwork, which does not depend on the original API key remaining active.\n\nShort-lived inline MCP credentials remain a valid simple choice. Durable host\nrenewal is opt-in through the existing host credential port and explicit host\nbinding provenance; it is not required to use `asUser` or Connect. See\n`docs/remote-mcp-credentials.md` when source is available. The host must validate\nlive actor/binding authority; the remote adapter alone does not implement\noffboarding, native linking or a complete external execution gateway.\n\nFor independent backend instances, the server-only organization admin client\ncan use `putHostMcpResolver(organizationId, externalSource, request)` once per\nstable workspace source. Future `ensureWorkspace` calls use the exact registered\nsource without workspace-specific resolver setup. PUT requires `operationId`,\n`expectedGeneration` (0 for create), `url`, and a complete `bearerToken`.\n`getHostMcpResolver` returns metadata only; `revokeHostMcpResolver` requires an\noperation ID/current generation. `asUser`, `asLinkedUser`, workspace keys and\nbrowser cookies cannot administer routes. Existing bindings/grants and accepted\ninitiators do not change when an admin rotates transport.\n\nThe first registration opts the whole organization into namespace routing; a\nconfigured legacy resolver requires `acknowledgeLegacyRoutingReplacement: true`.\nAny retained row, even revoked, prevents static fallback. Missing/inactive\nsources deny rather than choosing another instance. Retried operations return\nhistorical metadata without restoring old configuration. Always GET current\nstate before a new CAS update, and explicitly supply the secret for a new URL.\nGeneration checks invalidate old resolved credentials before physical use;\nalready-dispatched requests cannot be recalled. Keep every secret server-side.\n\nThe request-time workspace tool gateway accepts verified external users and\norganization service keys. Tool catalog/operation permission filtering and\nexisting approval semantics still apply. The new lanes recheck current key and\nidentity/membership permission ceilings around provider preparation and invocation;\nthey do not authorize an agent attempt as a service or inherit a creator's rights.\nThis request-time path is not a scheduled-delegation/binding-generation guarantee.\nFor explicit host references, it uses the separate optional `mcpGatewayCredentials`\ncallback (also implemented by the configured remote adapter). Its request has\n`surface: \"workspace_gateway\"`, a request ID and verified actor/permissions, not\nsession/turn IDs. Gateway responses echo that request ID instead of a session ID.\nExisting in-process `mcpCredentials` callbacks remain turn-only. Do not send\ndurable `hostBinding` references to this gateway; they still fail closed.\nThe SDK also exposes actor-scoped `createHostMcpBinding`, `getHostMcpBinding`, and\n`revokeHostMcpBinding` registry operations. Registration takes an operation ID and\na credential-free `{ serverId, destinationUrl, connectionRef }` definition with\nexplicit host authority. Revocation takes the observed generation and is terminal.\nBindings survive organization-key replacement for the same authorized external\nowner. These operations manage metadata only: no worker or scheduled execution\ncurrently consumes the registration ID as execution authority. The reserved\n`connectionRef.hostBinding` shape is `{ bindingId, generation }`; it fails closed\nunless the backend installs a live execution validator. The broker revalidates\nafter resolution and discards credentials after revocation. The worker's direct-turn\nvalidator requires an immutable accepted-work snapshot. Direct external-user\ncreates capture one through explicit selection; later turns do not inherit it.\nThe actor-bound SDK also exposes `issueHostMcpDelegation`, `getHostMcpDelegation`,\nand `revokeHostMcpDelegation`. Issuance takes an operation ID, binding ID, expected\nbinding generation, and a native-shaped user grant (`session` or `always`).\nSession grants require a session ID and expected authority epoch; shared-output\ngrants require acknowledgement. Read/write connection permissions and live\nexternal-owner checks apply, including at transaction commit. Revocation uses\nthe observed delegation generation. These operations persist grant metadata;\nselect them explicitly on `createSession` with\n`selectedHostMcpDelegations: [{serverId, delegationId, generation}]`. The selected\ntool's configured URL and host binding selection must match; the operator's host\nauthority admission switch must be enabled. This does not auto-install or rewrite\ntools. New sessions use reusable (`always`) grants with matching visibility.\nSelections participate in idempotency: changed or omitted replay selections\nconflict, and replay never recaptures. Capture rechecks external authority inside\nthe initial-turn transaction. `sendMessage` and `steerMessage` accept the same\nexplicit selection for each direct follow-up. Session-bound grants can be used\nthere; they must match that session and its authority epoch. The selected MCP\nserver must already belong to the session. Each message captures atomically and\nits operation ID binds the selection; omission does not inherit a prior grant.\nSame-session goal continuations and child-result resumptions inherit only the\nexact causal turn's accepted selection. Live revocation still blocks use; a\nrevoked selection is not revived by resumption. Children inherit only the exact\nspawning turn's `always` grants for servers they select with unchanged visibility;\nsession-bound grants never cross to a child. Fixed `{bindingId,generation}`\nreferences remain exact-match. Shared per-participant tools can explicitly use\n`connectionRef.hostBinding:{selection:\"accepted_turn\"}` with host authority,\nsubject scope and no configured connectionId. The complete configured\ndestination/provider/scope/resource definition remains exact; only the account\nidentifier comes from each accepted owner's selected binding. Registry bindings\nstill contain a concrete connectionId and no hostBinding. The worker resolves\nonly immutable accepted snapshots and revalidates at every physical use, including\nscheduled and child work. Missing selections never borrow creator credentials.\nRealtime empty-shell creation still takes no selection: call createSession with\n`startMode:\"realtime\"` and no initialMessage or selectedHostMcpDelegations, then\nsend the first text with its authenticated participant's explicit selection and\nclientEventId. The first real text turn captures normally; this grants no voice\nprovider authority.\nUse the same `selectedHostMcpDelegations` field on `createScheduledTask` or\n`updateScheduledTask` for browser-independent jobs. Omitted update selections\npreserve existing choices; `[]` clears them for future revisions. New/reusable\nsessions require `always` grants and shared-output acknowledgement; an existing\nsession can use its exact session-bound grant. Native task revisions, including\nfirst reusable-session materialization, freeze the selection. Runs and their\nsuccessors keep the scheduled origin and recheck live authority before credential\nresolution and physical use. The original API key is not a durable credential.\nAgent-created tasks automatically derive only eligible selections from their\nlive accepted turn when the selection field is omitted; do not assert an owner's\nunselected grant through an agent call. Explicit `[]` disables this inheritance.\nOrdinary inline credentials retain their existing behavior.\nLegacy OAuth starts without verified external continuations fail closed. Curated\nOAuth uses the shared Connect panel. Generic MCP OAuth is also available through\n`actor.startConnectionOAuth(workspaceId, { mcpUrl, returnUrl, ... })`: the trusted\nhost backend supplies an exact absolute HTTP(S) return URL, without credentials\nor control/space characters. The signed state encrypts the external continuation;\ncallbacks recheck the live key/identity/workspace before exchange and credential\ncommit, and consume the nonce before exchange. Both success and failure return\nto the original string without appended parameters; the host must reload\nauthenticated connection state rather than treating navigation as proof of\nsuccess. Native `returnPath` behavior is unchanged. The shared panel also offers\n`mcp-oauth`: server URL input, OAuth navigation, pending recovery, account listing\nand reconnect. Its callback commits credentials and the completion receipt in\none transaction; callback replay never exchanges the code again. Completion is\nconnection-only, not an installed integration or a grant to every server tool.\nThe `mcp-bearer` adapter accepts a server URL and bearer credential. The\n`mcp-headers` adapter accepts the URL and a JSON object in the secret `headers`\nfield for single- or multi-header authentication; transport headers, duplicate\ncase-insensitive names and malformed values are rejected before any commit.\nBoth persist only\nencrypted material, and uses keyed operation digests. HTTPS without URL userinfo\nor fragment is required. Reconnect keeps the same destination and observed account\nversion. Its connection-only completion means the credential was saved, not that\nthe server validated it or tools were installed; the normal credential resolver\nenforces the saved MCP destination when it is used. Dedicated workspace model\nprovider credentials still use their own guarded flows. Uncertain mutations are\nnot automatically retried with a new operation ID.\nExplicit provider denial or missing authorization code before exchange terminates\nthe attempt with a replayable failure receipt; start a new attempt to authorize\nagain. Unknown exchange or persistence outcomes are not treated as safe retries.\nProvider-specific completion is intentional: a saved credential does not mean\nthat repository access, a review webhook, or source synchronization is enabled.\n\n`github-personal` preserves personal OAuth account and repository-selection proof.\n`github-app` discovers installations, asks the host user to choose one, then\nrequires fresh owner proof before binding repository access. `github-lens` uses\nthe same chooser behavior but creates separate Review Bot registrations, webhook\nrouting and repository review bindings; it requires an active Review Bot Pack,\nmanaged compute, and workspace administration plus secret-write permission.\nNeither GitHub App flow is a generic stored user token. Pending organization-owner\napproval is incomplete setup, not a connected account. Discovery currently supports\nat most 99 existing installations plus the new-install option; a larger result\nfails explicitly instead of silently selecting or dropping installations.\n\n`slack-bot` is a workspace bot installation, while `slack-personal` is the official\npersonal Slack MCP authorization flow. Do not substitute one for the other.\n`x` and `reddit` use the existing social-account domain and OAuth scopes. Workspace\nsocial setup requires workspace administration; personal setup requires a verified\nowning user. Native and host clients share callback receipts and exact returns.\nSocial accounts remain limited by the existing one-personal-account-per-provider\nsemantics. Account IDs with `social:`, `github-installation:` and `lens-registration:`\nprefixes are opaque SDK identifiers; use Connect transport disconnect rather than\npassing these to generic credential APIs.\n\nSocial reconnect requires the observed account ID and version. The callback must\nprove the same upstream account and cannot overwrite a concurrent refresh,\ndisconnect or reconnect. Reload accounts after a conflict before asking the user\nto start another attempt.\n\n`mcp-install` installs an available, no-credential MCP capability after probing it;\nit does not manufacture a connection. The credential-input action can contain\nbounded `options` for fields: render these as selectors, not free-text account IDs.\nThe shared React setup surface already does this for MCP and API-source choices.\n\nModel accounts retain their dedicated SDK and pool APIs rather than pretending\nto be ordinary Connect credentials. `pollDeviceAuthorization` from\n`@opengeni/connect` supplies bounded, abortable device polling, and\n`DeviceAuthorization` from `@opengeni/react/connect` supplies optional presentation.\nKeep opaque device state on the server. SuperGrok user pools require ordinary\nworkspace membership; a synthetic personal-workspace owner grant alone is not\nenough. Verified external users follow the same restriction as native users.\n\nKnown Connect callbacks recover the exact saved return URL even after state\nexpiry. This is navigation recovery only: expired state cannot exchange or save\ncredentials. Poll the attempt for its actual status after returning.\n\n`openapi` and `graphql` setup accepts a document/endpoint URL and an optional\nexisting connection ID. The server performs pinned source discovery and returns\nan explicit operation preview. Public services do not create fake credentials.\nInstallation re-resolves the source and checks its revision/hash, ownership and\nthe selected operations. Personal installations require a personal connection.\nThe source is immutable once previewed. Each setup gets an independent named\ninstallation unless the host deliberately supplies an observed instance target.\n\n`atlassian`, `google-drive-knowledge` and `google-drive-publish` preserve the\nfirst-party connectors rather than substituting curated API definitions.\nThey require personal ownership; completion means the credential was committed,\nnot that all projects, spaces or folders were selected or synchronized.\nAtlassian source selection uses `browseAtlassianSources`, `saveAtlassianSources`\nand `setAtlassianLifecycle`, retaining explicit destination, cadence and read\npolicy. Google Drive publishing requires an existing knowledge connection in\n`reconnectAccountId`, additional provider consent and an explicitly picked writable\nfolder. Publication writes retain the existing default `ask` policy. Native\nAtlassian and Drive connect buttons consume the same durable setup surface.\n\n`examples/embedded-product` is a runnable loopback host reference with explicit\nauthentication/CSRF seams, shared Connect/Site UI, and Edit-with-Geni into ordinary\nsession hooks/timeline/approval/structured-input, versioned session control, and\nshared durable composer/queue controls, explicit schedule management, and bounded\nworkspace-file uploads. Schedule operations retain native permission and approval\nsemantics, not a new execution delegation guarantee. Its fixed-user demo auth\nis opt-in and must never be exposed publicly. Native and embedded authoring\nprompts live in their respective products, not the SDK. The example's optional\ntrusted `completionHref` keeps completion links in the host product.\nNo helper grants tool permissions or changes model billing/approval/scheduling.\n\nTest concurrent users without actor-header bleed, cross-workspace denial,\nmembership/key permission reduction, exact return URL preservation, pending\nrecovery after opener loss, duplicate callbacks, changed-source reapproval and\nexplicit operation selection. Keep existing session approvals and scheduling\nsemantics. Do not claim provider, browser or scheduled-renewal conformance from\na transport unit test."
|
|
4718
|
+
"content": "# External users and embedded connection setup\n\nUse these APIs only when the installed SDK and deployment expose them. This\nguide describes implemented external identity, service lifecycle, core\nPersonal/private session access, and curated OAuth paths\u2014not completion of every\nwhite-label surface. Optional native linking is explicit delegation, not account\nmerging. Full provider coverage remains unfinished. Do not infer guarantees from\nthe presence of a contract type.\n\n## Optional use of an existing native account\n\nOrdinary embedding needs only `asUser`; never require native registration or\nlinking for a product user. For an existing OpenGeni user who deliberately wants\nthe product to use their native workspace access, begin a link through the\nexternal client with `beginIdentityLink`. Show the returned challenge only in\nthe native consent URL fragment, never its query, logs or analytics. The native\n`/identity-links/:linkId?organization=:organizationId#challenge=:challenge` page\nrequires the user's real native login, displays both identities and the requested\npermissions, and permits narrowing before confirmation. The fragment is scrubbed\nbefore the application mounts. A page reload requires reopening the original\nconsent URL. Poll `getIdentityLink` from the product backend for confirmation.\n\nAfter explicit confirmation, choose linked mode on the backend:\n\n```ts\nconst linked = serviceClient.asLinkedUser(authenticatedUser.id, {\n source: \"my-product\",\n linkId: confirmedLink.id,\n expectedLinkRevision: confirmedLink.revision,\n});\n```\n\nUse the same source and opaque ID used at initiation. A confirmed link does not\nchange `asUser`, move external sessions or credentials, or merge accounts. New\nlinked work belongs to the native user. Requests intersect the key's permissions,\nthe live native user's permissions and the approved link ceiling. Never fall back\nto service or external mode when linked authorization fails.\n\nLink expiry is optional; null means until revoked. Either confirmed participant\ncan revoke using the observed revision. Accepted linked turns, scheduled task\nrevisions, child sessions and causal continuations retain the link restriction;\nrevocation denies later execution without requiring the original API key to stay\nactive. This is an execution-time check, not a promise to undo a remote operation\nalready started. Native access to native-owned resources remains intact. Do not\nassume external-owned host MCP bindings transfer to the native owner through a\nlink: create a separate binding while explicitly acting as the native user.\nIts owner revision is the native member's revision; linked work independently\nretains the live link restriction. `listIdentityLinks(workspaceId, cursor?)`\nprovides a participant-only inventory, and native workspace settings expose the\nsame list/revoke behavior without retaining the original consent URL.\n\n## Separate service administration from user requests\n\nKeep one organization-key client on the trusted product backend. After product\nauthentication, derive the immutable external identity from the server session:\n\n```ts\nconst actor = serviceClient.asUser(authenticatedUser.id, { source: \"my-product\" });\nconst transport = actor.connectTransport();\nconst providers = await transport.catalog(authorizedWorkspaceId);\n```\n\nHere `serviceClient`, `authenticatedUser`, and `authorizedWorkspaceId` are\nhost-owned dependencies, not fields accepted from the browser. `asUser` creates\na separate client and does not mutate the service client. It requires an\norganization key; a workspace key or deployment access key is not a substitute.\nNever retry a denied user request using the unscoped service client.\n\nAn organization key is trusted to assert and lazily provision product users;\nthere is no separate provisioning permission or registration ceremony. The first\nauthenticated request may create the identity anchor even if its later workspace\noperation is denied. Workload permissions still restrict that operation. Derive\nIDs from authenticated host records and bound onboarding in the host; never\nforward arbitrary browser-supplied identities. Personal identity anchors are not\nshared product-tenant workspaces or a way to obtain workspace membership.\n\nIdentity is scoped by organization, source and opaque external ID. Source\ndefaults to `default`; use a stable source namespace when multiple identity\nsystems share an organization. IDs are case-sensitive, not emails to normalize:\nmaximum 1024 UTF-8 bytes for the ID and 200 for source; empty strings, NUL and\ninvalid Unicode are rejected. A native-looking ID does not impersonate a native\nuser. Workspace mapping identity passed to `ensureWorkspace` is a separate\nconcept from this acting-user identity.\n\nUser mode lazily establishes an external identity but does not grant access to a\nshared workspace. An explicitly authorized service onboarding operation may use:\n\n```ts\nawait serviceClient.addExternalWorkspaceMember(authorizedWorkspaceId, {\n identity: { externalId: authenticatedUser.id, source: \"my-product\" },\n permissions: [\"workspace:read\", \"connections:read\", \"connections:write\"],\n operationId: onboardingOperationId,\n});\n```\n\nDo this only after the host has approved membership, not on every arbitrary\nbrowser request. The service needs `members:manage` and may not grant authority\nbeyond its ceiling. Persist `onboardingOperationId` before the call. Exact keyed\nreplays return historical identity without restoring removed membership; different\npermissions conflict instead of overwriting a subsequently reduced grant. Installation also\nneeds `capabilities:manage`; do not add it unless installation is a product\nfeature the user may perform. User requests intersect actual membership with\nthe initiating key's permissions. Service administration remains separate.\n\n### Removal and account-wide lifecycle\n\nFor recoverable onboarding, establish and retain the identity anchor before\ngranting membership. Service `lookupExternalIdentity(organizationId, identity)`\nis non-provisioning and returns content-free identity/membership IDs, statuses and\nseparate revisions, including suspended/offboarded identities. It requires\n`members:manage` and does not reactivate an identity. A missing result is not proof\nthat an earlier unkeyed request cannot still provision one.\n\nTo withdraw this external member's workspace access while fencing a pending keyed\ngrant, call `cancelExternalWorkspaceMemberGrant(organizationId, workspaceId,\norganizationMembershipId, { operationId: cancellationOperationId,\ncancelGrantOperationId: onboardingOperationId })`. Persist both distinct UUIDs;\nretry the exact cancellation body after response loss. This reuses native\nteardown and fences the named grant even if membership is absent. It withdraws\ncurrent workspace membership, not just one session. New grant IDs are explicit\nnew onboarding, never retries. Legacy requests without `operationId` remain\nunfenced: drain old writers before claiming late-grant protection.\n\nUse the service client's existing `removeWorkspaceMember(workspaceId, subjectId)`\nto remove an external actor from a shared workspace. It requires `members:manage`,\nrechecks the live key, preserves the last-admin guard, and uses the same fenced\nsettlement/cancellation path as native removal. It does not disable the actor in\nother workspaces. Ordinary `asUser` reads never restore removed membership.\n\nFor account-wide changes, call `serviceClient.updateExternalIdentityMembership(\norganizationId, organizationMembershipId, request)`. The membership ID comes from\nthe identity returned by onboarding; the initial membership authorization\nrevision is 1. The request contains `kind` (`suspend`, `reactivate`, or `offboard`),\n`expectedAuthorizationRevision`, a UUID `operationId`, and optional `reason`.\nKeep the returned membership revision for the next transition. Reuse the exact\noperation ID and body when reconciling an uncertain response, never a different\ntransition under the old ID. Replay still requires live service authority.\n\nThis endpoint requires the organization service key's explicit `account:admin`;\nthe external-user lane and native-user targets are rejected. Suspension disables\nnew external admission and uses the canonical organization protocol to revoke\nwork and grants. Reactivation restores admission only: workspace memberships,\nscheduled work, and resource grants are not restored. Offboarding is terminal\nthrough this API and follows the existing organization retention policy; it is\nnot immediate deletion of history or upstream provider consent. Audit records\nidentify the service key separately from native administering memberships.\n\n### Personal workspaces and private sessions\n\nAn admitted external actor can access its exact provisioned Personal workspace;\n`asUser` workspace discovery includes that pointer when the key permits\n`workspace:read`. This is not a service-key fallback, and `ensureWorkspace`\ncontinues to provision shared product-tenant workspaces only. Personal permissions\nuse the same non-administrative owner set as native Personal workspaces,\nintersected with the key ceiling. No Personal member-management wildcard is added.\n\nCore session creation, private-read authorization, listing, pinning, visibility\nchanges, and same-workspace fork operations use dedicated external owning-user\nproof. The native-cookie flag remains false. Private creation still requires\nplatform readiness and, in shared workspaces, the existing organization private\nsession setting. Request-time session creation/tenancy commits recheck the live\nkey and identity generation; a failed recheck rolls back the mutation. Forking\nprivate content into workspace visibility retains the existing explicit sharing\nacknowledgment. Private sessions do not make shared-workspace Files or Sites\nprivate. Full personal-resource, worker, stream, and scheduled-execution parity\nstill needs its own integrated verification; do not promise it from these core\nsession checks alone.\n\n## Host bridge and browser ownership\n\nExpose only the Connect operations the product needs through authenticated,\nsame-origin backend routes. Every route must authenticate the host session,\nderive the actor and workspace mapping server-side, and apply the host's normal\nCSRF protection to mutations. Never forward an arbitrary upstream URL, actor\nheader, organization ID or bearer supplied by the browser. Validate request\nbodies and return credential-free projections; redact errors before display or\nlogging. Forward cancellation without assuming it rolls back server effects.\n\nThe backend transport implements catalog, accounts, pending, begin, get,\nadvance, cancel and disconnect. A browser `ConnectTransport` calls those host\nroutes; it never contains the organization-key SDK client. Use one\n`ConnectController` per authenticated actor/workspace and dispose it when either\nchanges. `@opengeni/react/connect` provides optional unstyled `ConnectChooser`,\n`ConnectSetup`, `ConnectAccounts` and `useConnect`. `ConnectPanel` composes the\nthree surfaces; import `@opengeni/react/connect.css` for its opt-in scoped styles.\nThe host still owns navigation and controller lifetime. Controller replacement\nclears pending credential forms and prior account/catalog views.\nProviding `returnUrl` to `ConnectAccounts` enables explicit reconnect bound to\nthe selected account's provider, ownership and ID; `ConnectPanel` wires this\nautomatically. Reconnect does not silently substitute a different account.\n\nFor a host-owned capability library, `CapabilityCatalogRow` from the same\nsubpath supplies the shared icon/name/description row. Provide explicit\n`status` (`available`, `added`, `attention`, `unavailable`, or `loading`) and\n`onOpen`; the plus/check is decorative, not a second action. Normal status\nlabels remain accessible and exceptions remain visible. Use one setup entry\npoint from the catalog and conversation. Keep provider authentication,\nworkspace/account sharing, and explicit Plugin/Skill installation separate;\nmatching visual components never grants authority or implies installation.\nWhen using `ConnectionCatalog`, provide each option's typed `state` to use the\nquiet glyph treatment. Omitting it preserves the existing visible `status`\nstring, so older integrations cannot silently lose provider warnings.\n\nWire session timeline `onReconnect` to the host's connection experience. Use\n`findConnectRecoveryAccount` from `@opengeni/connect` with fresh account metadata\nand the event's exact connection ID, then begin setup for that account. A missing\nID or deleted account requires an explicit user choice, not a provider-name match.\nHost-owned credential recovery stays in the product's account flow rather than\nbeing sent to an OpenGeni credential setup page. The native session and runnable\nhost example use this same exact-account lookup.\n\n## Durable OAuth and explicit installation\n\n1. Read catalog readiness for the actual actor. The catalog covers generic,\n curated and first-party Connect adapters. Model-account pools retain their\n dedicated SDK APIs and device flow, described below; operator configuration\n is not a user-connect action.\n2. Begin with explicit provider, ownership, a stable idempotency key and the\n exact return URL chosen by the trusted host backend. Persist the attempt ID\n in authenticated host state before navigation. Do not derive the return URL\n from an unchecked browser field.\n3. For popup mode, invoke `authorizeConnectAttempt` directly from a user gesture\n with `createBrowserConnectNavigation(window)`. Blocked popups are errors;\n full redirect is an explicit host choice, not an automatic fallback.\n4. Recover through `get` or authenticated `pending`. The callback preserves the\n stored return URL, including escaping and fragment, without adding status\n parameters. Popup messages and URL parameters never prove completion.\n5. OAuth may commit credentials while the attempt remains\n `connected_but_incomplete`. Advance with `retry` to review an integration\n preview, then submit its preview ID/content hash and explicitly selected\n operation IDs. Never assume OAuth installed all operations.\n6. Changed source requires a new preview and approval. Preserve revision and\n idempotency fields on retries. An uncertain provider effect is not permission\n to start a duplicate mutation with a fresh key.\n\nAborting polling stops observation, not setup. Explicit `cancel` stops setup\nwithout revoking credentials already committed. `disconnect` currently revokes\nlocal OpenGeni connection access, not upstream provider consent. Pass the observed\naccount `version` as `expectedVersion` to reject a stale selection. The shared\naccount component requires that version and explicit confirmation; an unknown\noutcome requires live reload, not automatic replay. Provider-specific account\nmanagement remains unfinished.\n\n## Embedded Sites\n\n`@opengeni/react/sites` exports `SiteList`, `SiteDetail` and the structural\n`SiteClient` host-proxy interface. The SDK's existing published-artifact methods\nimplement it. `asUser` retains the public client class and artifact methods.\nSites remain workspace-shared artifacts, not private session outputs.\n\nUse `SiteList.onOpen` for host navigation. `SiteDetail` reuses the existing\nopaque-origin `PublishedHtmlArtifactFrame`; never introduce a second renderer\nor put backend keys in HTML. Supply only an authenticated, filtered `toolBridge`.\nUse `createSiteToolBridge` from `@opengeni/sdk/site` with the exact artifact ID,\nversion ID and that version's `requestedTools`. Provide an authenticated catalog\ntransport and `callTool` backed by the host's `callWorkspaceSiteTool` SDK method.\nThe native console uses this same bridge. Recreate it when the actor or version\nchanges. It strips iframe-supplied authority, pins the Site context, and retries\nonly an explicit pre-execution stale-catalog response, never an uncertain effect.\nFor the Site's ordinary session SDK, optionally supply `fetchResponse` with your\nauthenticated host transport. The shared bridge applies the same bounded\n`siteSessionPath` routing as the native console and forwards only content negotiation\nand event replay headers; host authentication and tenant selection remain outside\nthe iframe. The bridge adds its pinned Site ID/version headers (never trusts\niframe-supplied ones), enabling the API's verified Site-origin attribution on\nnew conversations. `originSiteId=current` resolves to that pinned Site for\nconversation filtering. Provenance does not grant access or replace the acting\nuser/workspace authority. Keep these headers through your authenticated proxy;\ndo not synthesize provenance from caller-supplied session metadata.\nOmit this transport for tools-only Sites. Display uses\n`getWorkspaceArtifactHtml` at the observed version, not a retained-source download.\nIt checks Site read authority every 15 seconds while loaded and clears the frame\non denial, scope replacement or version/status change. This is bounded UI\nrevalidation, not instantaneous revocation of downloaded HTML; bridge calls must\nindependently enforce current backend authority.\n\n`canPublish` controls presentation only. The backend still requires\n`artifacts:publish`; rollback/archive/restore preserve the observed current\nversion and require explicit confirmation. Failed mutations clear the loaded\nstate and require refresh rather than an unsafe retry. Authoring buttons and\nprompts belong to the host: create an ordinary authorized session and navigate\nto your existing session UI. There is no dedicated SDK authoring helper or\nbranded Site component button. Native Site UI reuse\nand complete visual acceptance remain separate integration work.\n\n## Credentials and focused acceptance\n\nFor a named curated API integration account, pass `installationTarget: {\ninstanceKey, displayName, expectedInstanceVersion? }` when beginning Connect.\nReconnect uses the exact current instance version; new accounts omit that version.\nThe attempt retains this choice through OAuth, operation preview and installation.\nWithout an explicit target, setup creates an independent account rather than\noverwriting a default instance. OAuth success alone still requires operation review.\n\nFiken's `fiken-token` catalog entry is workspace-only. Submit the `apiToken` and\noptional `defaultCompanySlug` fields through the credential action; OpenGeni verifies\nthe token and accessible companies before storage. Resume/replay the same attempt\nand operation identity rather than submitting the secret to a new attempt after an\nuncertain response. The separate workspace-only `fiken-oauth` entry uses the\ndeployment's registered Fiken OAuth application. It preserves the exact host\nreturn URL and atomically commits the verified company account and completion\nreceipt. Reconnect checks the observed account version; callback replay does not\nrepeat the provider exchange. Neither adapter grants personal ownership.\n\nNative/local/configured workspace setup and organization/workspace API-key setup\nuse the same durable Connect flow where the adapter supports them. Service keys\ncannot create personal connections. Keep keys on the product backend; a callback\nuses its signed initiating principal and current authority, not a new browser login.\nAn external Connect attempt also retains its original key/link restriction.\nChanging clients does not replace it: if the initiating key or link was revoked,\nstart a new authorized setup rather than expecting a new key to revive the old\nattempt. This short-lived setup rule is separate from accepted agent/scheduled\nwork, which does not depend on the original API key remaining active.\n\nShort-lived inline MCP credentials remain a valid simple choice. Durable host\nrenewal is opt-in through the existing host credential port and explicit host\nbinding provenance; it is not required to use `asUser` or Connect. See\n`docs/remote-mcp-credentials.md` when source is available. The host must validate\nlive actor/binding authority; the remote adapter alone does not implement\noffboarding, native linking or a complete external execution gateway.\n\nFor independent backend instances, the server-only organization admin client\ncan use `putHostMcpResolver(organizationId, externalSource, request)` once per\nstable workspace source. Future `ensureWorkspace` calls use the exact registered\nsource without workspace-specific resolver setup. PUT requires `operationId`,\n`expectedGeneration` (0 for create), `url`, and a complete `bearerToken`.\n`getHostMcpResolver` returns metadata only; `revokeHostMcpResolver` requires an\noperation ID/current generation. `asUser`, `asLinkedUser`, workspace keys and\nbrowser cookies cannot administer routes. Existing bindings/grants and accepted\ninitiators do not change when an admin rotates transport.\n\nThe first registration opts the whole organization into namespace routing; a\nconfigured legacy resolver requires `acknowledgeLegacyRoutingReplacement: true`.\nAny retained row, even revoked, prevents static fallback. Missing/inactive\nsources deny rather than choosing another instance. Retried operations return\nhistorical metadata without restoring old configuration. Always GET current\nstate before a new CAS update, and explicitly supply the secret for a new URL.\nGeneration checks invalidate old resolved credentials before physical use;\nalready-dispatched requests cannot be recalled. Keep every secret server-side.\n\nThe request-time workspace tool gateway accepts verified external users and\norganization service keys. Tool catalog/operation permission filtering and\nexisting approval semantics still apply. The new lanes recheck current key and\nidentity/membership permission ceilings around provider preparation and invocation;\nthey do not authorize an agent attempt as a service or inherit a creator's rights.\nThis request-time path is not a scheduled-delegation/binding-generation guarantee.\nFor explicit host references, it uses the separate optional `mcpGatewayCredentials`\ncallback (also implemented by the configured remote adapter). Its request has\n`surface: \"workspace_gateway\"`, a request ID and verified actor/permissions, not\nsession/turn IDs. Gateway responses echo that request ID instead of a session ID.\nExisting in-process `mcpCredentials` callbacks remain turn-only. Do not send\ndurable `hostBinding` references to this gateway; they still fail closed.\nThe SDK also exposes actor-scoped `createHostMcpBinding`, `getHostMcpBinding`, and\n`revokeHostMcpBinding` registry operations. Registration takes an operation ID and\na credential-free `{ serverId, destinationUrl, connectionRef }` definition with\nexplicit host authority. Revocation takes the observed generation and is terminal.\nBindings survive organization-key replacement for the same authorized external\nowner. These operations manage metadata only: no worker or scheduled execution\ncurrently consumes the registration ID as execution authority. The reserved\n`connectionRef.hostBinding` shape is `{ bindingId, generation }`; it fails closed\nunless the backend installs a live execution validator. The broker revalidates\nafter resolution and discards credentials after revocation. The worker's direct-turn\nvalidator requires an immutable accepted-work snapshot. Direct external-user\ncreates capture one through explicit selection; later turns do not inherit it.\nThe actor-bound SDK also exposes `issueHostMcpDelegation`, `getHostMcpDelegation`,\nand `revokeHostMcpDelegation`. Issuance takes an operation ID, binding ID, expected\nbinding generation, and a native-shaped user grant (`session` or `always`).\nSession grants require a session ID and expected authority epoch; shared-output\ngrants require acknowledgement. Read/write connection permissions and live\nexternal-owner checks apply, including at transaction commit. Revocation uses\nthe observed delegation generation. These operations persist grant metadata;\nselect them explicitly on `createSession` with\n`selectedHostMcpDelegations: [{serverId, delegationId, generation}]`. The selected\ntool's configured URL and host binding selection must match; the operator's host\nauthority admission switch must be enabled. This does not auto-install or rewrite\ntools. New sessions use reusable (`always`) grants with matching visibility.\nSelections participate in idempotency: changed or omitted replay selections\nconflict, and replay never recaptures. Capture rechecks external authority inside\nthe initial-turn transaction. `sendMessage` and `steerMessage` accept the same\nexplicit selection for each direct follow-up. Session-bound grants can be used\nthere; they must match that session and its authority epoch. The selected MCP\nserver must already belong to the session. Each message captures atomically and\nits operation ID binds the selection; omission does not inherit a prior grant.\nSame-session goal continuations and child-result resumptions inherit only the\nexact causal turn's accepted selection. Live revocation still blocks use; a\nrevoked selection is not revived by resumption. Children inherit only the exact\nspawning turn's `always` grants for servers they select with unchanged visibility;\nsession-bound grants never cross to a child. Fixed `{bindingId,generation}`\nreferences remain exact-match. Shared per-participant tools can explicitly use\n`connectionRef.hostBinding:{selection:\"accepted_turn\"}` with host authority,\nsubject scope and no configured connectionId. The complete configured\ndestination/provider/scope/resource definition remains exact; only the account\nidentifier comes from each accepted owner's selected binding. Registry bindings\nstill contain a concrete connectionId and no hostBinding. The worker resolves\nonly immutable accepted snapshots and revalidates at every physical use, including\nscheduled and child work. Missing selections never borrow creator credentials.\nRealtime empty-shell creation still takes no selection: call createSession with\n`startMode:\"realtime\"` and no initialMessage or selectedHostMcpDelegations, then\nsend the first text with its authenticated participant's explicit selection and\nclientEventId. The first real text turn captures normally; this grants no voice\nprovider authority.\nUse the same `selectedHostMcpDelegations` field on `createScheduledTask` or\n`updateScheduledTask` for browser-independent jobs. Omitted update selections\npreserve existing choices; `[]` clears them for future revisions. New/reusable\nsessions require `always` grants and shared-output acknowledgement; an existing\nsession can use its exact session-bound grant. Native task revisions, including\nfirst reusable-session materialization, freeze the selection. Runs and their\nsuccessors keep the scheduled origin and recheck live authority before credential\nresolution and physical use. The original API key is not a durable credential.\nAgent-created tasks automatically derive only eligible selections from their\nlive accepted turn when the selection field is omitted; do not assert an owner's\nunselected grant through an agent call. Explicit `[]` disables this inheritance.\nOrdinary inline credentials retain their existing behavior.\nLegacy OAuth starts without verified external continuations fail closed. Curated\nOAuth uses the shared Connect panel. Generic MCP OAuth is also available through\n`actor.startConnectionOAuth(workspaceId, { mcpUrl, returnUrl, ... })`: the trusted\nhost backend supplies an exact absolute HTTP(S) return URL, without credentials\nor control/space characters. The signed state encrypts the external continuation;\ncallbacks recheck the live key/identity/workspace before exchange and credential\ncommit, and consume the nonce before exchange. Both success and failure return\nto the original string without appended parameters; the host must reload\nauthenticated connection state rather than treating navigation as proof of\nsuccess. Native `returnPath` behavior is unchanged. The shared panel also offers\n`mcp-oauth`: server URL input, OAuth navigation, pending recovery, account listing\nand reconnect. Its callback commits credentials and the completion receipt in\none transaction; callback replay never exchanges the code again. Completion is\nconnection-only, not an installed integration or a grant to every server tool.\nThe `mcp-bearer` adapter accepts a server URL and bearer credential. The\n`mcp-headers` adapter accepts the URL and a JSON object in the secret `headers`\nfield for single- or multi-header authentication; transport headers, duplicate\ncase-insensitive names and malformed values are rejected before any commit.\nBoth persist only\nencrypted material, and uses keyed operation digests. HTTPS without URL userinfo\nor fragment is required. Reconnect keeps the same destination and observed account\nversion. Its connection-only completion means the credential was saved, not that\nthe server validated it or tools were installed; the normal credential resolver\nenforces the saved MCP destination when it is used. Dedicated workspace model\nprovider credentials still use their own guarded flows. Uncertain mutations are\nnot automatically retried with a new operation ID.\nExplicit provider denial or missing authorization code before exchange terminates\nthe attempt with a replayable failure receipt; start a new attempt to authorize\nagain. Unknown exchange or persistence outcomes are not treated as safe retries.\nProvider-specific completion is intentional: a saved credential does not mean\nthat repository access, a review webhook, or source synchronization is enabled.\n\n`github-personal` preserves personal OAuth account and repository-selection proof.\n`github-app` discovers installations, asks the host user to choose one, then\nrequires fresh owner proof before binding repository access. `github-lens` uses\nthe same chooser behavior but creates separate Review Bot registrations, webhook\nrouting and repository review bindings; it requires an active Review Bot Pack,\nmanaged compute, and workspace administration plus secret-write permission.\nNeither GitHub App flow is a generic stored user token. Pending organization-owner\napproval is incomplete setup, not a connected account. Discovery currently supports\nat most 99 existing installations plus the new-install option; a larger result\nfails explicitly instead of silently selecting or dropping installations.\n\n`slack-bot` is a workspace bot installation, while `slack-personal` is the official\npersonal Slack MCP authorization flow. Do not substitute one for the other.\n`x` and `reddit` use the existing social-account domain and OAuth scopes. Workspace\nsocial setup requires workspace administration; personal setup requires a verified\nowning user. Native and host clients share callback receipts and exact returns.\nSocial accounts remain limited by the existing one-personal-account-per-provider\nsemantics. Account IDs with `social:`, `github-installation:` and `lens-registration:`\nprefixes are opaque SDK identifiers; use Connect transport disconnect rather than\npassing these to generic credential APIs.\n\nSocial reconnect requires the observed account ID and version. The callback must\nprove the same upstream account and cannot overwrite a concurrent refresh,\ndisconnect or reconnect. Reload accounts after a conflict before asking the user\nto start another attempt.\n\n`mcp-install` installs an available, no-credential MCP capability after probing it;\nit does not manufacture a connection. The credential-input action can contain\nbounded `options` for fields: render these as selectors, not free-text account IDs.\nThe shared React setup surface already does this for MCP and API-source choices.\n\nModel accounts retain their dedicated SDK and pool APIs rather than pretending\nto be ordinary Connect credentials. `pollDeviceAuthorization` from\n`@opengeni/connect` supplies bounded, abortable device polling, and\n`DeviceAuthorization` from `@opengeni/react/connect` supplies optional presentation.\nKeep opaque device state on the server. SuperGrok user pools require ordinary\nworkspace membership; a synthetic personal-workspace owner grant alone is not\nenough. Verified external users follow the same restriction as native users.\n\nKnown Connect callbacks recover the exact saved return URL even after state\nexpiry. This is navigation recovery only: expired state cannot exchange or save\ncredentials. Poll the attempt for its actual status after returning.\n\n`openapi` and `graphql` setup accepts a document/endpoint URL and an optional\nexisting connection ID. The server performs pinned source discovery and returns\nan explicit operation preview. Public services do not create fake credentials.\nInstallation re-resolves the source and checks its revision/hash, ownership and\nthe selected operations. Personal installations require a personal connection.\nThe source is immutable once previewed. Each setup gets an independent named\ninstallation unless the host deliberately supplies an observed instance target.\n\n`atlassian`, `google-drive-knowledge` and `google-drive-publish` preserve the\nfirst-party connectors rather than substituting curated API definitions.\nThey require personal ownership; completion means the credential was committed,\nnot that all projects, spaces or folders were selected or synchronized.\nAtlassian source selection uses `browseAtlassianSources`, `saveAtlassianSources`\nand `setAtlassianLifecycle`, retaining explicit destination, cadence and read\npolicy. Google Drive publishing requires an existing knowledge connection in\n`reconnectAccountId`, additional provider consent and an explicitly picked writable\nfolder. Publication writes retain the existing default `ask` policy. Native\nAtlassian and Drive connect buttons consume the same durable setup surface.\n\n`examples/embedded-product` is a runnable loopback host reference with explicit\nauthentication/CSRF seams, shared Connect/Site UI, and Edit-with-Geni into ordinary\nsession hooks/timeline/approval/structured-input, versioned session control, and\nshared durable composer/queue controls, explicit schedule management, and bounded\nworkspace-file uploads. Schedule operations retain native permission and approval\nsemantics, not a new execution delegation guarantee. Its fixed-user demo auth\nis opt-in and must never be exposed publicly. Native and embedded authoring\nprompts live in their respective products, not the SDK. The example's optional\ntrusted `completionHref` keeps completion links in the host product.\nNo helper grants tool permissions or changes model billing/approval/scheduling.\n\nTest concurrent users without actor-header bleed, cross-workspace denial,\nmembership/key permission reduction, exact return URL preservation, pending\nrecovery after opener loss, duplicate callbacks, changed-source reapproval and\nexplicit operation selection. Keep existing session approvals and scheduling\nsemantics. Do not claim provider, browser or scheduled-renewal conformance from\na transport unit test."
|
|
4640
4719
|
},
|
|
4641
4720
|
{
|
|
4642
4721
|
"path": "references/implementation-overview.md",
|
|
@@ -4674,7 +4753,7 @@ var OPENGENI_PRODUCT_INTEGRATION_PACK = {
|
|
|
4674
4753
|
description: "Help an implementation agent add OpenGeni to an external product with adaptive discovery, tenant-safe boundaries, framework-native UI, authorized data tools, and the customer's chosen delivery autonomy. Installation stays inactive until one implementation session selects the Skill.",
|
|
4675
4754
|
role: "software-engineering",
|
|
4676
4755
|
category: "product-integration",
|
|
4677
|
-
version: "0.2.
|
|
4756
|
+
version: "0.2.1",
|
|
4678
4757
|
skills: [OPENGENI_PRODUCT_INTEGRATION_SKILL],
|
|
4679
4758
|
components: [],
|
|
4680
4759
|
tools: [],
|
|
@@ -5356,6 +5435,7 @@ async function enableCapability(input) {
|
|
|
5356
5435
|
return prepared.commit(input.db);
|
|
5357
5436
|
}
|
|
5358
5437
|
async function prepareCapabilityEnable(input) {
|
|
5438
|
+
input = { ...input, payload: structuredClone(input.payload) };
|
|
5359
5439
|
const item = await requireCatalogItem(
|
|
5360
5440
|
input.db,
|
|
5361
5441
|
input.workspaceId,
|
|
@@ -5398,6 +5478,37 @@ async function prepareCapabilityEnable(input) {
|
|
|
5398
5478
|
delete installationConfig.headerNames;
|
|
5399
5479
|
delete installationConfig.connectionRef;
|
|
5400
5480
|
if (item.kind === "mcp") {
|
|
5481
|
+
const unchanged = await withOrganizationIntegrationPolicyFence(
|
|
5482
|
+
input.db,
|
|
5483
|
+
input,
|
|
5484
|
+
async (tx, policy) => {
|
|
5485
|
+
const existing = await unchangedMcpInstallation(
|
|
5486
|
+
{ ...input, db: tx },
|
|
5487
|
+
item,
|
|
5488
|
+
installationConfig
|
|
5489
|
+
);
|
|
5490
|
+
if (existing) return existing;
|
|
5491
|
+
assertOrganizationIntegrationAllowed2(policy, "custom:mcp");
|
|
5492
|
+
return null;
|
|
5493
|
+
}
|
|
5494
|
+
);
|
|
5495
|
+
if (unchanged) {
|
|
5496
|
+
return {
|
|
5497
|
+
commit: (db) => withOrganizationIntegrationPolicyFence(db, input, async (tx) => {
|
|
5498
|
+
const current = await unchangedMcpInstallation(
|
|
5499
|
+
{ ...input, db: tx },
|
|
5500
|
+
item,
|
|
5501
|
+
installationConfig
|
|
5502
|
+
);
|
|
5503
|
+
if (!current || !isDeepStrictEqual(current, unchanged)) {
|
|
5504
|
+
throw new HTTPException14(409, {
|
|
5505
|
+
message: "Capability installation changed; reload before reconciling"
|
|
5506
|
+
});
|
|
5507
|
+
}
|
|
5508
|
+
return current;
|
|
5509
|
+
})
|
|
5510
|
+
};
|
|
5511
|
+
}
|
|
5401
5512
|
const headers = await resolveMcpCredentialHeaders(input, item);
|
|
5402
5513
|
const connectionRef = input.payload.connectionRef ? await validateMcpCapabilityConnectionRef(input, item, input.payload.connectionRef) : null;
|
|
5403
5514
|
assertRequiredMcpCredentialHeaders(item, headers, connectionRef);
|
|
@@ -5423,7 +5534,44 @@ async function prepareCapabilityEnable(input) {
|
|
|
5423
5534
|
config: installationConfig,
|
|
5424
5535
|
metadata: installationMetadata
|
|
5425
5536
|
};
|
|
5426
|
-
return {
|
|
5537
|
+
return {
|
|
5538
|
+
commit: (db) => item.kind === "mcp" ? withOrganizationIntegrationAcquisition(
|
|
5539
|
+
db,
|
|
5540
|
+
installation,
|
|
5541
|
+
["custom:mcp"],
|
|
5542
|
+
(tx) => enableCapabilityInstallation(tx, installation)
|
|
5543
|
+
) : enableCapabilityInstallation(db, installation)
|
|
5544
|
+
};
|
|
5545
|
+
}
|
|
5546
|
+
async function unchangedMcpInstallation(input, item, requestedConfig) {
|
|
5547
|
+
const connectionRef = input.payload.connectionRef ? await validateMcpCapabilityConnectionRef(input, item, input.payload.connectionRef) : null;
|
|
5548
|
+
const existing = await withLockedCapabilityInstallation(
|
|
5549
|
+
input.db,
|
|
5550
|
+
input.workspaceId,
|
|
5551
|
+
item.id,
|
|
5552
|
+
(tx) => getCapabilityInstallation(tx, input.workspaceId, item.id)
|
|
5553
|
+
);
|
|
5554
|
+
if (!existing || existing.kind !== "mcp" || existing.status !== "active") return null;
|
|
5555
|
+
const {
|
|
5556
|
+
headerNames: _names,
|
|
5557
|
+
headersEncrypted: _encrypted,
|
|
5558
|
+
headers: _headers,
|
|
5559
|
+
...storedConfig
|
|
5560
|
+
} = existing.config;
|
|
5561
|
+
const config = { ...requestedConfig, ...connectionRef ? { connectionRef } : {} };
|
|
5562
|
+
const { mcpConnectivity: _storedConnectivity, ...storedMetadata } = existing.metadata;
|
|
5563
|
+
const { mcpConnectivity: _requestedConnectivity, ...requestedMetadata } = input.payload.metadata;
|
|
5564
|
+
if (!isDeepStrictEqual(config, storedConfig) || !isDeepStrictEqual(requestedMetadata, storedMetadata))
|
|
5565
|
+
return null;
|
|
5566
|
+
const provided = normalizedMcpCredentialHeaders(input.payload.headers);
|
|
5567
|
+
if (provided) {
|
|
5568
|
+
const stored = await resolveMcpCredentialHeaders(
|
|
5569
|
+
{ ...input, payload: { ...input.payload, headers: {} } },
|
|
5570
|
+
item
|
|
5571
|
+
);
|
|
5572
|
+
if (!isDeepStrictEqual(provided, stored)) return null;
|
|
5573
|
+
}
|
|
5574
|
+
return existing;
|
|
5427
5575
|
}
|
|
5428
5576
|
async function resolveMcpCredentialHeaders(input, item) {
|
|
5429
5577
|
const provided = normalizedMcpCredentialHeaders(input.payload.headers);
|
|
@@ -6890,14 +7038,17 @@ function enabledCapabilityMcpToolRefs(settings, runtimeSettings) {
|
|
|
6890
7038
|
function withDefaultEnabledCapabilityMcpTools(tools, settings, runtimeSettings) {
|
|
6891
7039
|
return mergeToolRefs(tools, enabledCapabilityMcpToolRefs(settings, runtimeSettings));
|
|
6892
7040
|
}
|
|
6893
|
-
function withWorkspaceDefaultMcpTools(tools, settings, runtimeSettings,
|
|
6894
|
-
if (!
|
|
7041
|
+
function withWorkspaceDefaultMcpTools(tools, settings, runtimeSettings, defaults2) {
|
|
7042
|
+
if (!defaults2?.mcpServerIds) {
|
|
6895
7043
|
return withDefaultEnabledCapabilityMcpTools(tools, settings, runtimeSettings);
|
|
6896
7044
|
}
|
|
6897
7045
|
return mergeToolRefs(
|
|
6898
7046
|
tools,
|
|
6899
7047
|
validateToolRefs(
|
|
6900
|
-
|
|
7048
|
+
[
|
|
7049
|
+
...defaults2.mcpServerIds,
|
|
7050
|
+
...defaults2.inheritConnectedMcpServers ? runtimeSettings.mcpServers.filter((server) => !["opengeni", "files", "docs"].includes(server.id)).map((server) => server.id) : []
|
|
7051
|
+
].map((id) => ({ kind: "mcp", id, optional: true })),
|
|
6901
7052
|
runtimeSettings
|
|
6902
7053
|
)
|
|
6903
7054
|
);
|
|
@@ -8182,6 +8333,19 @@ var reservedSessionMcpServerIds = /* @__PURE__ */ new Set(["opengeni", "files",
|
|
|
8182
8333
|
var maxSessionMcpCredentialHeaders = 16;
|
|
8183
8334
|
var maxSessionMcpCredentialHeaderValueLength = 4096;
|
|
8184
8335
|
var maxToolPolicyAuditRefs = 40;
|
|
8336
|
+
function withoutExcludedMcpServers(tools, excludedIds = []) {
|
|
8337
|
+
const excluded = new Set(excludedIds);
|
|
8338
|
+
return tools.filter((tool) => tool.id === "opengeni" || !excluded.has(tool.id));
|
|
8339
|
+
}
|
|
8340
|
+
function defaultPolicyExclusions(ids = []) {
|
|
8341
|
+
const sorted = [...new Set(ids)].filter((id) => id !== "opengeni").sort();
|
|
8342
|
+
if (sorted.length > 64 || sorted.some((id) => id.length > 200 || !/^[A-Za-z0-9_-]+$/.test(id))) {
|
|
8343
|
+
throw new HTTPException18(422, {
|
|
8344
|
+
message: "connector exclusions must contain at most 64 valid MCP server IDs"
|
|
8345
|
+
});
|
|
8346
|
+
}
|
|
8347
|
+
return sorted.length ? { excludedMcpServerIds: sorted } : {};
|
|
8348
|
+
}
|
|
8185
8349
|
function isCatalogOverlayModel(modelId) {
|
|
8186
8350
|
return modelId?.startsWith(WORKSPACE_GATEWAY_MODEL_ID_PREFIX2) === true || modelId?.startsWith(WORKSPACE_OPENROUTER_MODEL_ID_PREFIX2) === true || modelId?.startsWith(ORGANIZATION_GATEWAY_MODEL_ID_PREFIX2) === true || modelId?.startsWith(ORGANIZATION_OPENROUTER_MODEL_ID_PREFIX2) === true;
|
|
8187
8351
|
}
|
|
@@ -9571,6 +9735,11 @@ async function createSessionForRequestInFileScope(unresolvedDeps, grant, workspa
|
|
|
9571
9735
|
});
|
|
9572
9736
|
}
|
|
9573
9737
|
const toolsProvided = hasOwnProperty(rawPayload, "tools");
|
|
9738
|
+
if (toolsProvided && payload.excludedMcpServerIds !== void 0) {
|
|
9739
|
+
throw new HTTPException18(422, {
|
|
9740
|
+
message: "connector exclusions require workspace-default tools"
|
|
9741
|
+
});
|
|
9742
|
+
}
|
|
9574
9743
|
const requestedTools = validateToolRefs(
|
|
9575
9744
|
toolsProvided ? payload.tools : parentSession?.tools ?? payload.tools,
|
|
9576
9745
|
runtimeSettings
|
|
@@ -9587,6 +9756,8 @@ async function createSessionForRequestInFileScope(unresolvedDeps, grant, workspa
|
|
|
9587
9756
|
workspaceSessionToolDefaults
|
|
9588
9757
|
) : parentSession.tools,
|
|
9589
9758
|
runtimeSettings
|
|
9759
|
+
).filter(
|
|
9760
|
+
(tool) => tool.id === "opengeni" || !parentSession.toolPolicy.excludedMcpServerIds?.includes(tool.id)
|
|
9590
9761
|
);
|
|
9591
9762
|
if (toolsProvided) {
|
|
9592
9763
|
assertToolRefsSubset(
|
|
@@ -9603,7 +9774,8 @@ async function createSessionForRequestInFileScope(unresolvedDeps, grant, workspa
|
|
|
9603
9774
|
selectedTools = parentEffective;
|
|
9604
9775
|
toolPolicy = {
|
|
9605
9776
|
mode: parentTracksWorkspaceDefaults ? "workspace_default" : "inherited",
|
|
9606
|
-
inheritedFromSessionId: parentSession.id
|
|
9777
|
+
inheritedFromSessionId: parentSession.id,
|
|
9778
|
+
...defaultPolicyExclusions(parentSession.toolPolicy.excludedMcpServerIds)
|
|
9607
9779
|
};
|
|
9608
9780
|
}
|
|
9609
9781
|
} else if (toolsProvided) {
|
|
@@ -9618,6 +9790,21 @@ async function createSessionForRequestInFileScope(unresolvedDeps, grant, workspa
|
|
|
9618
9790
|
);
|
|
9619
9791
|
toolPolicy = { mode: "workspace_default", inheritedFromSessionId: null };
|
|
9620
9792
|
}
|
|
9793
|
+
if (payload.excludedMcpServerIds !== void 0) {
|
|
9794
|
+
if (toolPolicy.mode !== "workspace_default") {
|
|
9795
|
+
throw new HTTPException18(403, {
|
|
9796
|
+
message: "connector exclusions require workspace-default tools"
|
|
9797
|
+
});
|
|
9798
|
+
}
|
|
9799
|
+
toolPolicy = {
|
|
9800
|
+
...toolPolicy,
|
|
9801
|
+
...defaultPolicyExclusions([
|
|
9802
|
+
...toolPolicy.excludedMcpServerIds ?? [],
|
|
9803
|
+
...payload.excludedMcpServerIds
|
|
9804
|
+
])
|
|
9805
|
+
};
|
|
9806
|
+
}
|
|
9807
|
+
selectedTools = withoutExcludedMcpServers(selectedTools, toolPolicy.excludedMcpServerIds);
|
|
9621
9808
|
const tools = withFirstPartyTools(selectedTools, runtimeSettings);
|
|
9622
9809
|
const captureSelectedHostAuthority = prepareSelectedHostTurnAuthority(
|
|
9623
9810
|
runtimeSettings,
|
|
@@ -9705,6 +9892,7 @@ async function createSessionForRequestInFileScope(unresolvedDeps, grant, workspa
|
|
|
9705
9892
|
reasoningEffort,
|
|
9706
9893
|
latencyMode,
|
|
9707
9894
|
options: {
|
|
9895
|
+
...payload.excludedMcpServerIds !== void 0 ? { excludedMcpServerIds: payload.excludedMcpServerIds } : {},
|
|
9708
9896
|
...visibilityProvided ? { visibility: payload.visibility } : {},
|
|
9709
9897
|
...payload.sandboxBackend ? { sandboxBackend: payload.sandboxBackend } : {},
|
|
9710
9898
|
...payload.targetSandboxId ? { targetSandboxId: payload.targetSandboxId } : {},
|
|
@@ -10599,13 +10787,17 @@ function toolPolicyAuditSnapshot(session, tools, firstPartyMcpTools, policy = se
|
|
|
10599
10787
|
return {
|
|
10600
10788
|
mode: policy.mode,
|
|
10601
10789
|
inheritedFromSessionId: policy.inheritedFromSessionId,
|
|
10790
|
+
...policy.excludedMcpServerIds?.length ? {
|
|
10791
|
+
excludedMcpServerIds: policy.excludedMcpServerIds.slice(0, maxToolPolicyAuditRefs),
|
|
10792
|
+
excludedMcpServerCount: policy.excludedMcpServerIds.length
|
|
10793
|
+
} : {},
|
|
10602
10794
|
// IDs only: no MCP URLs, names, headers, credentials, schemas, or args.
|
|
10603
10795
|
toolIds: [...toolRefs].sort((left, right) => `${left.kind}:${left.id}`.localeCompare(`${right.kind}:${right.id}`)).map((tool) => tool.id),
|
|
10604
10796
|
toolRefs,
|
|
10605
10797
|
toolCount: allToolRefs.length,
|
|
10606
10798
|
firstPartyMcpTools: [...firstPartyMcpTools].sort(),
|
|
10607
10799
|
firstPartyMcpToolCount: firstPartyMcpTools.length,
|
|
10608
|
-
truncated: allToolRefs.length > toolRefs.length
|
|
10800
|
+
truncated: allToolRefs.length > toolRefs.length || (policy.excludedMcpServerIds?.length ?? 0) > maxToolPolicyAuditRefs
|
|
10609
10801
|
};
|
|
10610
10802
|
}
|
|
10611
10803
|
async function updateSessionToolPolicy(deps, grant, sessionId, request) {
|
|
@@ -10631,8 +10823,21 @@ async function updateSessionToolPolicy(deps, grant, sessionId, request) {
|
|
|
10631
10823
|
);
|
|
10632
10824
|
const explicitRequest = request.mode === "workspace_default" ? null : request;
|
|
10633
10825
|
const requestedMode = explicitRequest ? "explicit" : "workspace_default";
|
|
10826
|
+
const connectorOnlyEdit = request.mode === "workspace_default" && request.excludedMcpServerIds !== void 0;
|
|
10827
|
+
const requestedExclusions = request.mode === "workspace_default" ? request.excludedMcpServerIds ?? [] : [];
|
|
10634
10828
|
const explicitRequestedTools = explicitRequest ? (() => {
|
|
10635
|
-
const
|
|
10829
|
+
const availableIds = new Set(runtimeSettings.mcpServers.map((server) => server.id));
|
|
10830
|
+
const retainedUnavailableRefs = explicitRequest.tools.filter(
|
|
10831
|
+
(tool) => !availableIds.has(tool.id) && existingSession.tools.some((existing) => stableJson4(existing) === stableJson4(tool))
|
|
10832
|
+
);
|
|
10833
|
+
const retainedIds = new Set(retainedUnavailableRefs.map((tool) => tool.id));
|
|
10834
|
+
const validatedCurrentRefs = validateToolRefs(
|
|
10835
|
+
explicitRequest.tools.filter((tool) => !retainedIds.has(tool.id)),
|
|
10836
|
+
runtimeSettings
|
|
10837
|
+
);
|
|
10838
|
+
const validatedTools = explicitRequest.tools.filter(
|
|
10839
|
+
(tool) => retainedUnavailableRefs.includes(tool) || validatedCurrentRefs.some((validated) => validated.id === tool.id)
|
|
10840
|
+
);
|
|
10636
10841
|
const validatedIds = new Set(validatedTools.map((tool) => `${tool.kind}:${tool.id}`));
|
|
10637
10842
|
const unknown = explicitRequest.tools.find(
|
|
10638
10843
|
(tool) => !validatedIds.has(`${tool.kind}:${tool.id}`)
|
|
@@ -10696,6 +10901,8 @@ async function updateSessionToolPolicy(deps, grant, sessionId, request) {
|
|
|
10696
10901
|
workspaceSessionToolDefaults
|
|
10697
10902
|
) : parent.tools,
|
|
10698
10903
|
runtimeSettings
|
|
10904
|
+
).filter(
|
|
10905
|
+
(tool) => tool.id === "opengeni" || !parent.toolPolicy.excludedMcpServerIds?.includes(tool.id)
|
|
10699
10906
|
);
|
|
10700
10907
|
const deploymentAllowedFirstPartyMcpTools = new Set(
|
|
10701
10908
|
deploymentFirstPartyMcpToolPolicy.allowed
|
|
@@ -10713,7 +10920,11 @@ async function updateSessionToolPolicy(deps, grant, sessionId, request) {
|
|
|
10713
10920
|
nextFirstPartyMcpTools = parentFirstPartyMcpTools;
|
|
10714
10921
|
nextPolicy = {
|
|
10715
10922
|
mode: "workspace_default",
|
|
10716
|
-
inheritedFromSessionId: parent.id
|
|
10923
|
+
inheritedFromSessionId: parent.id,
|
|
10924
|
+
...defaultPolicyExclusions([
|
|
10925
|
+
...parent.toolPolicy.excludedMcpServerIds ?? [],
|
|
10926
|
+
...requestedExclusions
|
|
10927
|
+
])
|
|
10717
10928
|
};
|
|
10718
10929
|
} else {
|
|
10719
10930
|
nextTools = explicitRequestedTools;
|
|
@@ -10740,35 +10951,73 @@ async function updateSessionToolPolicy(deps, grant, sessionId, request) {
|
|
|
10740
10951
|
} else {
|
|
10741
10952
|
nextTools = requestedMode === "workspace_default" ? workspaceDefaultTools : explicitRequestedTools;
|
|
10742
10953
|
nextFirstPartyMcpTools = requestedMode === "workspace_default" ? workspaceDefaultFirstPartyTools : explicitRequestedFirstPartyTools;
|
|
10743
|
-
nextPolicy = {
|
|
10744
|
-
|
|
10745
|
-
|
|
10746
|
-
|
|
10747
|
-
|
|
10748
|
-
|
|
10749
|
-
|
|
10750
|
-
|
|
10751
|
-
|
|
10752
|
-
|
|
10753
|
-
|
|
10754
|
-
|
|
10755
|
-
|
|
10756
|
-
|
|
10757
|
-
|
|
10758
|
-
|
|
10759
|
-
|
|
10760
|
-
|
|
10761
|
-
|
|
10762
|
-
|
|
10763
|
-
|
|
10764
|
-
|
|
10765
|
-
|
|
10766
|
-
|
|
10767
|
-
|
|
10768
|
-
|
|
10769
|
-
|
|
10770
|
-
|
|
10771
|
-
|
|
10954
|
+
nextPolicy = {
|
|
10955
|
+
mode: requestedMode,
|
|
10956
|
+
inheritedFromSessionId: null,
|
|
10957
|
+
...requestedMode === "workspace_default" ? defaultPolicyExclusions(requestedExclusions) : {}
|
|
10958
|
+
};
|
|
10959
|
+
}
|
|
10960
|
+
if (connectorOnlyEdit) {
|
|
10961
|
+
if (session.toolPolicy.mode !== "workspace_default") {
|
|
10962
|
+
throw new HTTPException18(409, {
|
|
10963
|
+
message: "adopt workspace defaults before editing connector exclusions"
|
|
10964
|
+
});
|
|
10965
|
+
}
|
|
10966
|
+
nextTools = session.tools;
|
|
10967
|
+
nextFirstPartyMcpTools = [
|
|
10968
|
+
...session.firstPartyMcpTools ?? deploymentFirstPartyMcpToolPolicy.default
|
|
10969
|
+
];
|
|
10970
|
+
}
|
|
10971
|
+
if (!connectorOnlyEdit) {
|
|
10972
|
+
nextTools = withoutExcludedMcpServers(nextTools, nextPolicy.excludedMcpServerIds);
|
|
10973
|
+
}
|
|
10974
|
+
if (agentAttemptCaller && !session.parentSessionId) {
|
|
10975
|
+
const sessionTracksWorkspaceDefaults = session.toolPolicy?.mode === "workspace_default";
|
|
10976
|
+
const currentEffectiveTools = withFirstPartyTools(
|
|
10977
|
+
sessionTracksWorkspaceDefaults ? withWorkspaceDefaultMcpTools(
|
|
10978
|
+
availableToolRefs(session.tools, runtimeSettings),
|
|
10979
|
+
deps.settings,
|
|
10980
|
+
runtimeSettings,
|
|
10981
|
+
workspaceSessionToolDefaults
|
|
10982
|
+
) : session.tools,
|
|
10983
|
+
runtimeSettings
|
|
10984
|
+
);
|
|
10985
|
+
const currentAllowedTools = withoutExcludedMcpServers(
|
|
10986
|
+
currentEffectiveTools,
|
|
10987
|
+
session.toolPolicy.excludedMcpServerIds
|
|
10988
|
+
);
|
|
10989
|
+
const nextEffectiveTools = nextPolicy.mode === "workspace_default" ? withFirstPartyTools(
|
|
10990
|
+
withWorkspaceDefaultMcpTools(
|
|
10991
|
+
availableToolRefs(nextTools, runtimeSettings),
|
|
10992
|
+
deps.settings,
|
|
10993
|
+
runtimeSettings,
|
|
10994
|
+
workspaceSessionToolDefaults
|
|
10995
|
+
),
|
|
10996
|
+
runtimeSettings
|
|
10997
|
+
) : nextTools;
|
|
10998
|
+
if (nextPolicy.mode === "workspace_default" && (session.toolPolicy.excludedMcpServerIds ?? []).some(
|
|
10999
|
+
(id) => !nextPolicy.excludedMcpServerIds?.includes(id)
|
|
11000
|
+
)) {
|
|
11001
|
+
throw new HTTPException18(403, {
|
|
11002
|
+
message: "an agent may not remove session connector exclusions"
|
|
11003
|
+
});
|
|
11004
|
+
}
|
|
11005
|
+
assertToolRefsSubset(
|
|
11006
|
+
withoutExcludedMcpServers(nextEffectiveTools, nextPolicy.excludedMcpServerIds),
|
|
11007
|
+
currentAllowedTools,
|
|
11008
|
+
"an agent may only narrow its session tool policy"
|
|
11009
|
+
);
|
|
11010
|
+
const currentFirstPartyCeiling = effectiveFirstPartyMcpToolCeiling(
|
|
11011
|
+
session.firstPartyMcpTools,
|
|
11012
|
+
deploymentFirstPartyMcpToolPolicy
|
|
11013
|
+
);
|
|
11014
|
+
const widenedFirstPartyTool = nextFirstPartyMcpTools.find(
|
|
11015
|
+
(tool) => !currentFirstPartyCeiling.has(tool)
|
|
11016
|
+
);
|
|
11017
|
+
if (widenedFirstPartyTool) {
|
|
11018
|
+
throw new HTTPException18(403, {
|
|
11019
|
+
message: `an agent may only narrow its session OpenGeni tools: ${widenedFirstPartyTool}`
|
|
11020
|
+
});
|
|
10772
11021
|
}
|
|
10773
11022
|
}
|
|
10774
11023
|
const currentPolicy = session.toolPolicy;
|
|
@@ -10967,12 +11216,14 @@ async function rotateSessionMcpCredentialsForRequest(deps, authorization, sessio
|
|
|
10967
11216
|
}
|
|
10968
11217
|
|
|
10969
11218
|
// src/application/connect-operation.ts
|
|
11219
|
+
import { assertOrganizationIntegrationAllowed as assertOrganizationIntegrationAllowed3 } from "@opengeni/contracts";
|
|
11220
|
+
import { withOrganizationIntegrationPolicyFence as withOrganizationIntegrationPolicyFence2 } from "@opengeni/db/organization-integration-policy";
|
|
10970
11221
|
import {
|
|
10971
11222
|
claimConnectOperation,
|
|
10972
11223
|
finishConnectOperation
|
|
10973
11224
|
} from "@opengeni/db";
|
|
10974
11225
|
async function executeConnectOperation(input) {
|
|
10975
|
-
const { db, authorize, execute } = input;
|
|
11226
|
+
const { db, authorize, execute, purpose = "acquisition" } = input;
|
|
10976
11227
|
const scope = { ...input.scope };
|
|
10977
11228
|
const operation = {
|
|
10978
11229
|
attemptId: input.attemptId,
|
|
@@ -10981,10 +11232,37 @@ async function executeConnectOperation(input) {
|
|
|
10981
11232
|
inputDigest: input.inputDigest,
|
|
10982
11233
|
authorize
|
|
10983
11234
|
};
|
|
10984
|
-
const claim = await
|
|
11235
|
+
const claim = await withOrganizationIntegrationPolicyFence2(
|
|
11236
|
+
db,
|
|
11237
|
+
scope,
|
|
11238
|
+
async (tx, policy) => claimConnectOperation(tx, scope, {
|
|
11239
|
+
...operation,
|
|
11240
|
+
authorizeAcquisition: async (_tx, attempt) => {
|
|
11241
|
+
if (purpose === "cancellation") return;
|
|
11242
|
+
assertOrganizationIntegrationAllowed3(
|
|
11243
|
+
policy,
|
|
11244
|
+
integrationKeyForConnectProvider(attempt.providerId)
|
|
11245
|
+
);
|
|
11246
|
+
}
|
|
11247
|
+
})
|
|
11248
|
+
);
|
|
10985
11249
|
if (claim.status === "replayed") return claim.attempt;
|
|
10986
11250
|
const prepared = await execute(structuredClone(claim.attempt));
|
|
10987
|
-
return
|
|
11251
|
+
return withOrganizationIntegrationPolicyFence2(
|
|
11252
|
+
db,
|
|
11253
|
+
scope,
|
|
11254
|
+
async (tx, policy) => finishConnectOperation(tx, scope, {
|
|
11255
|
+
...operation,
|
|
11256
|
+
commit: prepared.commit,
|
|
11257
|
+
authorizeAcquisition: async (_tx, attempt) => {
|
|
11258
|
+
if (purpose === "cancellation") return;
|
|
11259
|
+
assertOrganizationIntegrationAllowed3(
|
|
11260
|
+
policy,
|
|
11261
|
+
integrationKeyForConnectProvider(attempt.providerId)
|
|
11262
|
+
);
|
|
11263
|
+
}
|
|
11264
|
+
})
|
|
11265
|
+
);
|
|
10988
11266
|
}
|
|
10989
11267
|
|
|
10990
11268
|
// src/domain/skill-imports.ts
|
|
@@ -12546,13 +12824,16 @@ function resolveSessionToolPolicy(input) {
|
|
|
12546
12824
|
(id) => availableIds.has(id)
|
|
12547
12825
|
);
|
|
12548
12826
|
const mandatoryIdSet = new Set(mandatoryIds);
|
|
12549
|
-
const selectedRefs = mergeToolRefs2([], input.sessionTools);
|
|
12550
12827
|
const tracksWorkspaceDefaults = policy.mode === "workspace_default";
|
|
12828
|
+
const excludedIds = new Set(tracksWorkspaceDefaults ? policy.excludedMcpServerIds : []);
|
|
12829
|
+
const selectedRefs = mergeToolRefs2([], input.sessionTools).filter(
|
|
12830
|
+
(tool) => !excludedIds.has(tool.id) || mandatoryIdSet.has(tool.id)
|
|
12831
|
+
);
|
|
12551
12832
|
let toolRefs = selectedRefs.filter((tool) => availableIds.has(tool.id));
|
|
12552
12833
|
if (tracksWorkspaceDefaults) {
|
|
12553
12834
|
toolRefs = mergeToolRefs2(
|
|
12554
12835
|
toolRefs,
|
|
12555
|
-
sortedIds(defaultIds).filter((id) => availableIds.has(id)).map((id) => ({ kind: "mcp", id, optional: true }))
|
|
12836
|
+
sortedIds(defaultIds).filter((id) => availableIds.has(id) && !excludedIds.has(id)).map((id) => ({ kind: "mcp", id, optional: true }))
|
|
12556
12837
|
);
|
|
12557
12838
|
}
|
|
12558
12839
|
toolRefs = mergeToolRefs2(
|
|
@@ -12561,7 +12842,7 @@ function resolveSessionToolPolicy(input) {
|
|
|
12561
12842
|
);
|
|
12562
12843
|
const requestedEffectiveRefs = mergeToolRefs2(
|
|
12563
12844
|
selectedRefs,
|
|
12564
|
-
tracksWorkspaceDefaults ? sortedIds(defaultIds).filter((id) => availableIds.has(id)).map((id) => ({ kind: "mcp", id, optional: true })) : []
|
|
12845
|
+
tracksWorkspaceDefaults ? sortedIds(defaultIds).filter((id) => availableIds.has(id) && !excludedIds.has(id)).map((id) => ({ kind: "mcp", id, optional: true })) : []
|
|
12565
12846
|
);
|
|
12566
12847
|
const effectiveIds = sortedIds(
|
|
12567
12848
|
mergeToolRefs2(
|
|
@@ -12629,7 +12910,10 @@ async function workspaceSessionToolPolicyDefaultServerIds(db, workspaceId, setti
|
|
|
12629
12910
|
const configured = resolveWorkspaceSessionToolDefaults2(workspace.settings);
|
|
12630
12911
|
if (!configured?.mcpServerIds) return availableDefaults;
|
|
12631
12912
|
const available = new Set(availableDefaults);
|
|
12632
|
-
return sortedIds(
|
|
12913
|
+
return sortedIds([
|
|
12914
|
+
...configured.mcpServerIds.filter((id) => available.has(id)),
|
|
12915
|
+
...configured.inheritConnectedMcpServers ? availableDefaults.filter((id) => !["opengeni", "files", "docs"].includes(id)) : []
|
|
12916
|
+
]);
|
|
12633
12917
|
}
|
|
12634
12918
|
function sessionWithEffectiveToolPolicy(session, workspaceServerIds, workspaceDefaultServerIds = []) {
|
|
12635
12919
|
const availableIds = new Set(workspaceServerIds);
|
|
@@ -12694,7 +12978,7 @@ import {
|
|
|
12694
12978
|
resolveXaiProviderAccountAuthoritySnapshotForAcceptance
|
|
12695
12979
|
} from "@opengeni/db";
|
|
12696
12980
|
import { HTTPException as HTTPException24 } from "hono/http-exception";
|
|
12697
|
-
import { isDeepStrictEqual } from "util";
|
|
12981
|
+
import { isDeepStrictEqual as isDeepStrictEqual2 } from "util";
|
|
12698
12982
|
|
|
12699
12983
|
// src/domain/host-mcp-task-admission.ts
|
|
12700
12984
|
import { HTTPException as HTTPException23 } from "hono/http-exception";
|
|
@@ -13169,7 +13453,7 @@ async function validateScheduledTaskTarget(input) {
|
|
|
13169
13453
|
if (!session || session.accountId !== input.grant.accountId) {
|
|
13170
13454
|
throw new HTTPException24(404, { message: "target session not found" });
|
|
13171
13455
|
}
|
|
13172
|
-
if (input.agentConfig.bundledSkillIds !== void 0 && !
|
|
13456
|
+
if (input.agentConfig.bundledSkillIds !== void 0 && !isDeepStrictEqual2(input.agentConfig.bundledSkillIds, session.bundledSkillIds)) {
|
|
13173
13457
|
throw new HTTPException24(422, {
|
|
13174
13458
|
message: "An existing-session schedule cannot change that session's bundled Skill selection"
|
|
13175
13459
|
});
|
|
@@ -13312,7 +13596,7 @@ async function validatedScheduledTaskUpdate(input) {
|
|
|
13312
13596
|
throw new HTTPException24(422, { message: "Source ingestion uses an ordinary agent task" });
|
|
13313
13597
|
}
|
|
13314
13598
|
const knowledgeSource = input.payload.agentConfig?.knowledgeSource ?? scheduledTaskKnowledgeSource(input.existing);
|
|
13315
|
-
if (knowledgeSource && requestedKnowledgeSource && !
|
|
13599
|
+
if (knowledgeSource && requestedKnowledgeSource && !isDeepStrictEqual2(requestedKnowledgeSource, existingKnowledgeSource)) {
|
|
13316
13600
|
await validateKnowledgeSourceSyncAction({
|
|
13317
13601
|
db: input.db,
|
|
13318
13602
|
grant: input.grant,
|
|
@@ -13421,7 +13705,7 @@ async function validatedScheduledTaskUpdate(input) {
|
|
|
13421
13705
|
}
|
|
13422
13706
|
const nextAgentConfig = update.agentConfig ?? input.existing.agentConfig;
|
|
13423
13707
|
const authorityTargetChanged = nextRunMode !== input.existing.runMode || nextTargetSessionId !== input.existing.targetSessionId || input.payload.variableSetId !== void 0 && input.payload.variableSetId !== input.existing.variableSetId || input.payload.rigId !== void 0 && input.payload.rigId !== input.existing.rigId;
|
|
13424
|
-
const materialExecutionChange = authorityTargetChanged || input.payload.selectedHostMcpDelegations !== void 0 || input.payload.connectionAuthorities !== void 0 || !
|
|
13708
|
+
const materialExecutionChange = authorityTargetChanged || input.payload.selectedHostMcpDelegations !== void 0 || input.payload.connectionAuthorities !== void 0 || !isDeepStrictEqual2(nextAgentConfig, input.existing.agentConfig) || input.payload.action !== void 0 && !isDeepStrictEqual2(input.payload.action, input.existing.action) || input.payload.schedule !== void 0 && !isDeepStrictEqual2(input.payload.schedule, input.existing.schedule) || input.payload.overlapPolicy !== void 0 && input.payload.overlapPolicy !== input.existing.overlapPolicy || input.payload.metadata !== void 0 && !isDeepStrictEqual2(input.payload.metadata, input.existing.metadata) || input.existing.status === "paused" && input.payload.status === "active";
|
|
13425
13709
|
if (materialExecutionChange && nextRunMode !== "existing_session") {
|
|
13426
13710
|
const beforeUpdateCommit = workspaceCustomModelCommitGuard({
|
|
13427
13711
|
settings: input.settings,
|
|
@@ -13452,7 +13736,7 @@ async function validatedScheduledTaskUpdate(input) {
|
|
|
13452
13736
|
message: "material changes to a personal GitHub-authorized task require explicit connectionAuthorities"
|
|
13453
13737
|
});
|
|
13454
13738
|
}
|
|
13455
|
-
if (existingDelegations.length > 0 && !
|
|
13739
|
+
if (existingDelegations.length > 0 && !isDeepStrictEqual2(nextAgentConfig.tools, input.existing.agentConfig.tools)) {
|
|
13456
13740
|
throw new HTTPException24(409, {
|
|
13457
13741
|
message: "changing tools on a connection-authorized task requires explicit connectionAuthorities"
|
|
13458
13742
|
});
|
|
@@ -13876,7 +14160,7 @@ function validateIncidentTelemetryPreflightSelection(settings, agentConfig) {
|
|
|
13876
14160
|
});
|
|
13877
14161
|
}
|
|
13878
14162
|
for (const required of preflight.requiredResources) {
|
|
13879
|
-
if (!agentConfig.resources.some((selected) =>
|
|
14163
|
+
if (!agentConfig.resources.some((selected) => isDeepStrictEqual2(selected, required))) {
|
|
13880
14164
|
throw new HTTPException24(422, {
|
|
13881
14165
|
message: "incidentTelemetryPreflight.requiredResources must be exact selected resources"
|
|
13882
14166
|
});
|
|
@@ -17480,7 +17764,7 @@ import {
|
|
|
17480
17764
|
inspectKnowledgeFilePreparation
|
|
17481
17765
|
} from "@opengeni/db";
|
|
17482
17766
|
import { retryWhileMissing } from "@opengeni/storage";
|
|
17483
|
-
async function prepareKnowledgeFile(deps, context, fileId) {
|
|
17767
|
+
async function prepareKnowledgeFile(deps, context, fileId, purpose = "evidence") {
|
|
17484
17768
|
const inspected = await inspectKnowledgeFilePreparation(deps.db, context, fileId);
|
|
17485
17769
|
if (inspected.status !== "prepare") return inspected;
|
|
17486
17770
|
if (!deps.objectStorage) throw new Error("File storage is unavailable");
|
|
@@ -17492,21 +17776,126 @@ async function prepareKnowledgeFile(deps, context, fileId) {
|
|
|
17492
17776
|
if (object.bytes.byteLength !== inspected.file.sizeBytes || inspected.file.sha256 && inspected.file.sha256.toLowerCase() !== sourceVersion) {
|
|
17493
17777
|
throw new Error("The original file no longer matches its retained metadata");
|
|
17494
17778
|
}
|
|
17495
|
-
const
|
|
17496
|
-
|
|
17779
|
+
const originalOnly = purpose === "evidence" && inspected.file.contentType.startsWith("image/");
|
|
17780
|
+
const parsed = originalOnly ? { text: "" } : await deps.getDocumentServices().parser.parse(object.bytes, inspected.file);
|
|
17781
|
+
if (!originalOnly && !parsed.text.trim())
|
|
17782
|
+
throw new Error("No searchable text could be extracted from this file");
|
|
17497
17783
|
return completeKnowledgeFilePreparation(deps.db, context, {
|
|
17498
17784
|
fileId,
|
|
17499
17785
|
title: inspected.file.filename,
|
|
17500
17786
|
sourceVersion,
|
|
17501
|
-
content: parsed.text
|
|
17787
|
+
content: parsed.text,
|
|
17788
|
+
purpose
|
|
17502
17789
|
});
|
|
17503
17790
|
}
|
|
17504
17791
|
|
|
17792
|
+
// src/domain/knowledge-preparation.ts
|
|
17793
|
+
import {
|
|
17794
|
+
KnowledgeSavePreparationRequest
|
|
17795
|
+
} from "@opengeni/contracts";
|
|
17796
|
+
import {
|
|
17797
|
+
getKnowledgeEntry,
|
|
17798
|
+
listKnowledgeEntries as listKnowledgeEntries2
|
|
17799
|
+
} from "@opengeni/db";
|
|
17800
|
+
var defaults = {
|
|
17801
|
+
get: getKnowledgeEntry,
|
|
17802
|
+
list: listKnowledgeEntries2,
|
|
17803
|
+
search: searchKnowledgeEntries
|
|
17804
|
+
};
|
|
17805
|
+
var CATALOG_PAGE_SIZE = 25;
|
|
17806
|
+
var CATALOG_PAGE_BUDGET = 40;
|
|
17807
|
+
var DESCRIPTION_CHARS = 2e3;
|
|
17808
|
+
var CATALOG_BYTES = 256 * 1024;
|
|
17809
|
+
async function prepareKnowledgeSave(db, context, input, embedder, services = defaults) {
|
|
17810
|
+
const request = KnowledgeSavePreparationRequest.parse(input);
|
|
17811
|
+
if (context.actor.kind !== "agent" && !(context.actor.kind === "human" && context.actor.review)) {
|
|
17812
|
+
throw Object.assign(
|
|
17813
|
+
new Error("Preparing Knowledge requires a live agent or human Knowledge reviewer"),
|
|
17814
|
+
{ code: "42501" }
|
|
17815
|
+
);
|
|
17816
|
+
}
|
|
17817
|
+
let provider;
|
|
17818
|
+
let queryEmbedding;
|
|
17819
|
+
const sharedEmbedder = () => {
|
|
17820
|
+
provider ??= embedder();
|
|
17821
|
+
const current = provider;
|
|
17822
|
+
return {
|
|
17823
|
+
model: current.model,
|
|
17824
|
+
dimensions: current.dimensions,
|
|
17825
|
+
embedMany: (texts) => current.embedMany(texts),
|
|
17826
|
+
embedQuery: (query) => queryEmbedding ??= current.embedQuery(query)
|
|
17827
|
+
};
|
|
17828
|
+
};
|
|
17829
|
+
const collections = {
|
|
17830
|
+
entries: [],
|
|
17831
|
+
complete: true,
|
|
17832
|
+
nextCursors: { published: null, needs_review: null }
|
|
17833
|
+
};
|
|
17834
|
+
const matches = {
|
|
17835
|
+
published: await services.search(
|
|
17836
|
+
db,
|
|
17837
|
+
context,
|
|
17838
|
+
{ query: request.query, limit: request.limit, view: "published" },
|
|
17839
|
+
sharedEmbedder
|
|
17840
|
+
),
|
|
17841
|
+
needs_review: await services.search(
|
|
17842
|
+
db,
|
|
17843
|
+
context,
|
|
17844
|
+
{ query: request.query, limit: request.limit, view: "needs_review" },
|
|
17845
|
+
sharedEmbedder
|
|
17846
|
+
)
|
|
17847
|
+
};
|
|
17848
|
+
for (const view of ["published", "needs_review"]) {
|
|
17849
|
+
if (request.collectionCursors && request.collectionCursors[view] === null) continue;
|
|
17850
|
+
let cursor = request.collectionCursors?.[view] ?? void 0;
|
|
17851
|
+
let bytes = 0;
|
|
17852
|
+
for (let pageNumber = 0; pageNumber < CATALOG_PAGE_BUDGET; pageNumber++) {
|
|
17853
|
+
const page = await services.list(db, context, {
|
|
17854
|
+
kind: "group",
|
|
17855
|
+
view,
|
|
17856
|
+
limit: CATALOG_PAGE_SIZE,
|
|
17857
|
+
...cursor ? { cursor } : {}
|
|
17858
|
+
});
|
|
17859
|
+
const records = [];
|
|
17860
|
+
for (let offset = 0; offset < page.entries.length; offset += 4) {
|
|
17861
|
+
records.push(
|
|
17862
|
+
...await Promise.all(
|
|
17863
|
+
page.entries.slice(offset, offset + 4).map((summary) => services.get(db, context, summary.id, { view }))
|
|
17864
|
+
)
|
|
17865
|
+
);
|
|
17866
|
+
}
|
|
17867
|
+
for (const record3 of records) {
|
|
17868
|
+
if (!record3 || record3.archived || record3.revision.entry.kind !== "group" || record3.revision.outcome !== (view === "published" ? "published" : "pending"))
|
|
17869
|
+
continue;
|
|
17870
|
+
const entry = record3.revision.entry;
|
|
17871
|
+
const descriptor = {
|
|
17872
|
+
id: record3.id,
|
|
17873
|
+
revisionId: record3.revision.id,
|
|
17874
|
+
version: record3.version,
|
|
17875
|
+
scope: record3.scope,
|
|
17876
|
+
view,
|
|
17877
|
+
title: entry.title,
|
|
17878
|
+
description: entry.content.slice(0, DESCRIPTION_CHARS),
|
|
17879
|
+
descriptionTruncated: entry.content.length > DESCRIPTION_CHARS,
|
|
17880
|
+
parentIds: entry.groupIds
|
|
17881
|
+
};
|
|
17882
|
+
collections.entries.push(descriptor);
|
|
17883
|
+
bytes += Buffer.byteLength(JSON.stringify(descriptor), "utf8");
|
|
17884
|
+
}
|
|
17885
|
+
cursor = page.nextCursor ?? void 0;
|
|
17886
|
+
if (!cursor || bytes >= CATALOG_BYTES) break;
|
|
17887
|
+
}
|
|
17888
|
+
collections.nextCursors[view] = cursor ?? null;
|
|
17889
|
+
if (cursor) collections.complete = false;
|
|
17890
|
+
}
|
|
17891
|
+
return { collections, matches };
|
|
17892
|
+
}
|
|
17893
|
+
|
|
17505
17894
|
// src/domain/knowledge-messages.ts
|
|
17506
17895
|
import { createHash as createHash10 } from "crypto";
|
|
17507
17896
|
import {
|
|
17508
17897
|
freezeAgentLearningPolicy as freezeAgentLearningPolicy2,
|
|
17509
|
-
getKnowledgeEntry,
|
|
17898
|
+
getKnowledgeEntry as getKnowledgeEntry2,
|
|
17510
17899
|
getSessionEvent as getSessionEvent3,
|
|
17511
17900
|
getSessionTurn as getSessionTurn2,
|
|
17512
17901
|
nestedPostgresSqlState as nestedPostgresSqlState7,
|
|
@@ -17534,7 +17923,7 @@ async function retainKnowledgeMessage(db, context, messageId) {
|
|
|
17534
17923
|
const key = `${context.accountId}:${context.workspaceId}:${policy.defaultScope}:${policy.subjectId ?? ""}:${event.id}`;
|
|
17535
17924
|
const entryId = stableId(`knowledge-message:${key}`);
|
|
17536
17925
|
for (const view of ["published", "needs_review"]) {
|
|
17537
|
-
const existing = await
|
|
17926
|
+
const existing = await getKnowledgeEntry2(db, context, entryId, { view });
|
|
17538
17927
|
if (!existing) continue;
|
|
17539
17928
|
if (existing.archived || existing.revision.outcome === "rejected" || existing.revision.entry.content !== text2) {
|
|
17540
17929
|
return {
|
|
@@ -17564,6 +17953,7 @@ async function retainKnowledgeMessage(db, context, messageId) {
|
|
|
17564
17953
|
content: text2,
|
|
17565
17954
|
source: {
|
|
17566
17955
|
kind: "conversation",
|
|
17956
|
+
purpose: "evidence",
|
|
17567
17957
|
sessionId: event.sessionId,
|
|
17568
17958
|
externalId: event.id,
|
|
17569
17959
|
capturedAt: event.occurredAt,
|
|
@@ -17582,6 +17972,215 @@ async function retainKnowledgeMessage(db, context, messageId) {
|
|
|
17582
17972
|
}
|
|
17583
17973
|
return { retained: true, ...receipt2, messageId: event.id };
|
|
17584
17974
|
}
|
|
17975
|
+
|
|
17976
|
+
// src/domain/connector-tool-permissions.ts
|
|
17977
|
+
import { createHash as createHash11 } from "crypto";
|
|
17978
|
+
import { Client as Client2 } from "@modelcontextprotocol/sdk/client/index.js";
|
|
17979
|
+
import { StreamableHTTPClientTransport as StreamableHTTPClientTransport2 } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
17980
|
+
import {
|
|
17981
|
+
boundMcpResponseBody,
|
|
17982
|
+
MCP_MAX_RESPONSE_BYTES,
|
|
17983
|
+
assertMcpToolListWithinBounds
|
|
17984
|
+
} from "@opengeni/runtime/mcp-network";
|
|
17985
|
+
import { createPinnedIntegrationTransport as createPinnedIntegrationTransport2 } from "@opengeni/capabilities";
|
|
17986
|
+
import {
|
|
17987
|
+
buildConnectionTokenResolver,
|
|
17988
|
+
getConnectionMetadata as getConnectionMetadata5,
|
|
17989
|
+
listConnectionsMetadata as listConnectionsMetadata3,
|
|
17990
|
+
listEnabledMcpCapabilityServers as listEnabledMcpCapabilityServers2,
|
|
17991
|
+
listConnectorToolPermissionPolicies,
|
|
17992
|
+
updateConnectorToolPermissionPolicies,
|
|
17993
|
+
resolveConnectorActionPolicy as resolveConnectorActionPolicy2
|
|
17994
|
+
} from "@opengeni/db";
|
|
17995
|
+
import { HTTPException as HTTPException27 } from "hono/http-exception";
|
|
17996
|
+
function connectorToolGroup(tool) {
|
|
17997
|
+
if (tool.annotations?.destructiveHint === true || tool.annotations?.readOnlyHint === false)
|
|
17998
|
+
return "write";
|
|
17999
|
+
if (tool.annotations?.readOnlyHint === true) return "read";
|
|
18000
|
+
return "other";
|
|
18001
|
+
}
|
|
18002
|
+
async function resolveTarget2(input) {
|
|
18003
|
+
const catalog = await buildCapabilityCatalog(input);
|
|
18004
|
+
const item = catalog.items.find((candidate) => candidate.id === input.capabilityId);
|
|
18005
|
+
if (!item || !item.enabled || item.kind !== "mcp" || !item.runtime.mcpServerId || item.source === "built_in") {
|
|
18006
|
+
throw new HTTPException27(404, { message: "Connected MCP connector not found" });
|
|
18007
|
+
}
|
|
18008
|
+
const enabled = await listEnabledMcpCapabilityServers2(input.db, input.workspaceId);
|
|
18009
|
+
const settings = settingsWithMcpCapabilityServers(input.settings, enabled);
|
|
18010
|
+
const server = settings.mcpServers.find((candidate) => candidate.id === item.runtime.mcpServerId);
|
|
18011
|
+
if (!server || server.url !== item.endpointUrl)
|
|
18012
|
+
throw new HTTPException27(409, { message: "Connector configuration is unavailable" });
|
|
18013
|
+
const ref = server.connectionRef;
|
|
18014
|
+
if (ref?.authoritySource === "host")
|
|
18015
|
+
throw new HTTPException27(409, {
|
|
18016
|
+
message: "Tool permissions for this connection are managed by its host"
|
|
18017
|
+
});
|
|
18018
|
+
let connection = ref?.connectionId ? await getConnectionMetadata5(
|
|
18019
|
+
input.db,
|
|
18020
|
+
input.workspaceId,
|
|
18021
|
+
ref.connectionId,
|
|
18022
|
+
input.grant.subjectId
|
|
18023
|
+
) : null;
|
|
18024
|
+
if (ref?.subjectScope === "subject" && !ref.connectionId) {
|
|
18025
|
+
const visible = await listConnectionsMetadata3(
|
|
18026
|
+
input.db,
|
|
18027
|
+
input.workspaceId,
|
|
18028
|
+
input.grant.subjectId
|
|
18029
|
+
);
|
|
18030
|
+
connection = visible.find(
|
|
18031
|
+
(candidate) => candidate.subjectId === input.grant.subjectId && candidate.providerDomain === ref.providerDomain && (!ref.kind || candidate.kind === ref.kind) && candidate.status === "active"
|
|
18032
|
+
) ?? null;
|
|
18033
|
+
}
|
|
18034
|
+
if (ref && (!connection || connection.providerDomain !== ref.providerDomain || ref.kind && connection.kind !== ref.kind || (ref.subjectScope === "subject" ? connection.subjectId !== input.grant.subjectId : connection.subjectId !== null))) {
|
|
18035
|
+
throw new HTTPException27(409, { message: "Reconnect this connector to manage its tools" });
|
|
18036
|
+
}
|
|
18037
|
+
if (connection?.subjectId && !input.personalOwnerVerified)
|
|
18038
|
+
throw new HTTPException27(403, {
|
|
18039
|
+
message: "Only the authenticated connection owner may manage personal tool permissions"
|
|
18040
|
+
});
|
|
18041
|
+
const connectionId = connection?.id ?? `session-mcp:${server.id}:${createHash11("sha256").update(server.url, "utf8").digest("hex")}`;
|
|
18042
|
+
return { server, connection, connectionId };
|
|
18043
|
+
}
|
|
18044
|
+
async function listTools(input, target) {
|
|
18045
|
+
let headers = { ...target.server.headers };
|
|
18046
|
+
if (target.server.connectionRef) {
|
|
18047
|
+
const result = await buildConnectionTokenResolver(
|
|
18048
|
+
input.db,
|
|
18049
|
+
input.settings
|
|
18050
|
+
)({
|
|
18051
|
+
workspaceId: input.workspaceId,
|
|
18052
|
+
...target.connection?.subjectId ? { subjectId: input.grant.subjectId } : {},
|
|
18053
|
+
serverId: target.server.id,
|
|
18054
|
+
toolName: "tools/list",
|
|
18055
|
+
connectionRef: { ...target.server.connectionRef, connectionId: target.connectionId },
|
|
18056
|
+
destinationUrl: target.server.url
|
|
18057
|
+
});
|
|
18058
|
+
if (result.status !== "ok") throw new Error("Reconnect this connector to load its tools.");
|
|
18059
|
+
headers = { ...headers, ...result.headers };
|
|
18060
|
+
}
|
|
18061
|
+
const pinned = createPinnedIntegrationTransport2({ network: input.settings });
|
|
18062
|
+
const deadline = AbortSignal.timeout(15e3);
|
|
18063
|
+
const client = new Client2(
|
|
18064
|
+
{ name: "opengeni-tool-permissions", version: "0.1.0" },
|
|
18065
|
+
{ capabilities: {} }
|
|
18066
|
+
);
|
|
18067
|
+
try {
|
|
18068
|
+
const transport = new StreamableHTTPClientTransport2(new URL(target.server.url), {
|
|
18069
|
+
requestInit: { headers },
|
|
18070
|
+
fetch: async (url, init) => boundMcpResponseBody(
|
|
18071
|
+
await pinned.fetch(url.toString(), {
|
|
18072
|
+
...init,
|
|
18073
|
+
signal: init?.signal ? AbortSignal.any([deadline, init.signal]) : deadline
|
|
18074
|
+
}),
|
|
18075
|
+
MCP_MAX_RESPONSE_BYTES
|
|
18076
|
+
)
|
|
18077
|
+
});
|
|
18078
|
+
await client.connect(transport, {
|
|
18079
|
+
timeout: 15e3,
|
|
18080
|
+
maxTotalTimeout: 15e3
|
|
18081
|
+
});
|
|
18082
|
+
const tools = [];
|
|
18083
|
+
const cursors = /* @__PURE__ */ new Set();
|
|
18084
|
+
let cursor;
|
|
18085
|
+
do {
|
|
18086
|
+
const page = await client.listTools(cursor ? { cursor } : void 0, {
|
|
18087
|
+
timeout: 15e3,
|
|
18088
|
+
maxTotalTimeout: 15e3,
|
|
18089
|
+
signal: deadline
|
|
18090
|
+
});
|
|
18091
|
+
tools.push(...page.tools);
|
|
18092
|
+
assertMcpToolListWithinBounds(tools);
|
|
18093
|
+
if (new Set(tools.map((tool) => tool.name)).size !== tools.length)
|
|
18094
|
+
throw new Error("The connector returned duplicate tool names.");
|
|
18095
|
+
cursor = page.nextCursor;
|
|
18096
|
+
if (tools.length > 2048 || cursor && cursors.has(cursor))
|
|
18097
|
+
throw new Error("The connector returned an invalid or oversized tool catalog.");
|
|
18098
|
+
if (cursor) cursors.add(cursor);
|
|
18099
|
+
} while (cursor);
|
|
18100
|
+
return tools.filter(
|
|
18101
|
+
(tool) => !target.server.allowedTools || target.server.allowedTools.includes(tool.name)
|
|
18102
|
+
);
|
|
18103
|
+
} finally {
|
|
18104
|
+
await client.close().catch(() => void 0);
|
|
18105
|
+
}
|
|
18106
|
+
}
|
|
18107
|
+
async function getConnectorToolPermissions(input) {
|
|
18108
|
+
const target = await resolveTarget2(input);
|
|
18109
|
+
const policies = await listConnectorToolPermissionPolicies(input.db, {
|
|
18110
|
+
accountId: input.grant.accountId,
|
|
18111
|
+
workspaceId: input.workspaceId,
|
|
18112
|
+
connectionId: target.connectionId
|
|
18113
|
+
});
|
|
18114
|
+
const permission = (name) => {
|
|
18115
|
+
const resolved = resolveConnectorActionPolicy2(policies, {
|
|
18116
|
+
connectionId: target.connectionId,
|
|
18117
|
+
serverId: target.server.id,
|
|
18118
|
+
toolName: name,
|
|
18119
|
+
actionName: "*"
|
|
18120
|
+
});
|
|
18121
|
+
if (!resolved.managed) return { permission: "allow", inherited: true };
|
|
18122
|
+
return {
|
|
18123
|
+
permission: resolved.entry?.policy ?? "block",
|
|
18124
|
+
inherited: resolved.entry?.toolName !== name
|
|
18125
|
+
};
|
|
18126
|
+
};
|
|
18127
|
+
const defaultPolicy = policies.find(
|
|
18128
|
+
(row) => row.serverId === target.server.id && row.toolName === "*" && row.actionName === "*"
|
|
18129
|
+
);
|
|
18130
|
+
let tools = [];
|
|
18131
|
+
let discoveryError = null;
|
|
18132
|
+
try {
|
|
18133
|
+
tools = await listTools(input, target);
|
|
18134
|
+
if (tools.some((tool) => tool.name === "*" || tool.name !== tool.name.trim())) {
|
|
18135
|
+
tools = tools.filter((tool) => tool.name !== "*" && tool.name === tool.name.trim());
|
|
18136
|
+
discoveryError = "The connector exposes a reserved or whitespace-padded tool name. Individual permissions for these names are unsupported, so they are omitted from tool groups. The connector default still applies.";
|
|
18137
|
+
}
|
|
18138
|
+
} catch {
|
|
18139
|
+
discoveryError = "Could not load the connector's tools. Try again or reconnect. Your saved permissions still apply.";
|
|
18140
|
+
}
|
|
18141
|
+
return {
|
|
18142
|
+
connectionId: target.connectionId,
|
|
18143
|
+
serverId: target.server.id,
|
|
18144
|
+
defaultPermission: defaultPolicy?.policy ?? null,
|
|
18145
|
+
tools: tools.map(
|
|
18146
|
+
(tool) => ({
|
|
18147
|
+
name: tool.name,
|
|
18148
|
+
...tool.title ?? tool.annotations?.title ? { title: tool.title ?? tool.annotations?.title } : {},
|
|
18149
|
+
...tool.description ? { description: tool.description } : {},
|
|
18150
|
+
group: connectorToolGroup(tool),
|
|
18151
|
+
...permission(tool.name),
|
|
18152
|
+
approvalRequired: target.server.requireApproval === true || Array.isArray(target.server.requireApproval) && target.server.requireApproval.includes(tool.name)
|
|
18153
|
+
})
|
|
18154
|
+
),
|
|
18155
|
+
discoveryError,
|
|
18156
|
+
canManage: canManageConnectorPermissions(input.grant)
|
|
18157
|
+
};
|
|
18158
|
+
}
|
|
18159
|
+
function canManageConnectorPermissions(grant) {
|
|
18160
|
+
return hasPermission(grant.permissions, "capabilities:manage") && grant.principalKind !== "agent_attempt" && grant.principalKind !== "service" && !grant.serviceInitiator && !grant.serviceInitiatorContext;
|
|
18161
|
+
}
|
|
18162
|
+
async function updateConnectorToolPermissions(input) {
|
|
18163
|
+
if (!canManageConnectorPermissions(input.grant))
|
|
18164
|
+
throw new HTTPException27(403, { message: "Connector management permission required" });
|
|
18165
|
+
if (input.payload.target === "tools" && input.payload.toolNames.some((name) => name === "*" || name !== name.trim()))
|
|
18166
|
+
throw new HTTPException27(400, {
|
|
18167
|
+
message: "Tool names must be exact and cannot use the connector-default wildcard."
|
|
18168
|
+
});
|
|
18169
|
+
const target = await resolveTarget2(input);
|
|
18170
|
+
if (target.connectionId !== input.payload.connectionId)
|
|
18171
|
+
throw new HTTPException27(409, {
|
|
18172
|
+
message: "The connector account changed. Reload its permissions."
|
|
18173
|
+
});
|
|
18174
|
+
await updateConnectorToolPermissionPolicies(input.db, {
|
|
18175
|
+
accountId: input.grant.accountId,
|
|
18176
|
+
workspaceId: input.workspaceId,
|
|
18177
|
+
subjectId: input.grant.subjectId,
|
|
18178
|
+
connectionId: target.connectionId,
|
|
18179
|
+
serverId: target.server.id,
|
|
18180
|
+
toolNames: input.payload.target === "default" ? ["*"] : input.payload.toolNames,
|
|
18181
|
+
policy: input.payload.permission
|
|
18182
|
+
});
|
|
18183
|
+
}
|
|
17585
18184
|
export {
|
|
17586
18185
|
CODEX_COMPACTION_V2_PROVIDER_LOCKED,
|
|
17587
18186
|
CONVERSATION_ATTACHMENT_MAX_COUNT,
|
|
@@ -17767,6 +18366,7 @@ export {
|
|
|
17767
18366
|
classifyRigVerificationOutcome,
|
|
17768
18367
|
codexAppsCatalogItem,
|
|
17769
18368
|
compareCodeUnits,
|
|
18369
|
+
connectorToolGroup,
|
|
17770
18370
|
controlAgentSessionWorkstream,
|
|
17771
18371
|
controlHumanSessionWorkstream,
|
|
17772
18372
|
controlHumanSessionWorkstreamWithOutcome,
|
|
@@ -17849,6 +18449,7 @@ export {
|
|
|
17849
18449
|
getActorNewSessionDraft,
|
|
17850
18450
|
getAutomationAdapter,
|
|
17851
18451
|
getCapabilityPack,
|
|
18452
|
+
getConnectorToolPermissions,
|
|
17852
18453
|
getHumanComposerDraft,
|
|
17853
18454
|
getManagedAuthRequestActorAbortSignal,
|
|
17854
18455
|
getManagedAuthRequestActorAdmissionStamp,
|
|
@@ -17877,6 +18478,8 @@ export {
|
|
|
17877
18478
|
insightsSessionLabel,
|
|
17878
18479
|
inspectEditableArtifactLiveWireEnvelope,
|
|
17879
18480
|
installSkill,
|
|
18481
|
+
integrationKeyForConnectProvider,
|
|
18482
|
+
integrationSourceForOrganizationPolicy,
|
|
17880
18483
|
isAcceptedMimeType,
|
|
17881
18484
|
isAuthoritativeGitHubRepositorySelectionError,
|
|
17882
18485
|
isBuiltInCapabilityPack,
|
|
@@ -17937,6 +18540,7 @@ export {
|
|
|
17937
18540
|
officialMcpRegistryUrl,
|
|
17938
18541
|
ogatxEditableArtifactMutationIntentCodec,
|
|
17939
18542
|
openGeniSlackBotMetadata,
|
|
18543
|
+
organizationIntegrationCatalog,
|
|
17940
18544
|
organizationMembershipHttpStatus,
|
|
17941
18545
|
packInstallationUsesLegacyRuntime,
|
|
17942
18546
|
parseGitHubRepositoryCoordinates,
|
|
@@ -17966,6 +18570,7 @@ export {
|
|
|
17966
18570
|
prepareExternalLinkTurnAdmission,
|
|
17967
18571
|
prepareHostMcpOwnerAuthorization,
|
|
17968
18572
|
prepareKnowledgeFile,
|
|
18573
|
+
prepareKnowledgeSave,
|
|
17969
18574
|
previewCapabilityPackInstallation,
|
|
17970
18575
|
projectGitHubActionPolicyActor,
|
|
17971
18576
|
promoteSetupAppendChange,
|
|
@@ -18088,6 +18693,7 @@ export {
|
|
|
18088
18693
|
syncCreatedScheduledTask,
|
|
18089
18694
|
syncUpdatedScheduledTask,
|
|
18090
18695
|
unboundGitHubRepositoryResources,
|
|
18696
|
+
updateConnectorToolPermissions,
|
|
18091
18697
|
updateExternalIdentityMembershipForRequest,
|
|
18092
18698
|
updateGitHubActionPolicyGroup,
|
|
18093
18699
|
updateManagedHumanSessionVisibility,
|