@opengeni/api-router 3.1.3-canary.38890816741001 → 3.3.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.
Files changed (47) hide show
  1. package/dist/app.js +1 -1
  2. package/dist/{chunk-OT56OY4V.js → chunk-6CECHDZP.js} +1569 -399
  3. package/dist/{chunk-OT56OY4V.js.map → chunk-6CECHDZP.js.map} +1 -1
  4. package/dist/index.js +50 -2
  5. package/dist/index.js.map +1 -1
  6. package/dist/integrations/external-vault-providers.d.ts +29 -0
  7. package/dist/interaction-secrets-vault.d.ts +2 -2
  8. package/dist/mcp/action-descriptions.d.ts +1 -1
  9. package/dist/mcp/scheduled-task-view.d.ts +4 -4
  10. package/dist/routes/vault-providers.d.ts +4 -2
  11. package/dist/sandbox/channel-a.d.ts +10 -1
  12. package/dist/shutdown.d.ts +14 -0
  13. package/dist/site-publishing/agent-data-tools.d.ts +5 -5
  14. package/dist/site-publishing/agent-session.d.ts +3 -2
  15. package/dist/site-publishing/runtime.d.ts +7 -6
  16. package/dist/site-publishing/service.d.ts +8 -11
  17. package/dist/site-publishing/session-request-authority.d.ts +3 -0
  18. package/dist/workspace-tool-gateway-secrets.d.ts +1 -1
  19. package/package.json +25 -22
  20. package/src/access-grant-rls.ts +1 -0
  21. package/src/app.ts +14 -2
  22. package/src/http/sse.ts +5 -1
  23. package/src/index.ts +13 -1
  24. package/src/integrations/external-vault-providers.ts +136 -0
  25. package/src/interaction-secrets-vault.ts +6 -2
  26. package/src/mcp/action-catalog.gen.ts +253 -4
  27. package/src/mcp/action-descriptions.ts +32 -2
  28. package/src/mcp/action-schema-imports.gen.ts +3 -1
  29. package/src/routes/browser-sessions.ts +61 -17
  30. package/src/routes/computer-sessions.ts +102 -39
  31. package/src/routes/connections.ts +17 -0
  32. package/src/routes/insights-usage.ts +22 -0
  33. package/src/routes/sessions.ts +22 -4
  34. package/src/routes/site-runtime.ts +62 -20
  35. package/src/routes/vault-providers.ts +788 -29
  36. package/src/routes/vaults.ts +5 -1
  37. package/src/sandbox/channel-a.ts +18 -1
  38. package/src/sandbox/metrics-ingestion.ts +1 -0
  39. package/src/shutdown.ts +54 -0
  40. package/src/site-frame-guard.ts +11 -2
  41. package/src/site-publishing/agent-data-tools.ts +16 -8
  42. package/src/site-publishing/agent-session.ts +24 -6
  43. package/src/site-publishing/runtime.ts +29 -7
  44. package/src/site-publishing/service.ts +65 -77
  45. package/src/site-publishing/session-request-authority.ts +14 -0
  46. package/src/workspace-tool-gateway-secrets.ts +9 -2
  47. package/src/workspace-tool-gateway.ts +6 -2
@@ -0,0 +1,29 @@
1
+ import type { Settings } from "@opengeni/config";
2
+ import type { ExternalVaultProviderFactories } from "@opengeni/core";
3
+ import { type PinnedFetchOptions } from "@opengeni/network";
4
+ import type { KeeperRuntimeOptions } from "@opengeni/vaults-keeper";
5
+ /** Test seams only; production passes nothing. */
6
+ export type NativeExternalVaultOptions = {
7
+ keeper?: Pick<KeeperRuntimeOptions, "queryFunction" | "fetchFile" | "sdk">;
8
+ /** Applies when this call creates the pool for its CLI path. */
9
+ protonpass?: {
10
+ stateRoot?: string;
11
+ hostEnv?: Readonly<Record<string, string | undefined>>;
12
+ };
13
+ };
14
+ /**
15
+ * Native external vault adapters that run inside this server: Keeper Secrets
16
+ * Manager always, Proton Pass only when the operator installed `pass-cli`
17
+ * (`OPENGENI_PROTON_PASS_CLI_PATH`). Both packages load through `import()`
18
+ * on first use (packages/vaults-keeper/test/import-guard.test.ts). Keep this
19
+ * file identical to apps/worker/src/external-vault-providers.ts.
20
+ */
21
+ export declare function createNativeExternalVaultFactories(settings: Settings, options?: NativeExternalVaultOptions): ExternalVaultProviderFactories;
22
+ /** Logs out every pooled Proton Pass CLI session and wipes its state (graceful shutdown). */
23
+ export declare function closeNativeExternalVaultProviders(): Promise<void>;
24
+ /**
25
+ * The fetch every Keeper request and file download uses: DNS-pinned and
26
+ * policy-checked (no private or special-use targets). The adapter adds
27
+ * HTTPS-only, no redirects, deadlines, size bounds and the caller's abort.
28
+ */
29
+ export declare function nativeKeeperFetch(settings: Settings, pinned?: Pick<PinnedFetchOptions, "dnsLookup" | "fetchImpl" | "agentFactory">): (input: string | URL, init: RequestInit) => Promise<Response>;
@@ -7,7 +7,7 @@ import type { VaultProviderRegistry } from "@opengeni/vaults";
7
7
  import { type BrowserPasskeyCredentialMaterial } from "@opengeni/contracts";
8
8
  import { type VaultService } from "@opengeni/core";
9
9
  import { type Database } from "@opengeni/db";
10
- import type { InteractionSecretsPort } from "./interaction-secrets.js";
10
+ import type { AttemptSecretPrincipal, InteractionSecretsPort } from "./interaction-secrets.js";
11
11
  export declare function passkeyItemName(credential: BrowserPasskeyCredentialMaterial): string;
12
12
  export declare function createVaultInteractionSecretsPort(deps: {
13
13
  db: Database;
@@ -16,5 +16,5 @@ export declare function createVaultInteractionSecretsPort(deps: {
16
16
  /** Defaults to a service over the same database and key. */
17
17
  vaultService?: VaultService;
18
18
  /** Connected vaults (1Password, host) for a workspace's acting subject. */
19
- providersFor?: (workspaceId: string, subjectId: string | null) => VaultProviderRegistry;
19
+ providersFor?: (workspaceId: string, subjectId: string | null, principal?: AttemptSecretPrincipal) => VaultProviderRegistry;
20
20
  }): InteractionSecretsPort;
@@ -1,4 +1,4 @@
1
1
  import type { ActionCatalogEntry } from "./action-catalog-types.js";
2
- /** Actions that must carry a description (subscription and model settings). */
2
+ /** Actions that must carry a description (subscriptions, models and vault connections). */
3
3
  export declare const DESCRIBED_ACTION_PATH: RegExp;
4
4
  export declare function actionDescription(entry: Pick<ActionCatalogEntry, "method" | "path">): string | undefined;
@@ -83,8 +83,8 @@ export declare function scheduledTaskMcpSummary(task: ScheduledTask): {
83
83
  runMode: "existing_session" | "new_session_per_run" | "reusable_session";
84
84
  overlapPolicy: "allow_concurrent" | "buffer_one" | "skip";
85
85
  action: {
86
- readonly sourceId?: never;
87
86
  kind: "agent_turn";
87
+ sourceId?: never;
88
88
  sourceGeneration?: never;
89
89
  sourceLifecycleGeneration?: never;
90
90
  destination?: never;
@@ -221,8 +221,8 @@ export declare function boundScheduledTaskDetailMcp(task: ScheduledTask): {
221
221
  runMode: "existing_session" | "new_session_per_run" | "reusable_session";
222
222
  overlapPolicy: "allow_concurrent" | "buffer_one" | "skip";
223
223
  action: {
224
- readonly sourceId?: never;
225
224
  kind: "agent_turn";
225
+ sourceId?: never;
226
226
  sourceGeneration?: never;
227
227
  sourceLifecycleGeneration?: never;
228
228
  destination?: never;
@@ -408,8 +408,8 @@ export declare function boundScheduledTaskDetailMcp(task: ScheduledTask): {
408
408
  runMode: "existing_session" | "new_session_per_run" | "reusable_session";
409
409
  overlapPolicy: "allow_concurrent" | "buffer_one" | "skip";
410
410
  action: {
411
- readonly sourceId?: never;
412
411
  kind: "agent_turn";
412
+ sourceId?: never;
413
413
  sourceGeneration?: never;
414
414
  sourceLifecycleGeneration?: never;
415
415
  destination?: never;
@@ -601,8 +601,8 @@ export declare function boundScheduledTaskMcpPage(input: {
601
601
  runMode: "existing_session" | "new_session_per_run" | "reusable_session";
602
602
  overlapPolicy: "allow_concurrent" | "buffer_one" | "skip";
603
603
  action: {
604
- readonly sourceId?: never;
605
604
  kind: "agent_turn";
605
+ sourceId?: never;
606
606
  sourceGeneration?: never;
607
607
  sourceLifecycleGeneration?: never;
608
608
  destination?: never;
@@ -1,7 +1,7 @@
1
1
  import type { Hono } from "hono";
2
- import { type ApiRouteDeps, type OnePasswordConnectionMetadata, type OnePasswordVaultProviderFactory } from "@opengeni/core";
2
+ import { type ApiRouteDeps, type OnePasswordConnectionMetadata, type OnePasswordVaultProviderFactory, type ExternalVaultProviderFactories } from "@opengeni/core";
3
3
  import { type VaultDbContext } from "@opengeni/db";
4
- import { type VaultProvider, type VaultProviderRegistry } from "@opengeni/vaults";
4
+ import { type VaultProvider, type VaultProviderRegistry, type VaultProviderAuthority } from "@opengeni/vaults";
5
5
  export declare function onePasswordFactoryFor(deps: ApiRouteDeps): OnePasswordVaultProviderFactory;
6
6
  /**
7
7
  * The 1Password provider for a stored connection the caller can see, plus its
@@ -23,7 +23,9 @@ export declare function onePasswordProviderForConnection(deps: ApiRouteDeps, inp
23
23
  export declare function vaultProviderRegistryFor(deps: ApiRouteDeps, input: {
24
24
  workspaceId: string;
25
25
  subjectId: string | null;
26
+ executionContext?: Pick<VaultProviderAuthority, "sessionId" | "turnId" | "attemptId" | "siteId">;
26
27
  }): VaultProviderRegistry;
28
+ export declare function externalVaultFactoriesFor(deps: ApiRouteDeps): ExternalVaultProviderFactories;
27
29
  export type LinkedVaultLookup = (input: {
28
30
  workspaceId: string;
29
31
  connectionId: string;
@@ -1,6 +1,6 @@
1
1
  import { type Settings } from "@opengeni/config";
2
2
  import type { AccessGrant, Session } from "@opengeni/contracts";
3
- import { type Database, type LeaseSnapshot } from "@opengeni/db";
3
+ import { type Database, type EnrollmentRecord, type LeaseSnapshot } from "@opengeni/db";
4
4
  import { type EventBus } from "@opengeni/events";
5
5
  import { type Observability } from "@opengeni/observability";
6
6
  import type { ObjectStorage } from "@opengeni/storage";
@@ -56,6 +56,15 @@ export type ChannelAHandle = {
56
56
  * who started an agent's turn, or the calling person. Routes that look up
57
57
  * the routed machine again must use it rather than the raw caller grant. */
58
58
  machineAuthority: () => Promise<ChannelAMachineAuthority>;
59
+ /** For a machine-home request, the Connected Machine and live runner this
60
+ * request was pinned to under `machineAuthority`. The routing session fails
61
+ * rather than follow a changed pointer, so a route that resolved this same
62
+ * sandbox can use it instead of looking the machine up again. */
63
+ pinnedMachine: {
64
+ sandboxId: string;
65
+ sandboxWorkspaceId: string;
66
+ enrollment: EnrollmentRecord;
67
+ } | null;
59
68
  requestId: string;
60
69
  };
61
70
  export type EstablishedHandleCacheKind = "read" | "process" | "none";
@@ -0,0 +1,14 @@
1
+ type ShutdownSignal = "SIGINT" | "SIGTERM";
2
+ type ShutdownProcess = {
3
+ on(signal: ShutdownSignal, handler: () => void): unknown;
4
+ off(signal: ShutdownSignal, handler: () => void): unknown;
5
+ exit(code: number): void;
6
+ };
7
+ /** Standalone process boundary; embedded hosts keep owning their explicit close(). */
8
+ export declare function installApiShutdownHandlers(input: {
9
+ close: () => Promise<void>;
10
+ process?: ShutdownProcess;
11
+ timeoutMs?: number;
12
+ onFailure?: () => void;
13
+ }): () => void;
14
+ export {};
@@ -1,5 +1,5 @@
1
- import { type FirstPartyMcpToolName, type SiteLimitProfileKind } from "@opengeni/contracts";
2
- import type { ApiRouteDeps } from "@opengeni/core";
1
+ import { type FirstPartyMcpToolName, type Permission, type SiteLimitProfileKind } from "@opengeni/contracts";
2
+ import { type ApiRouteDeps } from "@opengeni/core";
3
3
  import { type SiteRuntimeConversationViewerKind } from "@opengeni/db";
4
4
  import type { PublishedSiteDataViewer } from "../site-data/service.js";
5
5
  import { type SiteRuntimeHost } from "./runtime.js";
@@ -21,8 +21,6 @@ import { type SiteRuntimeHost } from "./runtime.js";
21
21
  export declare const SITE_AGENT_DATA_TOOLS: readonly ["site_data_query", "site_data_write"];
22
22
  export declare const SITE_AGENT_SQL_TOOLS: readonly ["site_sql_run"];
23
23
  export declare const SITE_AGENT_MEMBER_SQL_TOOLS: readonly ["site_sql_query"];
24
- /** The first-party permissions those tools need (Site Data writes need publish). */
25
- export declare const SITE_AGENT_DATA_TOOL_PERMISSIONS: readonly ["artifacts:read", "artifacts:publish"];
26
24
  export declare const SITE_AGENT_OTHER_SITE_MESSAGE = "This Site's assistant can only use this Site's data";
27
25
  export declare const SITE_AGENT_MANAGEMENT_MESSAGE = "This Site's assistant can use the Site's data, not manage the Site";
28
26
  /** The data tools one viewer's Site agent conversation gets, within the deployment's tools. */
@@ -32,6 +30,8 @@ export declare function siteAgentDataToolNames(input: {
32
30
  siteSql: boolean;
33
31
  /** The deployment's first-party tool allowlist. */
34
32
  allowed: readonly string[];
33
+ /** Live member ceiling; omitted only where the native caller filters tools itself. */
34
+ memberPermissions?: readonly Permission[];
35
35
  }): FirstPartyMcpToolName[];
36
36
  /** Whether Site SQL is offered on this deployment and turned on for the Site. */
37
37
  export declare function siteAgentSiteSqlOn(deps: Pick<ApiRouteDeps, "db" | "settings">, scope: {
@@ -42,7 +42,7 @@ export declare function siteAgentSiteSqlOn(deps: Pick<ApiRouteDeps, "db" | "sett
42
42
  export declare function siteAgentDataToolsForStart(deps: Pick<ApiRouteDeps, "db" | "settings">, scope: {
43
43
  accountId: string;
44
44
  workspaceId: string;
45
- }, siteId: string, viewerKind: SiteRuntimeConversationViewerKind): Promise<FirstPartyMcpToolName[]>;
45
+ }, siteId: string, viewerKind: SiteRuntimeConversationViewerKind, memberPermissions?: readonly Permission[]): Promise<FirstPartyMcpToolName[]>;
46
46
  /** This Site's viewer (`user:<subject>` or `guest:<visitor>`) as Site Data and Site SQL see it. */
47
47
  export declare function sitePublishedDataViewer(host: Pick<SiteRuntimeHost, "scope" | "site">, principal: string, guest: boolean): PublishedSiteDataViewer;
48
48
  /**
@@ -1,7 +1,8 @@
1
+ import { type Permission } from "@opengeni/contracts";
1
2
  import { type ApiRouteDeps } from "@opengeni/core";
2
3
  import type { SiteRuntimeHost } from "./runtime.js";
3
4
  import type { SiteRuntimeViewer } from "./viewer.js";
4
- export declare const SITE_AGENT_DEFAULTS_VERSION = 1;
5
+ export declare const SITE_AGENT_DEFAULTS_VERSION = 2;
5
6
  /**
6
7
  * Before admitting more work, converge pre-redesign published-host sessions to
7
8
  * the same Site-local tools as a new conversation. This is a trusted host
@@ -13,4 +14,4 @@ export declare const SITE_AGENT_DEFAULTS_VERSION = 1;
13
14
  * audit event and completion marker atomic. Failed writes remain retryable.
14
15
  * Existing attempts keep their accepted execution policy; no turn is rewritten.
15
16
  */
16
- export declare function normalizeSiteAgentSession(deps: Pick<ApiRouteDeps, "db" | "settings" | "bus" | "objectStorage">, host: SiteRuntimeHost, viewer: SiteRuntimeViewer, sessionId: string, retainedToolIds: readonly string[]): Promise<void>;
17
+ export declare function normalizeSiteAgentSession(deps: Pick<ApiRouteDeps, "db" | "settings" | "bus" | "objectStorage">, host: SiteRuntimeHost, viewer: SiteRuntimeViewer, sessionId: string, retainedToolIds: readonly string[], memberPermissions?: readonly Permission[]): Promise<void>;
@@ -7,8 +7,8 @@ import { type SiteRuntimeViewer } from "./viewer.js";
7
7
  /**
8
8
  * Site-runtime: the narrow API a Site gateway calls for one published Site
9
9
  * host, after the gateway signature passed. Everything here works for exactly
10
- * one Site: the host names it, the viewer is resolved for it, and the agent
11
- * principal is `site:<siteId>`.
10
+ * one Site: the host names it and the viewer is resolved for it. Members retain
11
+ * their human identity; outsiders use the viewer-bound `site:<siteId>` principal.
12
12
  */
13
13
  export type SiteRuntimeDeps = Pick<ApiRouteDeps, "db" | "settings" | "objectStorage">;
14
14
  export type SiteRuntimeHost = {
@@ -43,7 +43,7 @@ export declare function resolveSiteRuntimeViewer(deps: SiteRuntimeDeps, host: Si
43
43
  ignoreViewAs?: boolean;
44
44
  }): Promise<SiteRuntimeViewer>;
45
45
  /** What a viewer cannot do on a Site: 403 with `details.code` `not_available_for_viewer`. */
46
- export type SiteViewerFeature = "agent" | "agent_model" | "workspace_tools" | "site_data" | "site_sql";
46
+ export type SiteViewerFeature = "agent" | "private_conversations" | "agent_model" | "workspace_tools" | "site_data" | "site_sql";
47
47
  export declare function siteNotAvailableForViewer(feature: SiteViewerFeature, message: string, extra?: Record<string, unknown>): import("../http/api-error.js").ApiHttpError;
48
48
  /** The viewer capabilities a page reads from `site.config` to hide what won't work. */
49
49
  export declare function siteViewerCapabilities(deps: SiteRuntimeDeps, host: SiteRuntimeHost, viewer: SiteRuntimeViewer, limits: SiteLimits): SiteViewerCapabilities;
@@ -160,9 +160,9 @@ export declare function meterSiteRuntime(deps: SiteRuntimeDeps, host: SiteRuntim
160
160
  viewer?: SiteRuntimeViewer;
161
161
  }): Promise<void>;
162
162
  /**
163
- * Run an ordinary session route in this process as the Site's service
164
- * principal (`site:<siteId>`), with a 60-second delegated token carrying only
165
- * session permissions and the viewer as its display label. The Site and the
163
+ * Run an ordinary session route in this process as the verified member, or
164
+ * the Site service principal for an outsider. The latter uses a 60-second
165
+ * delegated token plus the separate trusted viewer audience. The Site and the
166
166
  * served version ride along as the request's Site origin, so new sessions and
167
167
  * turns record where they came from. The route authorizes as usual.
168
168
  */
@@ -174,6 +174,7 @@ export declare function siteRuntimeSessionRequest(deps: SiteRuntimeDeps, host: S
174
174
  path: string;
175
175
  body?: unknown;
176
176
  extraPermissions?: readonly Permission[];
177
+ reauthorize: () => Promise<void>;
177
178
  }): Promise<Request>;
178
179
  /** A forwarded response without cookies (site-runtime never sets any). */
179
180
  export declare function siteRuntimeForwardedResponse(response: Response): Response;
@@ -1,6 +1,6 @@
1
1
  import { type ErrorCode, type Permission, type SiteHostMapRecord, type SiteHostnameAvailability, type SitePublication, type UpsertSitePublicationRequest } from "@opengeni/contracts";
2
2
  import type { AccessGrant } from "@opengeni/contracts";
3
- import { type ApiRouteDeps } from "@opengeni/core";
3
+ import { type ApiRouteDeps, type cancelPublishedSiteSession } from "@opengeni/core";
4
4
  import { SitePublishingError, type PublishedSiteRecord, type SiteDataSite, type SiteHostnameEdgeState, type SitePublicationRecord, type SitePublishingScope, type StoredSiteLimitProfile } from "@opengeni/db";
5
5
  import { ApiHttpError } from "../http/api-error.js";
6
6
  import { type SiteContentScanner } from "./content-scanner.js";
@@ -40,13 +40,12 @@ export type SitePublishingEffects = {
40
40
  };
41
41
  /** Background effects running at once per process. */
42
42
  export declare const SITE_EFFECT_CONCURRENCY = 4;
43
+ type SiteCancellation = (input: Parameters<typeof cancelPublishedSiteSession>[1]) => Promise<unknown>;
43
44
  /**
44
- * The app's in-process dispatch, registered with the site-runtime routes, so
45
- * publication changes can cancel a Site's conversations through the ordinary
46
- * session routes. Without it (the operator script) cancellation is skipped.
45
+ * Internal host lifecycle adapter. This never grants the Site service bearer
46
+ * ordinary session access and never impersonates a conversation's viewer.
47
47
  */
48
- export declare function setSitePublishingDispatch(settings: ApiRouteDeps["settings"], dispatch: (request: Request) => Promise<Response>): void;
49
- export declare function sitePublishingDispatch(settings: ApiRouteDeps["settings"]): ((request: Request) => Promise<Response>) | null;
48
+ export declare function setSitePublishingCancellation(settings: ApiRouteDeps["settings"], cancel: SiteCancellation): void;
50
49
  type SitePublishingEffectsOptions = {
51
50
  fetch?: typeof fetch;
52
51
  backoffMs?: number;
@@ -164,11 +163,9 @@ export declare function siteServiceSessionRequest(deps: Pick<SitePublishingDeps,
164
163
  * Stop a Site's viewers after a takedown, unpublish or narrower access:
165
164
  * revoke every site-session now, and after commit cancel the Site's agent
166
165
  * conversations that may still be working (in-flight turns and queued
167
- * messages), as the Site itself through the ordinary control route. The
168
- * gateway refuses new work at once regardless: every site-runtime call
169
- * re-checks the publication. Without an in-process dispatch (the operator
170
- * script) the cancellation is skipped and logged; per-conversation caps
171
- * still bound what running turns can spend.
166
+ * messages), through the native cancel-only lifecycle adapter. The original
167
+ * actor survives queued background execution. No transcript access is granted.
168
+ * Every page is drained, including old conversations; there is no age cutoff.
172
169
  */
173
170
  export declare function stopSiteViewers(deps: SitePublishingDeps, scope: SitePublishingScope, siteId: string, options: {
174
171
  revokeSessions?: boolean;
@@ -0,0 +1,3 @@
1
+ export declare function bindSiteSessionRequestAuthority(request: Request, reauthorize: () => Promise<void>): void;
2
+ /** Periodic SSE reauthorization must check the Site cookie, not only its delegate. */
3
+ export declare function reauthorizeSiteSessionRequest(request: Request): Promise<void>;
@@ -23,5 +23,5 @@ export declare function workspaceGatewaySecretArguments(input: {
23
23
  settings: Settings;
24
24
  grant: AccessGrant;
25
25
  /** Connected vaults (1Password, host) for the resolved acting subject. */
26
- providersForSubject?: ((subjectId: string | null) => VaultProviderRegistry) | undefined;
26
+ providersForSubject?: ((subjectId: string | null, site?: SiteSecretContext | null) => VaultProviderRegistry) | undefined;
27
27
  }): ToolGatewayArgumentResolver;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengeni/api-router",
3
- "version": "3.1.3-canary.38890816741001",
3
+ "version": "3.3.0",
4
4
  "description": "Opengeni HTTP surface: the Hono adapter/router (createApp), routes, MCP HTTP transport, and HTTP access adapters over @opengeni/core. An engine-distribution surface — its runtime closure includes engine-internal packages.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -42,29 +42,32 @@
42
42
  "@aws-sdk/client-s3": "^3.1044.0",
43
43
  "@aws-sdk/s3-request-presigner": "^3.1044.0",
44
44
  "@hono/zod-validator": "^0.7.6",
45
+ "@keeper-security/secrets-manager-core": "17.6.0",
45
46
  "@llamaindex/liteparse": "2.14.2",
46
47
  "@modelcontextprotocol/sdk": "^1.29.0",
47
- "@opengeni/agent-proto": "3.1.3-canary.38890816741001",
48
- "@opengeni/artifact-tool": "3.1.3-canary.38890816741001",
49
- "@opengeni/capabilities": "3.1.3-canary.38890816741001",
50
- "@opengeni/codemode": "3.1.3-canary.38890816741001",
51
- "@opengeni/codex": "3.1.3-canary.38890816741001",
52
- "@opengeni/config": "3.1.3-canary.38890816741001",
53
- "@opengeni/contracts": "3.1.3-canary.38890816741001",
54
- "@opengeni/core": "3.1.3-canary.38890816741001",
55
- "@opengeni/db": "3.1.3-canary.38890816741001",
56
- "@opengeni/documents": "3.1.3-canary.38890816741001",
57
- "@opengeni/events": "3.1.3-canary.38890816741001",
58
- "@opengeni/github": "3.1.3-canary.38890816741001",
59
- "@opengeni/interaction": "3.1.3-canary.38890816741001",
60
- "@opengeni/network": "3.1.3-canary.38890816741001",
61
- "@opengeni/observability": "3.1.3-canary.38890816741001",
62
- "@opengeni/runtime": "3.1.3-canary.38890816741001",
63
- "@opengeni/storage": "3.1.3-canary.38890816741001",
64
- "@opengeni/tool-gateway": "3.1.3-canary.38890816741001",
65
- "@opengeni/vaults": "3.1.3-canary.38890816741001",
66
- "@opengeni/vaults-onepassword": "3.1.3-canary.38890816741001",
67
- "@opengeni/xai-subscription": "3.1.3-canary.38890816741001",
48
+ "@opengeni/agent-proto": "3.3.0",
49
+ "@opengeni/artifact-tool": "3.3.0",
50
+ "@opengeni/capabilities": "3.3.0",
51
+ "@opengeni/codemode": "3.3.0",
52
+ "@opengeni/codex": "3.3.0",
53
+ "@opengeni/config": "3.3.0",
54
+ "@opengeni/contracts": "3.3.0",
55
+ "@opengeni/core": "3.3.0",
56
+ "@opengeni/db": "3.3.0",
57
+ "@opengeni/documents": "3.3.0",
58
+ "@opengeni/events": "3.3.0",
59
+ "@opengeni/github": "3.3.0",
60
+ "@opengeni/interaction": "3.3.0",
61
+ "@opengeni/network": "3.3.0",
62
+ "@opengeni/observability": "3.3.0",
63
+ "@opengeni/runtime": "3.3.0",
64
+ "@opengeni/storage": "3.3.0",
65
+ "@opengeni/tool-gateway": "3.3.0",
66
+ "@opengeni/vaults": "3.3.0",
67
+ "@opengeni/vaults-keeper": "3.3.0",
68
+ "@opengeni/vaults-onepassword": "3.3.0",
69
+ "@opengeni/vaults-protonpass": "3.3.0",
70
+ "@opengeni/xai-subscription": "3.3.0",
68
71
  "@temporalio/client": "^1.17.0",
69
72
  "ajv": "^8.20.0",
70
73
  "better-auth": "^1.6.14",
@@ -87,6 +87,7 @@ export async function withAccessGrantSessionRlsContext<T>(
87
87
  {
88
88
  subjectId: actor.subjectId,
89
89
  initiatingHumanSubjectId: actor.initiatingHumanSubjectId,
90
+ agentRootSessionId: actor.callerRootSessionId,
90
91
  privateFileOwnerSubjectId:
91
92
  learning.defaultScope === "personal" ? learning.subjectId : null,
92
93
  },
package/src/app.ts CHANGED
@@ -2200,8 +2200,20 @@ export function createAppComposition(deps: AppDependencies): {
2200
2200
  const interactionSecrets = createVaultInteractionSecretsPort({
2201
2201
  db: routeDeps.db,
2202
2202
  encryptionKey: environmentsEncryptionKeyBytes(routeDeps.settings) ?? null,
2203
- providersFor: (workspaceId, subjectId) =>
2204
- vaultProviderRegistryFor(routeDeps, { workspaceId, subjectId }),
2203
+ providersFor: (workspaceId, subjectId, principal) =>
2204
+ vaultProviderRegistryFor(routeDeps, {
2205
+ workspaceId,
2206
+ subjectId,
2207
+ ...(principal
2208
+ ? {
2209
+ executionContext: {
2210
+ sessionId: principal.sessionId,
2211
+ turnId: principal.turnId,
2212
+ attemptId: principal.attemptId,
2213
+ },
2214
+ }
2215
+ : {}),
2216
+ }),
2205
2217
  });
2206
2218
  registerBrowserSessionRoutes(app, routeDeps, { interactionSecrets });
2207
2219
  registerComputerSessionRoutes(app, routeDeps, { interactionSecrets });
package/src/http/sse.ts CHANGED
@@ -7,6 +7,7 @@ import {
7
7
  getWorkspaceInteractionRevisionState,
8
8
  listSessionEventPage,
9
9
  listWorkspaceControlEvents,
10
+ getWorkspaceControlEvent,
10
11
  readSessionEventStorageGapEnd,
11
12
  type Database,
12
13
  } from "@opengeni/db";
@@ -677,7 +678,10 @@ export async function sseWorkspaceControlStream(
677
678
  }
678
679
  }
679
680
  }
680
- await writeFrame(formatWorkspaceControlEventSse(event));
681
+ // The bus is workspace-wide, not an authorization projection. Re-read the
682
+ // exact durable row under this stream's actor before delivering live data.
683
+ const visible = await getWorkspaceControlEvent(db, workspaceId, event.id);
684
+ if (visible) await writeFrame(formatWorkspaceControlEventSse(visible));
681
685
  lastSent = event.sequence;
682
686
  };
683
687
  delivery = createLatestWinsDelivery(send, fail);
package/src/index.ts CHANGED
@@ -52,6 +52,7 @@ import { createAutomaticAgentUpdater } from "./sandbox/agent-update";
52
52
  import { startSlackInteractionPump } from "./integrations/slack-interactions";
53
53
  import { startMemorySlackPublicationPump } from "./memory-slack-delivery";
54
54
  import { startTemporalScheduleCleanupPump } from "./temporal-schedule-cleanup";
55
+ import { closeNativeExternalVaultProviders } from "./integrations/external-vault-providers";
55
56
  import { startScheduledTaskLifecyclePump } from "./scheduled-task-lifecycle";
56
57
  import { createTemporalScheduleSynchronizer } from "./temporal-schedule-sync";
57
58
  import { startNativePushDispatchPump } from "./native-push-dispatch";
@@ -73,6 +74,7 @@ import {
73
74
  type StandaloneEditableArtifactApplication,
74
75
  } from "./editable-artifact-production";
75
76
  import { installApiFatalProcessBoundary } from "./fatal-process-boundary";
77
+ import { installApiShutdownHandlers } from "./shutdown";
76
78
 
77
79
  /**
78
80
  * A REJECT_DUPLICATE start collides on the deterministic workflowId when the
@@ -590,6 +592,8 @@ export async function startApi(
590
592
  server,
591
593
  close: async () => {
592
594
  server.stop(true);
595
+ // Start private CLI session cleanup even if another service's drain stalls.
596
+ const vaultProvidersClosed = closeNativeExternalVaultProviders().catch(() => undefined);
593
597
  metricsServer?.stop(true);
594
598
  stopSlackInteractionPump?.();
595
599
  await stopMemorySlackPublicationPump();
@@ -602,6 +606,7 @@ export async function startApi(
602
606
  // Write queued presence before the database pool closes.
603
607
  await routeDeps.userPresence?.close().catch(() => undefined);
604
608
  await Promise.allSettled([
609
+ vaultProvidersClosed,
605
610
  Promise.resolve(editableArtifactComposition?.close()),
606
611
  authCalloutResponder?.close(),
607
612
  bus.close(),
@@ -618,7 +623,14 @@ if (import.meta.main) {
618
623
  const settings = getSettings();
619
624
  const observability = createObservability(settings, { component: "api" });
620
625
  fatalBoundary.attachObservability(observability);
621
- await startApi({ settings, observability });
626
+ const api = await startApi({ settings, observability });
627
+ installApiShutdownHandlers({
628
+ close: api.close,
629
+ onFailure: () =>
630
+ observability.warn("Opengeni API shutdown did not finish cleanly", {
631
+ errorCode: "api_shutdown_failed",
632
+ }),
633
+ });
622
634
  fatalBoundary.markRunning();
623
635
  } catch (error) {
624
636
  await fatalBoundary.reportStartupFailure(error);
@@ -0,0 +1,136 @@
1
+ import type { Settings } from "@opengeni/config";
2
+ import type {
3
+ ExternalVaultProviderFactories,
4
+ ExternalVaultProviderFactory,
5
+ StoredVaultProviderCredentials,
6
+ } from "@opengeni/core";
7
+ import { pinnedFetch, type PinnedFetchOptions } from "@opengeni/network";
8
+ import { VaultProviderError } from "@opengeni/vaults";
9
+ import type { KeeperRuntimeOptions } from "@opengeni/vaults-keeper";
10
+ import type { ProtonPassProviderPool } from "@opengeni/vaults-protonpass";
11
+
12
+ /** Test seams only; production passes nothing. */
13
+ export type NativeExternalVaultOptions = {
14
+ keeper?: Pick<KeeperRuntimeOptions, "queryFunction" | "fetchFile" | "sdk">;
15
+ /** Applies when this call creates the pool for its CLI path. */
16
+ protonpass?: { stateRoot?: string; hostEnv?: Readonly<Record<string, string | undefined>> };
17
+ };
18
+
19
+ /**
20
+ * Native external vault adapters that run inside this server: Keeper Secrets
21
+ * Manager always, Proton Pass only when the operator installed `pass-cli`
22
+ * (`OPENGENI_PROTON_PASS_CLI_PATH`). Both packages load through `import()`
23
+ * on first use (packages/vaults-keeper/test/import-guard.test.ts). Keep this
24
+ * file identical to apps/worker/src/external-vault-providers.ts.
25
+ */
26
+ export function createNativeExternalVaultFactories(
27
+ settings: Settings,
28
+ options: NativeExternalVaultOptions = {},
29
+ ): ExternalVaultProviderFactories {
30
+ const factories: ExternalVaultProviderFactories = {
31
+ keeper: keeperFactory(settings, options.keeper ?? {}),
32
+ };
33
+ const cliPath = settings.protonPassCliPath;
34
+ if (cliPath) factories.protonpass = protonPassFactory(cliPath, options.protonpass ?? {});
35
+ return factories;
36
+ }
37
+
38
+ /** Logs out every pooled Proton Pass CLI session and wipes its state (graceful shutdown). */
39
+ export async function closeNativeExternalVaultProviders(): Promise<void> {
40
+ const pools = [...protonPools.values()];
41
+ protonPools.clear();
42
+ await Promise.all(pools.map(async (pool) => (await pool.catch(() => null))?.close()));
43
+ }
44
+
45
+ function mismatch(): never {
46
+ throw new VaultProviderError(
47
+ "provider_unavailable",
48
+ "The configured adapter does not match this connection.",
49
+ );
50
+ }
51
+
52
+ /**
53
+ * The fetch every Keeper request and file download uses: DNS-pinned and
54
+ * policy-checked (no private or special-use targets). The adapter adds
55
+ * HTTPS-only, no redirects, deadlines, size bounds and the caller's abort.
56
+ */
57
+ export function nativeKeeperFetch(
58
+ settings: Settings,
59
+ pinned: Pick<PinnedFetchOptions, "dnsLookup" | "fetchImpl" | "agentFactory"> = {},
60
+ ) {
61
+ return (input: string | URL, init: RequestInit) =>
62
+ pinnedFetch(input, init, settings, {
63
+ ...pinned,
64
+ label: "Keeper Secrets Manager",
65
+ requireHttpsOutsideLocalTest: true,
66
+ });
67
+ }
68
+
69
+ function keeperFactory(
70
+ settings: Settings,
71
+ seams: NonNullable<NativeExternalVaultOptions["keeper"]>,
72
+ ): ExternalVaultProviderFactory {
73
+ const fetch = nativeKeeperFetch(settings);
74
+ return async (input) => {
75
+ const credentials = input.credentials;
76
+ if (credentials.provider !== "keeper") mismatch();
77
+ const persist = input.persistCredentials;
78
+ const { openKeeperConnection } = await import("@opengeni/vaults-keeper");
79
+ const opened = await openKeeperConnection({
80
+ credentials:
81
+ "oneTimeToken" in credentials
82
+ ? {
83
+ oneTimeToken: credentials.oneTimeToken,
84
+ ...(credentials.hostname ? { hostname: credentials.hostname } : {}),
85
+ }
86
+ : { configuration: credentials.configuration },
87
+ // Without it openKeeperConnection refuses: a redeemed token or rotated
88
+ // key must never live only in memory.
89
+ ...(persist
90
+ ? {
91
+ persistConfiguration: (configuration: Record<string, string>) =>
92
+ persist({ provider: "keeper", configuration }),
93
+ }
94
+ : {}),
95
+ ...(input.operations ? { operations: input.operations } : {}),
96
+ // Cancels a pending bootstrap; a stored configuration opens without a request.
97
+ ...(input.signal ? { signal: input.signal } : {}),
98
+ fetch,
99
+ ...seams,
100
+ });
101
+ const stored: StoredVaultProviderCredentials = {
102
+ provider: "keeper",
103
+ configuration: opened.configuration,
104
+ };
105
+ return { provider: opened.provider, credentials: stored };
106
+ };
107
+ }
108
+
109
+ /** One pool per CLI path per process, so repeated factory creation never leaks sessions. */
110
+ const protonPools = new Map<string, Promise<ProtonPassProviderPool>>();
111
+
112
+ function protonPassFactory(
113
+ binaryPath: string,
114
+ seams: NonNullable<NativeExternalVaultOptions["protonpass"]>,
115
+ ): ExternalVaultProviderFactory {
116
+ return async (input) => {
117
+ const credentials = input.credentials;
118
+ if (credentials.provider !== "protonpass") mismatch();
119
+ let pool = protonPools.get(binaryPath);
120
+ if (!pool) {
121
+ pool = import("@opengeni/vaults-protonpass").then(({ createProtonPassProviderPool }) =>
122
+ createProtonPassProviderPool({ ...seams, binaryPath }),
123
+ );
124
+ protonPools.set(binaryPath, pool);
125
+ pool.catch(() => protonPools.delete(binaryPath));
126
+ }
127
+ // Sessions are scoped to the connection, so two connections never share
128
+ // CLI state even with the same token. Without a connection id (a setup
129
+ // probe) the provider gets an isolated session of its own.
130
+ const provider = (await pool).provider(credentials.token, {
131
+ ...(input.connectionId === undefined ? {} : { connectionId: input.connectionId }),
132
+ ...(input.operations ? { operations: input.operations } : {}),
133
+ });
134
+ return { provider, credentials: { provider: "protonpass", token: credentials.token } };
135
+ };
136
+ }
@@ -59,7 +59,11 @@ export function createVaultInteractionSecretsPort(deps: {
59
59
  /** Defaults to a service over the same database and key. */
60
60
  vaultService?: VaultService;
61
61
  /** Connected vaults (1Password, host) for a workspace's acting subject. */
62
- providersFor?: (workspaceId: string, subjectId: string | null) => VaultProviderRegistry;
62
+ providersFor?: (
63
+ workspaceId: string,
64
+ subjectId: string | null,
65
+ principal?: AttemptSecretPrincipal,
66
+ ) => VaultProviderRegistry;
63
67
  }): InteractionSecretsPort {
64
68
  const encryptionKey = deps.encryptionKey;
65
69
  let service = deps.vaultService ?? null;
@@ -203,7 +207,7 @@ export function createVaultInteractionSecretsPort(deps: {
203
207
  ...(providersFor
204
208
  ? {
205
209
  providersForSubject: (subjectId: string | null) =>
206
- providersFor(principal.workspaceId, subjectId),
210
+ providersFor(principal.workspaceId, subjectId, principal),
207
211
  }
208
212
  : {}),
209
213
  });