@opengeni/core 0.24.1 → 0.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/canonical-human-identities.d.ts +12 -0
- package/dist/canonical-human-identities.js +23 -0
- package/dist/canonical-human-identities.js.map +1 -0
- package/dist/chunk-IBOEYG6N.js +34 -0
- package/dist/chunk-IBOEYG6N.js.map +1 -0
- package/dist/dependencies.d.ts +5 -1
- package/dist/domain/conversation-integrations.d.ts +225 -0
- package/dist/domain/fiken.d.ts +35 -0
- package/dist/domain/sessions.d.ts +11 -3
- package/dist/domain/video-generation-capabilities.d.ts +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1697 -694
- package/dist/index.js.map +1 -1
- package/dist/managed-session.d.ts +5 -1
- package/dist/sandbox/fleet.d.ts +8 -5
- package/dist/session-authorization.d.ts +3 -1
- package/dist/transcription.d.ts +5 -0
- package/package.json +14 -10
- package/src/access/index.ts +14 -2
- package/src/canonical-human-identities.ts +27 -0
- package/src/dependencies.ts +5 -1
- package/src/domain/capabilities.ts +220 -283
- package/src/domain/conversation-integrations.ts +1238 -0
- package/src/domain/fiken.ts +88 -0
- package/src/domain/insights.ts +27 -17
- package/src/domain/scheduled-tasks.ts +16 -0
- package/src/domain/sessions.ts +122 -18
- package/src/domain/video-generation-capabilities.ts +37 -2
- package/src/index.ts +2 -0
- package/src/managed-session.ts +19 -2
- package/src/sandbox/fleet.ts +72 -66
- package/src/session-authorization.ts +187 -55
- package/src/transcription.ts +5 -0
package/src/managed-session.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { Context } from "hono";
|
|
2
2
|
import type { ManagedAuth } from "./managed-auth-type";
|
|
3
|
+
import type { Database } from "@opengeni/db";
|
|
4
|
+
import { validateCanonicalHumanSession } from "@opengeni/db/canonical-human-identities";
|
|
3
5
|
|
|
4
6
|
/**
|
|
5
7
|
* Read a Better Auth session without bypassing its sliding-cookie renewal.
|
|
@@ -8,7 +10,11 @@ import type { ManagedAuth } from "./managed-auth-type";
|
|
|
8
10
|
* Programmatic callers must explicitly request and forward the returned cookie
|
|
9
11
|
* headers; the HTTP handler does this automatically, but direct API calls do not.
|
|
10
12
|
*/
|
|
11
|
-
export async function getManagedSession(
|
|
13
|
+
export async function getManagedSession(
|
|
14
|
+
c: Context,
|
|
15
|
+
auth: ManagedAuth,
|
|
16
|
+
options?: { db?: Database; allowIdentityRecovery?: boolean },
|
|
17
|
+
) {
|
|
12
18
|
const result = await auth.api.getSession({
|
|
13
19
|
headers: c.req.raw.headers,
|
|
14
20
|
returnHeaders: true,
|
|
@@ -18,7 +24,18 @@ export async function getManagedSession(c: Context, auth: ManagedAuth) {
|
|
|
18
24
|
c.header("set-cookie", cookie, { append: true });
|
|
19
25
|
}
|
|
20
26
|
|
|
21
|
-
|
|
27
|
+
const session = result.response;
|
|
28
|
+
if (!session?.user || !options?.db) return session;
|
|
29
|
+
const authSessionId = session.session?.id;
|
|
30
|
+
if (typeof authSessionId !== "string") return null;
|
|
31
|
+
const valid = await validateCanonicalHumanSession(options.db, {
|
|
32
|
+
authSessionId,
|
|
33
|
+
authUserId: session.user.id,
|
|
34
|
+
...(options.allowIdentityRecovery === undefined
|
|
35
|
+
? {}
|
|
36
|
+
: { allowRecovery: options.allowIdentityRecovery }),
|
|
37
|
+
});
|
|
38
|
+
return valid ? session : null;
|
|
22
39
|
}
|
|
23
40
|
|
|
24
41
|
function setCookieHeaders(headers: Headers): string[] {
|
package/src/sandbox/fleet.ts
CHANGED
|
@@ -37,7 +37,6 @@ import {
|
|
|
37
37
|
type SelfhostedRelayConfig,
|
|
38
38
|
type SelfhostedOpStreamDeps,
|
|
39
39
|
} from "@opengeni/runtime/sandbox";
|
|
40
|
-
import { HTTPException } from "hono/http-exception";
|
|
41
40
|
import { relayConfigFromSettings } from "./routing";
|
|
42
41
|
|
|
43
42
|
export type FleetServices = {
|
|
@@ -69,8 +68,9 @@ export type FleetContext = {
|
|
|
69
68
|
|
|
70
69
|
/**
|
|
71
70
|
* Build a session-scoped {@link FleetContext}: load the session (workspace-
|
|
72
|
-
* scoped)
|
|
73
|
-
*
|
|
71
|
+
* scoped) and project its group backend/id. A backend:none session has no home
|
|
72
|
+
* box, but it may still discover, run on, and attach an owned Connected Machine.
|
|
73
|
+
* Shared
|
|
74
74
|
* by the worker-signed MCP fleet tools and the user-authenticated swap REST
|
|
75
75
|
* route so both resolve the SAME context (no drift). The `accountId`/`workspaceId`/
|
|
76
76
|
* `sessionId` come from the trusted grant/route; the backend + group id come from
|
|
@@ -81,11 +81,6 @@ export async function buildFleetContextForSession(
|
|
|
81
81
|
ctx: { accountId: string; workspaceId: string; sessionId: string },
|
|
82
82
|
): Promise<FleetContext> {
|
|
83
83
|
const session = await requireSession(deps.db, ctx.workspaceId, ctx.sessionId);
|
|
84
|
-
if (session.sandboxBackend === "none") {
|
|
85
|
-
throw new HTTPException(422, {
|
|
86
|
-
message: "this session has no sandbox (backend: none); the fleet is unavailable",
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
84
|
return {
|
|
90
85
|
accountId: ctx.accountId,
|
|
91
86
|
workspaceId: ctx.workspaceId,
|
|
@@ -229,9 +224,11 @@ async function probeEnrollment(
|
|
|
229
224
|
}
|
|
230
225
|
|
|
231
226
|
/**
|
|
232
|
-
* List the fleet: the session's own
|
|
233
|
-
* workspace's first-class selfhosted sandboxes (each probed for
|
|
234
|
-
* with an `active` marker derived from the session's active
|
|
227
|
+
* List the fleet: the session's own group box when it has one (a synthetic
|
|
228
|
+
* entry) + the workspace's first-class selfhosted sandboxes (each probed for
|
|
229
|
+
* liveness), each with an `active` marker derived from the session's active
|
|
230
|
+
* pointer. A backend:none session has no synthetic home entry; a null pointer
|
|
231
|
+
* then means no compute is attached.
|
|
235
232
|
*/
|
|
236
233
|
export async function listFleet(
|
|
237
234
|
services: FleetServices,
|
|
@@ -245,61 +242,63 @@ export async function listFleet(
|
|
|
245
242
|
|
|
246
243
|
const entries: FleetSandboxEntry[] = [];
|
|
247
244
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
groupLease.
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
groupLease.recovery.restore.status === "
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
245
|
+
if (ctx.sessionBackend !== "none") {
|
|
246
|
+
// The session's own group box (the default/home sandbox; null active pointer ==
|
|
247
|
+
// this box). A session/group row is not provider existence. Online requires a
|
|
248
|
+
// warm lease, observed provider existence, and verified workspace readiness.
|
|
249
|
+
const groupActive = pointer.activeSandboxId === null;
|
|
250
|
+
const groupLease = await readLease(db, ctx.workspaceId, ctx.sessionGroupId);
|
|
251
|
+
const groupOnline = Boolean(
|
|
252
|
+
groupLease?.liveness === "warm" &&
|
|
253
|
+
groupLease.recovery.provider.status === "exists" &&
|
|
254
|
+
groupLease.recovery.workspace.status === "ready",
|
|
255
|
+
);
|
|
256
|
+
const groupRecovering = Boolean(
|
|
257
|
+
groupLease &&
|
|
258
|
+
(groupLease.liveness === "warming" ||
|
|
259
|
+
groupLease.recovery.restore.status === "pending" ||
|
|
260
|
+
groupLease.recovery.restore.status === "restoring" ||
|
|
261
|
+
groupLease.recovery.restore.status === "verifying"),
|
|
262
|
+
);
|
|
263
|
+
const groupRecoveryUnavailable = Boolean(
|
|
264
|
+
groupLease &&
|
|
265
|
+
(groupLease.recovery.restore.status === "degraded" ||
|
|
266
|
+
groupLease.recovery.restore.status === "unrecoverable" ||
|
|
267
|
+
groupLease.recovery.workspace.status === "degraded" ||
|
|
268
|
+
groupLease.recovery.workspace.status === "unrecoverable"),
|
|
269
|
+
);
|
|
270
|
+
const groupOperationAvailability: FleetOperationAvailability = groupOnline
|
|
271
|
+
? "ready"
|
|
272
|
+
: groupRecoveryUnavailable
|
|
273
|
+
? "unavailable"
|
|
274
|
+
: groupRecovering
|
|
275
|
+
? "recovering"
|
|
276
|
+
: ctx.sessionBackend === "selfhosted"
|
|
277
|
+
? "unavailable"
|
|
278
|
+
: "wakeable";
|
|
279
|
+
entries.push({
|
|
280
|
+
id: ctx.sessionGroupId,
|
|
281
|
+
kind: ctx.sessionBackend === "selfhosted" ? "selfhosted" : "modal",
|
|
282
|
+
name: "session sandbox",
|
|
283
|
+
liveness: groupOnline ? "online" : groupRecovering ? "reconnecting" : "offline",
|
|
284
|
+
active: groupActive,
|
|
285
|
+
isSessionGroup: true,
|
|
286
|
+
enrollmentId: null,
|
|
287
|
+
attachable: groupOnline,
|
|
288
|
+
operationAvailability: groupOperationAvailability,
|
|
289
|
+
providerStatus: groupLease?.recovery.provider.status ?? "not_created",
|
|
290
|
+
leaseLiveness: groupLease?.liveness ?? null,
|
|
291
|
+
routeStatus: groupActive ? "attached" : "detached",
|
|
292
|
+
archiveStatus: groupLease?.recovery.archive.status ?? "none",
|
|
293
|
+
restoreStatus: groupLease?.recovery.restore.status ?? "not_required",
|
|
294
|
+
workspaceStatus: groupLease?.recovery.workspace.status ?? "unknown",
|
|
295
|
+
leaseEpoch: groupLease?.leaseEpoch ?? null,
|
|
296
|
+
routeEpoch: pointer.activeEpoch,
|
|
297
|
+
workspaceGeneration: groupLease?.workspaceGeneration ?? null,
|
|
298
|
+
archiveGeneration: groupLease?.archiveGeneration ?? null,
|
|
299
|
+
archiveComplete: groupLease?.archiveComplete ?? false,
|
|
300
|
+
});
|
|
301
|
+
}
|
|
303
302
|
|
|
304
303
|
// The workspace's first-class selfhosted sandboxes (enrolled machines). Probe
|
|
305
304
|
// each for liveness; a missing enrollment is offline.
|
|
@@ -374,6 +373,13 @@ async function resolveTarget(
|
|
|
374
373
|
> {
|
|
375
374
|
// The session's own group box → the default pointer (null).
|
|
376
375
|
if (target === ctx.sessionGroupId || target === "session" || target === "default") {
|
|
376
|
+
if (ctx.sessionBackend === "none") {
|
|
377
|
+
return {
|
|
378
|
+
ok: false,
|
|
379
|
+
reason: "this session has no home sandbox; attach a Connected Machine",
|
|
380
|
+
code: "unsupported_backend_context",
|
|
381
|
+
};
|
|
382
|
+
}
|
|
377
383
|
return { ok: true, targetSandboxId: null };
|
|
378
384
|
}
|
|
379
385
|
const sandbox = await getSandbox(services.db, ctx.workspaceId, target);
|
|
@@ -8,11 +8,13 @@ import {
|
|
|
8
8
|
type SessionAuthorizationTarget,
|
|
9
9
|
} from "@opengeni/contracts";
|
|
10
10
|
import {
|
|
11
|
+
getSessionAuthorityProjection,
|
|
11
12
|
getSession,
|
|
12
|
-
getSessionRootId,
|
|
13
13
|
getSessionTurnForAttempt,
|
|
14
14
|
getSlackInteractionSessionAccessForSession,
|
|
15
|
+
withSessionRlsActorContext,
|
|
15
16
|
type Database,
|
|
17
|
+
type SessionRlsActorContext,
|
|
16
18
|
} from "@opengeni/db";
|
|
17
19
|
import type { AppDependencies } from "./dependencies";
|
|
18
20
|
|
|
@@ -46,6 +48,91 @@ export type ResolvedSessionAuthorization = {
|
|
|
46
48
|
reauthorizeAfterMs: number | null;
|
|
47
49
|
};
|
|
48
50
|
|
|
51
|
+
type ResolvedSessionAuthorizationActor = {
|
|
52
|
+
actor: SessionAuthorizationActor;
|
|
53
|
+
callerParentSessionId: string | null;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
type ResolvedSessionAuthorizationTarget = {
|
|
57
|
+
target: SessionAuthorizationTarget;
|
|
58
|
+
parentSessionId: string | null;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
function grantHasAgentAttemptAuthority(grant: AccessGrant): boolean {
|
|
62
|
+
const hasAgentAttemptClaim =
|
|
63
|
+
grant.metadata?.["turnId"] !== undefined ||
|
|
64
|
+
grant.metadata?.["attemptId"] !== undefined ||
|
|
65
|
+
grant.metadata?.["executionGeneration"] !== undefined;
|
|
66
|
+
return grant.principalKind ? grant.principalKind === "agent_attempt" : hasAgentAttemptClaim;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Read-only access an immediate child may use against its parent. Upstream
|
|
71
|
+
* mutation is deliberately limited to `session.append`: letting a child Pause,
|
|
72
|
+
* Steer, or otherwise mutate its parent would let it influence siblings through
|
|
73
|
+
* the parent's recursive control and shared state.
|
|
74
|
+
*/
|
|
75
|
+
const AGENT_PARENT_READ_OPERATIONS = new Set<SessionAuthorizationOperation>([
|
|
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(
|
|
93
|
+
actor: Extract<SessionAuthorizationActor, { kind: "agent_attempt" }>,
|
|
94
|
+
callerParentSessionId: string | null,
|
|
95
|
+
target: ResolvedSessionAuthorizationTarget,
|
|
96
|
+
operation: SessionAuthorizationOperation,
|
|
97
|
+
): "target" | "root" {
|
|
98
|
+
if (target.target.sessionId === actor.callerSessionId) return "root";
|
|
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");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function sessionRlsActorForAuthorization(
|
|
119
|
+
authorization: ResolvedSessionAuthorization,
|
|
120
|
+
): SessionRlsActorContext {
|
|
121
|
+
return authorization.actor.kind === "agent_attempt"
|
|
122
|
+
? {
|
|
123
|
+
subjectId: authorization.actor.subjectId,
|
|
124
|
+
initiatingHumanSubjectId: authorization.actor.initiatingHumanSubjectId,
|
|
125
|
+
}
|
|
126
|
+
: { subjectId: authorization.actor.subjectId };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export async function withResolvedSessionAuthorization<T>(
|
|
130
|
+
authorization: ResolvedSessionAuthorization,
|
|
131
|
+
fn: () => Promise<T>,
|
|
132
|
+
): Promise<T> {
|
|
133
|
+
return await withSessionRlsActorContext(sessionRlsActorForAuthorization(authorization), fn);
|
|
134
|
+
}
|
|
135
|
+
|
|
49
136
|
/**
|
|
50
137
|
* Prove that a first-party request belongs to the exact currently active
|
|
51
138
|
* attempt of the named caller session. Unlike the optional embedding-host ACL
|
|
@@ -56,7 +143,7 @@ export async function requireLiveAgentAttemptAuthorization(
|
|
|
56
143
|
grant: AccessGrant,
|
|
57
144
|
callerSessionId: string,
|
|
58
145
|
): Promise<Extract<SessionAuthorizationActor, { kind: "agent_attempt" }>> {
|
|
59
|
-
const actor = await resolveSessionAuthorizationActor(db, grant);
|
|
146
|
+
const { actor } = await resolveSessionAuthorizationActor(db, grant);
|
|
60
147
|
if (actor.kind !== "agent_attempt" || actor.callerSessionId !== callerSessionId) {
|
|
61
148
|
throw new SessionAuthorizationDeniedError("caller_stale");
|
|
62
149
|
}
|
|
@@ -84,17 +171,52 @@ export async function requireSessionAuthorization(
|
|
|
84
171
|
},
|
|
85
172
|
): Promise<ResolvedSessionAuthorization | null> {
|
|
86
173
|
const port = deps.sessionAuthorization;
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
174
|
+
const isAgentAttempt = grantHasAgentAttemptAuthority(grant);
|
|
175
|
+
const [slackAccess, authority] = await Promise.all([
|
|
176
|
+
getSlackInteractionSessionAccessForSession(deps.db, {
|
|
177
|
+
accountId: grant.accountId,
|
|
178
|
+
workspaceId: grant.workspaceId,
|
|
179
|
+
sessionId: input.sessionId,
|
|
180
|
+
}),
|
|
181
|
+
getSessionAuthorityProjection(deps.db, grant.workspaceId, input.sessionId),
|
|
182
|
+
]);
|
|
183
|
+
|
|
184
|
+
// Preserve the standalone workspace-shared path. Private sessions continue
|
|
185
|
+
// through the durable actor and ownership checks even without a host port.
|
|
186
|
+
if (
|
|
187
|
+
!port &&
|
|
188
|
+
!isAgentAttempt &&
|
|
189
|
+
slackAccess?.visibility !== "private" &&
|
|
190
|
+
authority?.visibility !== "user_private"
|
|
191
|
+
) {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
if (!authority) throw new SessionAuthorizationDeniedError("not_found");
|
|
195
|
+
|
|
196
|
+
const [resolvedActor, resolvedTarget] = await Promise.all([
|
|
197
|
+
resolveSessionAuthorizationActor(deps.db, grant),
|
|
198
|
+
resolveSessionAuthorizationTarget(deps.db, grant, input.sessionId),
|
|
199
|
+
]);
|
|
200
|
+
const actor = resolvedActor.actor;
|
|
201
|
+
const target = resolvedTarget.target;
|
|
202
|
+
const agentRelatedSessionAccess =
|
|
203
|
+
actor.kind === "agent_attempt"
|
|
204
|
+
? enforceAgentSessionHierarchy(
|
|
205
|
+
actor,
|
|
206
|
+
resolvedActor.callerParentSessionId,
|
|
207
|
+
resolvedTarget,
|
|
208
|
+
input.operation,
|
|
209
|
+
)
|
|
210
|
+
: null;
|
|
211
|
+
|
|
212
|
+
if (authority.visibility === "user_private") {
|
|
213
|
+
const allowed =
|
|
214
|
+
authority.ownerSubjectId !== null &&
|
|
215
|
+
(actor.kind === "subject"
|
|
216
|
+
? actor.subjectId === authority.ownerSubjectId
|
|
217
|
+
: actor.initiatingHumanSubjectId === authority.ownerSubjectId);
|
|
218
|
+
if (!allowed) throw new SessionAuthorizationDeniedError("forbidden");
|
|
219
|
+
}
|
|
98
220
|
if (slackAccess?.visibility === "private") {
|
|
99
221
|
const allowed =
|
|
100
222
|
actor.kind === "subject"
|
|
@@ -102,7 +224,14 @@ export async function requireSessionAuthorization(
|
|
|
102
224
|
: actor.callerRootSessionId === target.rootSessionId;
|
|
103
225
|
if (!allowed) throw new SessionAuthorizationDeniedError("forbidden");
|
|
104
226
|
}
|
|
105
|
-
if (!port)
|
|
227
|
+
if (!port) {
|
|
228
|
+
return {
|
|
229
|
+
actor,
|
|
230
|
+
target,
|
|
231
|
+
relatedSessionAccess: agentRelatedSessionAccess ?? "root",
|
|
232
|
+
reauthorizeAfterMs: null,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
106
235
|
|
|
107
236
|
let rawDecision: unknown;
|
|
108
237
|
try {
|
|
@@ -127,7 +256,10 @@ export async function requireSessionAuthorization(
|
|
|
127
256
|
return {
|
|
128
257
|
actor,
|
|
129
258
|
target,
|
|
130
|
-
relatedSessionAccess:
|
|
259
|
+
relatedSessionAccess:
|
|
260
|
+
agentRelatedSessionAccess === "target"
|
|
261
|
+
? "target"
|
|
262
|
+
: (parsed.data.relatedSessionAccess ?? "target"),
|
|
131
263
|
reauthorizeAfterMs: parsed.data.reauthorizeAfterMs ?? null,
|
|
132
264
|
};
|
|
133
265
|
}
|
|
@@ -139,8 +271,12 @@ export async function requireSessionAuthorizationListScope(
|
|
|
139
271
|
surface: SessionAuthorizationSurface,
|
|
140
272
|
): Promise<SessionAuthorizationListScope | null> {
|
|
141
273
|
const port = deps.sessionAuthorization;
|
|
274
|
+
const isAgentAttempt = grantHasAgentAttemptAuthority(grant);
|
|
275
|
+
if (!port && !isAgentAttempt) return null;
|
|
276
|
+
const { actor } = await resolveSessionAuthorizationActor(deps.db, grant);
|
|
277
|
+
// Standalone agents may retain compact workspace discovery, but only while
|
|
278
|
+
// the signed caller attempt is still the exact live attempt.
|
|
142
279
|
if (!port) return null;
|
|
143
|
-
const actor = await resolveSessionAuthorizationActor(deps.db, grant);
|
|
144
280
|
let rawScope: unknown;
|
|
145
281
|
try {
|
|
146
282
|
rawScope = await port.resolveListScope({
|
|
@@ -168,39 +304,34 @@ async function resolveSessionAuthorizationTarget(
|
|
|
168
304
|
db: Database,
|
|
169
305
|
grant: AccessGrant,
|
|
170
306
|
sessionId: string,
|
|
171
|
-
): Promise<
|
|
307
|
+
): Promise<ResolvedSessionAuthorizationTarget> {
|
|
172
308
|
const session = await getSession(db, grant.workspaceId, sessionId);
|
|
173
309
|
if (!session || session.accountId !== grant.accountId) {
|
|
174
310
|
throw new SessionAuthorizationDeniedError("not_found");
|
|
175
311
|
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
}
|
|
180
|
-
throw new SessionAuthorizationUnavailableError({ cause: error });
|
|
181
|
-
}
|
|
182
|
-
if (!rootSessionId) {
|
|
183
|
-
throw new SessionAuthorizationDeniedError("not_found");
|
|
184
|
-
}
|
|
185
|
-
return { sessionId: session.id, rootSessionId };
|
|
312
|
+
return {
|
|
313
|
+
target: { sessionId: session.id, rootSessionId: session.rootSessionId },
|
|
314
|
+
parentSessionId: session.parentSessionId,
|
|
315
|
+
};
|
|
186
316
|
}
|
|
187
|
-
|
|
188
317
|
async function resolveSessionAuthorizationActor(
|
|
189
318
|
db: Database,
|
|
190
319
|
grant: AccessGrant,
|
|
191
|
-
): Promise<
|
|
320
|
+
): Promise<ResolvedSessionAuthorizationActor> {
|
|
192
321
|
const callerSessionId = grant.metadata?.["sessionId"];
|
|
193
322
|
const turnId = grant.metadata?.["turnId"];
|
|
194
323
|
const attemptId = grant.metadata?.["attemptId"];
|
|
195
324
|
const executionGeneration = grant.metadata?.["executionGeneration"];
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
325
|
+
const isAgentAttempt = grantHasAgentAttemptAuthority(grant);
|
|
326
|
+
if (!isAgentAttempt) {
|
|
327
|
+
return {
|
|
328
|
+
actor: SessionAuthorizationActor.parse({
|
|
329
|
+
kind: "subject",
|
|
330
|
+
subjectId: grant.subjectId,
|
|
331
|
+
...(grant.subjectLabel ? { subjectLabel: grant.subjectLabel } : {}),
|
|
332
|
+
}),
|
|
333
|
+
callerParentSessionId: null,
|
|
334
|
+
};
|
|
204
335
|
}
|
|
205
336
|
if (
|
|
206
337
|
typeof callerSessionId !== "string" ||
|
|
@@ -212,10 +343,9 @@ async function resolveSessionAuthorizationActor(
|
|
|
212
343
|
) {
|
|
213
344
|
throw new SessionAuthorizationDeniedError("caller_stale");
|
|
214
345
|
}
|
|
215
|
-
const [callerSession, turn
|
|
346
|
+
const [callerSession, turn] = await Promise.all([
|
|
216
347
|
getSession(db, grant.workspaceId, callerSessionId),
|
|
217
348
|
getSessionTurnForAttempt(db, grant.workspaceId, callerSessionId, attemptId),
|
|
218
|
-
getSessionRootId(db, grant.workspaceId, callerSessionId).catch(() => null),
|
|
219
349
|
]);
|
|
220
350
|
if (
|
|
221
351
|
!callerSession ||
|
|
@@ -223,23 +353,25 @@ async function resolveSessionAuthorizationActor(
|
|
|
223
353
|
!turn ||
|
|
224
354
|
turn.id !== turnId ||
|
|
225
355
|
turn.executionGeneration !== executionGeneration ||
|
|
226
|
-
callerSession.activeTurnId !== turn.id
|
|
227
|
-
!callerRootSessionId
|
|
356
|
+
callerSession.activeTurnId !== turn.id
|
|
228
357
|
) {
|
|
229
358
|
throw new SessionAuthorizationDeniedError("caller_stale");
|
|
230
359
|
}
|
|
231
|
-
return
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
360
|
+
return {
|
|
361
|
+
actor: SessionAuthorizationActor.parse({
|
|
362
|
+
kind: "agent_attempt",
|
|
363
|
+
subjectId: grant.subjectId,
|
|
364
|
+
callerSessionId,
|
|
365
|
+
callerRootSessionId: callerSession.rootSessionId,
|
|
366
|
+
turnId,
|
|
367
|
+
attemptId,
|
|
368
|
+
executionGeneration,
|
|
369
|
+
initiator: turn.initiator,
|
|
370
|
+
initiatorContext: turn.initiatorContext,
|
|
371
|
+
initiatingHumanSubjectId:
|
|
372
|
+
turn.initiatingHumanSubjectId ??
|
|
373
|
+
(turn.initiator.kind === "subject" ? turn.initiator.subjectId : null),
|
|
374
|
+
}),
|
|
375
|
+
callerParentSessionId: callerSession.parentSessionId,
|
|
376
|
+
};
|
|
245
377
|
}
|
package/src/transcription.ts
CHANGED
|
@@ -19,6 +19,8 @@ export type TranscriptionLimits = {
|
|
|
19
19
|
export type TranscriptionRequest = {
|
|
20
20
|
workspaceId: string;
|
|
21
21
|
accountId: string;
|
|
22
|
+
/** Authenticated human/service subject used for provider-account authority. */
|
|
23
|
+
subjectId: string;
|
|
22
24
|
audio: Uint8Array;
|
|
23
25
|
mimeType: string;
|
|
24
26
|
/** Optional client-reported duration; enforced as a soft ceiling before upstream. */
|
|
@@ -87,6 +89,7 @@ export function statusForVoiceInputError(code: VoiceInputErrorCode): number {
|
|
|
87
89
|
/** Optional workspace scope for readiness checks during provider selection. */
|
|
88
90
|
export type TranscriptionAvailabilityContext = {
|
|
89
91
|
workspaceId?: string | undefined;
|
|
92
|
+
subjectId?: string | undefined;
|
|
90
93
|
};
|
|
91
94
|
|
|
92
95
|
/**
|
|
@@ -109,6 +112,8 @@ export type TranscriptionProvider = {
|
|
|
109
112
|
mimeType: string;
|
|
110
113
|
filename: string;
|
|
111
114
|
workspaceId: string;
|
|
115
|
+
accountId: string;
|
|
116
|
+
subjectId: string;
|
|
112
117
|
requestId: string;
|
|
113
118
|
signal?: AbortSignal | undefined;
|
|
114
119
|
}): Promise<{ text: string; languages: string[] }>;
|