@opengeni/api-router 2.4.2-canary.0 → 2.6.3-canary.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/app.js +1 -1
- package/dist/auth/managed-auth-attempt-context.d.ts +5 -1
- package/dist/auth/managed-auth.d.ts +24 -1
- package/dist/{chunk-L7GVVSSQ.js → chunk-XTLI3CBH.js} +2884 -483
- package/dist/chunk-XTLI3CBH.js.map +1 -0
- package/dist/http/sse.d.ts +9 -0
- package/dist/index.js +104 -4
- package/dist/index.js.map +1 -1
- package/dist/integrations/slack-interactions.d.ts +1 -1
- package/dist/interaction-metrics.d.ts +2 -0
- package/dist/mcp/company-brain-governed-writes.d.ts +1 -1
- package/dist/mcp/company-profile-agent-admin.d.ts +6 -6
- package/dist/mcp/remember.d.ts +2 -2
- package/dist/mcp/server.d.ts +34 -0
- package/dist/mcp/session-view.d.ts +1 -0
- package/dist/mcp/session-wait.d.ts +19 -0
- package/dist/routes/api-keys.d.ts +2 -0
- package/dist/routes/browser-sessions.d.ts +1 -0
- package/dist/routes/computer-sessions.d.ts +12 -0
- package/dist/routes/managed-auth-session-sets.d.ts +7 -0
- package/dist/routes/sessions.d.ts +17 -5
- package/dist/routes/workspaces.d.ts +1 -1
- package/dist/sandbox/metrics-ingestion.d.ts +16 -0
- package/dist/work-discovery-observability.d.ts +33 -0
- package/dist/workspace-delete-observability.d.ts +10 -0
- package/package.json +19 -19
- package/src/app.ts +172 -20
- package/src/auth/managed-auth-attempt-context.ts +40 -3
- package/src/auth/managed-auth-session-adapter.ts +1 -0
- package/src/auth/managed-auth.ts +164 -4
- package/src/http/sse.ts +279 -45
- package/src/integrations/oauth-client.ts +8 -1
- package/src/integrations/provider-oauth.ts +12 -2
- package/src/interaction-metrics.ts +30 -0
- package/src/mcp/company-brain-governed-writes.ts +38 -23
- package/src/mcp/company-profile-agent-admin.ts +7 -7
- package/src/mcp/remember.ts +19 -8
- package/src/mcp/server.ts +621 -54
- package/src/mcp/session-wait.ts +56 -8
- package/src/routes/api-integrations.ts +2 -2
- package/src/routes/api-keys.ts +149 -7
- package/src/routes/browser-sessions.ts +10 -2
- package/src/routes/capabilities.ts +3 -3
- package/src/routes/codex.ts +483 -74
- package/src/routes/company-profile.ts +64 -0
- package/src/routes/computer-sessions.ts +103 -1
- package/src/routes/integration-facets.ts +8 -5
- package/src/routes/interaction-resources.ts +7 -1
- package/src/routes/managed-auth-session-sets.ts +199 -2
- package/src/routes/organization-memberships.ts +28 -8
- package/src/routes/packs.ts +5 -5
- package/src/routes/plugins.ts +2 -2
- package/src/routes/scheduled-tasks.ts +9 -0
- package/src/routes/sessions.ts +380 -87
- package/src/routes/skills.ts +3 -3
- package/src/routes/workspaces.ts +293 -54
- package/src/sandbox/channel-a.ts +10 -4
- package/src/sandbox/machines.ts +13 -6
- package/src/sandbox/metrics-ingestion.ts +157 -3
- package/src/sandbox/viewer.ts +20 -1
- package/src/work-discovery-observability.ts +121 -0
- package/src/workspace-delete-observability.ts +75 -0
- package/dist/chunk-L7GVVSSQ.js.map +0 -1
package/src/routes/sessions.ts
CHANGED
|
@@ -66,12 +66,23 @@ import {
|
|
|
66
66
|
UpdateSessionToolPolicyRequest,
|
|
67
67
|
ViewerHeartbeatRequest,
|
|
68
68
|
WORKSPACE_CONTROL_ACTOR_MAX_BYTES,
|
|
69
|
+
WORK_CLAIM_CANONICAL_KEY_MAX_BYTES,
|
|
70
|
+
WORK_CLAIM_DISCOVERY_LIMIT,
|
|
71
|
+
WORK_CLAIM_NAMESPACE_MAX_BYTES,
|
|
72
|
+
WORK_DISCOVERY_QUERY_MAX_CHARS,
|
|
73
|
+
WORK_DISCOVERY_RECENT_HOURS_MAX,
|
|
74
|
+
WorkClaimSubjectFilter as WorkClaimSubjectFilterSchema,
|
|
75
|
+
WorkClaimSubjectType,
|
|
76
|
+
normalizeWorkClaimCanonicalKey,
|
|
77
|
+
normalizeWorkClaimNamespace,
|
|
69
78
|
workspaceControlUtf8Bytes,
|
|
70
79
|
type AccessGrant,
|
|
71
80
|
type AttachViewerResponse,
|
|
72
81
|
type SandboxBackend,
|
|
73
82
|
type LineageNode,
|
|
74
83
|
type Session,
|
|
84
|
+
type SessionStatus,
|
|
85
|
+
type WorkClaimSubjectFilter,
|
|
75
86
|
type SessionGoalRevision,
|
|
76
87
|
type AgentTopologyPageResponse,
|
|
77
88
|
type ErrorCode,
|
|
@@ -173,6 +184,7 @@ import {
|
|
|
173
184
|
type SandboxRetainedProcess,
|
|
174
185
|
type Database,
|
|
175
186
|
type SessionDiscoveryCursor,
|
|
187
|
+
type SessionDiscoveryOrderBy,
|
|
176
188
|
type SessionDiscoveryAncestor,
|
|
177
189
|
} from "@opengeni/db";
|
|
178
190
|
import {
|
|
@@ -272,7 +284,7 @@ import {
|
|
|
272
284
|
validateVariableSetAttachment,
|
|
273
285
|
} from "@opengeni/core";
|
|
274
286
|
import { assertSessionExists, boundedLimit } from "../http/common";
|
|
275
|
-
import { sseSessionStream } from "../http/sse";
|
|
287
|
+
import { browserSseDeliveryOptions, sseSessionStream } from "../http/sse";
|
|
276
288
|
import {
|
|
277
289
|
serveWorkspaceCapture,
|
|
278
290
|
serveWorkspaceCaptureFile,
|
|
@@ -280,6 +292,7 @@ import {
|
|
|
280
292
|
} from "./workspace-capture";
|
|
281
293
|
import { publishSandboxFileArtifact } from "../sandbox-file-artifacts";
|
|
282
294
|
import { ApiHttpError } from "../http/api-error";
|
|
295
|
+
import { observeWorkDiscovery, summarizeWorkDiscoveryRows } from "../work-discovery-observability";
|
|
283
296
|
|
|
284
297
|
type SessionRouteDeps = ApiRouteDeps & Pick<ViewerServices, "establishSandboxSession">;
|
|
285
298
|
|
|
@@ -700,71 +713,144 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
700
713
|
throw sessionAuthorizationHttpError(error);
|
|
701
714
|
}
|
|
702
715
|
const query = agentTopologyQuery(c.req.query());
|
|
703
|
-
const
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
716
|
+
const startedAtMs = performance.now();
|
|
717
|
+
const mode = query.subject ? "subject" : query.query ? "query" : "browse";
|
|
718
|
+
const metricAuthorizationScope = authorizationScope?.kind === "scoped" ? "scoped" : "workspace";
|
|
719
|
+
if ((query.query || query.subject) && !settings.workDiscoveryEnabled) {
|
|
720
|
+
observeWorkDiscovery(deps.observability, {
|
|
721
|
+
surface: "agent_topology",
|
|
722
|
+
mode,
|
|
723
|
+
outcome: "disabled",
|
|
724
|
+
authorizationScope: metricAuthorizationScope,
|
|
725
|
+
durationMs: performance.now() - startedAtMs,
|
|
726
|
+
responseBytes: 0,
|
|
727
|
+
resultCount: 0,
|
|
728
|
+
overlapCount: 0,
|
|
729
|
+
matchCounts: {},
|
|
730
|
+
});
|
|
731
|
+
throw new HTTPException(503, {
|
|
732
|
+
message: "Agent work discovery is disabled by the operator.",
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
try {
|
|
736
|
+
const orderBy: SessionDiscoveryOrderBy =
|
|
737
|
+
query.query || query.subject ? "relevance" : "updatedAt";
|
|
738
|
+
const page = await listSessionDiscoverySummaries(db, workspaceId, {
|
|
739
|
+
limit: query.limit,
|
|
740
|
+
orderBy,
|
|
741
|
+
subjectId: grant.subjectId,
|
|
742
|
+
...(query.cursor ? { cursor: query.cursor } : {}),
|
|
743
|
+
...(query.parentSessionId !== undefined ? { parentSessionId: query.parentSessionId } : {}),
|
|
744
|
+
...(query.rootSessionId ? { rootSessionId: query.rootSessionId } : {}),
|
|
745
|
+
...(query.query ? { query: query.query } : {}),
|
|
746
|
+
...(query.statuses ? { statuses: query.statuses } : {}),
|
|
747
|
+
activeOnly: query.activeOnly,
|
|
748
|
+
...(query.recentHours !== undefined ? { recentHours: query.recentHours } : {}),
|
|
749
|
+
...(query.subject ? { subject: query.subject } : {}),
|
|
750
|
+
...(query.claimLimit !== undefined ? { claimLimit: query.claimLimit } : {}),
|
|
751
|
+
includeWorkDiscovery: settings.workDiscoveryEnabled,
|
|
752
|
+
...(authorizationScope ? { authorizationScope } : {}),
|
|
753
|
+
});
|
|
754
|
+
const ancestorPaths =
|
|
755
|
+
query.query || query.subject
|
|
756
|
+
? await listSessionDiscoveryAncestorPaths(
|
|
757
|
+
db,
|
|
758
|
+
workspaceId,
|
|
759
|
+
page.sessions.map((session) => session.id),
|
|
760
|
+
authorizationScope ?? undefined,
|
|
761
|
+
grant.subjectId,
|
|
762
|
+
)
|
|
763
|
+
: new Map<string, SessionDiscoveryAncestor[]>();
|
|
764
|
+
const sessions: AgentTopologyPageResponse["sessions"] = page.sessions.map((session) => {
|
|
765
|
+
const blocker = session.effectiveControl.primaryBlocker;
|
|
766
|
+
return {
|
|
767
|
+
id: session.id,
|
|
768
|
+
title: session.title,
|
|
733
769
|
titleTruncated:
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
770
|
+
session.titleOriginalChars !== null &&
|
|
771
|
+
session.titleOriginalChars > Array.from(session.title ?? "").length,
|
|
772
|
+
parentSessionId: session.parentSessionId,
|
|
773
|
+
rootSessionId: session.rootSessionId,
|
|
774
|
+
nestedAgentDepth: session.nestedAgentDepth,
|
|
775
|
+
ancestorPath: (ancestorPaths.get(session.id) ?? []).map((ancestor) => ({
|
|
776
|
+
id: ancestor.id,
|
|
777
|
+
title: ancestor.title,
|
|
778
|
+
titleTruncated:
|
|
779
|
+
ancestor.titleOriginalChars !== null &&
|
|
780
|
+
ancestor.titleOriginalChars > Array.from(ancestor.title ?? "").length,
|
|
781
|
+
})),
|
|
782
|
+
status: session.status,
|
|
783
|
+
goal: session.goal
|
|
742
784
|
? {
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
blocker.displayNameOriginalChars > Array.from(blocker.displayName).length,
|
|
785
|
+
status: session.goal.status,
|
|
786
|
+
summary: session.goal.text,
|
|
787
|
+
summaryTruncated:
|
|
788
|
+
session.goal.textOriginalChars > Array.from(session.goal.text).length,
|
|
748
789
|
}
|
|
749
790
|
: null,
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
791
|
+
pause: {
|
|
792
|
+
state: session.effectiveControl.state,
|
|
793
|
+
additionalBlockerCount: session.effectiveControl.additionalBlockerCount,
|
|
794
|
+
source: blocker
|
|
795
|
+
? {
|
|
796
|
+
kind: blocker.kind,
|
|
797
|
+
...(blocker.sessionId ? { sessionId: blocker.sessionId } : {}),
|
|
798
|
+
displayName: blocker.displayName,
|
|
799
|
+
displayNameTruncated:
|
|
800
|
+
blocker.displayNameOriginalChars > Array.from(blocker.displayName).length,
|
|
801
|
+
}
|
|
802
|
+
: null,
|
|
803
|
+
},
|
|
804
|
+
children: session.treeStats,
|
|
805
|
+
relatedWork: session.workDiscovery,
|
|
806
|
+
createdAt: session.createdAt,
|
|
807
|
+
updatedAt: session.updatedAt,
|
|
808
|
+
};
|
|
809
|
+
});
|
|
810
|
+
const response = {
|
|
811
|
+
sessions,
|
|
812
|
+
total: page.total,
|
|
813
|
+
hasMore: page.hasMore,
|
|
814
|
+
humanAdvisoriesEnabled:
|
|
815
|
+
settings.workDiscoveryEnabled && settings.workDiscoveryHumanAdvisoriesEnabled,
|
|
816
|
+
nextCursor: page.nextCursor
|
|
817
|
+
? encodeAgentTopologyCursor({
|
|
818
|
+
cursor: page.nextCursor,
|
|
819
|
+
parentSessionId: query.parentSessionId === undefined ? "all" : query.parentSessionId,
|
|
820
|
+
rootSessionId: query.rootSessionId ?? null,
|
|
821
|
+
query: query.query ?? null,
|
|
822
|
+
statuses: query.statuses ?? [],
|
|
823
|
+
activeOnly: query.activeOnly,
|
|
824
|
+
recentHours: query.recentHours ?? null,
|
|
825
|
+
subject: query.subject ?? null,
|
|
826
|
+
claimLimit: query.claimLimit ?? null,
|
|
827
|
+
})
|
|
828
|
+
: null,
|
|
829
|
+
} satisfies AgentTopologyPageResponse;
|
|
830
|
+
observeWorkDiscovery(deps.observability, {
|
|
831
|
+
surface: "agent_topology",
|
|
832
|
+
mode,
|
|
833
|
+
outcome: sessions.length === 0 ? "empty" : "ok",
|
|
834
|
+
authorizationScope: metricAuthorizationScope,
|
|
835
|
+
durationMs: performance.now() - startedAtMs,
|
|
836
|
+
responseBytes: Buffer.byteLength(JSON.stringify(response), "utf8"),
|
|
837
|
+
...summarizeWorkDiscoveryRows(sessions),
|
|
838
|
+
});
|
|
839
|
+
return c.json(response);
|
|
840
|
+
} catch (error) {
|
|
841
|
+
observeWorkDiscovery(deps.observability, {
|
|
842
|
+
surface: "agent_topology",
|
|
843
|
+
mode,
|
|
844
|
+
outcome: "error",
|
|
845
|
+
authorizationScope: metricAuthorizationScope,
|
|
846
|
+
durationMs: performance.now() - startedAtMs,
|
|
847
|
+
responseBytes: 0,
|
|
848
|
+
resultCount: 0,
|
|
849
|
+
overlapCount: 0,
|
|
850
|
+
matchCounts: {},
|
|
851
|
+
});
|
|
852
|
+
throw error;
|
|
853
|
+
}
|
|
768
854
|
});
|
|
769
855
|
|
|
770
856
|
app.get("/v1/workspaces/:workspaceId/sessions/:sessionId", async (c) => {
|
|
@@ -2573,6 +2659,7 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
2573
2659
|
Number.isFinite(after) ? after : 0,
|
|
2574
2660
|
c.req.raw.signal,
|
|
2575
2661
|
{
|
|
2662
|
+
...browserSseDeliveryOptions(c.req.query("transport")),
|
|
2576
2663
|
observability: deps.observability,
|
|
2577
2664
|
actorEpoch: getManagedAuthRequestActorEpoch(c.req.raw) ?? undefined,
|
|
2578
2665
|
reauthorizeAfterMs:
|
|
@@ -3115,12 +3202,8 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
3115
3202
|
// Human take-control: when the desktop is available + this policy is on
|
|
3116
3203
|
// (default), the cell is mode "interactive" — the noVNC viewer drives :0
|
|
3117
3204
|
// (x11vnc runs without -viewonly). Off → mode "read-only" (client disables
|
|
3118
|
-
// take-control).
|
|
3205
|
+
// take-control). Agent interaction uses managed ComputerSession tools.
|
|
3119
3206
|
desktopInteractive: settings.sandboxDesktopInteractive,
|
|
3120
|
-
// P4.3 computer-use: the agent drives the active display; availability
|
|
3121
|
-
// tracks the desktop tier + a desktop-capable backend.
|
|
3122
|
-
computerUseEnabled: settings.computerUseEnabled,
|
|
3123
|
-
computerUseReadOnly: settings.computerUseReadOnly,
|
|
3124
3207
|
// Graceful degrade when scoped stream credentials cannot be minted.
|
|
3125
3208
|
streamTokenSecretAvailable: !streamTokenDegraded(settings),
|
|
3126
3209
|
desktopAcknowledged: acknowledged,
|
|
@@ -4509,12 +4592,18 @@ function sessionListQuery(
|
|
|
4509
4592
|
|
|
4510
4593
|
export type AgentTopologyCursorEnvelope = {
|
|
4511
4594
|
cursor: SessionDiscoveryCursor;
|
|
4512
|
-
parentSessionId: string | null;
|
|
4513
|
-
|
|
4595
|
+
parentSessionId: string | null | "all";
|
|
4596
|
+
rootSessionId: string | null;
|
|
4597
|
+
query: string | null;
|
|
4598
|
+
statuses: SessionStatus[];
|
|
4599
|
+
activeOnly: boolean;
|
|
4600
|
+
recentHours: number | null;
|
|
4601
|
+
subject: WorkClaimSubjectFilter | null;
|
|
4602
|
+
claimLimit: number | null;
|
|
4514
4603
|
};
|
|
4515
4604
|
|
|
4516
4605
|
export function encodeAgentTopologyCursor(value: AgentTopologyCursorEnvelope): string {
|
|
4517
|
-
return Buffer.from(JSON.stringify({ v:
|
|
4606
|
+
return Buffer.from(JSON.stringify({ v: 2, ...value }), "utf8").toString("base64url");
|
|
4518
4607
|
}
|
|
4519
4608
|
|
|
4520
4609
|
function decodeAgentTopologyCursor(value: string): AgentTopologyCursorEnvelope {
|
|
@@ -4524,25 +4613,92 @@ function decodeAgentTopologyCursor(value: string): AgentTopologyCursorEnvelope {
|
|
|
4524
4613
|
});
|
|
4525
4614
|
}
|
|
4526
4615
|
try {
|
|
4527
|
-
const
|
|
4616
|
+
const decoded = JSON.parse(Buffer.from(value, "base64url").toString("utf8")) as unknown;
|
|
4617
|
+
const legacy = z
|
|
4528
4618
|
.object({
|
|
4529
4619
|
v: z.literal(1),
|
|
4530
4620
|
parentSessionId: z.string().uuid().nullable(),
|
|
4531
|
-
search: z.string().max(
|
|
4621
|
+
search: z.string().max(WORK_DISCOVERY_QUERY_MAX_CHARS).nullable(),
|
|
4622
|
+
cursor: z.object({
|
|
4623
|
+
orderBy: z.literal("updatedAt"),
|
|
4624
|
+
sortRevision: z.string().max(64),
|
|
4625
|
+
sortAt: z.string().max(64),
|
|
4626
|
+
id: z.string().uuid(),
|
|
4627
|
+
snapshotAt: z.string().max(64),
|
|
4628
|
+
snapshotRevision: z.string().max(64),
|
|
4629
|
+
updatedAfter: z.null(),
|
|
4630
|
+
}),
|
|
4631
|
+
})
|
|
4632
|
+
.safeParse(decoded);
|
|
4633
|
+
if (legacy.success) {
|
|
4634
|
+
if (legacy.data.search !== null) {
|
|
4635
|
+
throw new Error("legacy search cursor is not relevance-fenced");
|
|
4636
|
+
}
|
|
4637
|
+
const cursor = {
|
|
4638
|
+
...legacy.data.cursor,
|
|
4639
|
+
sortRank: null,
|
|
4640
|
+
filterHash: null,
|
|
4641
|
+
} satisfies SessionDiscoveryCursor;
|
|
4642
|
+
return {
|
|
4643
|
+
cursor,
|
|
4644
|
+
parentSessionId: legacy.data.parentSessionId,
|
|
4645
|
+
rootSessionId: null,
|
|
4646
|
+
query: null,
|
|
4647
|
+
statuses: [],
|
|
4648
|
+
activeOnly: false,
|
|
4649
|
+
recentHours: null,
|
|
4650
|
+
subject: null,
|
|
4651
|
+
claimLimit: null,
|
|
4652
|
+
};
|
|
4653
|
+
}
|
|
4654
|
+
const parsed = z
|
|
4655
|
+
.object({
|
|
4656
|
+
v: z.literal(2),
|
|
4657
|
+
parentSessionId: z.union([z.string().uuid(), z.literal("all"), z.null()]),
|
|
4658
|
+
rootSessionId: z.string().uuid().nullable(),
|
|
4659
|
+
query: z.string().max(WORK_DISCOVERY_QUERY_MAX_CHARS).nullable(),
|
|
4660
|
+
statuses: z
|
|
4661
|
+
.array(
|
|
4662
|
+
z.enum([
|
|
4663
|
+
"queued",
|
|
4664
|
+
"running",
|
|
4665
|
+
"idle",
|
|
4666
|
+
"requires_action",
|
|
4667
|
+
"recovering",
|
|
4668
|
+
"waiting_capacity",
|
|
4669
|
+
"failed",
|
|
4670
|
+
"cancelled",
|
|
4671
|
+
]),
|
|
4672
|
+
)
|
|
4673
|
+
.max(8),
|
|
4674
|
+
activeOnly: z.boolean(),
|
|
4675
|
+
recentHours: z.number().int().positive().max(WORK_DISCOVERY_RECENT_HOURS_MAX).nullable(),
|
|
4676
|
+
subject: z
|
|
4677
|
+
.object({
|
|
4678
|
+
namespace: z.string().min(1).max(WORK_CLAIM_NAMESPACE_MAX_BYTES),
|
|
4679
|
+
type: WorkClaimSubjectType,
|
|
4680
|
+
canonicalKey: z.string().min(1).max(WORK_CLAIM_CANONICAL_KEY_MAX_BYTES),
|
|
4681
|
+
})
|
|
4682
|
+
.strict()
|
|
4683
|
+
.nullable(),
|
|
4684
|
+
claimLimit: z.number().int().positive().max(WORK_CLAIM_DISCOVERY_LIMIT).nullable(),
|
|
4532
4685
|
cursor: z.object({
|
|
4533
|
-
orderBy: z.enum(["
|
|
4686
|
+
orderBy: z.enum(["updatedAt", "relevance"]),
|
|
4687
|
+
sortRank: z.number().int().nonnegative().nullable(),
|
|
4534
4688
|
sortRevision: z.string().max(64),
|
|
4535
4689
|
sortAt: z.string().max(64),
|
|
4536
4690
|
id: z.string().uuid(),
|
|
4537
4691
|
snapshotAt: z.string().max(64),
|
|
4538
4692
|
snapshotRevision: z.string().max(64),
|
|
4539
|
-
updatedAfter: z.
|
|
4693
|
+
updatedAfter: z.null(),
|
|
4694
|
+
filterHash: z
|
|
4695
|
+
.string()
|
|
4696
|
+
.regex(/^[0-9a-f]{64}$/)
|
|
4697
|
+
.nullable(),
|
|
4540
4698
|
}),
|
|
4541
4699
|
})
|
|
4542
|
-
.parse(
|
|
4700
|
+
.parse(decoded);
|
|
4543
4701
|
if (
|
|
4544
|
-
parsed.cursor.orderBy !== "updatedAt" ||
|
|
4545
|
-
parsed.cursor.updatedAfter !== null ||
|
|
4546
4702
|
!/^(?:0|[1-9]\d*)$/.test(parsed.cursor.sortRevision) ||
|
|
4547
4703
|
!/^(?:0|[1-9]\d*)$/.test(parsed.cursor.snapshotRevision) ||
|
|
4548
4704
|
BigInt(parsed.cursor.sortRevision) > 9_223_372_036_854_775_807n ||
|
|
@@ -4552,6 +4708,14 @@ function decodeAgentTopologyCursor(value: string): AgentTopologyCursorEnvelope {
|
|
|
4552
4708
|
) {
|
|
4553
4709
|
throw new Error("invalid topology cursor fields");
|
|
4554
4710
|
}
|
|
4711
|
+
if (
|
|
4712
|
+
(parsed.cursor.orderBy === "relevance" &&
|
|
4713
|
+
(parsed.cursor.sortRank === null || parsed.cursor.filterHash === null)) ||
|
|
4714
|
+
(parsed.cursor.orderBy === "updatedAt" &&
|
|
4715
|
+
(parsed.cursor.sortRank !== null || parsed.cursor.filterHash !== null))
|
|
4716
|
+
) {
|
|
4717
|
+
throw new Error("invalid topology cursor relevance fields");
|
|
4718
|
+
}
|
|
4555
4719
|
return parsed;
|
|
4556
4720
|
} catch {
|
|
4557
4721
|
throw new HTTPException(400, {
|
|
@@ -4562,8 +4726,14 @@ function decodeAgentTopologyCursor(value: string): AgentTopologyCursorEnvelope {
|
|
|
4562
4726
|
|
|
4563
4727
|
export function agentTopologyQuery(query: Record<string, string>): {
|
|
4564
4728
|
limit: number;
|
|
4565
|
-
parentSessionId: string | null;
|
|
4566
|
-
|
|
4729
|
+
parentSessionId: string | null | undefined;
|
|
4730
|
+
rootSessionId: string | undefined;
|
|
4731
|
+
query: string | undefined;
|
|
4732
|
+
statuses: SessionStatus[] | undefined;
|
|
4733
|
+
activeOnly: boolean;
|
|
4734
|
+
recentHours: number | undefined;
|
|
4735
|
+
subject: WorkClaimSubjectFilter | undefined;
|
|
4736
|
+
claimLimit: number | undefined;
|
|
4567
4737
|
cursor: SessionDiscoveryCursor | undefined;
|
|
4568
4738
|
} {
|
|
4569
4739
|
const rawLimit = query.limit;
|
|
@@ -4583,22 +4753,139 @@ export function agentTopologyQuery(query: Record<string, string>): {
|
|
|
4583
4753
|
message: 'parentSessionId must be a session id or the literal "null"',
|
|
4584
4754
|
});
|
|
4585
4755
|
}
|
|
4586
|
-
const
|
|
4587
|
-
|
|
4588
|
-
|
|
4756
|
+
const rootSessionId = query.rootSessionId?.trim();
|
|
4757
|
+
if (rootSessionId && !z.string().uuid().safeParse(rootSessionId).success) {
|
|
4758
|
+
throw new HTTPException(400, { message: "rootSessionId must be a session id" });
|
|
4759
|
+
}
|
|
4760
|
+
const normalizeSearchQuery = (value: string | undefined): string | undefined => {
|
|
4761
|
+
if (value === undefined) return undefined;
|
|
4762
|
+
const canonical = value.normalize("NFKC");
|
|
4763
|
+
if (/[\u0000-\u001f\u007f-\u009f]/u.test(canonical)) {
|
|
4764
|
+
throw new HTTPException(400, { message: "query must not contain control characters" });
|
|
4765
|
+
}
|
|
4766
|
+
const normalized = canonical.trim().replace(/\s+/gu, " ").toLowerCase();
|
|
4767
|
+
if (!normalized) return undefined;
|
|
4768
|
+
if (Array.from(normalized).length > WORK_DISCOVERY_QUERY_MAX_CHARS) {
|
|
4769
|
+
throw new HTTPException(400, {
|
|
4770
|
+
message: `query must be at most ${WORK_DISCOVERY_QUERY_MAX_CHARS} characters`,
|
|
4771
|
+
});
|
|
4772
|
+
}
|
|
4773
|
+
return normalized;
|
|
4774
|
+
};
|
|
4775
|
+
const requestedQuery = normalizeSearchQuery(query.query);
|
|
4776
|
+
const legacySearch = normalizeSearchQuery(query.search);
|
|
4777
|
+
const searchQuery = requestedQuery ?? legacySearch;
|
|
4778
|
+
if (requestedQuery && legacySearch && requestedQuery !== legacySearch) {
|
|
4779
|
+
throw new HTTPException(400, { message: "query and legacy search must match" });
|
|
4780
|
+
}
|
|
4781
|
+
const statuses = query.statuses
|
|
4782
|
+
? [
|
|
4783
|
+
...new Set(
|
|
4784
|
+
query.statuses
|
|
4785
|
+
.split(",")
|
|
4786
|
+
.map((status) => status.trim())
|
|
4787
|
+
.filter(Boolean),
|
|
4788
|
+
),
|
|
4789
|
+
].sort()
|
|
4790
|
+
: [];
|
|
4791
|
+
const parsedStatuses = z
|
|
4792
|
+
.array(
|
|
4793
|
+
z.enum([
|
|
4794
|
+
"queued",
|
|
4795
|
+
"running",
|
|
4796
|
+
"idle",
|
|
4797
|
+
"requires_action",
|
|
4798
|
+
"recovering",
|
|
4799
|
+
"waiting_capacity",
|
|
4800
|
+
"failed",
|
|
4801
|
+
"cancelled",
|
|
4802
|
+
]),
|
|
4803
|
+
)
|
|
4804
|
+
.max(8)
|
|
4805
|
+
.safeParse(statuses);
|
|
4806
|
+
if (!parsedStatuses.success) {
|
|
4807
|
+
throw new HTTPException(400, { message: "statuses contains an unsupported lifecycle state" });
|
|
4808
|
+
}
|
|
4809
|
+
const activeOnly = query.activeOnly === "true";
|
|
4810
|
+
if (
|
|
4811
|
+
query.activeOnly !== undefined &&
|
|
4812
|
+
query.activeOnly !== "true" &&
|
|
4813
|
+
query.activeOnly !== "false"
|
|
4814
|
+
) {
|
|
4815
|
+
throw new HTTPException(400, { message: "activeOnly must be true or false" });
|
|
4816
|
+
}
|
|
4817
|
+
const recentHours = query.recentHours === undefined ? undefined : Number(query.recentHours);
|
|
4818
|
+
if (
|
|
4819
|
+
recentHours !== undefined &&
|
|
4820
|
+
(!Number.isSafeInteger(recentHours) ||
|
|
4821
|
+
recentHours < 1 ||
|
|
4822
|
+
recentHours > WORK_DISCOVERY_RECENT_HOURS_MAX)
|
|
4823
|
+
) {
|
|
4589
4824
|
throw new HTTPException(400, {
|
|
4590
|
-
message:
|
|
4825
|
+
message: `recentHours must be an integer between 1 and ${WORK_DISCOVERY_RECENT_HOURS_MAX}`,
|
|
4826
|
+
});
|
|
4827
|
+
}
|
|
4828
|
+
const subjectFields = [query.subjectNamespace, query.subjectType, query.subjectKey];
|
|
4829
|
+
if (subjectFields.some((value) => value !== undefined) && subjectFields.some((value) => !value)) {
|
|
4830
|
+
throw new HTTPException(400, {
|
|
4831
|
+
message: "subjectNamespace, subjectType, and subjectKey must be supplied together",
|
|
4591
4832
|
});
|
|
4592
4833
|
}
|
|
4593
|
-
|
|
4834
|
+
const parsedSubject = subjectFields.every((value) => value !== undefined)
|
|
4835
|
+
? WorkClaimSubjectFilterSchema.safeParse({
|
|
4836
|
+
namespace: normalizeWorkClaimNamespace(query.subjectNamespace!),
|
|
4837
|
+
type: query.subjectType,
|
|
4838
|
+
canonicalKey: normalizeWorkClaimCanonicalKey(query.subjectKey!),
|
|
4839
|
+
})
|
|
4840
|
+
: null;
|
|
4841
|
+
if (parsedSubject && !parsedSubject.success) {
|
|
4842
|
+
throw new HTTPException(400, { message: "exact subject filter is invalid" });
|
|
4843
|
+
}
|
|
4844
|
+
const subject = parsedSubject?.success ? parsedSubject.data : undefined;
|
|
4845
|
+
if (searchQuery && subject) {
|
|
4846
|
+
throw new HTTPException(400, { message: "query cannot be combined with an exact subject" });
|
|
4847
|
+
}
|
|
4848
|
+
const relevanceRequested = Boolean(searchQuery || subject);
|
|
4849
|
+
const parentSessionId =
|
|
4850
|
+
rawParent === undefined
|
|
4851
|
+
? relevanceRequested
|
|
4852
|
+
? undefined
|
|
4853
|
+
: null
|
|
4854
|
+
: rawParent === "null"
|
|
4855
|
+
? null
|
|
4856
|
+
: rawParent;
|
|
4857
|
+
const claimLimit = query.claimLimit === undefined ? undefined : Number(query.claimLimit);
|
|
4858
|
+
if (
|
|
4859
|
+
claimLimit !== undefined &&
|
|
4860
|
+
(!Number.isSafeInteger(claimLimit) || claimLimit < 1 || claimLimit > WORK_CLAIM_DISCOVERY_LIMIT)
|
|
4861
|
+
) {
|
|
4594
4862
|
throw new HTTPException(400, {
|
|
4595
|
-
message:
|
|
4863
|
+
message: `claimLimit must be an integer between 1 and ${WORK_CLAIM_DISCOVERY_LIMIT}`,
|
|
4596
4864
|
});
|
|
4597
4865
|
}
|
|
4598
4866
|
const envelope = query.cursor ? decodeAgentTopologyCursor(query.cursor) : undefined;
|
|
4867
|
+
const expectedEnvelope = {
|
|
4868
|
+
parentSessionId: parentSessionId === undefined ? "all" : parentSessionId,
|
|
4869
|
+
rootSessionId: rootSessionId ?? null,
|
|
4870
|
+
query: searchQuery || null,
|
|
4871
|
+
statuses: parsedStatuses.data,
|
|
4872
|
+
activeOnly,
|
|
4873
|
+
recentHours: recentHours ?? null,
|
|
4874
|
+
subject: subject ?? null,
|
|
4875
|
+
claimLimit: claimLimit ?? null,
|
|
4876
|
+
};
|
|
4599
4877
|
if (
|
|
4600
4878
|
envelope &&
|
|
4601
|
-
|
|
4879
|
+
JSON.stringify({
|
|
4880
|
+
parentSessionId: envelope.parentSessionId,
|
|
4881
|
+
rootSessionId: envelope.rootSessionId,
|
|
4882
|
+
query: envelope.query,
|
|
4883
|
+
statuses: envelope.statuses,
|
|
4884
|
+
activeOnly: envelope.activeOnly,
|
|
4885
|
+
recentHours: envelope.recentHours,
|
|
4886
|
+
subject: envelope.subject,
|
|
4887
|
+
claimLimit: envelope.claimLimit,
|
|
4888
|
+
}) !== JSON.stringify(expectedEnvelope)
|
|
4602
4889
|
) {
|
|
4603
4890
|
throw new HTTPException(400, {
|
|
4604
4891
|
message: "agent topology cursor does not match its filters",
|
|
@@ -4607,7 +4894,13 @@ export function agentTopologyQuery(query: Record<string, string>): {
|
|
|
4607
4894
|
return {
|
|
4608
4895
|
limit,
|
|
4609
4896
|
parentSessionId,
|
|
4610
|
-
|
|
4897
|
+
rootSessionId: rootSessionId || undefined,
|
|
4898
|
+
query: searchQuery || undefined,
|
|
4899
|
+
statuses: parsedStatuses.data.length > 0 ? parsedStatuses.data : undefined,
|
|
4900
|
+
activeOnly,
|
|
4901
|
+
recentHours,
|
|
4902
|
+
subject,
|
|
4903
|
+
claimLimit,
|
|
4611
4904
|
cursor: envelope?.cursor,
|
|
4612
4905
|
};
|
|
4613
4906
|
}
|
package/src/routes/skills.ts
CHANGED
|
@@ -55,7 +55,7 @@ export function registerSkillRoutes(
|
|
|
55
55
|
|
|
56
56
|
app.post("/v1/workspaces/:workspaceId/skills/library/:libraryId/install", async (c) => {
|
|
57
57
|
const workspaceId = c.req.param("workspaceId");
|
|
58
|
-
const grant = await requireAccessGrant(c, deps, workspaceId, "
|
|
58
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "capabilities:manage");
|
|
59
59
|
const libraryId = decodeURIComponent(c.req.param("libraryId"));
|
|
60
60
|
const payload = InstallLibrarySkillRequest.parse(await c.req.json());
|
|
61
61
|
let loaded: ReturnType<typeof loadSkillLibrarySkill>;
|
|
@@ -136,7 +136,7 @@ export function registerSkillRoutes(
|
|
|
136
136
|
|
|
137
137
|
app.post("/v1/workspaces/:workspaceId/skills/install", async (c) => {
|
|
138
138
|
const workspaceId = c.req.param("workspaceId");
|
|
139
|
-
const grant = await requireAccessGrant(c, deps, workspaceId, "
|
|
139
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "capabilities:manage");
|
|
140
140
|
const payload = InstallSkillRequest.parse(await c.req.json());
|
|
141
141
|
const resolved = await resolveForRoute(payload.url, github);
|
|
142
142
|
if (
|
|
@@ -204,7 +204,7 @@ export function registerSkillRoutes(
|
|
|
204
204
|
|
|
205
205
|
app.delete("/v1/workspaces/:workspaceId/skills/:capabilityId", async (c) => {
|
|
206
206
|
const workspaceId = c.req.param("workspaceId");
|
|
207
|
-
const grant = await requireAccessGrant(c, deps, workspaceId, "
|
|
207
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "capabilities:manage");
|
|
208
208
|
const capabilityId = decodeURIComponent(c.req.param("capabilityId"));
|
|
209
209
|
const payload = UninstallSkillRequest.parse(await c.req.json());
|
|
210
210
|
try {
|