@opengeni/core 0.28.1 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/access/external-actor-authority.d.ts +47 -0
- package/dist/access/index.d.ts +182 -3
- package/dist/access/service-initiator.d.ts +3 -0
- package/dist/application/api-integration-servers.d.ts +20 -0
- package/dist/application/archived-session-imports.d.ts +6 -0
- package/dist/application/composer-submit.d.ts +24 -0
- package/dist/application/connect-authority.d.ts +13 -0
- package/dist/application/connect-operation.d.ts +31 -0
- package/dist/application/external-continuation.d.ts +15 -0
- package/dist/application/external-identity-lifecycle.d.ts +20 -0
- package/dist/application/external-link-work-admission.d.ts +12 -0
- package/dist/application/external-workspace-members.d.ts +34 -0
- package/dist/application/modal-native-worker-host-transport.d.ts +297 -0
- package/dist/application/new-session-drafts.d.ts +39 -2
- package/dist/application/sandbox-recovery.d.ts +58 -0
- package/dist/application/session-commands.d.ts +26 -13
- package/dist/application/session-mcp-credential-rotation.d.ts +29 -0
- package/dist/application/session-tenancy.d.ts +19 -0
- package/dist/application/user-resource-grants.d.ts +20 -0
- package/dist/billing/limits.d.ts +11 -0
- package/dist/canonical-human-identities.d.ts +7 -2
- package/dist/canonical-human-identities.js +17 -4
- package/dist/canonical-human-identities.js.map +1 -1
- package/dist/{chunk-6WKPWE5S.js → chunk-2MKMRGSY.js} +108 -24
- package/dist/chunk-2MKMRGSY.js.map +1 -0
- package/dist/{chunk-NYPMQ7NO.js → chunk-GVO3N37G.js} +205 -37
- package/dist/chunk-GVO3N37G.js.map +1 -0
- package/dist/{chunk-JJU6I5XD.js → chunk-LCQXNR62.js} +36 -10
- package/dist/chunk-LCQXNR62.js.map +1 -0
- package/dist/chunk-MM6N7CTO.js +387 -0
- package/dist/chunk-MM6N7CTO.js.map +1 -0
- package/dist/chunk-QO5GVFFO.js +235 -0
- package/dist/chunk-QO5GVFFO.js.map +1 -0
- package/dist/codex-model-availability.d.ts +14 -0
- package/dist/default-session-model.d.ts +139 -0
- package/dist/dependencies.d.ts +87 -4
- 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 +58 -0
- package/dist/domain/capabilities.d.ts +22 -4
- package/dist/domain/company-profile-agent-admin.d.ts +25 -0
- package/dist/domain/connection-account-selection-error.d.ts +8 -0
- package/dist/domain/connector-tool-permissions.d.ts +36 -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 +4 -3
- package/dist/domain/environments.d.ts +5 -9
- package/dist/domain/external-creation-attribution.d.ts +6 -0
- package/dist/domain/file-owner.d.ts +8 -0
- package/dist/domain/github-action-policies.d.ts +36 -0
- package/dist/domain/github-repository-bindings.d.ts +117 -0
- package/dist/domain/github-skill-source.d.ts +5 -0
- package/dist/domain/governed-learning-slack-publication.d.ts +45 -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 +28 -1
- package/dist/domain/knowledge-files.d.ts +20 -0
- package/dist/domain/knowledge-messages.d.ts +39 -0
- package/dist/domain/knowledge-messages.test.d.ts +1 -0
- 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 -0
- package/dist/domain/knowledge-search.test.d.ts +1 -0
- package/dist/domain/knowledge.d.ts +9 -0
- package/dist/domain/mcp-account-bindings.d.ts +19 -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/organization-membership-lifecycle.d.ts +1 -0
- package/dist/domain/personal-connection-delegations.d.ts +87 -4
- package/dist/domain/pr-review.d.ts +249 -0
- package/dist/domain/resources.d.ts +24 -3
- package/dist/domain/scheduled-task-access.d.ts +247 -0
- package/dist/domain/scheduled-tasks.d.ts +103 -6
- package/dist/domain/session-connection-accounts.d.ts +16 -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 +93 -4
- package/dist/domain/sessions.d.ts +275 -28
- package/dist/domain/skill-imports.d.ts +1 -0
- package/dist/domain/skill-search.d.ts +30 -0
- package/dist/domain/skills.d.ts +117 -0
- package/dist/domain/slack-bot.d.ts +46 -1
- package/dist/domain/workspace-members.d.ts +8 -1
- package/dist/editable-artifact-live/application.d.ts +12 -3
- package/dist/editable-artifact-live/ports.d.ts +0 -1
- package/dist/editable-artifact-live/server.d.ts +5 -2
- package/dist/editable-artifact-live/ticket.d.ts +2 -2
- package/dist/editable-artifact-live/types.d.ts +14 -2
- package/dist/editable-artifact-live.js +2 -2
- package/dist/editable-artifacts.js +10 -4
- package/dist/index.d.ts +49 -3
- package/dist/index.js +18812 -6833
- package/dist/index.js.map +1 -1
- package/dist/managed-auth-session-sets.d.ts +131 -0
- package/dist/managed-auth-session-sets.js +51 -0
- package/dist/managed-auth-session-sets.js.map +1 -0
- package/dist/managed-session.d.ts +98 -11
- package/dist/model-catalog.d.ts +117 -0
- package/dist/product-usage-metrics.d.ts +30 -0
- package/dist/rigs/index.d.ts +22 -5
- package/dist/rigs/provider-images.d.ts +1 -1
- package/dist/sandbox/fleet.d.ts +59 -10
- package/dist/sandbox/routing.d.ts +22 -2
- package/dist/sandbox/runtime-settings.d.ts +56 -0
- package/dist/session-authorization.d.ts +38 -1
- package/dist/site-session-origin.d.ts +9 -0
- package/dist/transcription.d.ts +12 -1
- package/dist/turn-surface.d.ts +18 -0
- package/dist/user-presence.d.ts +32 -0
- package/package.json +18 -11
- package/src/access/external-actor-authority.ts +94 -0
- package/src/access/index.ts +1386 -44
- package/src/access/service-initiator.ts +50 -0
- package/src/application/api-integration-servers.ts +215 -0
- package/src/application/archived-session-imports.ts +161 -0
- package/src/application/composer-submit.ts +77 -0
- package/src/application/connect-authority.ts +77 -0
- package/src/application/connect-operation.ts +80 -0
- package/src/application/external-continuation.ts +129 -0
- package/src/application/external-identity-lifecycle.ts +48 -0
- package/src/application/external-link-work-admission.ts +90 -0
- package/src/application/external-workspace-members.ts +232 -0
- package/src/application/modal-native-worker-host-transport.ts +263 -0
- package/src/application/new-session-drafts.ts +222 -29
- package/src/application/sandbox-recovery.ts +78 -0
- package/src/application/session-commands.ts +430 -73
- package/src/application/session-mcp-credential-rotation.ts +217 -0
- package/src/application/session-tenancy.ts +445 -0
- package/src/application/user-resource-grants.ts +154 -0
- package/src/billing/limits.ts +103 -24
- package/src/canonical-human-identities.ts +22 -3
- package/src/codex-model-availability.ts +136 -0
- package/src/default-session-model.ts +656 -0
- package/src/dependencies.ts +97 -5
- package/src/developer-setup-credential-ceiling.ts +60 -0
- package/src/domain/agent-config-resolution.ts +148 -0
- package/src/domain/automations.ts +270 -0
- package/src/domain/capabilities.ts +244 -151
- package/src/domain/company-profile-agent-admin.ts +56 -0
- package/src/domain/connection-account-selection-error.ts +19 -0
- package/src/domain/connector-tool-permissions.ts +371 -0
- package/src/domain/direct-model-provider.ts +62 -0
- package/src/domain/editable-artifacts/agent-application.ts +61 -15
- 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 +32 -1
- package/src/domain/editable-artifacts/snapshot-compaction.ts +9 -5
- package/src/domain/editable-artifacts/snapshot-verifier-genesis.ts +10 -8
- package/src/domain/editable-artifacts/snapshot-verifier.ts +10 -3
- package/src/domain/environments.ts +25 -45
- package/src/domain/external-creation-attribution.ts +22 -0
- package/src/domain/file-owner.ts +67 -0
- package/src/domain/github-action-policies.ts +169 -0
- package/src/domain/github-repository-bindings.ts +295 -0
- package/src/domain/github-skill-source.ts +233 -0
- package/src/domain/governed-learning-slack-publication.ts +157 -0
- package/src/domain/insights-timing.ts +95 -0
- package/src/domain/insights-usage.ts +67 -0
- package/src/domain/insights.ts +270 -169
- package/src/domain/knowledge-files.ts +46 -0
- package/src/domain/knowledge-messages.test.ts +91 -0
- package/src/domain/knowledge-messages.ts +106 -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 +561 -0
- package/src/domain/knowledge-search.ts +256 -0
- package/src/domain/knowledge.ts +133 -0
- package/src/domain/mcp-account-bindings.ts +171 -0
- package/src/domain/mcp-account-routes.ts +121 -0
- package/src/domain/native-mcp-connection-admission.ts +14 -0
- package/src/domain/organization-integration-catalog.ts +88 -0
- package/src/domain/organization-membership-lifecycle.ts +10 -0
- package/src/domain/personal-connection-delegations.ts +758 -115
- package/src/domain/pr-review.ts +687 -0
- package/src/domain/resources.ts +182 -25
- package/src/domain/scheduled-task-access.ts +1052 -0
- package/src/domain/scheduled-tasks.ts +1952 -215
- package/src/domain/session-connection-accounts.ts +67 -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 +55 -0
- package/src/domain/session-tool-policy.ts +443 -19
- package/src/domain/sessions.ts +3417 -619
- package/src/domain/skill-imports.ts +137 -17
- package/src/domain/skill-search.ts +221 -0
- package/src/domain/skills.ts +104 -0
- package/src/domain/slack-bot.ts +128 -3
- package/src/domain/workspace-members.ts +34 -2
- package/src/editable-artifact-live/application.ts +29 -4
- package/src/editable-artifact-live/ports.ts +0 -1
- package/src/editable-artifact-live/server.ts +167 -13
- package/src/editable-artifact-live/ticket.ts +87 -6
- package/src/editable-artifact-live/types.ts +16 -2
- package/src/editable-artifact-live/wire.ts +0 -10
- package/src/index.ts +69 -4
- package/src/managed-auth-session-sets.ts +371 -0
- package/src/managed-session.ts +520 -2
- package/src/model-catalog.ts +873 -0
- package/src/product-usage-metrics.ts +101 -0
- package/src/rigs/index.ts +179 -63
- package/src/rigs/provider-images.ts +2 -3
- package/src/sandbox/fleet.ts +471 -59
- package/src/sandbox/routing.ts +160 -33
- package/src/sandbox/runtime-settings.ts +281 -0
- package/src/session-authorization.ts +262 -78
- package/src/site-session-origin.ts +22 -0
- package/src/transcription.ts +13 -1
- package/src/turn-surface.ts +45 -0
- package/src/user-presence.ts +134 -0
- package/dist/chunk-6WKPWE5S.js.map +0 -1
- package/dist/chunk-IBOEYG6N.js +0 -34
- package/dist/chunk-IBOEYG6N.js.map +0 -1
- package/dist/chunk-JJU6I5XD.js.map +0 -1
- package/dist/chunk-NYPMQ7NO.js.map +0 -1
- package/dist/domain/durable-learning-slack-publication.d.ts +0 -72
- package/dist/domain/packs.d.ts +0 -55
- package/src/domain/durable-learning-slack-publication.ts +0 -202
- package/src/domain/packs.ts +0 -556
|
@@ -3,11 +3,16 @@ import {
|
|
|
3
3
|
SessionAuthorizationDecision,
|
|
4
4
|
SessionAuthorizationListScope,
|
|
5
5
|
type AccessGrant,
|
|
6
|
+
type SessionAgentAccess,
|
|
7
|
+
type SessionAgentAccessViewer,
|
|
6
8
|
type SessionAuthorizationOperation,
|
|
7
9
|
type SessionAuthorizationSurface,
|
|
8
10
|
type SessionAuthorizationTarget,
|
|
11
|
+
type SessionScopeSubjectId,
|
|
9
12
|
} from "@opengeni/contracts";
|
|
13
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
10
14
|
import {
|
|
15
|
+
currentSessionRlsActorIdentityKey,
|
|
11
16
|
getSessionAuthorityProjection,
|
|
12
17
|
getSession,
|
|
13
18
|
getSessionTurnForAttempt,
|
|
@@ -20,6 +25,77 @@ import type { AppDependencies } from "./dependencies";
|
|
|
20
25
|
|
|
21
26
|
export type SessionAuthorizationDependencies = Pick<AppDependencies, "db" | "sessionAuthorization">;
|
|
22
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Identical caller-session and attempt reads shared by the authorization
|
|
30
|
+
* checks of one first-party MCP request. Phases bound reuse to checks that run
|
|
31
|
+
* back to back: the route's own checks, then only checks a tool handler starts
|
|
32
|
+
* synchronously on entry. Anything later (a re-check after a wait or a write)
|
|
33
|
+
* reads fresh. Results are keyed by database handle and session RLS actor, so
|
|
34
|
+
* a read is only reused under the exact visibility it was made with.
|
|
35
|
+
*/
|
|
36
|
+
type SessionAuthorizationReads = {
|
|
37
|
+
phase: "route" | "handoff" | "dispatch" | "closed";
|
|
38
|
+
readonly results: WeakMap<Database, Map<string, unknown>>;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const sessionAuthorizationReads = new AsyncLocalStorage<SessionAuthorizationReads>();
|
|
42
|
+
|
|
43
|
+
/** Scope read reuse to one request; it never outlives `fn`. */
|
|
44
|
+
export async function withSessionAuthorizationReadReuse<T>(
|
|
45
|
+
fn: (reuse: { handOffToToolDispatch(): void }) => Promise<T>,
|
|
46
|
+
): Promise<T> {
|
|
47
|
+
const reads: SessionAuthorizationReads = { phase: "route", results: new WeakMap() };
|
|
48
|
+
const handOffToToolDispatch = () => {
|
|
49
|
+
if (reads.phase === "route") reads.phase = "handoff";
|
|
50
|
+
};
|
|
51
|
+
try {
|
|
52
|
+
return await sessionAuthorizationReads.run(reads, () => fn({ handOffToToolDispatch }));
|
|
53
|
+
} finally {
|
|
54
|
+
reads.phase = "closed";
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Invoke a tool handler so that authorization checks it starts before its
|
|
60
|
+
* first await reuse the route's reads. Later checks, and every other dispatch
|
|
61
|
+
* in the same request, read fresh.
|
|
62
|
+
*/
|
|
63
|
+
export function dispatchWithSessionAuthorizationReadReuse<T>(handler: () => T): T {
|
|
64
|
+
const reads = sessionAuthorizationReads.getStore();
|
|
65
|
+
if (reads?.phase !== "handoff") return handler();
|
|
66
|
+
reads.phase = "dispatch";
|
|
67
|
+
try {
|
|
68
|
+
return handler();
|
|
69
|
+
} finally {
|
|
70
|
+
reads.phase = "closed";
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Call before the first await, so the phase is the one the check started in. */
|
|
75
|
+
function claimSessionAuthorizationReads(): SessionAuthorizationReads | null {
|
|
76
|
+
const reads = sessionAuthorizationReads.getStore();
|
|
77
|
+
return reads?.phase === "route" || reads?.phase === "dispatch" ? reads : null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function reusableAuthorizationRead<T>(
|
|
81
|
+
reads: SessionAuthorizationReads | null,
|
|
82
|
+
db: Database,
|
|
83
|
+
key: readonly string[],
|
|
84
|
+
read: () => Promise<T>,
|
|
85
|
+
): Promise<T> {
|
|
86
|
+
if (!reads) return await read();
|
|
87
|
+
const scopedKey = JSON.stringify([...key, currentSessionRlsActorIdentityKey()]);
|
|
88
|
+
let results = reads.results.get(db);
|
|
89
|
+
if (results?.has(scopedKey)) return results.get(scopedKey) as T;
|
|
90
|
+
const value = await read();
|
|
91
|
+
if (!results) {
|
|
92
|
+
results = new Map();
|
|
93
|
+
reads.results.set(db, results);
|
|
94
|
+
}
|
|
95
|
+
results.set(scopedKey, value);
|
|
96
|
+
return value;
|
|
97
|
+
}
|
|
98
|
+
|
|
23
99
|
/** Maximum time an omitted host hint leaves a live session stream unchecked. */
|
|
24
100
|
export const SESSION_AUTHORIZATION_DEFAULT_REAUTHORIZE_MS = 15_000;
|
|
25
101
|
|
|
@@ -48,17 +124,85 @@ export type ResolvedSessionAuthorization = {
|
|
|
48
124
|
reauthorizeAfterMs: number | null;
|
|
49
125
|
};
|
|
50
126
|
|
|
127
|
+
/** The frozen agent-access facts of one session, as the pairwise rule sees them. */
|
|
128
|
+
export type SessionAgentAccessFacts = {
|
|
129
|
+
agentAccess: SessionAgentAccess;
|
|
130
|
+
scopeSubjectId: SessionScopeSubjectId | null;
|
|
131
|
+
};
|
|
132
|
+
|
|
51
133
|
type ResolvedSessionAuthorizationActor = {
|
|
52
134
|
actor: SessionAuthorizationActor;
|
|
53
135
|
callerParentSessionId: string | null;
|
|
136
|
+
/** The caller session's own access facts; null for non-agent principals. */
|
|
137
|
+
callerAccess: SessionAgentAccessFacts | null;
|
|
54
138
|
};
|
|
55
139
|
|
|
140
|
+
function sameScopeSubject(
|
|
141
|
+
left: SessionScopeSubjectId | null,
|
|
142
|
+
right: SessionScopeSubjectId | null,
|
|
143
|
+
): boolean {
|
|
144
|
+
return left !== null && right !== null && left === right;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* The agent-to-agent reach rule (migration 0427) for one caller/target pair
|
|
149
|
+
* that live in DIFFERENT root trees. A caller always keeps its own tree, so
|
|
150
|
+
* this is never consulted for same-root access. Only the caller's task scope
|
|
151
|
+
* restricts outgoing reach. The target's visibility and ownership remain
|
|
152
|
+
* independently enforced; its agent scope does not block incoming access.
|
|
153
|
+
*/
|
|
154
|
+
export function agentAccessPermitsCrossTreeAccess(
|
|
155
|
+
caller: SessionAgentAccessFacts,
|
|
156
|
+
target: SessionAgentAccessFacts,
|
|
157
|
+
): boolean {
|
|
158
|
+
if (caller.agentAccess === "session") return false;
|
|
159
|
+
if (caller.agentAccess === "user") {
|
|
160
|
+
return sameScopeSubject(caller.scopeSubjectId, target.scopeSubjectId);
|
|
161
|
+
}
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* The in-database list scope for one calling attempt. A `session` caller is
|
|
167
|
+
* pinned to its own root tree when the host does not narrow it. Every caller
|
|
168
|
+
* carries the viewer so `sessionAuthorizationScopeFilter` intersects the host
|
|
169
|
+
* scope with the same pairwise rule in SQL, including for session callers.
|
|
170
|
+
*/
|
|
171
|
+
export function agentAccessListScopeForViewer(
|
|
172
|
+
viewer: SessionAgentAccessViewer,
|
|
173
|
+
hostScope: SessionAuthorizationListScope | null = null,
|
|
174
|
+
): SessionAuthorizationListScope {
|
|
175
|
+
if (viewer.agentAccess === "session" && (!hostScope || hostScope.kind === "all")) {
|
|
176
|
+
return {
|
|
177
|
+
kind: "scoped",
|
|
178
|
+
rootSessionIds: [viewer.callerRootSessionId],
|
|
179
|
+
sessionIds: [],
|
|
180
|
+
agentAccessViewer: viewer,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
if (!hostScope || hostScope.kind === "all") {
|
|
184
|
+
return { kind: "all", agentAccessViewer: viewer };
|
|
185
|
+
}
|
|
186
|
+
return {
|
|
187
|
+
kind: "scoped",
|
|
188
|
+
rootSessionIds: [...new Set(hostScope.rootSessionIds)],
|
|
189
|
+
sessionIds: [...new Set(hostScope.sessionIds)],
|
|
190
|
+
agentAccessViewer: viewer,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
56
194
|
type ResolvedSessionAuthorizationTarget = {
|
|
57
195
|
target: SessionAuthorizationTarget;
|
|
58
196
|
parentSessionId: string | null;
|
|
59
197
|
};
|
|
60
198
|
|
|
61
|
-
|
|
199
|
+
/**
|
|
200
|
+
* Whether a grant acts with live agent-attempt authority: an explicit
|
|
201
|
+
* `agent_attempt` principal kind, or (for legacy tokens without one) any
|
|
202
|
+
* worker-signed exact attempt claim. Shared by the session seam and by the
|
|
203
|
+
* narrowing fences that let a human widen a session an agent may only narrow.
|
|
204
|
+
*/
|
|
205
|
+
export function grantHasAgentAttemptAuthority(grant: AccessGrant): boolean {
|
|
62
206
|
const hasAgentAttemptClaim =
|
|
63
207
|
grant.metadata?.["turnId"] !== undefined ||
|
|
64
208
|
grant.metadata?.["attemptId"] !== undefined ||
|
|
@@ -67,52 +211,16 @@ function grantHasAgentAttemptAuthority(grant: AccessGrant): boolean {
|
|
|
67
211
|
}
|
|
68
212
|
|
|
69
213
|
/**
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
214
|
+
* Live agent attempts may address any workspace session that later private,
|
|
215
|
+
* Slack-owner, and optional host checks still allow. Parent/child lineage is
|
|
216
|
+
* not an access deny. Cross-session projections stay exact-target so a peer
|
|
217
|
+
* read does not receive another session's derived metadata.
|
|
74
218
|
*/
|
|
75
|
-
|
|
76
|
-
"session.read",
|
|
77
|
-
"session.events.read",
|
|
78
|
-
"session.stream.read",
|
|
79
|
-
"session.turns.read",
|
|
80
|
-
"session.queue.read",
|
|
81
|
-
"session.composer.read",
|
|
82
|
-
"session.lineage.read",
|
|
83
|
-
"session.capture.read",
|
|
84
|
-
"session.files.read",
|
|
85
|
-
"session.git.read",
|
|
86
|
-
"session.terminal.read",
|
|
87
|
-
"session.viewer.read",
|
|
88
|
-
"session.goal.read",
|
|
89
|
-
"session.human_input.read",
|
|
90
|
-
]);
|
|
91
|
-
|
|
92
|
-
function enforceAgentSessionHierarchy(
|
|
219
|
+
function relatedSessionAccessForAgentAttempt(
|
|
93
220
|
actor: Extract<SessionAuthorizationActor, { kind: "agent_attempt" }>,
|
|
94
|
-
|
|
95
|
-
target: ResolvedSessionAuthorizationTarget,
|
|
96
|
-
operation: SessionAuthorizationOperation,
|
|
221
|
+
targetSessionId: string,
|
|
97
222
|
): "target" | "root" {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
// A manager may inspect and operate an immediate child. Existing permission
|
|
101
|
-
// and host-policy checks still apply; lineage never grants a capability.
|
|
102
|
-
if (target.parentSessionId === actor.callerSessionId) return "target";
|
|
103
|
-
|
|
104
|
-
// A child may report to and inspect its immediate parent, but cannot mutate
|
|
105
|
-
// the parent except through the canonical machine-input message boundary.
|
|
106
|
-
if (callerParentSessionId === target.target.sessionId) {
|
|
107
|
-
if (operation === "session.append" || AGENT_PARENT_READ_OPERATIONS.has(operation)) {
|
|
108
|
-
return "target";
|
|
109
|
-
}
|
|
110
|
-
throw new SessionAuthorizationDeniedError("forbidden");
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// Siblings, skipped generations, other branches, and unrelated roots are
|
|
114
|
-
// never cross-session authority for a live agent attempt.
|
|
115
|
-
throw new SessionAuthorizationDeniedError("forbidden");
|
|
223
|
+
return targetSessionId === actor.callerSessionId ? "root" : "target";
|
|
116
224
|
}
|
|
117
225
|
|
|
118
226
|
export function sessionRlsActorForAuthorization(
|
|
@@ -143,7 +251,8 @@ export async function requireLiveAgentAttemptAuthorization(
|
|
|
143
251
|
grant: AccessGrant,
|
|
144
252
|
callerSessionId: string,
|
|
145
253
|
): Promise<Extract<SessionAuthorizationActor, { kind: "agent_attempt" }>> {
|
|
146
|
-
const
|
|
254
|
+
const reads = claimSessionAuthorizationReads();
|
|
255
|
+
const { actor } = await resolveSessionAuthorizationActor(db, grant, reads);
|
|
147
256
|
if (actor.kind !== "agent_attempt" || actor.callerSessionId !== callerSessionId) {
|
|
148
257
|
throw new SessionAuthorizationDeniedError("caller_stale");
|
|
149
258
|
}
|
|
@@ -170,16 +279,31 @@ export async function requireSessionAuthorization(
|
|
|
170
279
|
surface: SessionAuthorizationSurface;
|
|
171
280
|
},
|
|
172
281
|
): Promise<ResolvedSessionAuthorization | null> {
|
|
282
|
+
const reads = claimSessionAuthorizationReads();
|
|
173
283
|
const port = deps.sessionAuthorization;
|
|
174
284
|
const isAgentAttempt = grantHasAgentAttemptAuthority(grant);
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
285
|
+
// Callers can supply a transaction handle. Both reads open nested RLS
|
|
286
|
+
// savepoints, which must not race on the same PostgreSQL connection.
|
|
287
|
+
const slackAccess = await getSlackInteractionSessionAccessForSession(deps.db, {
|
|
288
|
+
accountId: grant.accountId,
|
|
289
|
+
workspaceId: grant.workspaceId,
|
|
290
|
+
sessionId: input.sessionId,
|
|
291
|
+
});
|
|
292
|
+
const authority = await getSessionAuthorityProjection(
|
|
293
|
+
deps.db,
|
|
294
|
+
grant.workspaceId,
|
|
295
|
+
input.sessionId,
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
// The projection is read under the caller's own RLS context, so a missing
|
|
299
|
+
// session and one the caller may not see (another member's private session)
|
|
300
|
+
// are the same answer. Every request-facing surface refuses both here,
|
|
301
|
+
// before a route-specific read can observe the difference or fail on the
|
|
302
|
+
// absent row. In-process `core` callers keep the standalone null result and
|
|
303
|
+
// their own not-found handling.
|
|
304
|
+
if (!authority && input.surface !== "core") {
|
|
305
|
+
throw new SessionAuthorizationDeniedError("not_found");
|
|
306
|
+
}
|
|
183
307
|
|
|
184
308
|
// Preserve the standalone workspace-shared path. Private sessions continue
|
|
185
309
|
// through the durable actor and ownership checks even without a host port.
|
|
@@ -193,22 +317,29 @@ export async function requireSessionAuthorization(
|
|
|
193
317
|
}
|
|
194
318
|
if (!authority) throw new SessionAuthorizationDeniedError("not_found");
|
|
195
319
|
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
320
|
+
const resolvedActor = await resolveSessionAuthorizationActor(deps.db, grant, reads);
|
|
321
|
+
const resolvedTarget = await resolveSessionAuthorizationTarget(
|
|
322
|
+
deps.db,
|
|
323
|
+
grant,
|
|
324
|
+
input.sessionId,
|
|
325
|
+
reads,
|
|
326
|
+
);
|
|
200
327
|
const actor = resolvedActor.actor;
|
|
201
328
|
const target = resolvedTarget.target;
|
|
202
329
|
const agentRelatedSessionAccess =
|
|
203
330
|
actor.kind === "agent_attempt"
|
|
204
|
-
?
|
|
205
|
-
actor,
|
|
206
|
-
resolvedActor.callerParentSessionId,
|
|
207
|
-
resolvedTarget,
|
|
208
|
-
input.operation,
|
|
209
|
-
)
|
|
331
|
+
? relatedSessionAccessForAgentAttempt(actor, target.sessionId)
|
|
210
332
|
: null;
|
|
211
333
|
|
|
334
|
+
// Tool approvals are a human decision. An agent attempt may answer another
|
|
335
|
+
// session's structured human-input request (`session.human_input.write`,
|
|
336
|
+
// through `session_human_input_respond`) but may never approve or reject a
|
|
337
|
+
// pending tool approval on any session, including a child it spawned; an
|
|
338
|
+
// embedding-host port cannot widen this.
|
|
339
|
+
if (actor.kind === "agent_attempt" && input.operation === "session.approval.write") {
|
|
340
|
+
throw new SessionAuthorizationDeniedError("forbidden");
|
|
341
|
+
}
|
|
342
|
+
|
|
212
343
|
if (authority.visibility === "user_private") {
|
|
213
344
|
const allowed =
|
|
214
345
|
authority.ownerSubjectId !== null &&
|
|
@@ -224,6 +355,22 @@ export async function requireSessionAuthorization(
|
|
|
224
355
|
: actor.callerRootSessionId === target.rootSessionId;
|
|
225
356
|
if (!allowed) throw new SessionAuthorizationDeniedError("forbidden");
|
|
226
357
|
}
|
|
358
|
+
// Agent-access scope (migration 0427): an attempt always keeps its own root
|
|
359
|
+
// tree; across trees only its own outgoing task scope restricts reach.
|
|
360
|
+
// User matching reads canonical target identity, not target task scope.
|
|
361
|
+
// Private ownership above and the host authorization below still apply.
|
|
362
|
+
if (actor.kind === "agent_attempt" && target.rootSessionId !== actor.callerRootSessionId) {
|
|
363
|
+
const callerAccess = resolvedActor.callerAccess;
|
|
364
|
+
if (
|
|
365
|
+
!callerAccess ||
|
|
366
|
+
!agentAccessPermitsCrossTreeAccess(callerAccess, {
|
|
367
|
+
agentAccess: authority.agentAccess,
|
|
368
|
+
scopeSubjectId: authority.scopeSubjectId,
|
|
369
|
+
})
|
|
370
|
+
) {
|
|
371
|
+
throw new SessionAuthorizationDeniedError("forbidden");
|
|
372
|
+
}
|
|
373
|
+
}
|
|
227
374
|
if (!port) {
|
|
228
375
|
return {
|
|
229
376
|
actor,
|
|
@@ -270,13 +417,24 @@ export async function requireSessionAuthorizationListScope(
|
|
|
270
417
|
grant: AccessGrant,
|
|
271
418
|
surface: SessionAuthorizationSurface,
|
|
272
419
|
): Promise<SessionAuthorizationListScope | null> {
|
|
420
|
+
const reads = claimSessionAuthorizationReads();
|
|
273
421
|
const port = deps.sessionAuthorization;
|
|
274
422
|
const isAgentAttempt = grantHasAgentAttemptAuthority(grant);
|
|
275
423
|
if (!port && !isAgentAttempt) return null;
|
|
276
|
-
const { actor } = await resolveSessionAuthorizationActor(deps.db, grant);
|
|
277
|
-
// Standalone agents
|
|
278
|
-
//
|
|
279
|
-
|
|
424
|
+
const { actor, callerAccess } = await resolveSessionAuthorizationActor(deps.db, grant, reads);
|
|
425
|
+
// Standalone agents retain compact workspace discovery only while the signed
|
|
426
|
+
// caller attempt is still the exact live attempt, and only within their own
|
|
427
|
+
// declared agent-access reach (migration 0427). The viewer is derived from
|
|
428
|
+
// the caller session row and applied as one SQL predicate by every list.
|
|
429
|
+
const viewer: SessionAgentAccessViewer | null =
|
|
430
|
+
actor.kind === "agent_attempt" && callerAccess
|
|
431
|
+
? {
|
|
432
|
+
callerRootSessionId: actor.callerRootSessionId,
|
|
433
|
+
agentAccess: callerAccess.agentAccess,
|
|
434
|
+
scopeSubjectId: callerAccess.scopeSubjectId,
|
|
435
|
+
}
|
|
436
|
+
: null;
|
|
437
|
+
if (!port) return viewer ? agentAccessListScopeForViewer(viewer) : null;
|
|
280
438
|
let rawScope: unknown;
|
|
281
439
|
try {
|
|
282
440
|
rawScope = await port.resolveListScope({
|
|
@@ -292,20 +450,31 @@ export async function requireSessionAuthorizationListScope(
|
|
|
292
450
|
if (!parsed.success) {
|
|
293
451
|
throw new SessionAuthorizationUnavailableError({ cause: parsed.error });
|
|
294
452
|
}
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
453
|
+
// A host never supplies the viewer: OpenGeni resolved it above from durable
|
|
454
|
+
// state, so a host-returned value is dropped before the intersection.
|
|
455
|
+
const hostScope: SessionAuthorizationListScope =
|
|
456
|
+
parsed.data.kind === "all"
|
|
457
|
+
? { kind: "all" }
|
|
458
|
+
: {
|
|
459
|
+
kind: "scoped",
|
|
460
|
+
rootSessionIds: [...new Set(parsed.data.rootSessionIds)],
|
|
461
|
+
sessionIds: [...new Set(parsed.data.sessionIds)],
|
|
462
|
+
};
|
|
463
|
+
return viewer ? agentAccessListScopeForViewer(viewer, hostScope) : hostScope;
|
|
301
464
|
}
|
|
302
465
|
|
|
303
466
|
async function resolveSessionAuthorizationTarget(
|
|
304
467
|
db: Database,
|
|
305
468
|
grant: AccessGrant,
|
|
306
469
|
sessionId: string,
|
|
470
|
+
reads: SessionAuthorizationReads | null,
|
|
307
471
|
): Promise<ResolvedSessionAuthorizationTarget> {
|
|
308
|
-
const session = await
|
|
472
|
+
const session = await reusableAuthorizationRead(
|
|
473
|
+
reads,
|
|
474
|
+
db,
|
|
475
|
+
["session", grant.workspaceId, sessionId],
|
|
476
|
+
() => getSession(db, grant.workspaceId, sessionId),
|
|
477
|
+
);
|
|
309
478
|
if (!session || session.accountId !== grant.accountId) {
|
|
310
479
|
throw new SessionAuthorizationDeniedError("not_found");
|
|
311
480
|
}
|
|
@@ -317,6 +486,7 @@ async function resolveSessionAuthorizationTarget(
|
|
|
317
486
|
async function resolveSessionAuthorizationActor(
|
|
318
487
|
db: Database,
|
|
319
488
|
grant: AccessGrant,
|
|
489
|
+
reads: SessionAuthorizationReads | null,
|
|
320
490
|
): Promise<ResolvedSessionAuthorizationActor> {
|
|
321
491
|
const callerSessionId = grant.metadata?.["sessionId"];
|
|
322
492
|
const turnId = grant.metadata?.["turnId"];
|
|
@@ -331,6 +501,7 @@ async function resolveSessionAuthorizationActor(
|
|
|
331
501
|
...(grant.subjectLabel ? { subjectLabel: grant.subjectLabel } : {}),
|
|
332
502
|
}),
|
|
333
503
|
callerParentSessionId: null,
|
|
504
|
+
callerAccess: null,
|
|
334
505
|
};
|
|
335
506
|
}
|
|
336
507
|
if (
|
|
@@ -343,10 +514,19 @@ async function resolveSessionAuthorizationActor(
|
|
|
343
514
|
) {
|
|
344
515
|
throw new SessionAuthorizationDeniedError("caller_stale");
|
|
345
516
|
}
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
517
|
+
// These scoped reads can also open nested savepoints on a transaction handle.
|
|
518
|
+
const callerSession = await reusableAuthorizationRead(
|
|
519
|
+
reads,
|
|
520
|
+
db,
|
|
521
|
+
["session", grant.workspaceId, callerSessionId],
|
|
522
|
+
() => getSession(db, grant.workspaceId, callerSessionId),
|
|
523
|
+
);
|
|
524
|
+
const turn = await reusableAuthorizationRead(
|
|
525
|
+
reads,
|
|
526
|
+
db,
|
|
527
|
+
["turn", grant.workspaceId, callerSessionId, attemptId],
|
|
528
|
+
() => getSessionTurnForAttempt(db, grant.workspaceId, callerSessionId, attemptId),
|
|
529
|
+
);
|
|
350
530
|
if (
|
|
351
531
|
!callerSession ||
|
|
352
532
|
callerSession.accountId !== grant.accountId ||
|
|
@@ -373,5 +553,9 @@ async function resolveSessionAuthorizationActor(
|
|
|
373
553
|
(turn.initiator.kind === "subject" ? turn.initiator.subjectId : null),
|
|
374
554
|
}),
|
|
375
555
|
callerParentSessionId: callerSession.parentSessionId,
|
|
556
|
+
callerAccess: {
|
|
557
|
+
agentAccess: callerSession.agentAccess,
|
|
558
|
+
scopeSubjectId: callerSession.scopeSubjectId,
|
|
559
|
+
},
|
|
376
560
|
};
|
|
377
561
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
|
|
3
|
+
export type SiteSessionOrigin = { siteId: string; title: string };
|
|
4
|
+
const origins = new AsyncLocalStorage<SiteSessionOrigin>();
|
|
5
|
+
|
|
6
|
+
/** Request-local provenance, set only after the API validates a published Site version. */
|
|
7
|
+
export function withSiteSessionOrigin<T>(origin: SiteSessionOrigin, run: () => T): T {
|
|
8
|
+
return origins.run(origin, run);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function sessionCreationMetadata(
|
|
12
|
+
metadata: Record<string, unknown>,
|
|
13
|
+
): Record<string, unknown> {
|
|
14
|
+
const { _opengeniSiteOrigin: _ignored, ...rest } = metadata;
|
|
15
|
+
const origin = origins.getStore();
|
|
16
|
+
return origin ? { ...rest, _opengeniSiteOrigin: origin } : rest;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** The validated Site origin of the current request, if any. Provenance only. */
|
|
20
|
+
export function currentSiteSessionOrigin(): SiteSessionOrigin | undefined {
|
|
21
|
+
return origins.getStore();
|
|
22
|
+
}
|
package/src/transcription.ts
CHANGED
|
@@ -31,6 +31,9 @@ export type TranscriptionRequest = {
|
|
|
31
31
|
providerDeadlineAt?: Date | undefined;
|
|
32
32
|
/** Exact provider selected before a resumable segment is first sent upstream. */
|
|
33
33
|
providerId?: string | undefined;
|
|
34
|
+
preferredProvider?: string | null | undefined;
|
|
35
|
+
fallbackEnabled?: boolean | undefined;
|
|
36
|
+
excludedProviders?: readonly string[] | undefined;
|
|
34
37
|
};
|
|
35
38
|
|
|
36
39
|
export type TranscriptionResult = TranscribeAudioResponse & {
|
|
@@ -44,18 +47,22 @@ export class TranscriptionServiceError extends Error {
|
|
|
44
47
|
readonly code: VoiceInputErrorCode;
|
|
45
48
|
readonly status: number;
|
|
46
49
|
readonly retryable: boolean;
|
|
50
|
+
/** Explicit rejection before any transcription result; safe to try another provider. */
|
|
51
|
+
readonly fallbackSafe: boolean;
|
|
47
52
|
|
|
48
53
|
constructor(input: {
|
|
49
54
|
code: VoiceInputErrorCode;
|
|
50
55
|
message: string;
|
|
51
56
|
status?: number;
|
|
52
57
|
retryable?: boolean;
|
|
58
|
+
fallbackSafe?: boolean;
|
|
53
59
|
}) {
|
|
54
60
|
super(input.message);
|
|
55
61
|
this.name = "TranscriptionServiceError";
|
|
56
62
|
this.code = input.code;
|
|
57
63
|
this.status = input.status ?? statusForVoiceInputError(input.code);
|
|
58
64
|
this.retryable = input.retryable ?? false;
|
|
65
|
+
this.fallbackSafe = input.fallbackSafe ?? false;
|
|
59
66
|
}
|
|
60
67
|
}
|
|
61
68
|
|
|
@@ -88,6 +95,10 @@ export function statusForVoiceInputError(code: VoiceInputErrorCode): number {
|
|
|
88
95
|
|
|
89
96
|
/** Optional workspace scope for readiness checks during provider selection. */
|
|
90
97
|
export type TranscriptionAvailabilityContext = {
|
|
98
|
+
afterProvider?: string | undefined;
|
|
99
|
+
preferredProvider?: string | null | undefined;
|
|
100
|
+
fallbackEnabled?: boolean | undefined;
|
|
101
|
+
excludedProviders?: readonly string[] | undefined;
|
|
91
102
|
workspaceId?: string | undefined;
|
|
92
103
|
subjectId?: string | undefined;
|
|
93
104
|
};
|
|
@@ -95,7 +106,8 @@ export type TranscriptionAvailabilityContext = {
|
|
|
95
106
|
/**
|
|
96
107
|
* Extensible transcription provider port. Implementations own credentials and
|
|
97
108
|
* upstream request shape. Selection happens before audio is sent; providers must
|
|
98
|
-
*
|
|
109
|
+
* only fall back after an explicit rejection and before any successful or
|
|
110
|
+
* uncertain provider attempt. Recording persistence owns that eligibility.
|
|
99
111
|
*/
|
|
100
112
|
export type TranscriptionProvider = {
|
|
101
113
|
readonly id: string;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { AccessGrant, SessionTurnSurface } from "@opengeni/contracts";
|
|
2
|
+
import type { AccessGrantAuthorization } from "./access";
|
|
3
|
+
import { currentSiteSessionOrigin } from "./site-session-origin";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Resolve the content-free product surface a request entered through.
|
|
7
|
+
*
|
|
8
|
+
* The result is an analytics label frozen on the accepted turn. It never
|
|
9
|
+
* authorizes anything, so it reads trusted provenance only where it already
|
|
10
|
+
* exists: an explicit entry-point choice (Slack, automations, maintenance), the
|
|
11
|
+
* API-validated Site scope, and the access path the grant was resolved from.
|
|
12
|
+
* A grant whose path this function cannot place yields null rather than a
|
|
13
|
+
* guess.
|
|
14
|
+
*/
|
|
15
|
+
export function resolveTurnSurface(input: {
|
|
16
|
+
grant: AccessGrant;
|
|
17
|
+
authorization?: AccessGrantAuthorization | null | undefined;
|
|
18
|
+
/** An entry point that knows its surface (for example Slack) passes it here. */
|
|
19
|
+
requested?: SessionTurnSurface | null | undefined;
|
|
20
|
+
}): SessionTurnSurface | null {
|
|
21
|
+
if (input.requested) return input.requested;
|
|
22
|
+
if (currentSiteSessionOrigin()) return "site";
|
|
23
|
+
const { grant } = input;
|
|
24
|
+
const metadata = grant.metadata ?? {};
|
|
25
|
+
if (
|
|
26
|
+
grant.principalKind === "agent_attempt" ||
|
|
27
|
+
metadata.turnId !== undefined ||
|
|
28
|
+
metadata.attemptId !== undefined
|
|
29
|
+
) {
|
|
30
|
+
return "agent";
|
|
31
|
+
}
|
|
32
|
+
if (metadata.externalActor !== undefined) return "embedded";
|
|
33
|
+
if (metadata.mcpOAuth === true) return "mcp";
|
|
34
|
+
if (grant.principalKind === "api_key" || grant.principalKind === "configured_key") {
|
|
35
|
+
return "api_key";
|
|
36
|
+
}
|
|
37
|
+
if (metadata.delegated === true || grant.serviceInitiator) return "embedded";
|
|
38
|
+
if (
|
|
39
|
+
input.authorization?.canonicalManagedHumanSession ||
|
|
40
|
+
input.authorization?.canonicalLocalHumanSession
|
|
41
|
+
) {
|
|
42
|
+
return "web";
|
|
43
|
+
}
|
|
44
|
+
return grant.principalKind === "human_session" ? "web" : null;
|
|
45
|
+
}
|