@opengeni/core 2.7.2-canary.0 → 2.7.5-canary.1
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/access/index.d.ts +10 -0
- package/dist/application/api-integration-servers.d.ts +20 -0
- package/dist/domain/github-action-policies.d.ts +36 -0
- package/dist/domain/pr-review.d.ts +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +267 -11
- package/dist/index.js.map +1 -1
- package/package.json +12 -10
- package/src/access/index.ts +25 -0
- package/src/application/api-integration-servers.ts +215 -0
- package/src/application/new-session-drafts.ts +6 -0
- package/src/application/session-commands.ts +2 -2
- package/src/domain/github-action-policies.ts +169 -0
- package/src/index.ts +2 -0
- package/src/sandbox/routing.ts +6 -9
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opengeni/core",
|
|
3
|
-
"version": "2.7.
|
|
3
|
+
"version": "2.7.5-canary.1",
|
|
4
4
|
"description": "OpenGeni framework-agnostic core: the domain, access, and billing layers (neutral access, off-HTTP V2 surface). Behavior-preserving extraction from apps/api — keeps Hono's HTTPException for error throwing (typed-errors cleanup deferred).",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -54,15 +54,17 @@
|
|
|
54
54
|
},
|
|
55
55
|
"dependencies": {
|
|
56
56
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
57
|
-
"@opengeni/
|
|
58
|
-
"@opengeni/
|
|
59
|
-
"@opengeni/
|
|
60
|
-
"@opengeni/
|
|
61
|
-
"@opengeni/
|
|
62
|
-
"@opengeni/
|
|
63
|
-
"@opengeni/
|
|
64
|
-
"@opengeni/
|
|
65
|
-
"@opengeni/
|
|
57
|
+
"@opengeni/capabilities": "^0.3.2-canary.1",
|
|
58
|
+
"@opengeni/codex": "^0.2.21-canary.1",
|
|
59
|
+
"@opengeni/config": "^1.0.0-canary.1",
|
|
60
|
+
"@opengeni/contracts": "^2.13.0-canary.1",
|
|
61
|
+
"@opengeni/db": "^4.0.0-canary.1",
|
|
62
|
+
"@opengeni/documents": "^0.8.19-canary.1",
|
|
63
|
+
"@opengeni/events": "^0.4.17-canary.1",
|
|
64
|
+
"@opengeni/network": "^0.3.0-canary.1",
|
|
65
|
+
"@opengeni/observability": "^0.8.19-canary.1",
|
|
66
|
+
"@opengeni/runtime": "^2.3.0-canary.1",
|
|
67
|
+
"@opengeni/storage": "^0.2.120-canary.1",
|
|
66
68
|
"hono": "^4.12.18",
|
|
67
69
|
"zod": "^4.2.1"
|
|
68
70
|
},
|
package/src/access/index.ts
CHANGED
|
@@ -243,6 +243,31 @@ export function requireAccountAdminAuthorizationStamp(
|
|
|
243
243
|
});
|
|
244
244
|
}
|
|
245
245
|
|
|
246
|
+
/**
|
|
247
|
+
* Verify that an access authorization was minted by the canonical request
|
|
248
|
+
* resolver for this exact subject and workspace.
|
|
249
|
+
*
|
|
250
|
+
* This is the protocol-neutral boundary for request-local services that need
|
|
251
|
+
* the authenticated grant rather than a caller-supplied grant-shaped object.
|
|
252
|
+
* Object identity is intentional: matching fields alone are not proof that the
|
|
253
|
+
* request authenticated the named subject.
|
|
254
|
+
*/
|
|
255
|
+
export function requireResolvedAccessGrantAuthorization(
|
|
256
|
+
authorization: AccessGrantAuthorization,
|
|
257
|
+
workspaceId: string,
|
|
258
|
+
): AccessGrant {
|
|
259
|
+
const { grant } = authorization;
|
|
260
|
+
if (
|
|
261
|
+
!resolvedAccessGrantAuthorizations.has(authorization) ||
|
|
262
|
+
!authorization.contextIntegrity ||
|
|
263
|
+
authorization.authenticatedSubjectId !== grant.subjectId ||
|
|
264
|
+
grant.workspaceId !== workspaceId
|
|
265
|
+
) {
|
|
266
|
+
throw new HTTPException(403, { message: "workspace access authorization is invalid" });
|
|
267
|
+
}
|
|
268
|
+
return grant;
|
|
269
|
+
}
|
|
270
|
+
|
|
246
271
|
/**
|
|
247
272
|
* Resolve the exact built-in single-user local administrator for an account.
|
|
248
273
|
*
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createGraphqlMcpServer,
|
|
3
|
+
createOpenApiMcpServer,
|
|
4
|
+
createPinnedIntegrationTransport,
|
|
5
|
+
directIntegrationTransport,
|
|
6
|
+
IntegrationInvocationError,
|
|
7
|
+
type IntegrationCredentialResolver,
|
|
8
|
+
type IntegrationInvocationAuthority,
|
|
9
|
+
type IntegrationTransport,
|
|
10
|
+
} from "@opengeni/capabilities";
|
|
11
|
+
import type { Settings } from "@opengeni/config";
|
|
12
|
+
import type { ToolAuthNeededPayload } from "@opengeni/contracts";
|
|
13
|
+
import type {
|
|
14
|
+
ApiIntegrationRuntime,
|
|
15
|
+
ResolveConnectionCredentialInput,
|
|
16
|
+
ResolveConnectionCredentialResult,
|
|
17
|
+
} from "@opengeni/db";
|
|
18
|
+
import type { FetchLike } from "@opengeni/network";
|
|
19
|
+
import type { LocalMcpServerRegistration } from "@opengeni/runtime";
|
|
20
|
+
|
|
21
|
+
export type BuildApiIntegrationServersInput = {
|
|
22
|
+
settings: Settings;
|
|
23
|
+
integrations: readonly ApiIntegrationRuntime[];
|
|
24
|
+
authority: Omit<IntegrationInvocationAuthority, "connectionRef">;
|
|
25
|
+
resolveCredential: (
|
|
26
|
+
input: ResolveConnectionCredentialInput,
|
|
27
|
+
) => Promise<ResolveConnectionCredentialResult>;
|
|
28
|
+
onAuthNeeded?: (payload: ToolAuthNeededPayload) => Promise<void> | void;
|
|
29
|
+
fetchImpl?: FetchLike;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Compile active persisted API facets into ordinary in-process MCP providers.
|
|
34
|
+
* The caller supplies its authority resolver, so agent attempts and current
|
|
35
|
+
* humans use the same provider assembly without sharing credentials or policy.
|
|
36
|
+
*/
|
|
37
|
+
export function buildApiIntegrationMcpServers(
|
|
38
|
+
input: BuildApiIntegrationServersInput,
|
|
39
|
+
): LocalMcpServerRegistration[] {
|
|
40
|
+
const transport = createPinnedIntegrationTransport({
|
|
41
|
+
network: input.settings,
|
|
42
|
+
...(input.fetchImpl ? { fetchImpl: input.fetchImpl } : {}),
|
|
43
|
+
});
|
|
44
|
+
return input.integrations.map((integration) => {
|
|
45
|
+
const authority: IntegrationInvocationAuthority = {
|
|
46
|
+
...input.authority,
|
|
47
|
+
...(integration.connectionRef?.connectionId
|
|
48
|
+
? { connectionRef: integration.connectionRef.connectionId }
|
|
49
|
+
: {}),
|
|
50
|
+
};
|
|
51
|
+
const credentialResolver = integration.connectionRef
|
|
52
|
+
? integrationCredentialResolver(input, integration, "execution")
|
|
53
|
+
: undefined;
|
|
54
|
+
const buildServer = (
|
|
55
|
+
serverTransport: IntegrationTransport,
|
|
56
|
+
serverCredentialResolver = credentialResolver,
|
|
57
|
+
) =>
|
|
58
|
+
integration.revision.protocol === "openapi"
|
|
59
|
+
? createOpenApiMcpServer({
|
|
60
|
+
revision: integration.revision,
|
|
61
|
+
transport: serverTransport,
|
|
62
|
+
authority,
|
|
63
|
+
...(serverCredentialResolver ? { credentialResolver: serverCredentialResolver } : {}),
|
|
64
|
+
})
|
|
65
|
+
: createGraphqlMcpServer({
|
|
66
|
+
revision: integration.revision,
|
|
67
|
+
endpoint: integration.baseUrl,
|
|
68
|
+
transport: serverTransport,
|
|
69
|
+
authority,
|
|
70
|
+
...(serverCredentialResolver ? { credentialResolver: serverCredentialResolver } : {}),
|
|
71
|
+
});
|
|
72
|
+
const server = buildServer(transport);
|
|
73
|
+
const preflightCredentialResolver = integration.connectionRef
|
|
74
|
+
? integrationCredentialResolver(input, integration, "preflight")
|
|
75
|
+
: undefined;
|
|
76
|
+
const preflightServer = preflightCredentialResolver
|
|
77
|
+
? buildServer(providerBlockingPreflightTransport, preflightCredentialResolver)
|
|
78
|
+
: null;
|
|
79
|
+
return {
|
|
80
|
+
id: integration.serverId,
|
|
81
|
+
server,
|
|
82
|
+
approvalAuthority: {
|
|
83
|
+
kind: "api_integration",
|
|
84
|
+
capabilityId: integration.capabilityId,
|
|
85
|
+
pluginKey: integration.pluginKey,
|
|
86
|
+
pluginInstallationId: integration.pluginInstallationId,
|
|
87
|
+
installationVersion: integration.installationVersion,
|
|
88
|
+
instanceId: integration.instanceId,
|
|
89
|
+
instanceKey: integration.instanceKey,
|
|
90
|
+
instanceVersion: integration.instanceVersion,
|
|
91
|
+
definitionId: integration.definitionId,
|
|
92
|
+
definitionProvenance: integration.definitionProvenance,
|
|
93
|
+
revisionId: integration.revision.id,
|
|
94
|
+
baseUrl: integration.baseUrl,
|
|
95
|
+
providerDomain: integration.providerDomain,
|
|
96
|
+
connectionRef: integration.connectionRef,
|
|
97
|
+
connectionAuthorityGeneration: integration.connectionAuthorityGeneration,
|
|
98
|
+
},
|
|
99
|
+
...(integration.connectionRef?.connectionId
|
|
100
|
+
? { resolvedConnectionId: integration.connectionRef.connectionId }
|
|
101
|
+
: {}),
|
|
102
|
+
...(credentialResolver
|
|
103
|
+
? {
|
|
104
|
+
preflightCall: async (
|
|
105
|
+
toolName: string,
|
|
106
|
+
args: Record<string, unknown>,
|
|
107
|
+
options?: { signal?: AbortSignal },
|
|
108
|
+
) => await preflightApiIntegrationCall(preflightServer!, toolName, args, options),
|
|
109
|
+
}
|
|
110
|
+
: {}),
|
|
111
|
+
};
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const providerBlockingPreflightTransport = directIntegrationTransport(async () => {
|
|
116
|
+
throw new Error("integration provider request blocked by preflight");
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
async function preflightApiIntegrationCall(
|
|
120
|
+
server: LocalMcpServerRegistration["server"],
|
|
121
|
+
toolName: string,
|
|
122
|
+
args: Record<string, unknown>,
|
|
123
|
+
options?: { signal?: AbortSignal },
|
|
124
|
+
): Promise<void> {
|
|
125
|
+
try {
|
|
126
|
+
await server.callTool(toolName, args, null, options);
|
|
127
|
+
} catch (error) {
|
|
128
|
+
if (error instanceof IntegrationInvocationError && error.code === "request_failed") return;
|
|
129
|
+
throw error;
|
|
130
|
+
}
|
|
131
|
+
throw new Error("integration preflight unexpectedly crossed the provider request boundary");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function integrationCredentialResolver(
|
|
135
|
+
input: BuildApiIntegrationServersInput,
|
|
136
|
+
integration: ApiIntegrationRuntime,
|
|
137
|
+
credentialResolutionMode: "execution" | "preflight",
|
|
138
|
+
): IntegrationCredentialResolver {
|
|
139
|
+
const connectionRef = integration.connectionRef;
|
|
140
|
+
if (!connectionRef) throw new Error("Integration credential resolver requires a connection");
|
|
141
|
+
const expectedAuthorityGeneration = integration.connectionAuthorityGeneration;
|
|
142
|
+
if (expectedAuthorityGeneration === null) {
|
|
143
|
+
throw new Error("Integration credential resolver requires a connection authority generation");
|
|
144
|
+
}
|
|
145
|
+
return {
|
|
146
|
+
resolve: async (request) => {
|
|
147
|
+
const result = await input.resolveCredential({
|
|
148
|
+
workspaceId: input.authority.workspaceId,
|
|
149
|
+
serverId: integration.serverId,
|
|
150
|
+
toolName: request.operationKey,
|
|
151
|
+
connectionRef,
|
|
152
|
+
destinationUrl: request.destinationUrl,
|
|
153
|
+
credentialTarget: "http_api",
|
|
154
|
+
forceRefresh: request.forceRefresh === true,
|
|
155
|
+
credentialResolutionMode,
|
|
156
|
+
expectedAuthorityGeneration,
|
|
157
|
+
});
|
|
158
|
+
if (result.status === "auth_needed") {
|
|
159
|
+
await publishAuthNeeded(
|
|
160
|
+
input,
|
|
161
|
+
integration.serverId,
|
|
162
|
+
request.operationKey,
|
|
163
|
+
result,
|
|
164
|
+
connectionRef,
|
|
165
|
+
);
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
const destination = new URL(request.destinationUrl);
|
|
169
|
+
return {
|
|
170
|
+
audience: { origin: destination.origin, pathPrefix: "/" },
|
|
171
|
+
placements:
|
|
172
|
+
result.placements ??
|
|
173
|
+
Object.entries(result.headers).map(([name, value]) => ({
|
|
174
|
+
carrier: "header" as const,
|
|
175
|
+
name,
|
|
176
|
+
value,
|
|
177
|
+
})),
|
|
178
|
+
...(result.authorizeProviderRequest
|
|
179
|
+
? { authorizeProviderRequest: result.authorizeProviderRequest }
|
|
180
|
+
: {}),
|
|
181
|
+
...(result.expiresAt ? { expiresAt: result.expiresAt.toISOString() } : {}),
|
|
182
|
+
...(connectionRef.scopes ? { scope: [...connectionRef.scopes] } : {}),
|
|
183
|
+
};
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async function publishAuthNeeded(
|
|
189
|
+
input: BuildApiIntegrationServersInput,
|
|
190
|
+
serverId: string,
|
|
191
|
+
toolName: string,
|
|
192
|
+
result: Extract<ResolveConnectionCredentialResult, { status: "auth_needed" }>,
|
|
193
|
+
connectionRef: NonNullable<ApiIntegrationRuntime["connectionRef"]>,
|
|
194
|
+
): Promise<void> {
|
|
195
|
+
try {
|
|
196
|
+
await input.onAuthNeeded?.({
|
|
197
|
+
serverId,
|
|
198
|
+
toolName,
|
|
199
|
+
providerDomain: result.providerDomain,
|
|
200
|
+
...(result.provider ? { provider: result.provider } : {}),
|
|
201
|
+
reason: result.reason,
|
|
202
|
+
...(result.connectionId ? { connectionId: result.connectionId } : {}),
|
|
203
|
+
...(result.authoritySource === "host" || connectionRef.authoritySource === "host"
|
|
204
|
+
? { authoritySource: "host" as const }
|
|
205
|
+
: {}),
|
|
206
|
+
...(result.scopes ? { scopes: result.scopes } : {}),
|
|
207
|
+
...(result.resource ? { resource: result.resource } : {}),
|
|
208
|
+
...(result.selectedResources ? { selectedResources: result.selectedResources } : {}),
|
|
209
|
+
...(result.authorizationUrl ? { authorizationUrl: result.authorizationUrl } : {}),
|
|
210
|
+
});
|
|
211
|
+
} catch {
|
|
212
|
+
// Authentication notices are advisory UI/audit signals. The local tool
|
|
213
|
+
// still returns the fixed connection-required result when publication fails.
|
|
214
|
+
}
|
|
215
|
+
}
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
getSandbox,
|
|
12
12
|
getVariableSet,
|
|
13
13
|
NewSessionDraftAccessError,
|
|
14
|
+
newSessionDraftSelectedProjectChannelId,
|
|
14
15
|
newSessionDraftToolsProvided,
|
|
15
16
|
newSessionSelectionHistory,
|
|
16
17
|
publicNewSessionDraftOptions,
|
|
@@ -41,6 +42,7 @@ function mapNewSessionDraft(
|
|
|
41
42
|
row: Awaited<ReturnType<typeof getNewSessionDraftInTransaction>>,
|
|
42
43
|
): NewSessionDraftValue | null {
|
|
43
44
|
if (!row) return null;
|
|
45
|
+
const selectedProjectChannelId = newSessionDraftSelectedProjectChannelId(row);
|
|
44
46
|
return NewSessionDraft.parse({
|
|
45
47
|
revision: row.revision,
|
|
46
48
|
text: row.text,
|
|
@@ -50,6 +52,7 @@ function mapNewSessionDraft(
|
|
|
50
52
|
model: row.model,
|
|
51
53
|
reasoningEffort: row.reasoningEffort,
|
|
52
54
|
latencyMode: row.latencyMode,
|
|
55
|
+
...(selectedProjectChannelId !== undefined ? { selectedProjectChannelId } : {}),
|
|
53
56
|
options: publicNewSessionDraftOptions(row),
|
|
54
57
|
selectionHistory: newSessionSelectionHistory(row),
|
|
55
58
|
updatedAt: row.updatedAt.toISOString(),
|
|
@@ -264,6 +267,9 @@ export async function saveActorNewSessionDraft(
|
|
|
264
267
|
model: input.model,
|
|
265
268
|
reasoningEffort: input.reasoningEffort,
|
|
266
269
|
latencyMode: input.latencyMode,
|
|
270
|
+
...(input.selectedProjectChannelId !== undefined
|
|
271
|
+
? { selectedProjectChannelId: input.selectedProjectChannelId }
|
|
272
|
+
: {}),
|
|
267
273
|
options: input.options,
|
|
268
274
|
// Only managed people are removed through removeWorkspaceMember().
|
|
269
275
|
// API keys and delegated service actors (for example the first-party
|
|
@@ -555,7 +555,7 @@ export async function controlAgentSessionWorkstream(
|
|
|
555
555
|
wakeRevision: result.workflowWake.wakeRevision,
|
|
556
556
|
shouldSignal: true,
|
|
557
557
|
interruptionCount: result.interruptionCount,
|
|
558
|
-
controlRequested:
|
|
558
|
+
controlRequested: input.action === "pause",
|
|
559
559
|
});
|
|
560
560
|
},
|
|
561
561
|
},
|
|
@@ -928,7 +928,7 @@ export async function controlHumanSessionWorkstreamWithOutcome(
|
|
|
928
928
|
wakeRevision: result.workflowWake.wakeRevision,
|
|
929
929
|
shouldSignal: true,
|
|
930
930
|
interruptionCount: result.interruptionCount,
|
|
931
|
-
controlRequested:
|
|
931
|
+
controlRequested: input.action === "pause",
|
|
932
932
|
});
|
|
933
933
|
},
|
|
934
934
|
},
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
GitHubActionPolicyActor,
|
|
3
|
+
GitHubActionPolicyActorState,
|
|
4
|
+
GitHubActionPolicyDecision,
|
|
5
|
+
GitHubActionPolicyEffectiveDecision,
|
|
6
|
+
GitHubActionPolicyGroup,
|
|
7
|
+
} from "@opengeni/contracts";
|
|
8
|
+
import {
|
|
9
|
+
listConnectorActionPolicies,
|
|
10
|
+
resolveConnectorActionPolicy,
|
|
11
|
+
upsertConnectorActionPolicies,
|
|
12
|
+
type ConnectorActionPolicySnapshotEntry,
|
|
13
|
+
type Database,
|
|
14
|
+
} from "@opengeni/db";
|
|
15
|
+
import {
|
|
16
|
+
GITHUB_REST_MCP_APP_SERVER_ID,
|
|
17
|
+
GITHUB_REST_MCP_PERSONAL_SERVER_ID,
|
|
18
|
+
GITHUB_REST_WRITE_TOOL_NAMES,
|
|
19
|
+
} from "@opengeni/runtime/github-rest-mcp";
|
|
20
|
+
|
|
21
|
+
export const GITHUB_ACTION_POLICY_GROUP_TOOL_NAMES = {
|
|
22
|
+
routine: GITHUB_REST_WRITE_TOOL_NAMES.filter(
|
|
23
|
+
(toolName) => toolName !== "pull_request_review_submit" && toolName !== "pull_request_merge",
|
|
24
|
+
),
|
|
25
|
+
review: ["pull_request_review_submit"],
|
|
26
|
+
merge: ["pull_request_merge"],
|
|
27
|
+
} as const satisfies Record<GitHubActionPolicyGroup, readonly string[]>;
|
|
28
|
+
|
|
29
|
+
const groupedToolNames = Object.values(GITHUB_ACTION_POLICY_GROUP_TOOL_NAMES).flat();
|
|
30
|
+
if (
|
|
31
|
+
groupedToolNames.length !== new Set(groupedToolNames).size ||
|
|
32
|
+
GITHUB_REST_WRITE_TOOL_NAMES.some((toolName) => !groupedToolNames.includes(toolName))
|
|
33
|
+
) {
|
|
34
|
+
throw new Error("GitHub action policy groups must cover every write tool exactly once");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type GitHubActionPolicyActorBinding = {
|
|
38
|
+
actor: GitHubActionPolicyActor;
|
|
39
|
+
label: string;
|
|
40
|
+
connectionId: string;
|
|
41
|
+
serverId: typeof GITHUB_REST_MCP_APP_SERVER_ID | typeof GITHUB_REST_MCP_PERSONAL_SERVER_ID;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
function effectiveToolDecision(
|
|
45
|
+
policies: readonly ConnectorActionPolicySnapshotEntry[],
|
|
46
|
+
actor: GitHubActionPolicyActorBinding,
|
|
47
|
+
toolName: string,
|
|
48
|
+
): GitHubActionPolicyDecision {
|
|
49
|
+
const resolved = resolveConnectorActionPolicy(policies, {
|
|
50
|
+
connectionId: actor.connectionId,
|
|
51
|
+
serverId: actor.serverId,
|
|
52
|
+
toolName,
|
|
53
|
+
actionName: toolName,
|
|
54
|
+
});
|
|
55
|
+
// Match connector_write admission: absent policy inherits repository capability.
|
|
56
|
+
// This is a policy projection, not a grant of repository or provider access.
|
|
57
|
+
if (!resolved.managed) return "allow";
|
|
58
|
+
if (resolved.entry) return resolved.entry.policy;
|
|
59
|
+
return resolved.decision;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function effectiveGroupDecision(
|
|
63
|
+
policies: readonly ConnectorActionPolicySnapshotEntry[],
|
|
64
|
+
actor: GitHubActionPolicyActorBinding,
|
|
65
|
+
group: GitHubActionPolicyGroup,
|
|
66
|
+
): GitHubActionPolicyEffectiveDecision {
|
|
67
|
+
const decisions = new Set(
|
|
68
|
+
GITHUB_ACTION_POLICY_GROUP_TOOL_NAMES[group].map((toolName) =>
|
|
69
|
+
effectiveToolDecision(policies, actor, toolName),
|
|
70
|
+
),
|
|
71
|
+
);
|
|
72
|
+
return decisions.size === 1 ? decisions.values().next().value! : "mixed";
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function projectGitHubActionPolicyActor(
|
|
76
|
+
policies: readonly ConnectorActionPolicySnapshotEntry[],
|
|
77
|
+
binding: GitHubActionPolicyActorBinding,
|
|
78
|
+
): GitHubActionPolicyActorState {
|
|
79
|
+
const groups = {
|
|
80
|
+
routine: effectiveGroupDecision(policies, binding, "routine"),
|
|
81
|
+
review: effectiveGroupDecision(policies, binding, "review"),
|
|
82
|
+
merge: effectiveGroupDecision(policies, binding, "merge"),
|
|
83
|
+
};
|
|
84
|
+
return binding.actor.kind === "workspace_app"
|
|
85
|
+
? {
|
|
86
|
+
kind: "workspace_app",
|
|
87
|
+
installationId: binding.actor.installationId,
|
|
88
|
+
label: binding.label,
|
|
89
|
+
groups,
|
|
90
|
+
}
|
|
91
|
+
: {
|
|
92
|
+
kind: "personal",
|
|
93
|
+
connectionId: binding.actor.connectionId,
|
|
94
|
+
label: binding.label,
|
|
95
|
+
groups,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export async function listGitHubActionPolicyActors(
|
|
100
|
+
db: Database,
|
|
101
|
+
input: {
|
|
102
|
+
accountId: string;
|
|
103
|
+
workspaceId: string;
|
|
104
|
+
actors: readonly GitHubActionPolicyActorBinding[];
|
|
105
|
+
},
|
|
106
|
+
): Promise<GitHubActionPolicyActorState[]> {
|
|
107
|
+
const policies = await listConnectorActionPolicies(db, {
|
|
108
|
+
accountId: input.accountId,
|
|
109
|
+
workspaceId: input.workspaceId,
|
|
110
|
+
connectionIds: input.actors.map((actor) => actor.connectionId),
|
|
111
|
+
});
|
|
112
|
+
return input.actors.map((actor) => projectGitHubActionPolicyActor(policies, actor));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export async function updateGitHubActionPolicyGroup(
|
|
116
|
+
db: Database,
|
|
117
|
+
input: {
|
|
118
|
+
accountId: string;
|
|
119
|
+
workspaceId: string;
|
|
120
|
+
subjectId: string;
|
|
121
|
+
actor: GitHubActionPolicyActorBinding;
|
|
122
|
+
group: GitHubActionPolicyGroup;
|
|
123
|
+
decision: GitHubActionPolicyDecision;
|
|
124
|
+
},
|
|
125
|
+
): Promise<GitHubActionPolicyActorState> {
|
|
126
|
+
await upsertConnectorActionPolicies(
|
|
127
|
+
db,
|
|
128
|
+
GITHUB_ACTION_POLICY_GROUP_TOOL_NAMES[input.group].map((toolName) => ({
|
|
129
|
+
accountId: input.accountId,
|
|
130
|
+
workspaceId: input.workspaceId,
|
|
131
|
+
subjectId: input.subjectId,
|
|
132
|
+
connectionId: input.actor.connectionId,
|
|
133
|
+
serverId: input.actor.serverId,
|
|
134
|
+
toolName,
|
|
135
|
+
actionName: toolName,
|
|
136
|
+
policy: input.decision,
|
|
137
|
+
})),
|
|
138
|
+
);
|
|
139
|
+
const policies = await listConnectorActionPolicies(db, {
|
|
140
|
+
accountId: input.accountId,
|
|
141
|
+
workspaceId: input.workspaceId,
|
|
142
|
+
connectionIds: [input.actor.connectionId],
|
|
143
|
+
});
|
|
144
|
+
return projectGitHubActionPolicyActor(policies, input.actor);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function githubAppActionPolicyActor(input: {
|
|
148
|
+
installationId: number;
|
|
149
|
+
accountLogin: string | null;
|
|
150
|
+
}): GitHubActionPolicyActorBinding {
|
|
151
|
+
return {
|
|
152
|
+
actor: { kind: "workspace_app", installationId: input.installationId },
|
|
153
|
+
label: input.accountLogin ? `OpenGeni bot on ${input.accountLogin}` : "OpenGeni bot",
|
|
154
|
+
connectionId: `github-app:${input.installationId}`,
|
|
155
|
+
serverId: GITHUB_REST_MCP_APP_SERVER_ID,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function personalGitHubActionPolicyActor(input: {
|
|
160
|
+
connectionId: string;
|
|
161
|
+
githubLogin: string;
|
|
162
|
+
}): GitHubActionPolicyActorBinding {
|
|
163
|
+
return {
|
|
164
|
+
actor: { kind: "personal", connectionId: input.connectionId },
|
|
165
|
+
label: `@${input.githubLogin}`,
|
|
166
|
+
connectionId: input.connectionId,
|
|
167
|
+
serverId: GITHUB_REST_MCP_PERSONAL_SERVER_ID,
|
|
168
|
+
};
|
|
169
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -80,6 +80,7 @@ export * from "./domain/product-integration-pack";
|
|
|
80
80
|
export * from "./domain/personal-connection-delegations";
|
|
81
81
|
export * from "./domain/resources";
|
|
82
82
|
export * from "./domain/github-repository-bindings";
|
|
83
|
+
export * from "./domain/github-action-policies";
|
|
83
84
|
export * from "./domain/session-tool-policy";
|
|
84
85
|
export * from "./domain/scheduled-tasks";
|
|
85
86
|
export * from "./domain/sessions";
|
|
@@ -106,6 +107,7 @@ export * from "./application/composer-submit";
|
|
|
106
107
|
export * from "./application/session-commands";
|
|
107
108
|
export * from "./application/session-tenancy";
|
|
108
109
|
export * from "./application/user-resource-grants";
|
|
110
|
+
export * from "./application/api-integration-servers";
|
|
109
111
|
|
|
110
112
|
// Durable editable-artifact live broker, ticket, ports, and projection types.
|
|
111
113
|
export * from "./editable-artifact-live";
|
package/src/sandbox/routing.ts
CHANGED
|
@@ -29,7 +29,6 @@ import {
|
|
|
29
29
|
type SandboxRetainedProcess,
|
|
30
30
|
type SandboxWorkspaceMutationAdmission,
|
|
31
31
|
} from "@opengeni/db";
|
|
32
|
-
import { settleSessionBackgroundCommandForRetainedProcess } from "@opengeni/db/session-background-commands";
|
|
33
32
|
import { appendAndPublishEvents, type EventBus } from "@opengeni/events";
|
|
34
33
|
import {
|
|
35
34
|
isProviderSandboxGoneDuringRoutedOperation,
|
|
@@ -449,14 +448,11 @@ export function wrapChannelABoxWithRouting(
|
|
|
449
448
|
reason: proof.reason,
|
|
450
449
|
idleGraceMs: settings.sandboxIdleGraceMs,
|
|
451
450
|
});
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
retainedProcessId: process.id,
|
|
458
|
-
...backgroundSettlement,
|
|
459
|
-
});
|
|
451
|
+
if (settlement.backgroundCommandEvents.length > 0 && bus) {
|
|
452
|
+
await bus
|
|
453
|
+
.publish(ids.workspaceId, ids.sessionId, settlement.backgroundCommandEvents)
|
|
454
|
+
.catch(() => undefined);
|
|
455
|
+
}
|
|
460
456
|
}
|
|
461
457
|
: undefined;
|
|
462
458
|
const resolver = makeActiveBackendResolver({
|
|
@@ -520,6 +516,7 @@ export function wrapChannelABoxWithRouting(
|
|
|
520
516
|
});
|
|
521
517
|
|
|
522
518
|
const proxy = new RoutingSandboxSession({
|
|
519
|
+
bindActiveRouteOnFirstResolve: true,
|
|
523
520
|
defaultResolved: {
|
|
524
521
|
session: established.session as RoutableBackendSession,
|
|
525
522
|
sandboxId: null,
|