@opengeni/api-router 0.9.0 → 0.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app.js +1 -1
- package/dist/{chunk-QOYQBYHM.js → chunk-7PQKPKW5.js} +9874 -9276
- package/dist/chunk-7PQKPKW5.js.map +1 -0
- package/dist/index.js +1 -1
- package/package.json +10 -10
- package/src/mcp/server.ts +91 -77
- package/src/routes/machines.ts +22 -2
- package/src/routes/sessions.ts +482 -142
- package/src/sandbox/channel-a.ts +54 -59
- package/src/sandbox/machines.ts +7 -0
- package/src/sandbox/rematerialize.ts +287 -0
- package/src/sandbox/viewer.ts +200 -128
- package/dist/chunk-QOYQBYHM.js.map +0 -1
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opengeni/api-router",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.1",
|
|
4
4
|
"description": "OpenGeni HTTP surface: the Hono adapter/router (createApp), routes, MCP HTTP transport, and HTTP access adapters over @opengeni/core. An engine-distribution surface — its runtime closure includes engine-internal packages.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -44,17 +44,17 @@
|
|
|
44
44
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
45
45
|
"@opengeni/agent-proto": "^0.3.0",
|
|
46
46
|
"@opengeni/codex": "^0.2.7",
|
|
47
|
-
"@opengeni/config": "^0.
|
|
48
|
-
"@opengeni/contracts": "^0.
|
|
49
|
-
"@opengeni/core": "^0.
|
|
50
|
-
"@opengeni/db": "^0.
|
|
51
|
-
"@opengeni/documents": "^0.2.
|
|
52
|
-
"@opengeni/events": "^0.3.
|
|
53
|
-
"@opengeni/github": "^0.3.
|
|
47
|
+
"@opengeni/config": "^0.7.0",
|
|
48
|
+
"@opengeni/contracts": "^0.19.0",
|
|
49
|
+
"@opengeni/core": "^0.11.1",
|
|
50
|
+
"@opengeni/db": "^0.12.0",
|
|
51
|
+
"@opengeni/documents": "^0.2.30",
|
|
52
|
+
"@opengeni/events": "^0.3.21",
|
|
53
|
+
"@opengeni/github": "^0.3.12",
|
|
54
54
|
"@opengeni/network": "^0.1.1",
|
|
55
55
|
"@opengeni/observability": "^0.3.0",
|
|
56
|
-
"@opengeni/runtime": "^0.13.
|
|
57
|
-
"@opengeni/storage": "^0.2.
|
|
56
|
+
"@opengeni/runtime": "^0.13.3",
|
|
57
|
+
"@opengeni/storage": "^0.2.24",
|
|
58
58
|
"@temporalio/client": "^1.17.0",
|
|
59
59
|
"better-auth": "^1.6.14",
|
|
60
60
|
"hono": "^4.12.18",
|
package/src/mcp/server.ts
CHANGED
|
@@ -58,15 +58,15 @@ import {
|
|
|
58
58
|
saveWorkspaceMemory,
|
|
59
59
|
searchWorkspaceMemories,
|
|
60
60
|
serializeEffectiveSessionControl,
|
|
61
|
-
|
|
61
|
+
setSessionGoalStatusWithEvent,
|
|
62
62
|
setVariableSetVariable,
|
|
63
63
|
updateScheduledTask,
|
|
64
|
-
|
|
65
|
-
|
|
64
|
+
updateSessionGoalWithEvent,
|
|
65
|
+
upsertSessionGoalWithEvent,
|
|
66
66
|
RigChangeAlreadyVerifyingError,
|
|
67
67
|
RigChangeTransitionError,
|
|
68
68
|
} from "@opengeni/db";
|
|
69
|
-
import { appendAndPublishEvents } from "@opengeni/events";
|
|
69
|
+
import { appendAndPublishEvents, publishDurableSessionEvents } from "@opengeni/events";
|
|
70
70
|
import {
|
|
71
71
|
createGitHubAppInstallationToken,
|
|
72
72
|
GitHubAppConfigurationError,
|
|
@@ -109,6 +109,8 @@ import {
|
|
|
109
109
|
controlAgentSessionWorkstream,
|
|
110
110
|
controlHumanSessionWorkstream,
|
|
111
111
|
createSessionForRequest,
|
|
112
|
+
SessionSpawnDeniedError,
|
|
113
|
+
sessionSpawnDenialEnvelope,
|
|
112
114
|
sendAgentSessionMessage,
|
|
113
115
|
steerAgentSession,
|
|
114
116
|
updateSessionTitle,
|
|
@@ -135,6 +137,7 @@ import {
|
|
|
135
137
|
SESSION_EVENT_MCP_MAX_BYTES,
|
|
136
138
|
} from "./session-view";
|
|
137
139
|
import type { ToolspaceMcpSurface } from "./toolspace";
|
|
140
|
+
import { ensureSessionGroupReady as ensureViewerSessionGroupReady } from "../sandbox/viewer";
|
|
138
141
|
|
|
139
142
|
export type McpServerOptions = {
|
|
140
143
|
// Origin of the HTTP request that reached the MCP route. Retained in the
|
|
@@ -739,7 +742,7 @@ function registerGoalTools(
|
|
|
739
742
|
? (grant.metadata["turnId"] as string)
|
|
740
743
|
: null;
|
|
741
744
|
await assertGoalReactivationAllowed(deps, grant.workspaceId, sessionId, callerTurnId);
|
|
742
|
-
const { goal,
|
|
745
|
+
const { goal, events } = await upsertSessionGoalWithEvent(deps.db, {
|
|
743
746
|
accountId: grant.accountId,
|
|
744
747
|
workspaceId: grant.workspaceId,
|
|
745
748
|
sessionId,
|
|
@@ -747,20 +750,11 @@ function registerGoalTools(
|
|
|
747
750
|
successCriteria: successCriteria ?? null,
|
|
748
751
|
maxAutoContinuations: maxAutoContinuations ?? null,
|
|
749
752
|
createdBy: "agent",
|
|
753
|
+
actor: "agent",
|
|
750
754
|
});
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
payload: {
|
|
755
|
-
goalId: goal.id,
|
|
756
|
-
text: goal.text,
|
|
757
|
-
...(goal.successCriteria ? { successCriteria: goal.successCriteria } : {}),
|
|
758
|
-
version: goal.version,
|
|
759
|
-
actor: "agent",
|
|
760
|
-
replaced,
|
|
761
|
-
},
|
|
762
|
-
},
|
|
763
|
-
]);
|
|
755
|
+
if (events.length > 0) {
|
|
756
|
+
await deps.bus.publish(grant.workspaceId, sessionId, events);
|
|
757
|
+
}
|
|
764
758
|
return json(goal);
|
|
765
759
|
},
|
|
766
760
|
);
|
|
@@ -774,36 +768,36 @@ function registerGoalTools(
|
|
|
774
768
|
text: z4.string().min(1).optional(),
|
|
775
769
|
successCriteria: z4.string().min(1).optional(),
|
|
776
770
|
progressNote: z4.string().min(1).optional(),
|
|
771
|
+
idempotencyKey: z4.string().uuid(),
|
|
777
772
|
},
|
|
778
773
|
},
|
|
779
|
-
async ({ text, successCriteria, progressNote }) => {
|
|
774
|
+
async ({ text, successCriteria, progressNote, idempotencyKey }) => {
|
|
780
775
|
await authorizeFirstPartySession(deps, grant, sessionId, "session.goal.write");
|
|
781
|
-
|
|
782
|
-
const
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
if (existing.status === "completed") {
|
|
787
|
-
throw new Error("session goal is completed; use goal_set to start a new goal");
|
|
788
|
-
}
|
|
789
|
-
const goal = await updateSessionGoal(deps.db, grant.workspaceId, sessionId, {
|
|
790
|
-
...(text !== undefined ? { text } : {}),
|
|
791
|
-
...(successCriteria !== undefined ? { successCriteria } : {}),
|
|
792
|
-
});
|
|
793
|
-
await appendAndPublishEvents(deps.db, deps.bus, grant.workspaceId, sessionId, [
|
|
776
|
+
const context = exactAgentCommandContext(grant, sessionId);
|
|
777
|
+
const { goal, events, operationId, replay } = await updateSessionGoalWithEvent(
|
|
778
|
+
deps.db,
|
|
779
|
+
grant.workspaceId,
|
|
780
|
+
sessionId,
|
|
794
781
|
{
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
782
|
+
...(text !== undefined ? { text } : {}),
|
|
783
|
+
...(successCriteria !== undefined ? { successCriteria } : {}),
|
|
784
|
+
...(progressNote !== undefined ? { progressNote } : {}),
|
|
785
|
+
actor: "agent",
|
|
786
|
+
command: {
|
|
787
|
+
accountId: grant.accountId,
|
|
788
|
+
actor: {
|
|
789
|
+
type: "agent_attempt",
|
|
790
|
+
attemptId: context.callerAttemptId,
|
|
791
|
+
sessionId: context.callerSessionId,
|
|
792
|
+
turnId: context.callerTurnId,
|
|
793
|
+
executionGeneration: context.callerExecutionGeneration,
|
|
794
|
+
},
|
|
795
|
+
operationKey: idempotencyKey,
|
|
803
796
|
},
|
|
804
797
|
},
|
|
805
|
-
|
|
806
|
-
|
|
798
|
+
);
|
|
799
|
+
await publishDurableSessionEvents(deps.bus, grant.workspaceId, sessionId, events);
|
|
800
|
+
return json({ ...goal, operationId, replay });
|
|
807
801
|
},
|
|
808
802
|
);
|
|
809
803
|
|
|
@@ -821,17 +815,18 @@ function registerGoalTools(
|
|
|
821
815
|
if (!existing) {
|
|
822
816
|
throw new Error("this session has no goal; use goal_set first");
|
|
823
817
|
}
|
|
824
|
-
const { goal,
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
818
|
+
const { goal, events } = await setSessionGoalStatusWithEvent(
|
|
819
|
+
deps.db,
|
|
820
|
+
grant.workspaceId,
|
|
821
|
+
sessionId,
|
|
822
|
+
{
|
|
823
|
+
status: "completed",
|
|
824
|
+
evidence,
|
|
825
|
+
event: { type: "goal.completed", evidence },
|
|
826
|
+
},
|
|
827
|
+
);
|
|
828
|
+
if (events.length > 0) {
|
|
829
|
+
await deps.bus.publish(grant.workspaceId, sessionId, events);
|
|
835
830
|
}
|
|
836
831
|
return json(goal);
|
|
837
832
|
},
|
|
@@ -851,25 +846,24 @@ function registerGoalTools(
|
|
|
851
846
|
if (!existing) {
|
|
852
847
|
throw new Error("this session has no goal; use goal_set first");
|
|
853
848
|
}
|
|
854
|
-
const { goal,
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
849
|
+
const { goal, events } = await setSessionGoalStatusWithEvent(
|
|
850
|
+
deps.db,
|
|
851
|
+
grant.workspaceId,
|
|
852
|
+
sessionId,
|
|
853
|
+
{
|
|
854
|
+
status: "paused",
|
|
855
|
+
rationale,
|
|
856
|
+
pausedReason: "agent",
|
|
857
|
+
event: {
|
|
862
858
|
type: "goal.paused",
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
reason: "agent",
|
|
867
|
-
rationale,
|
|
868
|
-
autoContinuations: goal.autoContinuations,
|
|
869
|
-
noProgressStreak: goal.noProgressStreak,
|
|
870
|
-
},
|
|
859
|
+
actor: "agent",
|
|
860
|
+
reason: "agent",
|
|
861
|
+
rationale,
|
|
871
862
|
},
|
|
872
|
-
|
|
863
|
+
},
|
|
864
|
+
);
|
|
865
|
+
if (events.length > 0) {
|
|
866
|
+
await deps.bus.publish(grant.workspaceId, sessionId, events);
|
|
873
867
|
}
|
|
874
868
|
return json(goal);
|
|
875
869
|
},
|
|
@@ -1038,6 +1032,13 @@ function registerFleetTools(
|
|
|
1038
1032
|
db: deps.db,
|
|
1039
1033
|
settings: deps.settings,
|
|
1040
1034
|
bus: deps.bus,
|
|
1035
|
+
ensureSessionGroupReady: async (ctx) => {
|
|
1036
|
+
const session = await requireSession(deps.db, ctx.workspaceId, ctx.sessionId);
|
|
1037
|
+
return await ensureViewerSessionGroupReady(
|
|
1038
|
+
{ db: deps.db, settings: deps.settings, bus: deps.bus },
|
|
1039
|
+
{ accountId: ctx.accountId, workspaceId: ctx.workspaceId, session },
|
|
1040
|
+
);
|
|
1041
|
+
},
|
|
1041
1042
|
};
|
|
1042
1043
|
|
|
1043
1044
|
// Resolve the session's group sandbox (the default/home fleet member) at
|
|
@@ -1055,7 +1056,7 @@ function registerFleetTools(
|
|
|
1055
1056
|
"sandboxes_list",
|
|
1056
1057
|
{
|
|
1057
1058
|
description:
|
|
1058
|
-
"List the sandboxes this session can run on: its own session sandbox
|
|
1059
|
+
"List the sandboxes this session can run on: its own session sandbox plus enrolled selfhosted machines. `liveness` is conservative: online requires observed provider existence and verified workspace readiness. Provider, lease, route, archive, restore, workspace, lease epoch, and route epoch are also reported separately. Use an entry `id` as an attach/swap/run_on target.",
|
|
1059
1060
|
inputSchema: {},
|
|
1060
1061
|
},
|
|
1061
1062
|
async () => json(await listFleet(services, await fleetContext())),
|
|
@@ -1065,7 +1066,7 @@ function registerFleetTools(
|
|
|
1065
1066
|
"sandbox_attach",
|
|
1066
1067
|
{
|
|
1067
1068
|
description:
|
|
1068
|
-
'Attach this session to a sandbox
|
|
1069
|
+
'Attach this session to a sandbox for the next tool call. The target must be owned and verified ready. A same-target attach is a repair request: it revalidates readiness and advances the route epoch rather than returning unchanged success. Recovery-in-progress/degraded/unrecoverable outcomes are typed. Use a sandboxes_list `id`, or "session"/"default" for home.',
|
|
1069
1070
|
inputSchema: { target: z4.string().min(1) },
|
|
1070
1071
|
},
|
|
1071
1072
|
async ({ target }) => json(await swapActiveSandbox(services, await fleetContext(), target)),
|
|
@@ -1075,7 +1076,7 @@ function registerFleetTools(
|
|
|
1075
1076
|
"sandbox_swap",
|
|
1076
1077
|
{
|
|
1077
1078
|
description:
|
|
1078
|
-
'Swap the active sandbox for this session mid-conversation
|
|
1079
|
+
'Swap the active sandbox for this session mid-conversation. Validates ownership and verified readiness, then advances the route epoch. Same-target swaps also revalidate and fence stale route caches. An operation that encountered provider disappearance is not replayed; retry only after a typed recovery-ready result. Use a sandboxes_list `id`, or "session"/"default" for home.',
|
|
1079
1080
|
inputSchema: { target: z4.string().min(1) },
|
|
1080
1081
|
},
|
|
1081
1082
|
async ({ target }) => json(await swapActiveSandbox(services, await fleetContext(), target)),
|
|
@@ -1575,6 +1576,9 @@ function registerWorkspaceOrchestrationTools(
|
|
|
1575
1576
|
// Workspace-scoped CREATE idempotency key: a retried session_create with
|
|
1576
1577
|
// the same key returns the already-spawned worker instead of a duplicate.
|
|
1577
1578
|
idempotencyKey: z4.string().min(1).max(200).optional(),
|
|
1579
|
+
// Per-session/agent descendant policy. Reductions need only create;
|
|
1580
|
+
// increases are authorized server-side with workspace:admin.
|
|
1581
|
+
maxNestedAgentDepth: z4.number().int().nonnegative().optional(),
|
|
1578
1582
|
// First-party MCP token permissions for the spawned session; every
|
|
1579
1583
|
// permission must be held by this grant (validated in the domain).
|
|
1580
1584
|
// A goal requires goals:manage in the resulting set; it is never
|
|
@@ -1619,11 +1623,21 @@ function registerWorkspaceOrchestrationTools(
|
|
|
1619
1623
|
},
|
|
1620
1624
|
},
|
|
1621
1625
|
async (args) => {
|
|
1622
|
-
|
|
1623
|
-
|
|
1626
|
+
try {
|
|
1627
|
+
if (callerSessionId !== null) {
|
|
1628
|
+
await authorizeFirstPartySession(deps, grant, callerSessionId, "session.child.create");
|
|
1629
|
+
}
|
|
1630
|
+
const created = await createSessionForRequest(deps, grant, grant.workspaceId, args);
|
|
1631
|
+
return json(await withMcpEffectivePolicy(deps, grant.workspaceId, created));
|
|
1632
|
+
} catch (error) {
|
|
1633
|
+
if (error instanceof SessionSpawnDeniedError) {
|
|
1634
|
+
return {
|
|
1635
|
+
...json(sessionSpawnDenialEnvelope(error)),
|
|
1636
|
+
isError: true,
|
|
1637
|
+
};
|
|
1638
|
+
}
|
|
1639
|
+
throw error;
|
|
1624
1640
|
}
|
|
1625
|
-
const created = await createSessionForRequest(deps, grant, grant.workspaceId, args);
|
|
1626
|
-
return json(await withMcpEffectivePolicy(deps, grant.workspaceId, created));
|
|
1627
1641
|
},
|
|
1628
1642
|
);
|
|
1629
1643
|
}
|
package/src/routes/machines.ts
CHANGED
|
@@ -19,13 +19,14 @@ import {
|
|
|
19
19
|
SwapActiveSandboxRequest,
|
|
20
20
|
SwapActiveSandboxResponse,
|
|
21
21
|
} from "@opengeni/contracts";
|
|
22
|
-
import { getEnrollment, readMachineMetricsSeries } from "@opengeni/db";
|
|
22
|
+
import { getEnrollment, readMachineMetricsSeries, requireSession } from "@opengeni/db";
|
|
23
23
|
import type { Hono } from "hono";
|
|
24
24
|
import { HTTPException } from "hono/http-exception";
|
|
25
25
|
import { requireAccessGrant } from "@opengeni/core";
|
|
26
26
|
import type { ApiRouteDeps } from "@opengeni/core";
|
|
27
27
|
import { buildFleetContextForSession, swapActiveSandbox } from "@opengeni/core";
|
|
28
28
|
import { listMachines, metricRowToSample } from "../sandbox/machines";
|
|
29
|
+
import { ensureSessionGroupReady as ensureViewerSessionGroupReady } from "../sandbox/viewer";
|
|
29
30
|
|
|
30
31
|
// The supported series windows → milliseconds. An unknown/absent window defaults
|
|
31
32
|
// to 1h (the default). Bounded so a caller cannot request an unbounded
|
|
@@ -102,7 +103,26 @@ export function registerMachineRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
102
103
|
workspaceId,
|
|
103
104
|
sessionId,
|
|
104
105
|
});
|
|
105
|
-
const result = await swapActiveSandbox(
|
|
106
|
+
const result = await swapActiveSandbox(
|
|
107
|
+
{
|
|
108
|
+
db,
|
|
109
|
+
settings,
|
|
110
|
+
bus,
|
|
111
|
+
ensureSessionGroupReady: async (fleetCtx) => {
|
|
112
|
+
const session = await requireSession(db, fleetCtx.workspaceId, fleetCtx.sessionId);
|
|
113
|
+
return await ensureViewerSessionGroupReady(
|
|
114
|
+
{ db, settings, bus },
|
|
115
|
+
{
|
|
116
|
+
accountId: fleetCtx.accountId,
|
|
117
|
+
workspaceId: fleetCtx.workspaceId,
|
|
118
|
+
session,
|
|
119
|
+
},
|
|
120
|
+
);
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
ctx,
|
|
124
|
+
body.target,
|
|
125
|
+
);
|
|
106
126
|
return c.json(SwapActiveSandboxResponse.parse(result));
|
|
107
127
|
});
|
|
108
128
|
}
|