@vantageos/agent-engine 0.1.0-alpha.3 → 0.1.0-alpha.4
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 +66 -50
- package/dist/component/engine/agents.d.ts +3 -0
- package/dist/component/engine/agents.js +11 -9
- package/dist/component/engine/durableJob.d.ts +3 -0
- package/dist/component/engine/durableJob.js +9 -8
- package/dist/component/engine/resolvers.d.ts +1 -0
- package/dist/component/engine/resolvers.js +4 -4
- package/dist/component/engine/sessions.d.ts +8 -0
- package/dist/component/engine/sessions.js +26 -20
- package/dist/component/engine/toolApprovals.d.ts +1 -0
- package/dist/component/engine/toolApprovals.js +4 -3
- package/dist/component/lib/auth.d.ts +1 -2
- package/dist/component/lib/auth.js +28 -15
- package/dist/component/testFixtures/callerSteps.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -321,80 +321,96 @@ app.use(agentEngine);
|
|
|
321
321
|
export default app;
|
|
322
322
|
```
|
|
323
323
|
|
|
324
|
+
### Trust model — READ THIS FIRST (CORRECTIF, VP k1741evjzm72rj91tkcbdhpskx8ah35a)
|
|
325
|
+
|
|
326
|
+
**Convex does not propagate the calling app's `ctx.auth` identity across a
|
|
327
|
+
component boundary.** This is a platform isolation guarantee, not a choice
|
|
328
|
+
this package makes (`convex-test` states it verbatim: "Auth doesn't
|
|
329
|
+
propagate across component boundaries"). A function inside this component
|
|
330
|
+
therefore can NEVER read `ctx.auth.getUserIdentity()` and expect it to
|
|
331
|
+
reflect the consumer app's end user.
|
|
332
|
+
|
|
333
|
+
**`orgId` is an explicit ARGUMENT on every public function in this
|
|
334
|
+
component.** The calling app is responsible for authenticating its user and
|
|
335
|
+
resolving that user's `orgId` on its OWN side (e.g. reading a Clerk
|
|
336
|
+
org-scoped claim from its own `ctx.auth`) BEFORE calling into this
|
|
337
|
+
component — this component cannot do that step for you, it is a platform
|
|
338
|
+
constraint, not a design preference.
|
|
339
|
+
|
|
340
|
+
What did NOT change: the component still denies by default
|
|
341
|
+
(`lib/auth.ts:requireOrgId` throws on a missing/empty `orgId`), `orgId` is
|
|
342
|
+
still the first field of every index, and every document fetched by `_id`
|
|
343
|
+
is still checked with `lib/auth.ts:assertSameOrg` before being returned or
|
|
344
|
+
mutated — a caller-supplied `orgId` can never read or write another org's
|
|
345
|
+
rows. The isolation boundary moved from "component reads `ctx.auth`"
|
|
346
|
+
(impossible across a component boundary) to "component enforces the
|
|
347
|
+
`orgId` argument the consumer already vouches for"; deny-by-default,
|
|
348
|
+
orgId-first-index, no-cross-org-reads are unchanged.
|
|
349
|
+
|
|
324
350
|
### What's callable, and what isn't (read this before wiring a consumer)
|
|
325
351
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
SESSIONS surface — EveVantage V4's conversation thread.
|
|
352
|
+
`component/` (sibling of `convex/`, NOT nested under it — see "Why
|
|
353
|
+
`component/` and not `convex/component/`" below) hosts the **actual,
|
|
354
|
+
compiled, published functions** for a real slice of the engine, not just
|
|
355
|
+
the component's identity:
|
|
331
356
|
|
|
332
357
|
- `component/schema.ts` — `durableJobs`, `agents`, `toolApprovals`,
|
|
333
358
|
`sessions`, `sessionEvents` (+ a test-only `callerStepInvocations`
|
|
334
359
|
fixture table).
|
|
335
|
-
- `component/engine/durableJob.ts` — `start` / `getStatus` / `cancel
|
|
360
|
+
- `component/engine/durableJob.ts` — `start` / `getStatus` / `cancel`
|
|
361
|
+
(public contract) + `durableJobWorkflowV1` (the versioned pause/resume/
|
|
362
|
+
replay workflow) + its internal step-runner mutations.
|
|
336
363
|
- `component/engine/agents.ts` — `create` / `get` (minimal root-sheet
|
|
337
364
|
bootstrap; no `update`/`list`/skills/connections in this pass).
|
|
338
365
|
- `component/engine/resolvers.ts` — `resolveManifest` (dynamic reread at
|
|
339
366
|
session/turn boundaries; returns `agentId`/`name`/`instructions`/`model`/
|
|
340
367
|
`toolApprovals` — `skills`/`connections` NOT included in this pass).
|
|
341
|
-
- `component/engine/sessions.ts` — the conversation thread contract
|
|
368
|
+
- `component/engine/sessions.ts` — the conversation thread contract:
|
|
369
|
+
`start` / `sendTurn` / `appendReasoning` / `requestToolCall` /
|
|
370
|
+
`completeToolCall` / `completeTurn` / `listEvents`.
|
|
342
371
|
- `component/engine/toolApprovals.ts` — `respond`.
|
|
343
|
-
- `component/lib/auth.ts` — `requireOrgId` / `assertSameOrg`:
|
|
344
|
-
|
|
345
|
-
default travels WITH the component, not just with this repo's own app.
|
|
372
|
+
- `component/lib/auth.ts` — `requireOrgId` / `assertSameOrg`: see "Trust
|
|
373
|
+
model" above.
|
|
346
374
|
|
|
347
|
-
**Callable through the published component today**
|
|
348
|
-
|
|
349
|
-
above for the exact `v.*` shapes):
|
|
375
|
+
**Callable through the published component today** — every function below
|
|
376
|
+
takes `orgId: v.string()` as an explicit argument (see the source files
|
|
377
|
+
above for the exact `v.*` shapes of the remaining args/returns):
|
|
350
378
|
|
|
351
379
|
```
|
|
352
|
-
components.agentEngine.engine.durableJob.start({ kind, stepFunctionHandle, totalSteps, maxAttemptsPerStep }) -> Id<"durableJobs">
|
|
353
|
-
components.agentEngine.engine.durableJob.getStatus({ jobId }) -> { status, cursor, totalSteps, lastError?, abandonedAtStep? }
|
|
354
|
-
components.agentEngine.engine.durableJob.cancel({ jobId }) -> null
|
|
380
|
+
components.agentEngine.engine.durableJob.start({ orgId, kind, stepFunctionHandle, totalSteps, maxAttemptsPerStep }) -> Id<"durableJobs">
|
|
381
|
+
components.agentEngine.engine.durableJob.getStatus({ orgId, jobId }) -> { status, cursor, totalSteps, lastError?, abandonedAtStep? }
|
|
382
|
+
components.agentEngine.engine.durableJob.cancel({ orgId, jobId }) -> null
|
|
355
383
|
|
|
356
|
-
components.agentEngine.engine.agents.create({ name, instructions, model }) -> Id<"agents">
|
|
357
|
-
components.agentEngine.engine.agents.get({ agentId }) -> agent | null
|
|
384
|
+
components.agentEngine.engine.agents.create({ orgId, createdBy, name, instructions, model }) -> Id<"agents">
|
|
385
|
+
components.agentEngine.engine.agents.get({ orgId, agentId }) -> agent | null
|
|
358
386
|
|
|
359
|
-
components.agentEngine.engine.resolvers.resolveManifest({ sessionId }) -> { agentId, name, instructions, model, toolApprovals[] }
|
|
387
|
+
components.agentEngine.engine.resolvers.resolveManifest({ orgId, sessionId }) -> { agentId, name, instructions, model, toolApprovals[] }
|
|
360
388
|
|
|
361
|
-
components.agentEngine.engine.sessions.start({ agentId }) -> Id<"sessions">
|
|
362
|
-
components.agentEngine.engine.sessions.sendTurn({ sessionId, input }) -> null
|
|
363
|
-
components.agentEngine.engine.sessions.appendReasoning({ sessionId, chunk }) -> null
|
|
364
|
-
components.agentEngine.engine.sessions.requestToolCall({ sessionId, toolName, argsJson }) -> { authorized, approvalPending }
|
|
365
|
-
components.agentEngine.engine.sessions.completeToolCall({ sessionId, toolName, resultJson }) -> null
|
|
366
|
-
components.agentEngine.engine.sessions.completeTurn({ sessionId }) -> null
|
|
367
|
-
components.agentEngine.engine.sessions.listEvents({ sessionId }) -> sessionEvent[]
|
|
389
|
+
components.agentEngine.engine.sessions.start({ orgId, agentId, createdBy }) -> Id<"sessions">
|
|
390
|
+
components.agentEngine.engine.sessions.sendTurn({ orgId, sessionId, input }) -> null
|
|
391
|
+
components.agentEngine.engine.sessions.appendReasoning({ orgId, sessionId, chunk }) -> null
|
|
392
|
+
components.agentEngine.engine.sessions.requestToolCall({ orgId, sessionId, toolName, argsJson }) -> { authorized, approvalPending }
|
|
393
|
+
components.agentEngine.engine.sessions.completeToolCall({ orgId, sessionId, toolName, resultJson }) -> null
|
|
394
|
+
components.agentEngine.engine.sessions.completeTurn({ orgId, sessionId }) -> null
|
|
395
|
+
components.agentEngine.engine.sessions.listEvents({ orgId, sessionId }) -> sessionEvent[]
|
|
368
396
|
|
|
369
|
-
components.agentEngine.engine.toolApprovals.respond({ sessionId, toolName, approval: "always"|"once"|"never" }) -> null
|
|
397
|
+
components.agentEngine.engine.toolApprovals.respond({ orgId, sessionId, toolName, approval: "always"|"once"|"never" }) -> null
|
|
370
398
|
```
|
|
371
399
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
subscribing.
|
|
378
|
-
|
|
379
|
-
Tool-approval gate (`toolApprovals.approval`, default `"never"`):
|
|
380
|
-
`"never"` never pauses; `"always"` pauses (raises
|
|
381
|
-
`tool.approval.requested`) on every `requestToolCall`; `"once"` pauses
|
|
382
|
-
unless `tool.approval.resolved` for that tool was already recorded EARLIER
|
|
383
|
-
IN THE SAME SESSION. `toolApprovals.respond` both writes the standing
|
|
384
|
-
policy (upsert on `orgId`+`agentId`+`toolName`) and appends
|
|
385
|
-
`tool.approval.resolved` for the calling session.
|
|
386
|
-
|
|
387
|
-
Verified two ways, same discipline as T2-QUATER: (1)
|
|
388
|
-
`component/durableJob.componentPackaging.test.ts` +
|
|
400
|
+
`createdBy` (on `agents.create` / `sessions.start`) is, for the same
|
|
401
|
+
platform reason as `orgId`, no longer read from `ctx.auth.getUserIdentity()`
|
|
402
|
+
— the consumer passes the acting user's own identifier.
|
|
403
|
+
|
|
404
|
+
Verified two ways: (1) `component/durableJob.componentPackaging.test.ts` +
|
|
389
405
|
`component/sessions.componentPackaging.test.ts` run `convex-test` directly
|
|
390
406
|
against `component/_generated/api` (the exact source tree
|
|
391
407
|
`tsc -p tsconfig.build.json` compiles into `dist/component/`, which is
|
|
392
|
-
what `npm pack` ships)
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
`
|
|
397
|
-
transcript.
|
|
408
|
+
what `npm pack` ships), so the compiled artifact has a real green test
|
|
409
|
+
suite behind it, not just the repo-root dev app's; (2) a from-scratch
|
|
410
|
+
consumer probe (install the published tarball into a bare `npx convex dev`
|
|
411
|
+
project, register the component, call `durableJob.start` + `sessions.start`
|
|
412
|
+
from the consumer's own function, passing `orgId` explicitly) — see the
|
|
413
|
+
completion report for the verbatim transcript.
|
|
398
414
|
|
|
399
415
|
**NOT yet callable through the published component** (still only exists in
|
|
400
416
|
this repo's OWN dev/test deployment under `convex/schema.ts` +
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
export declare const create: import("convex/server").RegisteredMutation<"public", {
|
|
2
2
|
name: string;
|
|
3
|
+
orgId: string;
|
|
3
4
|
instructions: string;
|
|
4
5
|
model: string;
|
|
6
|
+
createdBy: string;
|
|
5
7
|
}, Promise<import("convex/values").GenericId<"agents">>>;
|
|
6
8
|
export declare const get: import("convex/server").RegisteredQuery<"public", {
|
|
9
|
+
orgId: string;
|
|
7
10
|
agentId: import("convex/values").GenericId<"agents">;
|
|
8
11
|
}, Promise<{
|
|
9
12
|
_id: import("convex/values").GenericId<"agents">;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { v } from "convex/values";
|
|
2
|
-
import { mutation, query } from "../_generated/server";
|
|
3
|
-
import { requireOrgId } from "../lib/auth";
|
|
2
|
+
import { mutation, query } from "../_generated/server.js";
|
|
3
|
+
import { requireOrgId } from "../lib/auth.js";
|
|
4
4
|
// engine/agents — MINIMAL root-sheet bootstrap for the sessions surface
|
|
5
5
|
// (T2-QUINQUIES). `sessions.start` needs an `agentId` it can read INSIDE
|
|
6
6
|
// the component boundary (component isolation: it cannot reach into a
|
|
@@ -22,16 +22,18 @@ const agentDoc = v.object({
|
|
|
22
22
|
});
|
|
23
23
|
export const create = mutation({
|
|
24
24
|
args: {
|
|
25
|
+
orgId: v.string(),
|
|
26
|
+
createdBy: v.string(),
|
|
25
27
|
name: v.string(),
|
|
26
28
|
instructions: v.string(),
|
|
27
29
|
model: v.string(),
|
|
28
30
|
},
|
|
29
31
|
returns: v.id("agents"),
|
|
30
32
|
handler: async (ctx, args) => {
|
|
31
|
-
const orgId =
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
const orgId = requireOrgId(args.orgId);
|
|
34
|
+
if (args.createdBy.trim().length === 0) {
|
|
35
|
+
throw new Error("Validation: createdBy must not be empty");
|
|
36
|
+
}
|
|
35
37
|
if (args.name.trim().length === 0) {
|
|
36
38
|
throw new Error("Validation: name must not be empty");
|
|
37
39
|
}
|
|
@@ -48,17 +50,17 @@ export const create = mutation({
|
|
|
48
50
|
instructions: args.instructions,
|
|
49
51
|
model: args.model,
|
|
50
52
|
status: "live",
|
|
51
|
-
createdBy:
|
|
53
|
+
createdBy: args.createdBy,
|
|
52
54
|
createdAt: now,
|
|
53
55
|
updatedAt: now,
|
|
54
56
|
});
|
|
55
57
|
},
|
|
56
58
|
});
|
|
57
59
|
export const get = query({
|
|
58
|
-
args: { agentId: v.id("agents") },
|
|
60
|
+
args: { orgId: v.string(), agentId: v.id("agents") },
|
|
59
61
|
returns: v.union(agentDoc, v.null()),
|
|
60
62
|
handler: async (ctx, args) => {
|
|
61
|
-
const orgId =
|
|
63
|
+
const orgId = requireOrgId(args.orgId);
|
|
62
64
|
const doc = await ctx.db.get(args.agentId);
|
|
63
65
|
if (!doc || doc.orgId !== orgId)
|
|
64
66
|
return null;
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { WorkflowManager, type WorkflowId } from "@convex-dev/workflow";
|
|
2
2
|
export declare const workflow: WorkflowManager;
|
|
3
3
|
export declare const start: import("convex/server").RegisteredMutation<"public", {
|
|
4
|
+
orgId: string;
|
|
4
5
|
stepFunctionHandle: string;
|
|
5
6
|
totalSteps: number;
|
|
6
7
|
maxAttemptsPerStep: number;
|
|
7
8
|
kind: string;
|
|
8
9
|
}, Promise<import("convex/values").GenericId<"durableJobs">>>;
|
|
9
10
|
export declare const getStatus: import("convex/server").RegisteredQuery<"public", {
|
|
11
|
+
orgId: string;
|
|
10
12
|
jobId: import("convex/values").GenericId<"durableJobs">;
|
|
11
13
|
}, Promise<{
|
|
12
14
|
status: "running" | "completed" | "abandoned" | "cancelled";
|
|
@@ -16,6 +18,7 @@ export declare const getStatus: import("convex/server").RegisteredQuery<"public"
|
|
|
16
18
|
abandonedAtStep: number | undefined;
|
|
17
19
|
}>>;
|
|
18
20
|
export declare const cancel: import("convex/server").RegisteredMutation<"public", {
|
|
21
|
+
orgId: string;
|
|
19
22
|
jobId: import("convex/values").GenericId<"durableJobs">;
|
|
20
23
|
}, Promise<null>>;
|
|
21
24
|
export declare const getJobInternal: import("convex/server").RegisteredQuery<"internal", {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { v } from "convex/values";
|
|
2
2
|
import { WorkflowManager } from "@convex-dev/workflow";
|
|
3
|
-
import { components, internal } from "../_generated/api";
|
|
4
|
-
import { internalMutation, internalQuery, mutation, query, } from "../_generated/server";
|
|
5
|
-
import { assertSameOrg, requireOrgId } from "../lib/auth";
|
|
3
|
+
import { components, internal } from "../_generated/api.js";
|
|
4
|
+
import { internalMutation, internalQuery, mutation, query, } from "../_generated/server.js";
|
|
5
|
+
import { assertSameOrg, requireOrgId } from "../lib/auth.js";
|
|
6
6
|
// durableJobs — THE SURVIVAL CONTRACT (I1, SYNTHESE.md §5/§7): "mise en
|
|
7
7
|
// pause à la frontière d'étape, rejeu déterministe, reprise, abandon après
|
|
8
8
|
// N échecs." This ENVELOPS `@convex-dev/workflow` -- it does not reimplement
|
|
@@ -50,6 +50,7 @@ export const workflow = new WorkflowManager(components.workflow);
|
|
|
50
50
|
// ---------------------------------------------------------------------
|
|
51
51
|
export const start = mutation({
|
|
52
52
|
args: {
|
|
53
|
+
orgId: v.string(),
|
|
53
54
|
kind: v.string(),
|
|
54
55
|
stepFunctionHandle: v.string(),
|
|
55
56
|
totalSteps: v.number(),
|
|
@@ -57,7 +58,7 @@ export const start = mutation({
|
|
|
57
58
|
},
|
|
58
59
|
returns: v.id("durableJobs"),
|
|
59
60
|
handler: async (ctx, args) => {
|
|
60
|
-
const orgId =
|
|
61
|
+
const orgId = requireOrgId(args.orgId);
|
|
61
62
|
if (!Number.isInteger(args.totalSteps) || args.totalSteps <= 0) {
|
|
62
63
|
throw new Error("Validation: totalSteps must be a positive integer");
|
|
63
64
|
}
|
|
@@ -87,7 +88,7 @@ export const start = mutation({
|
|
|
87
88
|
},
|
|
88
89
|
});
|
|
89
90
|
export const getStatus = query({
|
|
90
|
-
args: { jobId: v.id("durableJobs") },
|
|
91
|
+
args: { orgId: v.string(), jobId: v.id("durableJobs") },
|
|
91
92
|
returns: v.object({
|
|
92
93
|
status: v.union(v.literal("running"), v.literal("completed"), v.literal("abandoned"), v.literal("cancelled")),
|
|
93
94
|
cursor: v.number(),
|
|
@@ -96,7 +97,7 @@ export const getStatus = query({
|
|
|
96
97
|
abandonedAtStep: v.optional(v.number()),
|
|
97
98
|
}),
|
|
98
99
|
handler: async (ctx, args) => {
|
|
99
|
-
const orgId =
|
|
100
|
+
const orgId = requireOrgId(args.orgId);
|
|
100
101
|
const job = await ctx.db.get(args.jobId);
|
|
101
102
|
assertSameOrg(orgId, job, "Job not found");
|
|
102
103
|
return {
|
|
@@ -109,10 +110,10 @@ export const getStatus = query({
|
|
|
109
110
|
},
|
|
110
111
|
});
|
|
111
112
|
export const cancel = mutation({
|
|
112
|
-
args: { jobId: v.id("durableJobs") },
|
|
113
|
+
args: { orgId: v.string(), jobId: v.id("durableJobs") },
|
|
113
114
|
returns: v.null(),
|
|
114
115
|
handler: async (ctx, args) => {
|
|
115
|
-
const orgId =
|
|
116
|
+
const orgId = requireOrgId(args.orgId);
|
|
116
117
|
const job = await ctx.db.get(args.jobId);
|
|
117
118
|
assertSameOrg(orgId, job, "Job not found");
|
|
118
119
|
if (job.status !== "running")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { v } from "convex/values";
|
|
2
|
-
import { query } from "../_generated/server";
|
|
3
|
-
import { assertSameOrg, requireOrgId } from "../lib/auth";
|
|
2
|
+
import { query } from "../_generated/server.js";
|
|
3
|
+
import { assertSameOrg, requireOrgId } from "../lib/auth.js";
|
|
4
4
|
// resolveManifest — the dynamic resolver (eve parity:
|
|
5
5
|
// `dynamicInstructions`/`dynamicSkills`/`dynamicTools`,
|
|
6
6
|
// packages/eve/src/compiler/manifest.ts:722-725). Produced FRESH from the
|
|
@@ -15,7 +15,7 @@ import { assertSameOrg, requireOrgId } from "../lib/auth";
|
|
|
15
15
|
// component/schema.ts SCOPE note); this resolver returns only what the
|
|
16
16
|
// component boundary currently owns.
|
|
17
17
|
export const resolveManifest = query({
|
|
18
|
-
args: { sessionId: v.id("sessions") },
|
|
18
|
+
args: { orgId: v.string(), sessionId: v.id("sessions") },
|
|
19
19
|
returns: v.object({
|
|
20
20
|
agentId: v.id("agents"),
|
|
21
21
|
name: v.string(),
|
|
@@ -27,7 +27,7 @@ export const resolveManifest = query({
|
|
|
27
27
|
})),
|
|
28
28
|
}),
|
|
29
29
|
handler: async (ctx, args) => {
|
|
30
|
-
const orgId =
|
|
30
|
+
const orgId = requireOrgId(args.orgId);
|
|
31
31
|
const session = await ctx.db.get(args.sessionId);
|
|
32
32
|
assertSameOrg(orgId, session, "Session not found");
|
|
33
33
|
const agent = await ctx.db.get(session.agentId);
|
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
export declare const start: import("convex/server").RegisteredMutation<"public", {
|
|
2
|
+
orgId: string;
|
|
3
|
+
createdBy: string;
|
|
2
4
|
agentId: import("convex/values").GenericId<"agents">;
|
|
3
5
|
}, Promise<import("convex/values").GenericId<"sessions">>>;
|
|
4
6
|
export declare const sendTurn: import("convex/server").RegisteredMutation<"public", {
|
|
7
|
+
orgId: string;
|
|
5
8
|
sessionId: import("convex/values").GenericId<"sessions">;
|
|
6
9
|
input: string;
|
|
7
10
|
}, Promise<null>>;
|
|
8
11
|
export declare const appendReasoning: import("convex/server").RegisteredMutation<"public", {
|
|
12
|
+
orgId: string;
|
|
9
13
|
sessionId: import("convex/values").GenericId<"sessions">;
|
|
10
14
|
chunk: string;
|
|
11
15
|
}, Promise<null>>;
|
|
12
16
|
export declare const requestToolCall: import("convex/server").RegisteredMutation<"public", {
|
|
17
|
+
orgId: string;
|
|
13
18
|
toolName: string;
|
|
14
19
|
sessionId: import("convex/values").GenericId<"sessions">;
|
|
15
20
|
argsJson: string;
|
|
@@ -18,14 +23,17 @@ export declare const requestToolCall: import("convex/server").RegisteredMutation
|
|
|
18
23
|
approvalPending: boolean;
|
|
19
24
|
}>>;
|
|
20
25
|
export declare const completeToolCall: import("convex/server").RegisteredMutation<"public", {
|
|
26
|
+
orgId: string;
|
|
21
27
|
toolName: string;
|
|
22
28
|
sessionId: import("convex/values").GenericId<"sessions">;
|
|
23
29
|
resultJson: string;
|
|
24
30
|
}, Promise<null>>;
|
|
25
31
|
export declare const completeTurn: import("convex/server").RegisteredMutation<"public", {
|
|
32
|
+
orgId: string;
|
|
26
33
|
sessionId: import("convex/values").GenericId<"sessions">;
|
|
27
34
|
}, Promise<null>>;
|
|
28
35
|
export declare const listEvents: import("convex/server").RegisteredQuery<"public", {
|
|
36
|
+
orgId: string;
|
|
29
37
|
sessionId: import("convex/values").GenericId<"sessions">;
|
|
30
38
|
}, Promise<{
|
|
31
39
|
_id: import("convex/values").GenericId<"sessionEvents">;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { v } from "convex/values";
|
|
2
|
-
import { api } from "../_generated/api";
|
|
3
|
-
import { mutation, query } from "../_generated/server";
|
|
4
|
-
import { assertSameOrg, requireOrgId } from "../lib/auth";
|
|
2
|
+
import { api } from "../_generated/api.js";
|
|
3
|
+
import { mutation, query } from "../_generated/server.js";
|
|
4
|
+
import { assertSameOrg, requireOrgId } from "../lib/auth.js";
|
|
5
5
|
// engine/sessions — THE FIL DE CONVERSATION contract (T2-QUINQUIES). This
|
|
6
6
|
// is what unblocks EveVantage V4: open a session, send a turn, stream
|
|
7
7
|
// reasoning/tool calls, gate on tool approval, replay the thread on
|
|
@@ -26,13 +26,13 @@ async function insertEvent(ctx, args) {
|
|
|
26
26
|
// start — opens a session (session.started boundary), rereading the
|
|
27
27
|
// manifest immediately so a caller can act on it before the first turn.
|
|
28
28
|
export const start = mutation({
|
|
29
|
-
args: { agentId: v.id("agents") },
|
|
29
|
+
args: { orgId: v.string(), agentId: v.id("agents"), createdBy: v.string() },
|
|
30
30
|
returns: v.id("sessions"),
|
|
31
31
|
handler: async (ctx, args) => {
|
|
32
|
-
const orgId =
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
32
|
+
const orgId = requireOrgId(args.orgId);
|
|
33
|
+
if (args.createdBy.trim().length === 0) {
|
|
34
|
+
throw new Error("Validation: createdBy must not be empty");
|
|
35
|
+
}
|
|
36
36
|
const agent = await ctx.db.get(args.agentId);
|
|
37
37
|
assertSameOrg(orgId, agent, "Agent not found");
|
|
38
38
|
const now = Date.now();
|
|
@@ -40,12 +40,15 @@ export const start = mutation({
|
|
|
40
40
|
orgId,
|
|
41
41
|
agentId: args.agentId,
|
|
42
42
|
status: "running",
|
|
43
|
-
createdBy:
|
|
43
|
+
createdBy: args.createdBy,
|
|
44
44
|
createdAt: now,
|
|
45
45
|
updatedAt: now,
|
|
46
46
|
});
|
|
47
47
|
await insertEvent(ctx, { orgId, sessionId, name: "session.started" });
|
|
48
|
-
await ctx.runQuery(api.engine.resolvers.resolveManifest, {
|
|
48
|
+
await ctx.runQuery(api.engine.resolvers.resolveManifest, {
|
|
49
|
+
orgId,
|
|
50
|
+
sessionId,
|
|
51
|
+
});
|
|
49
52
|
return sessionId;
|
|
50
53
|
},
|
|
51
54
|
});
|
|
@@ -55,10 +58,10 @@ export const start = mutation({
|
|
|
55
58
|
// this component does not interpret it -- it is stored verbatim as the
|
|
56
59
|
// event payload for replay.
|
|
57
60
|
export const sendTurn = mutation({
|
|
58
|
-
args: { sessionId: v.id("sessions"), input: v.string() },
|
|
61
|
+
args: { orgId: v.string(), sessionId: v.id("sessions"), input: v.string() },
|
|
59
62
|
returns: v.null(),
|
|
60
63
|
handler: async (ctx, args) => {
|
|
61
|
-
const orgId =
|
|
64
|
+
const orgId = requireOrgId(args.orgId);
|
|
62
65
|
const session = await ctx.db.get(args.sessionId);
|
|
63
66
|
assertSameOrg(orgId, session, "Session not found");
|
|
64
67
|
if (session.status !== "running") {
|
|
@@ -71,6 +74,7 @@ export const sendTurn = mutation({
|
|
|
71
74
|
payload: args.input,
|
|
72
75
|
});
|
|
73
76
|
await ctx.runQuery(api.engine.resolvers.resolveManifest, {
|
|
77
|
+
orgId,
|
|
74
78
|
sessionId: args.sessionId,
|
|
75
79
|
});
|
|
76
80
|
await ctx.db.patch(args.sessionId, { updatedAt: Date.now() });
|
|
@@ -81,10 +85,10 @@ export const sendTurn = mutation({
|
|
|
81
85
|
// on `listEvents` is the streaming transport: each insert here is pushed to
|
|
82
86
|
// every subscriber live, no separate channel needed.
|
|
83
87
|
export const appendReasoning = mutation({
|
|
84
|
-
args: { sessionId: v.id("sessions"), chunk: v.string() },
|
|
88
|
+
args: { orgId: v.string(), sessionId: v.id("sessions"), chunk: v.string() },
|
|
85
89
|
returns: v.null(),
|
|
86
90
|
handler: async (ctx, args) => {
|
|
87
|
-
const orgId =
|
|
91
|
+
const orgId = requireOrgId(args.orgId);
|
|
88
92
|
const session = await ctx.db.get(args.sessionId);
|
|
89
93
|
assertSameOrg(orgId, session, "Session not found");
|
|
90
94
|
await insertEvent(ctx, {
|
|
@@ -103,13 +107,14 @@ export const appendReasoning = mutation({
|
|
|
103
107
|
// `toolApprovals.respond`.
|
|
104
108
|
export const requestToolCall = mutation({
|
|
105
109
|
args: {
|
|
110
|
+
orgId: v.string(),
|
|
106
111
|
sessionId: v.id("sessions"),
|
|
107
112
|
toolName: v.string(),
|
|
108
113
|
argsJson: v.string(),
|
|
109
114
|
},
|
|
110
115
|
returns: v.object({ authorized: v.boolean(), approvalPending: v.boolean() }),
|
|
111
116
|
handler: async (ctx, args) => {
|
|
112
|
-
const orgId =
|
|
117
|
+
const orgId = requireOrgId(args.orgId);
|
|
113
118
|
const session = await ctx.db.get(args.sessionId);
|
|
114
119
|
assertSameOrg(orgId, session, "Session not found");
|
|
115
120
|
await insertEvent(ctx, {
|
|
@@ -165,13 +170,14 @@ export const requestToolCall = mutation({
|
|
|
165
170
|
// the gate).
|
|
166
171
|
export const completeToolCall = mutation({
|
|
167
172
|
args: {
|
|
173
|
+
orgId: v.string(),
|
|
168
174
|
sessionId: v.id("sessions"),
|
|
169
175
|
toolName: v.string(),
|
|
170
176
|
resultJson: v.string(),
|
|
171
177
|
},
|
|
172
178
|
returns: v.null(),
|
|
173
179
|
handler: async (ctx, args) => {
|
|
174
|
-
const orgId =
|
|
180
|
+
const orgId = requireOrgId(args.orgId);
|
|
175
181
|
const session = await ctx.db.get(args.sessionId);
|
|
176
182
|
assertSameOrg(orgId, session, "Session not found");
|
|
177
183
|
await insertEvent(ctx, {
|
|
@@ -185,10 +191,10 @@ export const completeToolCall = mutation({
|
|
|
185
191
|
});
|
|
186
192
|
// completeTurn — closes the current turn.
|
|
187
193
|
export const completeTurn = mutation({
|
|
188
|
-
args: { sessionId: v.id("sessions") },
|
|
194
|
+
args: { orgId: v.string(), sessionId: v.id("sessions") },
|
|
189
195
|
returns: v.null(),
|
|
190
196
|
handler: async (ctx, args) => {
|
|
191
|
-
const orgId =
|
|
197
|
+
const orgId = requireOrgId(args.orgId);
|
|
192
198
|
const session = await ctx.db.get(args.sessionId);
|
|
193
199
|
assertSameOrg(orgId, session, "Session not found");
|
|
194
200
|
await insertEvent(ctx, {
|
|
@@ -204,7 +210,7 @@ export const completeTurn = mutation({
|
|
|
204
210
|
// session. A client that drops and reconnects calls this once to rebuild
|
|
205
211
|
// the thread, then subscribes (Convex reactivity) for anything after.
|
|
206
212
|
export const listEvents = query({
|
|
207
|
-
args: { sessionId: v.id("sessions") },
|
|
213
|
+
args: { orgId: v.string(), sessionId: v.id("sessions") },
|
|
208
214
|
returns: v.array(v.object({
|
|
209
215
|
_id: v.id("sessionEvents"),
|
|
210
216
|
_creationTime: v.number(),
|
|
@@ -213,7 +219,7 @@ export const listEvents = query({
|
|
|
213
219
|
createdAt: v.number(),
|
|
214
220
|
})),
|
|
215
221
|
handler: async (ctx, args) => {
|
|
216
|
-
const orgId =
|
|
222
|
+
const orgId = requireOrgId(args.orgId);
|
|
217
223
|
const session = await ctx.db.get(args.sessionId);
|
|
218
224
|
assertSameOrg(orgId, session, "Session not found");
|
|
219
225
|
const events = await ctx.db
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { v } from "convex/values";
|
|
2
|
-
import { mutation } from "../_generated/server";
|
|
3
|
-
import { assertSameOrg, requireOrgId } from "../lib/auth";
|
|
2
|
+
import { mutation } from "../_generated/server.js";
|
|
3
|
+
import { assertSameOrg, requireOrgId } from "../lib/auth.js";
|
|
4
4
|
// engine/toolApprovals — respond to a pending approval request
|
|
5
5
|
// (`tool.approval.requested`, raised by `sessions.requestToolCall`). The
|
|
6
6
|
// decision (always/once/never) is WRITTEN as the agent's standing policy
|
|
@@ -9,13 +9,14 @@ import { assertSameOrg, requireOrgId } from "../lib/auth";
|
|
|
9
9
|
// "once" check (and any replaying client) can see it happened.
|
|
10
10
|
export const respond = mutation({
|
|
11
11
|
args: {
|
|
12
|
+
orgId: v.string(),
|
|
12
13
|
sessionId: v.id("sessions"),
|
|
13
14
|
toolName: v.string(),
|
|
14
15
|
approval: v.union(v.literal("always"), v.literal("once"), v.literal("never")),
|
|
15
16
|
},
|
|
16
17
|
returns: v.null(),
|
|
17
18
|
handler: async (ctx, args) => {
|
|
18
|
-
const orgId =
|
|
19
|
+
const orgId = requireOrgId(args.orgId);
|
|
19
20
|
const session = await ctx.db.get(args.sessionId);
|
|
20
21
|
assertSameOrg(orgId, session, "Session not found");
|
|
21
22
|
const existing = await ctx.db
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
export declare function requireOrgId(ctx: QueryCtx | MutationCtx): Promise<string>;
|
|
1
|
+
export declare function requireOrgId(orgId: string | null | undefined): string;
|
|
3
2
|
export declare function assertSameOrg(callerOrgId: string, doc: {
|
|
4
3
|
orgId: string;
|
|
5
4
|
} | null, notFoundMessage: string): asserts doc is {
|
|
@@ -1,21 +1,34 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
1
|
+
// Trust model (CORRECTIF BLOQUANT, VP k1741evjzm72rj91tkcbdhpskx8ah35a):
|
|
2
|
+
// Convex does NOT propagate the calling app's `ctx.auth` identity across a
|
|
3
|
+
// component boundary -- this is a platform isolation guarantee, not a
|
|
4
|
+
// choice this component makes (convex-test: "Auth doesn't propagate across
|
|
5
|
+
// component boundaries"). A handler inside this component can therefore
|
|
6
|
+
// NEVER read `ctx.auth.getUserIdentity()` and expect it to reflect the
|
|
7
|
+
// consumer app's end user -- there is no such identity to read here.
|
|
8
|
+
//
|
|
9
|
+
// The consumer app has ALREADY authenticated its user and resolved their
|
|
10
|
+
// orgId on ITS OWN side (e.g. Clerk org claim, read in the consumer's own
|
|
11
|
+
// `ctx.auth`) before ever calling into this component. `orgId` is passed
|
|
12
|
+
// as an explicit argument on every public function in this component.
|
|
13
|
+
//
|
|
14
|
+
// What does NOT change: this component still derives NOTHING from the
|
|
15
|
+
// client beyond what the consumer's public API surface exposes, still
|
|
16
|
+
// denies by default, and `orgId` is still the first field of every index
|
|
17
|
+
// and is still enforced on every document read via `assertSameOrg` below
|
|
18
|
+
// -- a caller-supplied `orgId` can never see another org's rows. The
|
|
19
|
+
// isolation boundary moved from "component reads ctx.auth" (impossible
|
|
20
|
+
// across a component boundary) to "component enforces the orgId the
|
|
21
|
+
// consumer already vouches for"; the boundary itself -- deny by default,
|
|
22
|
+
// orgId first, no cross-org reads -- is unchanged.
|
|
23
|
+
export function requireOrgId(orgId) {
|
|
24
|
+
if (!orgId || typeof orgId !== "string" || orgId.trim().length === 0) {
|
|
25
|
+
throw new Error("Unauthenticated: caller did not supply a non-empty orgId");
|
|
13
26
|
}
|
|
14
27
|
return orgId;
|
|
15
28
|
}
|
|
16
|
-
// Throws unless the document's orgId matches the caller
|
|
17
|
-
//
|
|
18
|
-
//
|
|
29
|
+
// Throws unless the document's orgId matches the caller-supplied orgId.
|
|
30
|
+
// Use on every read/write of a document fetched by _id, since db.get()
|
|
31
|
+
// does not filter by index.
|
|
19
32
|
export function assertSameOrg(callerOrgId, doc, notFoundMessage) {
|
|
20
33
|
if (!doc || doc.orgId !== callerOrgId) {
|
|
21
34
|
// Same error for "not found" and "cross-org" -- do not leak existence.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { v } from "convex/values";
|
|
2
|
-
import { internalMutation, internalQuery } from "../_generated/server";
|
|
2
|
+
import { internalMutation, internalQuery } from "../_generated/server.js";
|
|
3
3
|
// testFixtures/callerSteps — stand-ins for a CALLER'S OWN step function
|
|
4
4
|
// (the thing a real consumer, e.g. VantagePeers, would define in ITS OWN
|
|
5
5
|
// app and pass to `engine/durableJob.start` as `stepFunctionHandle` via
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vantageos/agent-engine",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.4",
|
|
4
4
|
"description": "Reusable Convex component: durable agent engine. Agent = data aggregate (root sheet + child tables), never compiled code. Built on @convex-dev/workflow + @convex-dev/workpool (+ @convex-dev/agent). Eve parity by design.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "UNLICENSED",
|