@opengeni/core 3.0.1 → 3.1.2
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/README.md +4 -4
- package/dist/access/index.d.ts +131 -7
- package/dist/access/service-initiator.d.ts +3 -0
- package/dist/application/archived-session-imports.d.ts +6 -0
- package/dist/application/composer-submit.d.ts +1 -1
- package/dist/application/connect-operation.d.ts +6 -0
- package/dist/application/external-workspace-members.d.ts +13 -1
- package/dist/application/modal-native-live-declaration-join.d.ts +37 -0
- package/dist/application/modal-native-original-configuration.d.ts +79 -0
- package/dist/application/modal-native-worker-host-transport.d.ts +297 -0
- package/dist/application/new-session-drafts.d.ts +29 -2
- package/dist/application/organization-privacy.d.ts +57 -0
- package/dist/application/prepared-mcp-connection.d.ts +31 -0
- package/dist/application/sandbox-recovery.d.ts +58 -0
- package/dist/application/session-commands.d.ts +3 -0
- package/dist/application/session-tenancy.d.ts +13 -4
- package/dist/billing/agent-run-admission.d.ts +21 -0
- package/dist/billing/limits.d.ts +27 -1
- package/dist/billing/model-call-admission.d.ts +34 -0
- package/dist/billing/model-usage-settlement.d.ts +112 -0
- package/dist/canonical-human-identities.js +1 -1
- package/dist/{chunk-VCZFFEPB.js → chunk-7T2SZ7BI.js} +16 -2
- package/dist/chunk-7T2SZ7BI.js.map +1 -0
- package/dist/{chunk-ZYCNLYGH.js → chunk-OMJFIIWK.js} +178 -12
- package/dist/chunk-OMJFIIWK.js.map +1 -0
- package/dist/{chunk-H7X52RI2.js → chunk-QI2M2B2K.js} +83 -8
- package/dist/chunk-QI2M2B2K.js.map +1 -0
- package/dist/{chunk-OF65T3PM.js → chunk-TTWTXQB2.js} +94 -6
- package/dist/chunk-TTWTXQB2.js.map +1 -0
- package/dist/codex-model-availability.d.ts +55 -0
- package/dist/default-session-model.d.ts +192 -0
- package/dist/dependencies.d.ts +41 -7
- package/dist/developer-setup-credential-ceiling.d.ts +10 -0
- package/dist/domain/agent-config-resolution.d.ts +49 -0
- package/dist/domain/automations.d.ts +8 -1
- package/dist/domain/capabilities.d.ts +112 -8
- package/dist/domain/connection-account-selection-error.d.ts +8 -0
- package/dist/domain/connector-tool-permissions.d.ts +62 -0
- package/dist/domain/direct-model-provider.d.ts +3 -0
- package/dist/domain/editable-artifacts/agent-application.d.ts +10 -1
- package/dist/domain/editable-artifacts/durable-export.d.ts +22 -1
- package/dist/domain/editable-artifacts/ports.d.ts +10 -0
- package/dist/domain/editable-artifacts/service.d.ts +3 -1
- package/dist/domain/editable-artifacts/types.d.ts +1 -1
- package/dist/domain/encryption-key.d.ts +12 -0
- package/dist/domain/insights-timing.d.ts +16 -0
- package/dist/domain/insights-usage.d.ts +27 -0
- package/dist/domain/insights.d.ts +21 -3
- package/dist/domain/knowledge-files.d.ts +1 -1
- package/dist/domain/knowledge-preparation.d.ts +10 -0
- package/dist/domain/knowledge-preparation.test.d.ts +1 -0
- package/dist/domain/knowledge-search.d.ts +15 -76
- package/dist/domain/mcp-account-bindings.d.ts +17 -0
- package/dist/domain/mcp-account-routes.d.ts +21 -0
- package/dist/domain/native-mcp-connection-admission.d.ts +3 -0
- package/dist/domain/organization-integration-catalog.d.ts +15 -0
- package/dist/domain/personal-connection-delegations.d.ts +73 -12
- package/dist/domain/pr-review.d.ts +121 -6
- package/dist/domain/realtime-voice-billing.d.ts +103 -0
- package/dist/domain/resources.d.ts +17 -1
- package/dist/domain/scheduled-task-access.d.ts +265 -0
- package/dist/domain/scheduled-task-account-audit.d.ts +27 -0
- package/dist/domain/scheduled-task-audience.d.ts +70 -0
- package/dist/domain/scheduled-tasks.d.ts +61 -18
- package/dist/domain/session-connection-accounts.d.ts +53 -0
- package/dist/domain/session-file-access.d.ts +4 -0
- package/dist/domain/session-native-connection-replacement.d.ts +14 -0
- package/dist/domain/session-start-timing.d.ts +7 -0
- package/dist/domain/session-tool-policy.d.ts +102 -4
- package/dist/domain/sessions.d.ts +179 -15
- package/dist/domain/site-audience.d.ts +52 -0
- package/dist/domain/skills.d.ts +29 -1
- package/dist/domain/slack-bot.d.ts +46 -1
- package/dist/domain/slack-publication-secret-safety.d.ts +1 -1
- package/dist/domain/vaults/index.d.ts +7 -0
- package/dist/domain/vaults/mapping.d.ts +25 -0
- package/dist/domain/vaults/provider-registry.d.ts +72 -0
- package/dist/domain/vaults/provider-sync.d.ts +176 -0
- package/dist/domain/vaults/provider-writes.d.ts +57 -0
- package/dist/domain/vaults/secret-resolver.d.ts +34 -0
- package/dist/domain/vaults/types.d.ts +24 -0
- package/dist/domain/vaults/vault-service.d.ts +35 -0
- package/dist/domain/voice-input-billing.d.ts +77 -0
- package/dist/domain/web-search-billing.d.ts +51 -0
- package/dist/domain/workspace-credits.d.ts +7 -0
- package/dist/editable-artifact-live/application.d.ts +2 -1
- package/dist/editable-artifact-live/server.d.ts +5 -1
- package/dist/editable-artifact-live/ticket.d.ts +2 -1
- package/dist/editable-artifact-live/types.d.ts +9 -0
- package/dist/editable-artifact-live.js +2 -2
- package/dist/editable-artifacts.js +10 -2
- package/dist/goal-admission.d.ts +627 -0
- package/dist/host-session-usage-limit.d.ts +15 -0
- package/dist/index.d.ts +43 -8
- package/dist/index.js +22682 -11743
- package/dist/index.js.map +1 -1
- package/dist/managed-session.d.ts +110 -7
- package/dist/model-call.d.ts +58 -0
- package/dist/model-catalog.d.ts +29 -3
- package/dist/model-provider-credentials.d.ts +43 -0
- package/dist/product-usage-metrics.d.ts +30 -0
- package/dist/rigs/index.d.ts +1 -13
- package/dist/sandbox/fleet.d.ts +58 -4
- package/dist/sandbox/routing.d.ts +42 -2
- package/dist/sandbox/runtime-settings.d.ts +27 -17
- package/dist/session-authorization.d.ts +10 -0
- package/dist/session-usage-limit-metrics.d.ts +60 -0
- package/dist/session-usage-limits.d.ts +89 -0
- package/dist/site-agent-clamp.d.ts +71 -0
- package/dist/site-session-origin.d.ts +39 -2
- package/dist/transcription.d.ts +120 -3
- package/dist/turn-surface.d.ts +20 -0
- package/dist/user-presence.d.ts +32 -0
- package/dist/workflow-wake-contract.d.ts +13 -1
- package/package.json +15 -22
- package/src/access/index.ts +961 -38
- package/src/access/service-initiator.ts +50 -0
- package/src/application/api-integration-servers.ts +9 -0
- package/src/application/archived-session-imports.ts +161 -0
- package/src/application/composer-submit.ts +3 -2
- package/src/application/connect-authority.ts +3 -3
- package/src/application/connect-operation.ts +37 -3
- package/src/application/external-continuation.ts +20 -3
- package/src/application/external-link-work-admission.ts +3 -0
- package/src/application/external-workspace-members.ts +51 -3
- package/src/application/modal-native-live-declaration-join.ts +196 -0
- package/src/application/modal-native-original-configuration.ts +261 -0
- package/src/application/modal-native-worker-host-transport.ts +263 -0
- package/src/application/new-session-drafts.ts +172 -55
- package/src/application/organization-privacy.ts +161 -0
- package/src/application/prepared-mcp-connection.ts +133 -0
- package/src/application/sandbox-recovery.ts +78 -0
- package/src/application/session-commands.ts +73 -1
- package/src/application/session-mcp-credential-rotation.ts +61 -18
- package/src/application/session-tenancy.ts +262 -44
- package/src/application/user-resource-grants.ts +1 -29
- package/src/billing/agent-run-admission.ts +117 -0
- package/src/billing/limits.ts +138 -14
- package/src/billing/model-call-admission.ts +118 -0
- package/src/billing/model-usage-settlement.ts +472 -0
- package/src/codex-model-availability.ts +264 -0
- package/src/default-session-model.ts +692 -0
- package/src/dependencies.ts +48 -6
- package/src/developer-setup-credential-ceiling.ts +60 -0
- package/src/domain/agent-config-resolution.ts +148 -0
- package/src/domain/automations.ts +76 -2
- package/src/domain/capabilities.ts +830 -181
- package/src/domain/connection-account-selection-error.ts +19 -0
- package/src/domain/connector-tool-permissions.ts +572 -0
- package/src/domain/direct-model-provider.ts +62 -0
- package/src/domain/editable-artifacts/agent-application.ts +44 -6
- package/src/domain/editable-artifacts/durable-export.ts +100 -5
- package/src/domain/editable-artifacts/in-memory.ts +6 -0
- package/src/domain/editable-artifacts/ports.ts +11 -0
- package/src/domain/editable-artifacts/service.ts +20 -0
- package/src/domain/editable-artifacts/types.ts +1 -1
- package/src/domain/encryption-key.ts +26 -0
- package/src/domain/file-owner.ts +16 -1
- package/src/domain/github-action-policies.ts +1 -1
- package/src/domain/github-skill-source.ts +3 -1
- package/src/domain/insights-timing.ts +95 -0
- package/src/domain/insights-usage.ts +67 -0
- package/src/domain/insights.ts +167 -43
- package/src/domain/knowledge-files.ts +10 -3
- package/src/domain/knowledge-messages.ts +1 -0
- package/src/domain/knowledge-preparation.test.ts +276 -0
- package/src/domain/knowledge-preparation.ts +147 -0
- package/src/domain/knowledge-search.test.ts +617 -5
- package/src/domain/knowledge-search.ts +240 -23
- package/src/domain/mcp-account-bindings.ts +169 -0
- package/src/domain/mcp-account-routes.ts +152 -0
- package/src/domain/native-mcp-connection-admission.ts +14 -0
- package/src/domain/organization-integration-catalog.ts +88 -0
- package/src/domain/personal-connection-delegations.ts +399 -257
- package/src/domain/pr-review.ts +24 -8
- package/src/domain/realtime-voice-billing.ts +425 -0
- package/src/domain/resources.ts +50 -4
- package/src/domain/scheduled-task-access.ts +1089 -0
- package/src/domain/scheduled-task-account-audit.ts +90 -0
- package/src/domain/scheduled-task-audience.ts +249 -0
- package/src/domain/scheduled-tasks.ts +1313 -374
- package/src/domain/session-connection-accounts.ts +239 -0
- package/src/domain/session-file-access.ts +47 -0
- package/src/domain/session-native-connection-replacement.ts +64 -0
- package/src/domain/session-start-timing.ts +60 -0
- package/src/domain/session-tool-policy.ts +489 -14
- package/src/domain/sessions.ts +2254 -717
- package/src/domain/site-audience.ts +130 -0
- package/src/domain/skills.ts +9 -0
- package/src/domain/slack-bot.ts +141 -4
- package/src/domain/slack-publication-secret-safety.ts +1 -1
- package/src/domain/vaults/index.ts +19 -0
- package/src/domain/vaults/mapping.ts +190 -0
- package/src/domain/vaults/provider-registry.ts +201 -0
- package/src/domain/vaults/provider-sync.ts +531 -0
- package/src/domain/vaults/provider-writes.ts +288 -0
- package/src/domain/vaults/secret-resolver.ts +464 -0
- package/src/domain/vaults/types.ts +26 -0
- package/src/domain/vaults/vault-service.ts +1316 -0
- package/src/domain/voice-input-billing.ts +330 -0
- package/src/domain/web-search-billing.ts +169 -0
- package/src/domain/workspace-credits.ts +13 -0
- package/src/editable-artifact-live/application.ts +8 -1
- package/src/editable-artifact-live/server.ts +154 -6
- package/src/editable-artifact-live/ticket.ts +86 -1
- package/src/editable-artifact-live/types.ts +10 -0
- package/src/goal-admission.ts +220 -0
- package/src/host-session-usage-limit.ts +25 -0
- package/src/index.ts +75 -11
- package/src/managed-session.ts +137 -6
- package/src/model-call.ts +79 -0
- package/src/model-catalog.ts +345 -91
- package/src/model-provider-credentials.ts +394 -0
- package/src/product-usage-metrics.ts +101 -0
- package/src/rigs/index.ts +23 -111
- package/src/sandbox/fleet.ts +253 -28
- package/src/sandbox/routing.ts +204 -43
- package/src/sandbox/runtime-settings.ts +159 -85
- package/src/session-authorization.ts +172 -23
- package/src/session-usage-limit-metrics.ts +95 -0
- package/src/session-usage-limits.ts +415 -0
- package/src/site-agent-clamp.ts +226 -0
- package/src/site-session-origin.ts +84 -5
- package/src/transcription.ts +123 -4
- package/src/turn-surface.ts +47 -0
- package/src/user-presence.ts +134 -0
- package/src/workflow-wake-contract.ts +14 -1
- package/dist/application/host-mcp-owner.d.ts +0 -6
- package/dist/application/host-mcp-resolvers.d.ts +0 -13
- package/dist/chunk-5WZYFL45.js +0 -266
- package/dist/chunk-5WZYFL45.js.map +0 -1
- package/dist/chunk-H7X52RI2.js.map +0 -1
- package/dist/chunk-OF65T3PM.js.map +0 -1
- package/dist/chunk-VCZFFEPB.js.map +0 -1
- package/dist/chunk-ZYCNLYGH.js.map +0 -1
- package/dist/domain/connection-authority.d.ts +0 -97
- package/dist/domain/connection-authority.js +0 -169
- package/dist/domain/connection-authority.js.map +0 -1
- package/dist/domain/environments.d.ts +0 -33
- package/dist/domain/host-mcp-authority-source-admission.d.ts +0 -10
- package/dist/domain/host-mcp-task-admission.d.ts +0 -18
- package/dist/domain/packs.d.ts +0 -59
- package/dist/domain/product-integration-pack.d.ts +0 -44
- package/dist/domain/product-integration-skill.gen.d.ts +0 -5
- package/dist/remote-mcp-credentials.d.ts +0 -23
- package/dist/remote-mcp-credentials.js +0 -11
- package/dist/remote-mcp-credentials.js.map +0 -1
- package/src/application/host-mcp-owner.ts +0 -41
- package/src/application/host-mcp-resolvers.ts +0 -84
- package/src/domain/connection-authority.ts +0 -329
- package/src/domain/environments.ts +0 -120
- package/src/domain/host-mcp-authority-source-admission.ts +0 -25
- package/src/domain/host-mcp-task-admission.ts +0 -121
- package/src/domain/packs.ts +0 -699
- package/src/domain/product-integration-pack.ts +0 -42
- package/src/domain/product-integration-skill.gen.ts +0 -52
- package/src/remote-mcp-credentials.ts +0 -361
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import type { AccessGrant, WorkspaceArtifactVisibility } from "@opengeni/contracts";
|
|
2
|
+
import {
|
|
3
|
+
getVisibleSessionTenancyVisibility,
|
|
4
|
+
scheduledTaskViewerSubject,
|
|
5
|
+
type Database,
|
|
6
|
+
type WorkspaceArtifactViewer,
|
|
7
|
+
} from "@opengeni/db";
|
|
8
|
+
import { HTTPException } from "hono/http-exception";
|
|
9
|
+
import type { PrivacySettings } from "../application/organization-privacy";
|
|
10
|
+
import { mayStartOnlyMe } from "./scheduled-task-audience";
|
|
11
|
+
import { creationInitiatorForGrant } from "./sessions";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A Site has an audience, like a chat and a schedule: the whole workspace, or
|
|
15
|
+
* only its owner ("Only me"). Everyone with workspace access sees workspace
|
|
16
|
+
* Sites. An Only me Site is visible to its owner alone; admins cannot see it,
|
|
17
|
+
* exactly like an Only me chat. Only the owner changes the audience.
|
|
18
|
+
*
|
|
19
|
+
* Every surface that reads or changes Sites (HTTP, agent tools, the artifact
|
|
20
|
+
* library, Site tools and Site conversations) resolves its viewer here and
|
|
21
|
+
* passes it to the database layer, which filters by it.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
export const SITE_PRIVATE_OWNER_MESSAGE = "Only a person can have an Only me Site.";
|
|
25
|
+
export const SITE_PRIVATE_DISABLED_MESSAGE = "Only me is turned off for this organization.";
|
|
26
|
+
export const SITE_AUDIENCE_HUMAN_MESSAGE =
|
|
27
|
+
"Only the Site's owner can change who can see it, from Opengeni.";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The person a request reads Sites as: the human caller, or the human an exact
|
|
31
|
+
* agent attempt acts for. Keys, services and runs without a person see
|
|
32
|
+
* workspace Sites only.
|
|
33
|
+
*/
|
|
34
|
+
export async function siteViewerForGrant(
|
|
35
|
+
db: Database,
|
|
36
|
+
grant: AccessGrant,
|
|
37
|
+
): Promise<WorkspaceArtifactViewer> {
|
|
38
|
+
const creator = creationInitiatorForGrant(grant);
|
|
39
|
+
const subjectId = await scheduledTaskViewerSubject(db, {
|
|
40
|
+
workspaceId: grant.workspaceId,
|
|
41
|
+
...(creator.initiator ? { createdBy: creator.initiator } : {}),
|
|
42
|
+
...(creator.context ? { createdByContext: creator.context } : {}),
|
|
43
|
+
createdByActor: creator.actor ?? null,
|
|
44
|
+
});
|
|
45
|
+
return { subjectId };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function stored(visibility: WorkspaceArtifactVisibility): "user_private" | "workspace_shared" {
|
|
49
|
+
return visibility === "private" ? "user_private" : "workspace_shared";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function requireOnlyMeAllowed(input: {
|
|
53
|
+
db: Database;
|
|
54
|
+
settings: PrivacySettings;
|
|
55
|
+
workspaceId: string;
|
|
56
|
+
ownerSubjectId: string | null;
|
|
57
|
+
}): Promise<string> {
|
|
58
|
+
if (!input.ownerSubjectId) throw new HTTPException(422, { message: SITE_PRIVATE_OWNER_MESSAGE });
|
|
59
|
+
if (!(await mayStartOnlyMe(input.db, input.settings, input.workspaceId, input.ownerSubjectId)))
|
|
60
|
+
throw new HTTPException(403, { message: SITE_PRIVATE_DISABLED_MESSAGE });
|
|
61
|
+
return input.ownerSubjectId;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The audience and owner to store for a new Site. An explicit choice wins;
|
|
66
|
+
* Only me needs a person who may start Only me work here. Without one, a Site
|
|
67
|
+
* published from a chat starts with that chat's audience, so a Site made in an
|
|
68
|
+
* Only me chat stays Only me until its owner shares it. Anything else starts
|
|
69
|
+
* shared with the workspace, as every Site did before audiences existed.
|
|
70
|
+
*/
|
|
71
|
+
export async function resolveNewSiteAudience(input: {
|
|
72
|
+
db: Database;
|
|
73
|
+
settings: PrivacySettings;
|
|
74
|
+
grant: AccessGrant;
|
|
75
|
+
viewer: WorkspaceArtifactViewer;
|
|
76
|
+
requested: WorkspaceArtifactVisibility | undefined;
|
|
77
|
+
sourceSessionId: string | null;
|
|
78
|
+
}): Promise<{
|
|
79
|
+
visibility: "user_private" | "workspace_shared";
|
|
80
|
+
ownerSubjectId: string | null;
|
|
81
|
+
}> {
|
|
82
|
+
const ownerSubjectId = input.viewer.subjectId;
|
|
83
|
+
if (input.requested === "private") {
|
|
84
|
+
await requireOnlyMeAllowed({
|
|
85
|
+
db: input.db,
|
|
86
|
+
settings: input.settings,
|
|
87
|
+
workspaceId: input.grant.workspaceId,
|
|
88
|
+
ownerSubjectId,
|
|
89
|
+
});
|
|
90
|
+
return { visibility: "user_private", ownerSubjectId };
|
|
91
|
+
}
|
|
92
|
+
if (input.requested === "workspace" || input.sourceSessionId === null || !ownerSubjectId)
|
|
93
|
+
return { visibility: "workspace_shared", ownerSubjectId };
|
|
94
|
+
// The chat already is Only me, so this narrows nothing and needs no fresh
|
|
95
|
+
// permission, like an existing Only me item when Only me is later turned off.
|
|
96
|
+
const chat = await getVisibleSessionTenancyVisibility(
|
|
97
|
+
input.db,
|
|
98
|
+
input.grant.workspaceId,
|
|
99
|
+
input.sourceSessionId,
|
|
100
|
+
);
|
|
101
|
+
return {
|
|
102
|
+
visibility: chat === "user_private" ? "user_private" : "workspace_shared",
|
|
103
|
+
ownerSubjectId,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* The stored audience for an owner's change. Sharing with the workspace is
|
|
109
|
+
* always allowed; limiting to Only me needs Only me to be allowed for them.
|
|
110
|
+
* Whether the caller owns the Site is checked atomically by the database.
|
|
111
|
+
*/
|
|
112
|
+
export async function resolveSiteAudienceChange(input: {
|
|
113
|
+
db: Database;
|
|
114
|
+
settings: PrivacySettings;
|
|
115
|
+
grant: AccessGrant;
|
|
116
|
+
viewer: WorkspaceArtifactViewer;
|
|
117
|
+
requested: WorkspaceArtifactVisibility;
|
|
118
|
+
}): Promise<"user_private" | "workspace_shared"> {
|
|
119
|
+
// Like a chat's audience, this is a person's own decision, not an agent's.
|
|
120
|
+
if (input.grant.principalKind === "agent_attempt")
|
|
121
|
+
throw new HTTPException(403, { message: SITE_AUDIENCE_HUMAN_MESSAGE });
|
|
122
|
+
if (input.requested === "private")
|
|
123
|
+
await requireOnlyMeAllowed({
|
|
124
|
+
db: input.db,
|
|
125
|
+
settings: input.settings,
|
|
126
|
+
workspaceId: input.grant.workspaceId,
|
|
127
|
+
ownerSubjectId: input.viewer.subjectId,
|
|
128
|
+
});
|
|
129
|
+
return stored(input.requested);
|
|
130
|
+
}
|
package/src/domain/skills.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type {
|
|
|
3
3
|
SkillInstallInput,
|
|
4
4
|
SkillRevisionInput,
|
|
5
5
|
SkillSaveInput,
|
|
6
|
+
SkillRemoveInput,
|
|
6
7
|
} from "@opengeni/contracts";
|
|
7
8
|
import { validateSkillTextFiles } from "@opengeni/contracts";
|
|
8
9
|
import { buildPortableSkillArtifact } from "@opengeni/runtime/skill-library";
|
|
@@ -30,6 +31,14 @@ export function skillBundleHash(files: readonly SkillFile[]): string {
|
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
export const listSkills = listSkillRecords;
|
|
34
|
+
export async function removeSkill(db: Database, input: SkillRemoveInput) {
|
|
35
|
+
const { accountId, workspaceId, actor, ...request } = input;
|
|
36
|
+
return applySkillLifecycle(
|
|
37
|
+
db,
|
|
38
|
+
{ accountId, workspaceId, actor },
|
|
39
|
+
{ ...request, operation: "remove" },
|
|
40
|
+
);
|
|
41
|
+
}
|
|
33
42
|
export async function readSkill(
|
|
34
43
|
db: Database,
|
|
35
44
|
context: SkillReadContext,
|
package/src/domain/slack-bot.ts
CHANGED
|
@@ -3,19 +3,26 @@ import {
|
|
|
3
3
|
OPENGENI_SLACK_BOT_CREDENTIAL_ROLE,
|
|
4
4
|
OPENGENI_SLACK_BOT_SESSION_METADATA_KEY,
|
|
5
5
|
OpenGeniSlackBotConnectionMetadata,
|
|
6
|
+
SCHEDULED_SLACK_BOT_POSTING_TOOLS,
|
|
6
7
|
areOpenGeniSlackBotScopesAccepted,
|
|
7
8
|
type AccessGrant,
|
|
8
9
|
type ConnectionMetadata,
|
|
10
|
+
type FirstPartyMcpToolName,
|
|
9
11
|
type OpenGeniSlackBotConnectionMetadata as OpenGeniSlackBotMetadata,
|
|
12
|
+
type ScheduledTask,
|
|
13
|
+
type ScheduledTaskAgentConfig,
|
|
10
14
|
type Session,
|
|
11
15
|
} from "@opengeni/contracts";
|
|
12
16
|
import {
|
|
13
17
|
getConnectionMetadata,
|
|
18
|
+
availableSlackBotConnectionMetadata,
|
|
19
|
+
requireWorkspace,
|
|
14
20
|
type ConnectionMetadataWithVerification,
|
|
15
21
|
type Database,
|
|
16
22
|
} from "@opengeni/db";
|
|
17
23
|
import { HTTPException } from "hono/http-exception";
|
|
18
|
-
import { requirePermission } from "../access";
|
|
24
|
+
import { requirePermission, type AccessGrantAuthorization } from "../access";
|
|
25
|
+
import { grantHasAgentAttemptAuthority } from "../session-authorization";
|
|
19
26
|
|
|
20
27
|
export function openGeniSlackBotMetadata(
|
|
21
28
|
metadata: Record<string, unknown>,
|
|
@@ -69,15 +76,22 @@ export async function requireOpenGeniSlackBotConnection(
|
|
|
69
76
|
workspaceId: string,
|
|
70
77
|
connectionId: string,
|
|
71
78
|
): Promise<ConnectionMetadata> {
|
|
72
|
-
const
|
|
79
|
+
const local = await getConnectionMetadata(db, workspaceId, connectionId, null);
|
|
80
|
+
const workspace = await requireWorkspace(db, workspaceId);
|
|
81
|
+
const connection =
|
|
82
|
+
local ??
|
|
83
|
+
(
|
|
84
|
+
await availableSlackBotConnectionMetadata(db, { accountId: workspace.accountId, workspaceId })
|
|
85
|
+
).find((candidate) => candidate.id === connectionId) ??
|
|
86
|
+
null;
|
|
73
87
|
if (!connection || !isOpenGeniSlackBotConnection(connection)) {
|
|
74
88
|
throw new HTTPException(422, {
|
|
75
|
-
message: "slackBotConnectionId must reference an
|
|
89
|
+
message: "slackBotConnectionId must reference an Opengeni Slack bot connection",
|
|
76
90
|
});
|
|
77
91
|
}
|
|
78
92
|
if (connection.status !== "active") {
|
|
79
93
|
throw new HTTPException(422, {
|
|
80
|
-
message: `
|
|
94
|
+
message: `Opengeni Slack bot connection is not active (${connection.status})`,
|
|
81
95
|
});
|
|
82
96
|
}
|
|
83
97
|
return connection;
|
|
@@ -126,3 +140,126 @@ export function isTrustedScheduledSlackBotSession(
|
|
|
126
140
|
function zUuid(value: string): boolean {
|
|
127
141
|
return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value);
|
|
128
142
|
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Proves, with the bot's own token, that it may post in the chosen channel:
|
|
146
|
+
* the bot is a member and the channel is active and not shared with another
|
|
147
|
+
* organization. Supplied only by the HTTP routes a person uses to edit tasks.
|
|
148
|
+
*/
|
|
149
|
+
export type ScheduledTaskSlackChannelVerifier = (input: {
|
|
150
|
+
connectionId: string;
|
|
151
|
+
channelId: string;
|
|
152
|
+
}) => Promise<void>;
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Whether this request is a signed-in person acting for themselves: never an
|
|
156
|
+
* agent attempt, a service, an API key, or a request carrying service
|
|
157
|
+
* provenance. The same rule gates Agent learning settings.
|
|
158
|
+
*/
|
|
159
|
+
export function isAuthenticatedPersonAuthorization(
|
|
160
|
+
authorization: AccessGrantAuthorization | undefined,
|
|
161
|
+
): boolean {
|
|
162
|
+
if (!authorization) return false;
|
|
163
|
+
const { grant } = authorization;
|
|
164
|
+
return (
|
|
165
|
+
grant.principalKind === "human_session" &&
|
|
166
|
+
authorization.contextIntegrity &&
|
|
167
|
+
authorization.authenticatedSubjectId === grant.subjectId &&
|
|
168
|
+
!grant.serviceInitiator &&
|
|
169
|
+
!grant.serviceInitiatorContext &&
|
|
170
|
+
grant.metadata?.attemptId === undefined &&
|
|
171
|
+
grant.metadata?.turnId === undefined &&
|
|
172
|
+
grant.metadata?.executionGeneration === undefined &&
|
|
173
|
+
!grantHasAgentAttemptAuthority(grant)
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* A scheduled task may post as the Opengeni workspace bot only to the one
|
|
179
|
+
* Slack channel a person chose on the task. Choosing or changing that channel
|
|
180
|
+
* therefore needs a signed-in person with `connections:write`, and the bot's
|
|
181
|
+
* membership is verified at that moment. Keeping or clearing the existing
|
|
182
|
+
* channel needs neither, so an agent editing other task fields cannot redirect
|
|
183
|
+
* posts but also does not have to re-prove the person's choice. Clearing is the
|
|
184
|
+
* safe direction and is never refused, even for a task that continues one chat.
|
|
185
|
+
*/
|
|
186
|
+
export async function validateScheduledTaskSlackChannel(input: {
|
|
187
|
+
grant: AccessGrant;
|
|
188
|
+
authorization?: AccessGrantAuthorization | undefined;
|
|
189
|
+
previous: Pick<ScheduledTaskAgentConfig, "slackBotConnectionId" | "slackBotChannelId"> | null;
|
|
190
|
+
next: Pick<ScheduledTaskAgentConfig, "slackBotConnectionId" | "slackBotChannelId">;
|
|
191
|
+
runMode: ScheduledTask["runMode"];
|
|
192
|
+
/**
|
|
193
|
+
* Null unless the task already created the reusable chat its runs continue.
|
|
194
|
+
* Otherwise reports whether that chat was created with the posting tools,
|
|
195
|
+
* which are fixed at creation.
|
|
196
|
+
*/
|
|
197
|
+
reusableSessionCanPost: (() => Promise<boolean>) | null;
|
|
198
|
+
verifySlackChannel?: ScheduledTaskSlackChannelVerifier | undefined;
|
|
199
|
+
}): Promise<void> {
|
|
200
|
+
const previousChannel = input.previous?.slackBotChannelId ?? null;
|
|
201
|
+
const nextChannel = input.next.slackBotChannelId ?? null;
|
|
202
|
+
// Clearing takes effect at once: the tools re-read the task and refuse.
|
|
203
|
+
if (!nextChannel) return;
|
|
204
|
+
if (!input.next.slackBotConnectionId) {
|
|
205
|
+
throw new HTTPException(422, {
|
|
206
|
+
message: "slackBotChannelId requires slackBotConnectionId",
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
if (input.runMode === "existing_session") {
|
|
210
|
+
throw new HTTPException(422, {
|
|
211
|
+
message: "Slack channel posting needs a task that starts its own chats",
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
if (
|
|
215
|
+
previousChannel === nextChannel &&
|
|
216
|
+
(input.previous?.slackBotConnectionId ?? null) === input.next.slackBotConnectionId
|
|
217
|
+
) {
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
if (!isAuthenticatedPersonAuthorization(input.authorization)) {
|
|
221
|
+
throw new HTTPException(403, {
|
|
222
|
+
message: "Only a person can choose the Slack channel a scheduled task posts to",
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
requirePermission(input.grant, "connections:write");
|
|
226
|
+
// A chat created without the posting tools keeps that tool set, so choosing
|
|
227
|
+
// a channel for it would silently post nothing.
|
|
228
|
+
if (input.reusableSessionCanPost && !(await input.reusableSessionCanPost())) {
|
|
229
|
+
throw new HTTPException(409, {
|
|
230
|
+
message:
|
|
231
|
+
"this task continues a chat that was created without Slack posting; recreate the task to post from it",
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
if (!input.verifySlackChannel) {
|
|
235
|
+
throw new HTTPException(422, {
|
|
236
|
+
message: "Choose the Slack channel in the schedule editor",
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
await input.verifySlackChannel({
|
|
240
|
+
connectionId: input.next.slackBotConnectionId,
|
|
241
|
+
channelId: nextChannel,
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Add the bot posting tools to a generated scheduled run when, and only when,
|
|
247
|
+
* a person chose the task's Slack channel. The deployment ceiling still
|
|
248
|
+
* applies: an operator who disallowed these tools keeps them off.
|
|
249
|
+
*/
|
|
250
|
+
export function withScheduledSlackBotPostingTools(
|
|
251
|
+
tools: readonly FirstPartyMcpToolName[],
|
|
252
|
+
agentConfig: Pick<ScheduledTaskAgentConfig, "slackBotConnectionId" | "slackBotChannelId">,
|
|
253
|
+
allowedTools: readonly FirstPartyMcpToolName[],
|
|
254
|
+
): FirstPartyMcpToolName[] {
|
|
255
|
+
const posting = new Set<FirstPartyMcpToolName>(SCHEDULED_SLACK_BOT_POSTING_TOOLS);
|
|
256
|
+
// Ordinary-chat defaults include these tools, but an unattended occurrence
|
|
257
|
+
// must have its own person-chosen destination before receiving them.
|
|
258
|
+
const next = tools.filter((tool) => !posting.has(tool));
|
|
259
|
+
if (!agentConfig.slackBotConnectionId || !agentConfig.slackBotChannelId) return next;
|
|
260
|
+
const allowed = new Set(allowedTools);
|
|
261
|
+
for (const tool of SCHEDULED_SLACK_BOT_POSTING_TOOLS) {
|
|
262
|
+
if (allowed.has(tool) && !next.includes(tool)) next.push(tool);
|
|
263
|
+
}
|
|
264
|
+
return next;
|
|
265
|
+
}
|
|
@@ -19,7 +19,7 @@ const CREDENTIAL_URL_PATTERN = /\b([a-z][a-z0-9+.-]*:\/\/[^:\s/@]+:)[^@\s/]+@/gi
|
|
|
19
19
|
* Deterministic, sink-local protection for text that is about to be persisted
|
|
20
20
|
* as a Slack publication projection or rendered to Slack. It intentionally
|
|
21
21
|
* does not scan or mutate canonical Memory, durable-learning receipts, model
|
|
22
|
-
* context, history, events, or any other internal
|
|
22
|
+
* context, history, events, or any other internal Opengeni content path.
|
|
23
23
|
*/
|
|
24
24
|
export function sanitizeSlackPublicationText(value: string): string {
|
|
25
25
|
let sanitized = value;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** Vaults and secret references: the only import point for the vault domain. */
|
|
2
|
+
export { buildMissingSecretAskCard } from "@opengeni/contracts";
|
|
3
|
+
export { isVaultError, VaultError, vaultErrorFromDatabase } from "@opengeni/db";
|
|
4
|
+
export {
|
|
5
|
+
toVaultItem,
|
|
6
|
+
toVaultItemSummary,
|
|
7
|
+
toVaultSummary,
|
|
8
|
+
vaultCanManage,
|
|
9
|
+
vaultCanWrite,
|
|
10
|
+
vaultItemReferences,
|
|
11
|
+
type VaultCallerAuthority,
|
|
12
|
+
} from "./mapping";
|
|
13
|
+
export {
|
|
14
|
+
chooseSecretCandidate,
|
|
15
|
+
createSecretResolver,
|
|
16
|
+
type SecretResolverOptions,
|
|
17
|
+
} from "./secret-resolver";
|
|
18
|
+
export type { VaultRequestActor, VaultServiceDeps } from "./types";
|
|
19
|
+
export { createVaultService, vaultActorIsApiKey, type VaultService } from "./vault-service";
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vault rows (encrypted, as the 0739 routines return them) to the public
|
|
3
|
+
* contract shapes. Only text-kind field values are decrypted for display;
|
|
4
|
+
* every other kind stays server-side until an audited reveal or a resolve.
|
|
5
|
+
*/
|
|
6
|
+
import {
|
|
7
|
+
formatSecretReference,
|
|
8
|
+
secretNameKey,
|
|
9
|
+
type VaultItem,
|
|
10
|
+
type VaultItemField,
|
|
11
|
+
type VaultItemReferences,
|
|
12
|
+
type VaultItemSummary,
|
|
13
|
+
type VaultSummary,
|
|
14
|
+
} from "@opengeni/contracts";
|
|
15
|
+
import {
|
|
16
|
+
decryptEnvironmentValue,
|
|
17
|
+
type VaultFieldRow,
|
|
18
|
+
type VaultItemRow,
|
|
19
|
+
type VaultRow,
|
|
20
|
+
} from "@opengeni/db";
|
|
21
|
+
|
|
22
|
+
/** What the caller may do beyond reading; decided by the route's grant. */
|
|
23
|
+
export type VaultCallerAuthority = {
|
|
24
|
+
/** `secrets:write` */
|
|
25
|
+
write: boolean;
|
|
26
|
+
/** `account:admin` on the organization */
|
|
27
|
+
accountAdmin: boolean;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export function vaultCanWrite(row: VaultRow, authority: VaultCallerAuthority): boolean {
|
|
31
|
+
// Read-write connected vaults are edited through their provider (W5, OP-3).
|
|
32
|
+
if (row.mode !== "read_write" || !authority.write) return false;
|
|
33
|
+
if (row.ownerKind === "user") return row.ownedByCaller;
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function vaultCanManage(row: VaultRow, authority: VaultCallerAuthority): boolean {
|
|
38
|
+
switch (row.ownerKind) {
|
|
39
|
+
case "task":
|
|
40
|
+
return false;
|
|
41
|
+
case "organization":
|
|
42
|
+
return authority.accountAdmin;
|
|
43
|
+
case "user":
|
|
44
|
+
return row.ownedByCaller && authority.write;
|
|
45
|
+
case "workspace":
|
|
46
|
+
return authority.write;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function toVaultSummary(row: VaultRow, authority: VaultCallerAuthority): VaultSummary {
|
|
51
|
+
return {
|
|
52
|
+
id: row.id,
|
|
53
|
+
workspaceId: row.workspaceId,
|
|
54
|
+
ownerKind: row.ownerKind,
|
|
55
|
+
ownerLabel: row.ownerLabel ?? "",
|
|
56
|
+
rootSessionId: row.rootSessionId,
|
|
57
|
+
name: row.name,
|
|
58
|
+
description: row.description,
|
|
59
|
+
source: row.source,
|
|
60
|
+
mode: row.mode,
|
|
61
|
+
syncStatus: row.syncStatus,
|
|
62
|
+
syncErrorCode: row.syncErrorCode,
|
|
63
|
+
syncErrorMessage: row.syncErrorMessage,
|
|
64
|
+
lastSyncedAt: row.lastSyncedAt,
|
|
65
|
+
rateLimitedUntil: row.rateLimitedUntil,
|
|
66
|
+
providerConnectionId: row.providerConnectionId,
|
|
67
|
+
availability: row.availability,
|
|
68
|
+
isDefault: row.isDefault,
|
|
69
|
+
itemCount: Number(row.itemCount),
|
|
70
|
+
canWrite: vaultCanWrite(row, authority),
|
|
71
|
+
canManage: vaultCanManage(row, authority),
|
|
72
|
+
generation: Number(row.generation),
|
|
73
|
+
createdAt: row.createdAt,
|
|
74
|
+
updatedAt: row.updatedAt,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function decryptOrNull(key: Uint8Array | null, stored: string | null): string | null {
|
|
79
|
+
if (stored === null || key === null) return null;
|
|
80
|
+
try {
|
|
81
|
+
return decryptEnvironmentValue(key, stored);
|
|
82
|
+
} catch {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** A reference string, falling back to the ID form when a name cannot be referenced. */
|
|
88
|
+
function referenceFor(
|
|
89
|
+
item: Pick<VaultItemRow, "id" | "name" | "vaultName">,
|
|
90
|
+
field: string | undefined,
|
|
91
|
+
otp = false,
|
|
92
|
+
): string {
|
|
93
|
+
const tail = { ...(field !== undefined ? { field } : {}), otp };
|
|
94
|
+
try {
|
|
95
|
+
return formatSecretReference({ vault: item.vaultName, item: item.name, ...tail });
|
|
96
|
+
} catch {
|
|
97
|
+
return formatSecretReference({ itemId: item.id, ...tail });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Ready references for an item. Every reference names the vault and the field
|
|
103
|
+
* (`{{secret:Vault/Item/field}}`), so it cannot become ambiguous when another
|
|
104
|
+
* reachable vault gains an item with the same name.
|
|
105
|
+
*/
|
|
106
|
+
export function vaultItemReferences(
|
|
107
|
+
item: Pick<VaultItemRow, "id" | "name" | "vaultName" | "fields" | "primaryFieldId">,
|
|
108
|
+
): VaultItemReferences {
|
|
109
|
+
const byField: Record<string, string> = {};
|
|
110
|
+
for (const field of item.fields) byField[field.name] = referenceFor(item, field.name);
|
|
111
|
+
const primary = item.fields.find((field) => field.id === item.primaryFieldId);
|
|
112
|
+
const totp = item.fields.find((field) => field.kind === "totp");
|
|
113
|
+
return {
|
|
114
|
+
primary: primary ? byField[primary.name]! : null,
|
|
115
|
+
otp: totp ? referenceFor(item, totp.name, true) : null,
|
|
116
|
+
byField,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function usernameOf(row: VaultItemRow, key: Uint8Array | null): string | null {
|
|
121
|
+
const field = row.fields.find(
|
|
122
|
+
(candidate) => candidate.kind === "text" && secretNameKey(candidate.name) === "username",
|
|
123
|
+
);
|
|
124
|
+
return field ? decryptOrNull(key, field.valueEncrypted) : null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function toVaultItemSummary(row: VaultItemRow, key: Uint8Array | null): VaultItemSummary {
|
|
128
|
+
return {
|
|
129
|
+
id: row.id,
|
|
130
|
+
vaultId: row.vaultId,
|
|
131
|
+
type: row.type,
|
|
132
|
+
name: row.name,
|
|
133
|
+
websites: row.websites ?? [],
|
|
134
|
+
username: usernameOf(row, key),
|
|
135
|
+
hasTotp: row.fields.some((field) => field.kind === "totp"),
|
|
136
|
+
hasPasskey: row.type === "passkey",
|
|
137
|
+
expiresAt: row.expiresAt,
|
|
138
|
+
archivedAt: row.archivedAt,
|
|
139
|
+
lastUsedAt: row.lastUsedAt,
|
|
140
|
+
updatedAt: row.updatedAt,
|
|
141
|
+
references: vaultItemReferences(row),
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function toVaultItemField(
|
|
146
|
+
item: VaultItemRow,
|
|
147
|
+
field: VaultFieldRow,
|
|
148
|
+
key: Uint8Array | null,
|
|
149
|
+
): VaultItemField {
|
|
150
|
+
return {
|
|
151
|
+
id: field.id,
|
|
152
|
+
name: field.name,
|
|
153
|
+
kind: field.kind,
|
|
154
|
+
position: field.position,
|
|
155
|
+
currentVersion: field.currentVersion,
|
|
156
|
+
value: field.kind === "text" ? decryptOrNull(key, field.valueEncrypted) : null,
|
|
157
|
+
fileName: field.fileName,
|
|
158
|
+
fileSize: field.fileSize,
|
|
159
|
+
reference: referenceFor(item, field.name),
|
|
160
|
+
updatedAt: field.updatedAt,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function toVaultItem(row: VaultItemRow, key: Uint8Array | null): VaultItem {
|
|
165
|
+
const textField = (name: string) => {
|
|
166
|
+
const field = row.fields.find((candidate) => secretNameKey(candidate.name) === name);
|
|
167
|
+
return field ? decryptOrNull(key, field.valueEncrypted) : null;
|
|
168
|
+
};
|
|
169
|
+
const credentialId = row.type === "passkey" ? (textField("credentialid") ?? "") : "";
|
|
170
|
+
return {
|
|
171
|
+
...toVaultItemSummary(row, key),
|
|
172
|
+
fields: row.fields.map((field) => toVaultItemField(row, field, key)),
|
|
173
|
+
passkey:
|
|
174
|
+
row.type === "passkey"
|
|
175
|
+
? {
|
|
176
|
+
rpId: textField("rpid") ?? "",
|
|
177
|
+
userName: textField("username"),
|
|
178
|
+
credentialIdSuffix: credentialId.slice(-8),
|
|
179
|
+
signCount: Number(row.passkeySignCount ?? 0),
|
|
180
|
+
leasedUntil: row.passkeyLeasedUntil,
|
|
181
|
+
lastUsedAt: row.lastUsedAt,
|
|
182
|
+
}
|
|
183
|
+
: null,
|
|
184
|
+
aliases: row.aliases ?? [],
|
|
185
|
+
primaryFieldId: row.primaryFieldId,
|
|
186
|
+
createdBySubjectId: row.createdBySubjectId,
|
|
187
|
+
createdAt: row.createdAt,
|
|
188
|
+
generation: Number(row.generation),
|
|
189
|
+
};
|
|
190
|
+
}
|