@vantageos/agent-engine 0.1.0-alpha.3 → 0.1.0-alpha.5

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 CHANGED
@@ -214,10 +214,31 @@ Two gates now exist, and they check different things:
214
214
  hand-edit of existing generated content.**
215
215
  2. **Content, requires a live deployment** --
216
216
  `scripts/check-generated-content.mjs` (`npm run
217
- check:generated:content`). Actually runs `npx convex codegen` against
218
- the dev deployment and diffs `convex/_generated/` byte-for-byte
219
- (`git diff --exit-code`). Catches everything, including a single hand-
220
- edited character.
217
+ check:generated:content`). Runs `npx convex codegen` against the dev
218
+ deployment for BOTH `convex/_generated/` (the host repo's own dev
219
+ deployment bindings) AND, since 0.1.0-alpha.5, `component/_generated/`
220
+ via `npx convex codegen --component-dir component` -- the checked-in
221
+ file `tsc -p tsconfig.build.json` copies verbatim into the PUBLISHED
222
+ `dist/component/_generated/component.d.ts`, i.e. the exact type a
223
+ consuming app checks `components.agentEngine.*` calls against. Diffs
224
+ each byte-for-byte (`git diff --exit-code`). Catches everything,
225
+ including a single hand-edited character.
226
+
227
+ **`component/_generated/` was NOT covered before 0.1.0-alpha.5** — that
228
+ gap is why `0.1.0-alpha.4` shipped a `component.d.ts` stale against the
229
+ real component schema (missing `orgId`/`createdBy` on
230
+ sessions/agents/durableJob/toolApprovals args), which broke
231
+ `tsc --noEmit` for any consumer calling `sessions.start`/`durableJob.start`
232
+ with `orgId` (Hestia, VP k17ea6x3tht8xa3rxm139t23gx8ajkf3, defaut 1).
233
+ The visibility (`"internal"`) on every entry is, by measured comparison
234
+ against `@convex-dev/rate-limiter`'s own published `component.d.ts`,
235
+ CORRECT and unrelated to that defect — every official Convex component
236
+ types its cross-component `ComponentApi` as `"internal"` regardless of
237
+ whether the underlying function is declared `mutation`/`query` or
238
+ `internalMutation`/`internalQuery`; `ctx.runMutation`'s own signature
239
+ (`convex/server`) accepts `FunctionReference<"mutation", "public" |
240
+ "internal">` either way. The root cause was staleness of the args
241
+ shape, not visibility.
221
242
 
222
243
  **Three-state exit contract** (never two -- a two-state gate cannot
223
244
  distinguish "I measured, it's clean" from "I could not measure at
@@ -321,80 +342,125 @@ app.use(agentEngine);
321
342
  export default app;
322
343
  ```
323
344
 
345
+ ### Trust model — READ THIS FIRST (CORRECTIF, VP k1741evjzm72rj91tkcbdhpskx8ah35a)
346
+
347
+ **Convex does not propagate the calling app's `ctx.auth` identity across a
348
+ component boundary.** This is a platform isolation guarantee, not a choice
349
+ this package makes (`convex-test` states it verbatim: "Auth doesn't
350
+ propagate across component boundaries"). A function inside this component
351
+ therefore can NEVER read `ctx.auth.getUserIdentity()` and expect it to
352
+ reflect the consumer app's end user.
353
+
354
+ **`orgId` is an explicit ARGUMENT on every public function in this
355
+ component.** The calling app is responsible for authenticating its user and
356
+ resolving that user's `orgId` on its OWN side (e.g. reading a Clerk
357
+ org-scoped claim from its own `ctx.auth`) BEFORE calling into this
358
+ component — this component cannot do that step for you, it is a platform
359
+ constraint, not a design preference.
360
+
361
+ What did NOT change: the component still denies by default
362
+ (`lib/auth.ts:requireOrgId` throws on a missing/empty `orgId`), `orgId` is
363
+ still the first field of every index, and every document fetched by `_id`
364
+ is still checked with `lib/auth.ts:assertSameOrg` before being returned or
365
+ mutated — a caller-supplied `orgId` can never read or write another org's
366
+ rows. The isolation boundary moved from "component reads `ctx.auth`"
367
+ (impossible across a component boundary) to "component enforces the
368
+ `orgId` argument the consumer already vouches for"; deny-by-default,
369
+ orgId-first-index, no-cross-org-reads are unchanged.
370
+
324
371
  ### What's callable, and what isn't (read this before wiring a consumer)
325
372
 
326
- Since T2-QUATER, `component/` (sibling of `convex/`, NOT nested under it —
327
- see "Why `component/` and not `convex/component/`" below) hosts the
328
- **actual, compiled, published functions** for a real slice of the engine,
329
- not just the component's identity. T2-QUINQUIES widens this to the
330
- SESSIONS surface — EveVantage V4's conversation thread.
373
+ `component/` (sibling of `convex/`, NOT nested under it — see "Why
374
+ `component/` and not `convex/component/`" below) hosts the **actual,
375
+ compiled, published functions** for a real slice of the engine, not just
376
+ the component's identity:
331
377
 
332
378
  - `component/schema.ts` — `durableJobs`, `agents`, `toolApprovals`,
333
379
  `sessions`, `sessionEvents` (+ a test-only `callerStepInvocations`
334
380
  fixture table).
335
- - `component/engine/durableJob.ts` — `start` / `getStatus` / `cancel`.
381
+ - `component/engine/durableJob.ts` — `start` / `getStatus` / `cancel`
382
+ (public contract) + `durableJobWorkflowV1` (the versioned pause/resume/
383
+ replay workflow) + its internal step-runner mutations.
336
384
  - `component/engine/agents.ts` — `create` / `get` (minimal root-sheet
337
385
  bootstrap; no `update`/`list`/skills/connections in this pass).
338
386
  - `component/engine/resolvers.ts` — `resolveManifest` (dynamic reread at
339
387
  session/turn boundaries; returns `agentId`/`name`/`instructions`/`model`/
340
388
  `toolApprovals` — `skills`/`connections` NOT included in this pass).
341
- - `component/engine/sessions.ts` — the conversation thread contract.
389
+ - `component/engine/sessions.ts` — the conversation thread contract:
390
+ `start` / `sendTurn` / `appendReasoning` / `requestToolCall` /
391
+ `completeToolCall` / `completeTurn` / `listEvents`.
342
392
  - `component/engine/toolApprovals.ts` — `respond`.
343
- - `component/lib/auth.ts` — `requireOrgId` / `assertSameOrg`: orgId is
344
- derived server-side and is the first field of every index; deny by
345
- default travels WITH the component, not just with this repo's own app.
393
+ - `component/lib/auth.ts` — `requireOrgId` / `assertSameOrg`: see "Trust
394
+ model" above.
346
395
 
347
- **Callable through the published component today** (function signatures —
348
- all args/returns are explicit Convex validators, see the source files
349
- above for the exact `v.*` shapes):
396
+ **Callable through the published component today** — every function below
397
+ takes `orgId: v.string()` as an explicit argument (see the source files
398
+ above for the exact `v.*` shapes of the remaining args/returns):
350
399
 
351
400
  ```
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
401
+ components.agentEngine.engine.durableJob.start({ orgId, kind, stepFunctionHandle, totalSteps, maxAttemptsPerStep }) -> Id<"durableJobs">
402
+ components.agentEngine.engine.durableJob.getStatus({ orgId, jobId }) -> { status, cursor, totalSteps, lastError?, abandonedAtStep? }
403
+ components.agentEngine.engine.durableJob.cancel({ orgId, jobId }) -> null
355
404
 
356
- components.agentEngine.engine.agents.create({ name, instructions, model }) -> Id<"agents">
357
- components.agentEngine.engine.agents.get({ agentId }) -> agent | null
405
+ components.agentEngine.engine.agents.create({ orgId, createdBy, name, instructions, model }) -> Id<"agents">
406
+ components.agentEngine.engine.agents.get({ orgId, agentId }) -> agent | null
358
407
 
359
- components.agentEngine.engine.resolvers.resolveManifest({ sessionId }) -> { agentId, name, instructions, model, toolApprovals[] }
408
+ components.agentEngine.engine.resolvers.resolveManifest({ orgId, sessionId }) -> { agentId, name, instructions, model, toolApprovals[] }
360
409
 
361
- components.agentEngine.engine.sessions.start({ agentId }) -> Id<"sessions"> // session.started
362
- components.agentEngine.engine.sessions.sendTurn({ sessionId, input }) -> null // turn.started, rereads resolveManifest
363
- components.agentEngine.engine.sessions.appendReasoning({ sessionId, chunk }) -> null // streamed reasoning event
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 // turn.completed
367
- components.agentEngine.engine.sessions.listEvents({ sessionId }) -> sessionEvent[] // replay on reconnect (ordered by createdAt)
410
+ components.agentEngine.engine.sessions.start({ orgId, agentId, createdBy }) -> Id<"sessions">
411
+ components.agentEngine.engine.sessions.sendTurn({ orgId, sessionId, input }) -> null
412
+ components.agentEngine.engine.sessions.appendReasoning({ orgId, sessionId, chunk }) -> null
413
+ components.agentEngine.engine.sessions.requestToolCall({ orgId, sessionId, toolName, argsJson }) -> { authorized, approvalPending, denied }
414
+ components.agentEngine.engine.sessions.completeToolCall({ orgId, sessionId, toolName, resultJson }) -> null
415
+ components.agentEngine.engine.sessions.completeTurn({ orgId, sessionId }) -> null
416
+ components.agentEngine.engine.sessions.listEvents({ orgId, sessionId }) -> sessionEvent[]
368
417
 
369
- components.agentEngine.engine.toolApprovals.respond({ sessionId, toolName, approval: "always"|"once"|"never" }) -> null
418
+ components.agentEngine.engine.toolApprovals.respond({ orgId, sessionId, toolName, decision: "approved"|"denied", updatePolicy?: "always"|"once"|"never" }) -> null
370
419
  ```
371
420
 
372
- Streaming model: `sessionEvents` IS the transport. Convex query
373
- subscriptions on `sessions.listEvents` push every new row (reasoning
374
- chunk, tool-call lifecycle, approval lifecycle) to every subscriber live —
375
- there is no separate streaming channel to wire. A client that reconnects
376
- calls `listEvents` once to rebuild the whole thread, then keeps
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` +
421
+ ### `toolApprovals.respond` — `decision` vs `updatePolicy` (0.1.0-alpha.5)
422
+
423
+ Two DISTINCT axes were conflated pre-alpha.5 (VP
424
+ k17ea6x3tht8xa3rxm139t23gx8ajkf3, defaut 2) — a single `approval:
425
+ "always"|"once"|"never"` argument was overloaded to mean both "the standing
426
+ policy for this tool" AND "the answer to this one pending request", and
427
+ there was no literal for "refuse this turn only". Fixed by splitting them:
428
+
429
+ - **`decision`** (required) — the response to THIS ONE pending
430
+ `tool.approval.requested` event, for THIS turn only. `"approved"` |
431
+ `"denied"`. **Eve parity, direct** — vocabulary matches Eve's
432
+ `ApprovalStatus` (`packages/eve/src/public/definitions/approval.ts`:
433
+ `"approved" | "denied" | "not-applicable" | "user-approval" | ...`); we
434
+ did not invent a `deny` literal, we aligned with the one Eve already has.
435
+ `"denied"` is a terminal refusal for the turn: `sessions.requestToolCall`
436
+ reports back `{ authorized: false, approvalPending: false, denied: true
437
+ }` (never re-authorized on retry within the session), and a dedicated
438
+ `turn.toolCall.denied` sessionEvent is appended — there is NO
439
+ `turn.toolCall.completed` for a denied call, i.e. the tool was never run
440
+ and the turn is NOT silently treated as done.
441
+ - **`updatePolicy`** (optional) — persists a NEW standing gate
442
+ (`"always"|"once"|"never"`, eve parity: `eve/tools/approval` helpers) for
443
+ this `(agentId, toolName)` pair going forward, independent of `decision`.
444
+ A human can deny this turn's call and leave the policy alone (ask again
445
+ next time), or approve this turn and simultaneously flip the policy to
446
+ `"always"`/`"never"` so future calls stop pausing. `"never"` still means
447
+ Eve's "never require approval" (auto-authorize) — that policy literal is
448
+ unchanged and is NOT the same thing as a `"denied"` decision.
449
+
450
+ `createdBy` (on `agents.create` / `sessions.start`) is, for the same
451
+ platform reason as `orgId`, no longer read from `ctx.auth.getUserIdentity()`
452
+ — the consumer passes the acting user's own identifier.
453
+
454
+ Verified two ways: (1) `component/durableJob.componentPackaging.test.ts` +
389
455
  `component/sessions.componentPackaging.test.ts` run `convex-test` directly
390
456
  against `component/_generated/api` (the exact source tree
391
457
  `tsc -p tsconfig.build.json` compiles into `dist/component/`, which is
392
- what `npm pack` ships); (2) a from-scratch consumer probe (install the
393
- published tarball into a bare `npx convex dev` project, register the
394
- component, call `sessions.start`/`sendTurn` from the consumer's own
395
- function, confirm unauthenticated calls throw from the package's own
396
- `lib/auth`) — see the T2-QUINQUIES completion report for the verbatim
397
- transcript.
458
+ what `npm pack` ships), so the compiled artifact has a real green test
459
+ suite behind it, not just the repo-root dev app's; (2) a from-scratch
460
+ consumer probe (install the published tarball into a bare `npx convex dev`
461
+ project, register the component, call `durableJob.start` + `sessions.start`
462
+ from the consumer's own function, passing `orgId` explicitly) — see the
463
+ completion report for the verbatim transcript.
398
464
 
399
465
  **NOT yet callable through the published component** (still only exists in
400
466
  this repo's OWN dev/test deployment under `convex/schema.ts` +
@@ -22,12 +22,15 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
22
22
  engine: {
23
23
  agents: {
24
24
  create: FunctionReference<"mutation", "internal", {
25
+ createdBy: string;
25
26
  instructions: string;
26
27
  model: string;
27
28
  name: string;
29
+ orgId: string;
28
30
  }, string, Name>;
29
31
  get: FunctionReference<"query", "internal", {
30
32
  agentId: string;
33
+ orgId: string;
31
34
  }, {
32
35
  _creationTime: number;
33
36
  _id: string;
@@ -44,9 +47,11 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
44
47
  durableJob: {
45
48
  cancel: FunctionReference<"mutation", "internal", {
46
49
  jobId: string;
50
+ orgId: string;
47
51
  }, null, Name>;
48
52
  getStatus: FunctionReference<"query", "internal", {
49
53
  jobId: string;
54
+ orgId: string;
50
55
  }, {
51
56
  abandonedAtStep?: number;
52
57
  cursor: number;
@@ -57,12 +62,14 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
57
62
  start: FunctionReference<"mutation", "internal", {
58
63
  kind: string;
59
64
  maxAttemptsPerStep: number;
65
+ orgId: string;
60
66
  stepFunctionHandle: string;
61
67
  totalSteps: number;
62
68
  }, string, Name>;
63
69
  };
64
70
  resolvers: {
65
71
  resolveManifest: FunctionReference<"query", "internal", {
72
+ orgId: string;
66
73
  sessionId: string;
67
74
  }, {
68
75
  agentId: string;
@@ -78,46 +85,57 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
78
85
  sessions: {
79
86
  appendReasoning: FunctionReference<"mutation", "internal", {
80
87
  chunk: string;
88
+ orgId: string;
81
89
  sessionId: string;
82
90
  }, null, Name>;
83
91
  completeToolCall: FunctionReference<"mutation", "internal", {
92
+ orgId: string;
84
93
  resultJson: string;
85
94
  sessionId: string;
86
95
  toolName: string;
87
96
  }, null, Name>;
88
97
  completeTurn: FunctionReference<"mutation", "internal", {
98
+ orgId: string;
89
99
  sessionId: string;
90
100
  }, null, Name>;
91
101
  listEvents: FunctionReference<"query", "internal", {
102
+ orgId: string;
92
103
  sessionId: string;
93
104
  }, Array<{
94
105
  _creationTime: number;
95
106
  _id: string;
96
107
  createdAt: number;
97
- name: "session.started" | "turn.started" | "turn.reasoning" | "turn.toolCall.requested" | "turn.toolCall.completed" | "turn.completed" | "tool.approval.requested" | "tool.approval.resolved";
108
+ name: "session.started" | "turn.started" | "turn.reasoning" | "turn.toolCall.requested" | "turn.toolCall.completed" | "turn.toolCall.denied" | "turn.completed" | "tool.approval.requested" | "tool.approval.resolved";
98
109
  payload?: string;
99
110
  }>, Name>;
100
111
  requestToolCall: FunctionReference<"mutation", "internal", {
101
112
  argsJson: string;
113
+ orgId: string;
102
114
  sessionId: string;
103
115
  toolName: string;
104
116
  }, {
105
117
  approvalPending: boolean;
106
118
  authorized: boolean;
119
+ denied: boolean;
107
120
  }, Name>;
108
121
  sendTurn: FunctionReference<"mutation", "internal", {
109
122
  input: string;
123
+ orgId: string;
110
124
  sessionId: string;
111
125
  }, null, Name>;
112
126
  start: FunctionReference<"mutation", "internal", {
113
127
  agentId: string;
128
+ createdBy: string;
129
+ orgId: string;
114
130
  }, string, Name>;
115
131
  };
116
132
  toolApprovals: {
117
133
  respond: FunctionReference<"mutation", "internal", {
118
- approval: "always" | "once" | "never";
134
+ decision: "approved" | "denied";
135
+ orgId: string;
119
136
  sessionId: string;
120
137
  toolName: string;
138
+ updatePolicy?: "always" | "once" | "never";
121
139
  }, null, Name>;
122
140
  };
123
141
  };
@@ -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 = await requireOrgId(ctx);
32
- const identity = await ctx.auth.getUserIdentity();
33
- if (!identity)
34
- throw new Error("Unauthenticated");
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: identity.subject,
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 = await requireOrgId(ctx);
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 = await requireOrgId(ctx);
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 = await requireOrgId(ctx);
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 = await requireOrgId(ctx);
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,4 +1,5 @@
1
1
  export declare const resolveManifest: import("convex/server").RegisteredQuery<"public", {
2
+ orgId: string;
2
3
  sessionId: import("convex/values").GenericId<"sessions">;
3
4
  }, Promise<{
4
5
  agentId: import("convex/values").GenericId<"agents">;
@@ -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 = await requireOrgId(ctx);
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,36 +1,45 @@
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;
16
21
  }, Promise<{
17
22
  authorized: boolean;
18
23
  approvalPending: boolean;
24
+ denied: boolean;
19
25
  }>>;
20
26
  export declare const completeToolCall: import("convex/server").RegisteredMutation<"public", {
27
+ orgId: string;
21
28
  toolName: string;
22
29
  sessionId: import("convex/values").GenericId<"sessions">;
23
30
  resultJson: string;
24
31
  }, Promise<null>>;
25
32
  export declare const completeTurn: import("convex/server").RegisteredMutation<"public", {
33
+ orgId: string;
26
34
  sessionId: import("convex/values").GenericId<"sessions">;
27
35
  }, Promise<null>>;
28
36
  export declare const listEvents: import("convex/server").RegisteredQuery<"public", {
37
+ orgId: string;
29
38
  sessionId: import("convex/values").GenericId<"sessions">;
30
39
  }, Promise<{
31
40
  _id: import("convex/values").GenericId<"sessionEvents">;
32
41
  _creationTime: number;
33
- name: "session.started" | "turn.started" | "turn.reasoning" | "turn.toolCall.requested" | "turn.toolCall.completed" | "turn.completed" | "tool.approval.requested" | "tool.approval.resolved";
42
+ name: "session.started" | "turn.started" | "turn.reasoning" | "turn.toolCall.requested" | "turn.toolCall.completed" | "turn.toolCall.denied" | "turn.completed" | "tool.approval.requested" | "tool.approval.resolved";
34
43
  payload: string | undefined;
35
44
  createdAt: number;
36
45
  }[]>>;
@@ -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 = await requireOrgId(ctx);
33
- const identity = await ctx.auth.getUserIdentity();
34
- if (!identity)
35
- throw new Error("Unauthenticated");
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: identity.subject,
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, { sessionId });
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 = await requireOrgId(ctx);
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 = await requireOrgId(ctx);
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,22 @@ 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
- returns: v.object({ authorized: v.boolean(), approvalPending: v.boolean() }),
115
+ returns: v.object({
116
+ authorized: v.boolean(),
117
+ approvalPending: v.boolean(),
118
+ // Explicit terminal refusal for THIS turn (VP
119
+ // k17ea6x3tht8xa3rxm139t23gx8ajkf3, defaut 2). Distinct from
120
+ // `approvalPending: false, authorized: true` (approved/never-gated)
121
+ // -- a caller must check this before treating the call as skippable.
122
+ denied: v.boolean(),
123
+ }),
111
124
  handler: async (ctx, args) => {
112
- const orgId = await requireOrgId(ctx);
125
+ const orgId = requireOrgId(args.orgId);
113
126
  const session = await ctx.db.get(args.sessionId);
114
127
  assertSameOrg(orgId, session, "Session not found");
115
128
  await insertEvent(ctx, {
@@ -127,7 +140,7 @@ export const requestToolCall = mutation({
127
140
  .unique();
128
141
  const approval = approvalRow?.approval ?? "never";
129
142
  if (approval === "never") {
130
- return { authorized: true, approvalPending: false };
143
+ return { authorized: true, approvalPending: false, denied: false };
131
144
  }
132
145
  if (approval === "always") {
133
146
  await insertEvent(ctx, {
@@ -136,20 +149,35 @@ export const requestToolCall = mutation({
136
149
  name: "tool.approval.requested",
137
150
  payload: JSON.stringify({ toolName: args.toolName }),
138
151
  });
139
- return { authorized: false, approvalPending: true };
152
+ return { authorized: false, approvalPending: true, denied: false };
140
153
  }
141
- // approval === "once": gate unless this tool was already resolved once
142
- // in this session.
154
+ // approval === "once": gate unless this tool's approval was already
155
+ // resolved with `decision: "approved"` in this session. A prior
156
+ // `decision: "denied"` resolution must NOT be treated as
157
+ // authorization (that was the pre-alpha.5 bug -- "outil sauté, tour
158
+ // complété quand même") -- it is reported back as a terminal refusal
159
+ // instead, and the caller re-requests (a fresh human decision) rather
160
+ // than silently proceeding.
143
161
  const events = await ctx.db
144
162
  .query("sessionEvents")
145
163
  .withIndex("by_org_session", (q) => q.eq("orgId", orgId).eq("sessionId", args.sessionId))
146
164
  .collect();
147
- const alreadyResolvedThisSession = events.some((e) => e.name === "tool.approval.resolved" &&
165
+ const resolutionsForTool = events
166
+ .filter((e) => e.name === "tool.approval.resolved" &&
148
167
  e.payload !== undefined &&
149
168
  JSON.parse(e.payload).toolName ===
150
- args.toolName);
151
- if (alreadyResolvedThisSession) {
152
- return { authorized: true, approvalPending: false };
169
+ args.toolName)
170
+ .sort((a, b) => a.createdAt - b.createdAt);
171
+ const latestResolution = resolutionsForTool[resolutionsForTool.length - 1];
172
+ const latestDecision = latestResolution?.payload
173
+ ? JSON.parse(latestResolution.payload)
174
+ .decision
175
+ : undefined;
176
+ if (latestDecision === "approved") {
177
+ return { authorized: true, approvalPending: false, denied: false };
178
+ }
179
+ if (latestDecision === "denied") {
180
+ return { authorized: false, approvalPending: false, denied: true };
153
181
  }
154
182
  await insertEvent(ctx, {
155
183
  orgId,
@@ -157,7 +185,7 @@ export const requestToolCall = mutation({
157
185
  name: "tool.approval.requested",
158
186
  payload: JSON.stringify({ toolName: args.toolName }),
159
187
  });
160
- return { authorized: false, approvalPending: true };
188
+ return { authorized: false, approvalPending: true, denied: false };
161
189
  },
162
190
  });
163
191
  // completeToolCall — the caller invokes this after actually running the
@@ -165,13 +193,14 @@ export const requestToolCall = mutation({
165
193
  // the gate).
166
194
  export const completeToolCall = mutation({
167
195
  args: {
196
+ orgId: v.string(),
168
197
  sessionId: v.id("sessions"),
169
198
  toolName: v.string(),
170
199
  resultJson: v.string(),
171
200
  },
172
201
  returns: v.null(),
173
202
  handler: async (ctx, args) => {
174
- const orgId = await requireOrgId(ctx);
203
+ const orgId = requireOrgId(args.orgId);
175
204
  const session = await ctx.db.get(args.sessionId);
176
205
  assertSameOrg(orgId, session, "Session not found");
177
206
  await insertEvent(ctx, {
@@ -185,10 +214,10 @@ export const completeToolCall = mutation({
185
214
  });
186
215
  // completeTurn — closes the current turn.
187
216
  export const completeTurn = mutation({
188
- args: { sessionId: v.id("sessions") },
217
+ args: { orgId: v.string(), sessionId: v.id("sessions") },
189
218
  returns: v.null(),
190
219
  handler: async (ctx, args) => {
191
- const orgId = await requireOrgId(ctx);
220
+ const orgId = requireOrgId(args.orgId);
192
221
  const session = await ctx.db.get(args.sessionId);
193
222
  assertSameOrg(orgId, session, "Session not found");
194
223
  await insertEvent(ctx, {
@@ -204,16 +233,16 @@ export const completeTurn = mutation({
204
233
  // session. A client that drops and reconnects calls this once to rebuild
205
234
  // the thread, then subscribes (Convex reactivity) for anything after.
206
235
  export const listEvents = query({
207
- args: { sessionId: v.id("sessions") },
236
+ args: { orgId: v.string(), sessionId: v.id("sessions") },
208
237
  returns: v.array(v.object({
209
238
  _id: v.id("sessionEvents"),
210
239
  _creationTime: v.number(),
211
- name: v.union(v.literal("session.started"), v.literal("turn.started"), v.literal("turn.reasoning"), v.literal("turn.toolCall.requested"), v.literal("turn.toolCall.completed"), v.literal("turn.completed"), v.literal("tool.approval.requested"), v.literal("tool.approval.resolved")),
240
+ name: v.union(v.literal("session.started"), v.literal("turn.started"), v.literal("turn.reasoning"), v.literal("turn.toolCall.requested"), v.literal("turn.toolCall.completed"), v.literal("turn.toolCall.denied"), v.literal("turn.completed"), v.literal("tool.approval.requested"), v.literal("tool.approval.resolved")),
212
241
  payload: v.optional(v.string()),
213
242
  createdAt: v.number(),
214
243
  })),
215
244
  handler: async (ctx, args) => {
216
- const orgId = await requireOrgId(ctx);
245
+ const orgId = requireOrgId(args.orgId);
217
246
  const session = await ctx.db.get(args.sessionId);
218
247
  assertSameOrg(orgId, session, "Session not found");
219
248
  const events = await ctx.db
@@ -1,5 +1,7 @@
1
1
  export declare const respond: import("convex/server").RegisteredMutation<"public", {
2
+ updatePolicy?: "always" | "once" | "never" | undefined;
3
+ orgId: string;
2
4
  toolName: string;
3
- approval: "always" | "once" | "never";
4
5
  sessionId: import("convex/values").GenericId<"sessions">;
6
+ decision: "approved" | "denied";
5
7
  }, Promise<null>>;
@@ -1,51 +1,90 @@
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
- // (`tool.approval.requested`, raised by `sessions.requestToolCall`). The
6
- // decision (always/once/never) is WRITTEN as the agent's standing policy
7
- // for that tool going forward (upsert on `by_org_agent_tool`), and
8
- // `tool.approval.resolved` is appended so `sessions.requestToolCall`'s
9
- // "once" check (and any replaying client) can see it happened.
5
+ // (`tool.approval.requested`, raised by `sessions.requestToolCall`).
6
+ //
7
+ // TWO DISTINCT AXES (VP k17ea6x3tht8xa3rxm139t23gx8ajkf3, defaut 2 --
8
+ // these were conflated pre-alpha.5, which is the defect this fixes):
9
+ //
10
+ // 1. `decision` -- the RESPONSE to THIS ONE pending approval request,
11
+ // for THIS turn only. Vocabulary aligned with Eve's `ApprovalStatus`
12
+ // (packages/eve/src/public/definitions/approval.ts: "approved" |
13
+ // "denied" | ...) -- eve parity, cited verbatim, not "notre ligne".
14
+ // `"denied"` means: this turn's tool call is refused, full stop. It
15
+ // does NOT change the agent's standing policy for the tool, and it
16
+ // must NOT be interpreted as "tool skipped, turn completed anyway" --
17
+ // it appends a `turn.toolCall.denied` sessionEvent instead of letting
18
+ // the caller silently proceed to `completeToolCall`.
19
+ //
20
+ // 2. `updatePolicy` -- optional, persists a NEW standing policy
21
+ // (always | once | never, eve parity: eve/tools/approval helpers) for
22
+ // this (agent, tool) pair going forward, upserted on
23
+ // `by_org_agent_tool`. Independent of `decision`: a human can deny
24
+ // this turn's call AND leave the policy untouched (ask again next
25
+ // time), or approve this turn AND simultaneously flip the policy to
26
+ // "always"/"never" so future calls don't pause.
10
27
  export const respond = mutation({
11
28
  args: {
29
+ orgId: v.string(),
12
30
  sessionId: v.id("sessions"),
13
31
  toolName: v.string(),
14
- approval: v.union(v.literal("always"), v.literal("once"), v.literal("never")),
32
+ decision: v.union(v.literal("approved"), v.literal("denied")),
33
+ updatePolicy: v.optional(v.union(v.literal("always"), v.literal("once"), v.literal("never"))),
15
34
  },
16
35
  returns: v.null(),
17
36
  handler: async (ctx, args) => {
18
- const orgId = await requireOrgId(ctx);
37
+ const orgId = requireOrgId(args.orgId);
19
38
  const session = await ctx.db.get(args.sessionId);
20
39
  assertSameOrg(orgId, session, "Session not found");
21
- const existing = await ctx.db
22
- .query("toolApprovals")
23
- .withIndex("by_org_agent_tool", (q) => q
24
- .eq("orgId", orgId)
25
- .eq("agentId", session.agentId)
26
- .eq("toolName", args.toolName))
27
- .unique();
28
40
  const now = Date.now();
29
- if (existing) {
30
- await ctx.db.patch(existing._id, { approval: args.approval, updatedAt: now });
31
- }
32
- else {
33
- await ctx.db.insert("toolApprovals", {
34
- orgId,
35
- agentId: session.agentId,
36
- toolName: args.toolName,
37
- approval: args.approval,
38
- createdAt: now,
39
- updatedAt: now,
40
- });
41
+ if (args.updatePolicy !== undefined) {
42
+ const existing = await ctx.db
43
+ .query("toolApprovals")
44
+ .withIndex("by_org_agent_tool", (q) => q
45
+ .eq("orgId", orgId)
46
+ .eq("agentId", session.agentId)
47
+ .eq("toolName", args.toolName))
48
+ .unique();
49
+ if (existing) {
50
+ await ctx.db.patch(existing._id, {
51
+ approval: args.updatePolicy,
52
+ updatedAt: now,
53
+ });
54
+ }
55
+ else {
56
+ await ctx.db.insert("toolApprovals", {
57
+ orgId,
58
+ agentId: session.agentId,
59
+ toolName: args.toolName,
60
+ approval: args.updatePolicy,
61
+ createdAt: now,
62
+ updatedAt: now,
63
+ });
64
+ }
41
65
  }
42
66
  await ctx.db.insert("sessionEvents", {
43
67
  orgId,
44
68
  sessionId: args.sessionId,
45
69
  name: "tool.approval.resolved",
46
- payload: JSON.stringify({ toolName: args.toolName, approval: args.approval }),
70
+ payload: JSON.stringify({
71
+ toolName: args.toolName,
72
+ decision: args.decision,
73
+ updatedPolicy: args.updatePolicy ?? null,
74
+ }),
47
75
  createdAt: now,
48
76
  });
77
+ if (args.decision === "denied") {
78
+ // Explicit refusal state for THIS turn's tool call -- never a
79
+ // silent "turn.toolCall.completed". See module doc above.
80
+ await ctx.db.insert("sessionEvents", {
81
+ orgId,
82
+ sessionId: args.sessionId,
83
+ name: "turn.toolCall.denied",
84
+ payload: JSON.stringify({ toolName: args.toolName }),
85
+ createdAt: now,
86
+ });
87
+ }
49
88
  return null;
50
89
  },
51
90
  });
@@ -1,5 +1,4 @@
1
- import type { QueryCtx, MutationCtx } from "../_generated/server";
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
- // Deny by default. orgId is NEVER read from client args — it is derived
2
- // server-side from the authenticated identity (Clerk org claim). Every
3
- // mutation/query in this component must call this before touching data.
4
- export async function requireOrgId(ctx) {
5
- const identity = await ctx.auth.getUserIdentity();
6
- if (!identity) {
7
- throw new Error("Unauthenticated: no identity on ctx.auth");
8
- }
9
- // Clerk org-scoped JWT template exposes `org_id` as a top-level claim.
10
- const orgId = identity.org_id;
11
- if (!orgId || typeof orgId !== "string" || orgId.length === 0) {
12
- throw new Error("Unauthenticated: identity carries no org_id claim");
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's server-derived
17
- // orgId. Use on every read/write of a document fetched by _id, since
18
- // db.get() does not filter by index.
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.
@@ -106,14 +106,14 @@ declare const _default: import("convex/server").SchemaDefinition<{
106
106
  }, {}, {}>;
107
107
  sessionEvents: import("convex/server").TableDefinition<import("convex/values").VObject<{
108
108
  payload?: string | undefined;
109
- name: "session.started" | "turn.started" | "turn.reasoning" | "turn.toolCall.requested" | "turn.toolCall.completed" | "turn.completed" | "tool.approval.requested" | "tool.approval.resolved";
109
+ name: "session.started" | "turn.started" | "turn.reasoning" | "turn.toolCall.requested" | "turn.toolCall.completed" | "turn.toolCall.denied" | "turn.completed" | "tool.approval.requested" | "tool.approval.resolved";
110
110
  orgId: string;
111
111
  createdAt: number;
112
112
  sessionId: import("convex/values").GenericId<"sessions">;
113
113
  }, {
114
114
  orgId: import("convex/values").VString<string, "required">;
115
115
  sessionId: import("convex/values").VId<import("convex/values").GenericId<"sessions">, "required">;
116
- name: import("convex/values").VUnion<"session.started" | "turn.started" | "turn.reasoning" | "turn.toolCall.requested" | "turn.toolCall.completed" | "turn.completed" | "tool.approval.requested" | "tool.approval.resolved", [import("convex/values").VLiteral<"session.started", "required">, import("convex/values").VLiteral<"turn.started", "required">, import("convex/values").VLiteral<"turn.reasoning", "required">, import("convex/values").VLiteral<"turn.toolCall.requested", "required">, import("convex/values").VLiteral<"turn.toolCall.completed", "required">, import("convex/values").VLiteral<"turn.completed", "required">, import("convex/values").VLiteral<"tool.approval.requested", "required">, import("convex/values").VLiteral<"tool.approval.resolved", "required">], "required", never>;
116
+ name: import("convex/values").VUnion<"session.started" | "turn.started" | "turn.reasoning" | "turn.toolCall.requested" | "turn.toolCall.completed" | "turn.toolCall.denied" | "turn.completed" | "tool.approval.requested" | "tool.approval.resolved", [import("convex/values").VLiteral<"session.started", "required">, import("convex/values").VLiteral<"turn.started", "required">, import("convex/values").VLiteral<"turn.reasoning", "required">, import("convex/values").VLiteral<"turn.toolCall.requested", "required">, import("convex/values").VLiteral<"turn.toolCall.completed", "required">, import("convex/values").VLiteral<"turn.toolCall.denied", "required">, import("convex/values").VLiteral<"turn.completed", "required">, import("convex/values").VLiteral<"tool.approval.requested", "required">, import("convex/values").VLiteral<"tool.approval.resolved", "required">], "required", never>;
117
117
  payload: import("convex/values").VString<string | undefined, "optional">;
118
118
  createdAt: import("convex/values").VFloat64<number, "required">;
119
119
  }, "required", "name" | "orgId" | "createdAt" | "sessionId" | "payload">, {
@@ -121,7 +121,14 @@ export default defineSchema({
121
121
  sessionEvents: defineTable({
122
122
  orgId: v.string(),
123
123
  sessionId: v.id("sessions"),
124
- name: v.union(v.literal("session.started"), v.literal("turn.started"), v.literal("turn.reasoning"), v.literal("turn.toolCall.requested"), v.literal("turn.toolCall.completed"), v.literal("turn.completed"), v.literal("tool.approval.requested"), v.literal("tool.approval.resolved")),
124
+ name: v.union(v.literal("session.started"), v.literal("turn.started"), v.literal("turn.reasoning"), v.literal("turn.toolCall.requested"), v.literal("turn.toolCall.completed"),
125
+ // "notre ligne" (VP k17ea6x3tht8xa3rxm139t23gx8ajkf3, defaut 2):
126
+ // explicit terminal state for a one-turn approval REFUSAL, distinct
127
+ // from "turn.toolCall.completed" (tool actually ran). Without this,
128
+ // a denied tool call had no event of its own -- a consumer had no
129
+ // way to tell "refused this turn" from "skipped, turn completed
130
+ // anyway", which is the wrong default Hestia had to code around.
131
+ v.literal("turn.toolCall.denied"), v.literal("turn.completed"), v.literal("tool.approval.requested"), v.literal("tool.approval.resolved")),
125
132
  payload: v.optional(v.string()), // JSON-serialized, free contract per event name
126
133
  createdAt: v.number(),
127
134
  })
@@ -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",
3
+ "version": "0.1.0-alpha.5",
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",