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

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.
Files changed (43) hide show
  1. package/LICENSE +110 -0
  2. package/README.md +154 -473
  3. package/dist/component/_generated/api.d.ts +31 -0
  4. package/dist/component/_generated/component.d.ts +346 -2
  5. package/dist/component/convex.config.js +18 -0
  6. package/dist/component/engine/agents.d.ts +10 -0
  7. package/dist/component/engine/agents.js +21 -0
  8. package/dist/component/engine/awaitEventTimeout.d.ts +13 -0
  9. package/dist/component/engine/awaitEventTimeout.js +81 -0
  10. package/dist/component/engine/delegation.d.ts +75 -0
  11. package/dist/component/engine/delegation.js +327 -0
  12. package/dist/component/engine/directory.d.ts +9 -0
  13. package/dist/component/engine/directory.js +45 -0
  14. package/dist/component/engine/durableJob.js +6 -6
  15. package/dist/component/engine/habits.d.ts +70 -0
  16. package/dist/component/engine/habits.js +218 -0
  17. package/dist/component/engine/mandates.d.ts +74 -0
  18. package/dist/component/engine/mandates.js +271 -0
  19. package/dist/component/engine/sandboxCallback.d.ts +36 -0
  20. package/dist/component/engine/sandboxCallback.js +259 -0
  21. package/dist/component/engine/sessions.d.ts +6 -2
  22. package/dist/component/engine/sessions.js +43 -7
  23. package/dist/component/engine/toolApprovals.js +1 -1
  24. package/dist/component/engine/vpConnection.d.ts +69 -0
  25. package/dist/component/engine/vpConnection.js +218 -0
  26. package/dist/component/engine/wakeup.d.ts +33 -0
  27. package/dist/component/engine/wakeup.js +228 -0
  28. package/dist/component/engine/workflows.d.ts +11 -0
  29. package/dist/component/engine/workflows.js +84 -0
  30. package/dist/component/schema.d.ts +365 -4
  31. package/dist/component/schema.js +345 -18
  32. package/dist/component/testFixtures/callerSteps.js +1 -1
  33. package/dist/component/testFixtures/delegationDemoWorkflow.d.ts +18 -0
  34. package/dist/component/testFixtures/delegationDemoWorkflow.js +75 -0
  35. package/dist/component/testFixtures/sandboxDemoWorkflow.d.ts +19 -0
  36. package/dist/component/testFixtures/sandboxDemoWorkflow.js +69 -0
  37. package/dist/component/testFixtures/vpTool.d.ts +4 -0
  38. package/dist/component/testFixtures/vpTool.js +25 -0
  39. package/dist/component/testFixtures/wakeupDemoWorkflow.d.ts +19 -0
  40. package/dist/component/testFixtures/wakeupDemoWorkflow.js +77 -0
  41. package/dist/component/testFixtures/wakeupWork.d.ts +8 -0
  42. package/dist/component/testFixtures/wakeupWork.js +21 -0
  43. package/package.json +17 -5
@@ -0,0 +1,218 @@
1
+ import { v } from "convex/values";
2
+ import { Crons } from "@convex-dev/crons";
3
+ import { internal, components } from "../_generated/api.js";
4
+ import { internalMutation, mutation, query } from "../_generated/server.js";
5
+ import { assertSameOrg, requireOrgId } from "../lib/auth.js";
6
+ // engine/habits — "intentional divergence from Eve": a habit is what turns
7
+ // a chat into an employee. A habit is a DYNAMIC cron per agent/org,
8
+ // registered at runtime via @convex-dev/crons — never a static
9
+ // `convex/crons.ts`. Creating, running, editing or disabling one deploys
10
+ // nothing: the `cronExpression` is DATA carried by the `habits` row, and
11
+ // the runtime cron is merely a reflection of it.
12
+ //
13
+ // orgId discipline (component/lib/auth.ts): every PUBLIC function takes
14
+ // `orgId` as an explicit ARGUMENT (auth does not propagate across a
15
+ // component boundary), same as engine/mandates.ts. The cron runtime
16
+ // target is `internal.engine.habits.run` -- the component-boundary path
17
+ // for this module, mirroring `internal.habits.run` at the app level.
18
+ const crons = new Crons(components.crons);
19
+ const habitDoc = v.object({
20
+ _id: v.id("habits"),
21
+ _creationTime: v.number(),
22
+ orgId: v.string(),
23
+ agentId: v.id("agents"),
24
+ name: v.string(),
25
+ cronExpression: v.string(),
26
+ prompt: v.string(),
27
+ enabled: v.boolean(),
28
+ cronJobId: v.optional(v.string()),
29
+ lastRunAt: v.optional(v.number()),
30
+ createdAt: v.number(),
31
+ updatedAt: v.number(),
32
+ });
33
+ // create — writes the habit row AND registers its runtime cron in the same
34
+ // transaction. @convex-dev/crons guarantees the cron "is guaranteed to
35
+ // exist after the mutation that creates them has run" (README) -- so the
36
+ // very next tick can fire it, with zero deploy.
37
+ export const create = mutation({
38
+ args: {
39
+ orgId: v.string(),
40
+ agentId: v.id("agents"),
41
+ name: v.string(),
42
+ cronExpression: v.string(),
43
+ prompt: v.string(),
44
+ },
45
+ returns: v.id("habits"),
46
+ handler: async (ctx, args) => {
47
+ const orgId = requireOrgId(args.orgId);
48
+ const agent = await ctx.db.get(args.agentId);
49
+ assertSameOrg(orgId, agent, "Agent not found");
50
+ if (args.name.trim().length === 0) {
51
+ throw new Error("Validation: name must not be empty");
52
+ }
53
+ if (args.cronExpression.trim().length === 0) {
54
+ throw new Error("Validation: cronExpression must not be empty");
55
+ }
56
+ const now = Date.now();
57
+ const habitId = await ctx.db.insert("habits", {
58
+ orgId,
59
+ agentId: args.agentId,
60
+ name: args.name,
61
+ cronExpression: args.cronExpression,
62
+ prompt: args.prompt,
63
+ enabled: true,
64
+ createdAt: now,
65
+ updatedAt: now,
66
+ });
67
+ const cronJobId = await crons.register(ctx, { kind: "cron", cronspec: args.cronExpression }, internal.engine.habits.run, { habitId }, `habit:${habitId}`);
68
+ await ctx.db.patch(habitId, { cronJobId, updatedAt: Date.now() });
69
+ return habitId;
70
+ },
71
+ });
72
+ export const get = query({
73
+ args: { orgId: v.string(), habitId: v.id("habits") },
74
+ returns: v.union(habitDoc, v.null()),
75
+ handler: async (ctx, args) => {
76
+ const orgId = requireOrgId(args.orgId);
77
+ const doc = await ctx.db.get(args.habitId);
78
+ if (!doc || doc.orgId !== orgId)
79
+ return null;
80
+ return doc;
81
+ },
82
+ });
83
+ export const listForAgent = query({
84
+ args: { orgId: v.string(), agentId: v.id("agents") },
85
+ returns: v.array(habitDoc),
86
+ handler: async (ctx, args) => {
87
+ const orgId = requireOrgId(args.orgId);
88
+ return await ctx.db
89
+ .query("habits")
90
+ .withIndex("by_org_agent", (q) => q.eq("orgId", orgId).eq("agentId", args.agentId))
91
+ .collect();
92
+ },
93
+ });
94
+ // updateSchedule — edits the habit AT EXECUTION TIME. Deletes the old
95
+ // runtime cron and registers a fresh one from the new `cronExpression`,
96
+ // still inside a single Convex mutation transaction. No redeploy, no
97
+ // downtime window: @convex-dev/crons crons are transactional (README).
98
+ export const updateSchedule = mutation({
99
+ args: {
100
+ orgId: v.string(),
101
+ habitId: v.id("habits"),
102
+ cronExpression: v.optional(v.string()),
103
+ prompt: v.optional(v.string()),
104
+ name: v.optional(v.string()),
105
+ },
106
+ returns: v.null(),
107
+ handler: async (ctx, args) => {
108
+ const orgId = requireOrgId(args.orgId);
109
+ const existing = await ctx.db.get(args.habitId);
110
+ assertSameOrg(orgId, existing, "Habit not found");
111
+ const { orgId: _orgId, habitId, cronExpression, ...rest } = args;
112
+ await ctx.db.patch(habitId, { ...rest, updatedAt: Date.now() });
113
+ if (cronExpression !== undefined && cronExpression !== existing.cronExpression) {
114
+ if (cronExpression.trim().length === 0) {
115
+ throw new Error("Validation: cronExpression must not be empty");
116
+ }
117
+ if (existing.enabled && existing.cronJobId) {
118
+ await crons.delete(ctx, { id: existing.cronJobId });
119
+ }
120
+ await ctx.db.patch(habitId, { cronExpression, updatedAt: Date.now() });
121
+ if (existing.enabled) {
122
+ const cronJobId = await crons.register(ctx, { kind: "cron", cronspec: cronExpression }, internal.engine.habits.run, { habitId }, `habit:${habitId}:${Date.now()}`);
123
+ await ctx.db.patch(habitId, { cronJobId, updatedAt: Date.now() });
124
+ }
125
+ }
126
+ return null;
127
+ },
128
+ });
129
+ // disable — stops future runs by DELETING the runtime cron registration.
130
+ // This is a hard stop, not a soft "skip if disabled" flag: after `disable`
131
+ // resolves, there is no scheduled function left that could fire this habit.
132
+ export const disable = mutation({
133
+ args: { orgId: v.string(), habitId: v.id("habits") },
134
+ returns: v.null(),
135
+ handler: async (ctx, args) => {
136
+ const orgId = requireOrgId(args.orgId);
137
+ const existing = await ctx.db.get(args.habitId);
138
+ assertSameOrg(orgId, existing, "Habit not found");
139
+ if (existing.enabled && existing.cronJobId) {
140
+ await crons.delete(ctx, { id: existing.cronJobId });
141
+ }
142
+ await ctx.db.patch(args.habitId, {
143
+ enabled: false,
144
+ cronJobId: undefined,
145
+ updatedAt: Date.now(),
146
+ });
147
+ return null;
148
+ },
149
+ });
150
+ // enable — re-registers the runtime cron from the habit's own
151
+ // `cronExpression` DATA. No redeploy: the schedule was never lost, only its
152
+ // runtime registration.
153
+ export const enable = mutation({
154
+ args: { orgId: v.string(), habitId: v.id("habits") },
155
+ returns: v.null(),
156
+ handler: async (ctx, args) => {
157
+ const orgId = requireOrgId(args.orgId);
158
+ const existing = await ctx.db.get(args.habitId);
159
+ assertSameOrg(orgId, existing, "Habit not found");
160
+ if (existing.enabled)
161
+ return null;
162
+ const cronJobId = await crons.register(ctx, { kind: "cron", cronspec: existing.cronExpression }, internal.engine.habits.run, { habitId: args.habitId }, `habit:${args.habitId}:${Date.now()}`);
163
+ await ctx.db.patch(args.habitId, {
164
+ enabled: true,
165
+ cronJobId,
166
+ updatedAt: Date.now(),
167
+ });
168
+ return null;
169
+ },
170
+ });
171
+ // run — the cron-invoked target (internal only, never client-facing).
172
+ // Writes a `habitRuns` proof row and bumps `lastRunAt`. This is what makes
173
+ // "a habit fires its first run without a deploy" and "a disabled habit does
174
+ // not fire again" PROVABLE rather than asserted on trust
175
+ // (component/habits.componentPackaging.test.ts). No `orgId` argument: this
176
+ // mutation is reachable ONLY by the runtime cron scheduler (never a
177
+ // client), and it derives `orgId` from the habit row itself, which is the
178
+ // same trust boundary `spendInternal` uses in engine/mandates.ts.
179
+ export const run = internalMutation({
180
+ args: { habitId: v.id("habits") },
181
+ returns: v.null(),
182
+ handler: async (ctx, args) => {
183
+ const habit = await ctx.db.get(args.habitId);
184
+ if (!habit || !habit.enabled) {
185
+ // Defence in depth: `disable` already removes the cron registration,
186
+ // but if a run was already in flight when disable landed, refuse to
187
+ // record it as having run.
188
+ return null;
189
+ }
190
+ const now = Date.now();
191
+ await ctx.db.insert("habitRuns", {
192
+ orgId: habit.orgId,
193
+ habitId: args.habitId,
194
+ agentId: habit.agentId,
195
+ ranAt: now,
196
+ });
197
+ await ctx.db.patch(args.habitId, { lastRunAt: now });
198
+ return null;
199
+ },
200
+ });
201
+ export const listRuns = query({
202
+ args: { orgId: v.string(), habitId: v.id("habits") },
203
+ returns: v.array(v.object({
204
+ _id: v.id("habitRuns"),
205
+ _creationTime: v.number(),
206
+ orgId: v.string(),
207
+ habitId: v.id("habits"),
208
+ agentId: v.id("agents"),
209
+ ranAt: v.number(),
210
+ })),
211
+ handler: async (ctx, args) => {
212
+ const orgId = requireOrgId(args.orgId);
213
+ return await ctx.db
214
+ .query("habitRuns")
215
+ .withIndex("by_org_habit", (q) => q.eq("orgId", orgId).eq("habitId", args.habitId))
216
+ .collect();
217
+ },
218
+ });
@@ -0,0 +1,74 @@
1
+ import type { Id } from "../_generated/dataModel.js";
2
+ export declare const set: import("convex/server").RegisteredMutation<"public", {
3
+ capacity?: number | undefined;
4
+ orgId: string;
5
+ kind: "token bucket" | "fixed window";
6
+ agentId: import("convex/values").GenericId<"agents">;
7
+ scope: string;
8
+ rate: number;
9
+ period: number;
10
+ }, Promise<import("convex/values").GenericId<"mandates">>>;
11
+ export declare const get: import("convex/server").RegisteredQuery<"public", {
12
+ orgId: string;
13
+ agentId: import("convex/values").GenericId<"agents">;
14
+ scope: string;
15
+ }, Promise<{
16
+ _id: import("convex/values").GenericId<"mandates">;
17
+ _creationTime: number;
18
+ capacity?: number | undefined;
19
+ orgId: string;
20
+ createdAt: number;
21
+ updatedAt: number;
22
+ kind: "token bucket" | "fixed window";
23
+ agentId: import("convex/values").GenericId<"agents">;
24
+ scope: string;
25
+ rate: number;
26
+ period: number;
27
+ } | null>>;
28
+ export declare const spend: import("convex/server").RegisteredMutation<"public", {
29
+ orgId: string;
30
+ agentId: import("convex/values").GenericId<"agents">;
31
+ scope: string;
32
+ reason: string;
33
+ cost: number;
34
+ }, Promise<{
35
+ ok: boolean;
36
+ approvalId?: Id<"approvalRequests">;
37
+ }>>;
38
+ export declare const spendInternal: import("convex/server").RegisteredMutation<"internal", {
39
+ orgId: string;
40
+ agentId: import("convex/values").GenericId<"agents">;
41
+ scope: string;
42
+ reason: string;
43
+ cost: number;
44
+ }, Promise<{
45
+ ok: boolean;
46
+ approvalId?: Id<"approvalRequests">;
47
+ }>>;
48
+ export declare const listApprovals: import("convex/server").RegisteredQuery<"public", {
49
+ agentId?: import("convex/values").GenericId<"agents"> | undefined;
50
+ orgId: string;
51
+ }, Promise<{
52
+ _id: import("convex/values").GenericId<"approvalRequests">;
53
+ _creationTime: number;
54
+ resolvedAt?: number | undefined;
55
+ resolvedBy?: string | undefined;
56
+ orgId: string;
57
+ status: "pending" | "approved" | "denied";
58
+ createdAt: number;
59
+ agentId: import("convex/values").GenericId<"agents">;
60
+ scope: string;
61
+ requestedCost: number;
62
+ reason: string;
63
+ }[]>>;
64
+ export declare const resolveApproval: import("convex/server").RegisteredMutation<"public", {
65
+ orgId: string;
66
+ resolvedBy: string;
67
+ decision: "approved" | "denied";
68
+ approvalId: import("convex/values").GenericId<"approvalRequests">;
69
+ }, Promise<null>>;
70
+ export declare const usageCount: import("convex/server").RegisteredQuery<"public", {
71
+ orgId: string;
72
+ agentId: import("convex/values").GenericId<"agents">;
73
+ scope: string;
74
+ }, Promise<number>>;
@@ -0,0 +1,271 @@
1
+ import { v } from "convex/values";
2
+ import { RateLimiter } from "@convex-dev/rate-limiter";
3
+ import { ShardedCounter } from "@convex-dev/sharded-counter";
4
+ import { internalMutation, mutation, query } from "../_generated/server.js";
5
+ import { components } from "../_generated/api.js";
6
+ import { assertSameOrg, requireOrgId } from "../lib/auth.js";
7
+ // engine/mandates — "intentional divergence from Eve": BOUNDED AUTHORITY.
8
+ // A mandate is a LINE OF DATA scoped
9
+ // orgId+agentId+scope, never a constant read from code (litmus test: a
10
+ // client changes their mind => zero deployment). The rate-limiter has NO
11
+ // static named config in this module -- every call to `spend` builds its
12
+ // `config` inline, dynamically, from the `mandates` row loaded from the
13
+ // database (@convex-dev/rate-limiter README §"Define a rate limit inline /
14
+ // dynamically"). Fails-closed: no mandate row => refusal, never a silent
15
+ // bypass.
16
+ //
17
+ // orgId discipline (component/lib/auth.ts): every PUBLIC function takes
18
+ // `orgId` as an explicit ARGUMENT (auth does not propagate across a
19
+ // component boundary), same as the rest of this component (agents,
20
+ // wakeup, sessions). `spendInternal` also takes `orgId` as an argument, but
21
+ // is an `internalMutation` -- the ONLY caller reachable from any client is
22
+ // `engine/wakeup.enqueue` (an action), which threads its own
23
+ // caller-supplied `orgId` argument through after validating it.
24
+ const rateLimiter = new RateLimiter(components.rateLimiter);
25
+ // Usage rollup, keyed identically to the rate-limiter key (org+agent+scope).
26
+ // internal research notes: counters via sharded-counter, never a canary/alpha dependency.
27
+ const usageCounter = new ShardedCounter(components.shardedCounter);
28
+ const mandateDoc = v.object({
29
+ _id: v.id("mandates"),
30
+ _creationTime: v.number(),
31
+ orgId: v.string(),
32
+ agentId: v.id("agents"),
33
+ scope: v.string(),
34
+ kind: v.union(v.literal("token bucket"), v.literal("fixed window")),
35
+ rate: v.number(),
36
+ period: v.number(),
37
+ capacity: v.optional(v.number()),
38
+ createdAt: v.number(),
39
+ updatedAt: v.number(),
40
+ });
41
+ // set — upserts the DATA row that bounds an agent's authority for a given
42
+ // scope. This is the ONLY place a cap value is ever written; there is no
43
+ // constant anywhere in the engine that plays this role.
44
+ export const set = mutation({
45
+ args: {
46
+ orgId: v.string(),
47
+ agentId: v.id("agents"),
48
+ scope: v.string(),
49
+ kind: v.union(v.literal("token bucket"), v.literal("fixed window")),
50
+ rate: v.number(),
51
+ period: v.number(),
52
+ capacity: v.optional(v.number()),
53
+ },
54
+ returns: v.id("mandates"),
55
+ handler: async (ctx, args) => {
56
+ const orgId = requireOrgId(args.orgId);
57
+ const agent = await ctx.db.get(args.agentId);
58
+ assertSameOrg(orgId, agent, "Agent not found");
59
+ if (args.scope.trim().length === 0) {
60
+ throw new Error("Validation: scope must not be empty");
61
+ }
62
+ if (args.rate <= 0 || args.period <= 0) {
63
+ throw new Error("Validation: rate and period must be positive");
64
+ }
65
+ const existing = await ctx.db
66
+ .query("mandates")
67
+ .withIndex("by_org_agent_scope", (q) => q.eq("orgId", orgId).eq("agentId", args.agentId).eq("scope", args.scope))
68
+ .unique();
69
+ const now = Date.now();
70
+ if (existing) {
71
+ await ctx.db.patch(existing._id, {
72
+ kind: args.kind,
73
+ rate: args.rate,
74
+ period: args.period,
75
+ capacity: args.capacity,
76
+ updatedAt: now,
77
+ });
78
+ return existing._id;
79
+ }
80
+ return await ctx.db.insert("mandates", {
81
+ orgId,
82
+ agentId: args.agentId,
83
+ scope: args.scope,
84
+ kind: args.kind,
85
+ rate: args.rate,
86
+ period: args.period,
87
+ capacity: args.capacity,
88
+ createdAt: now,
89
+ updatedAt: now,
90
+ });
91
+ },
92
+ });
93
+ export const get = query({
94
+ args: { orgId: v.string(), agentId: v.id("agents"), scope: v.string() },
95
+ returns: v.union(mandateDoc, v.null()),
96
+ handler: async (ctx, args) => {
97
+ const orgId = requireOrgId(args.orgId);
98
+ return await ctx.db
99
+ .query("mandates")
100
+ .withIndex("by_org_agent_scope", (q) => q.eq("orgId", orgId).eq("agentId", args.agentId).eq("scope", args.scope))
101
+ .unique();
102
+ },
103
+ });
104
+ // spend — the single choke point through which every costed action an
105
+ // agent takes must pass. `cost` is a WEIGHTED count (tokens, euros, calls —
106
+ // whatever the mandate's scope means), never a plain call counter.
107
+ //
108
+ // Deny by default, twice over:
109
+ // 1. No mandate row for this org+agent+scope => throw immediately. We do
110
+ // NOT fall back to "unlimited"; an unconfigured mandate is a refusal,
111
+ // not an escape hatch. Nothing needs to be persisted for this case, so
112
+ // throwing (and rolling back the whole transaction) is safe.
113
+ // 2. Mandate exists but is exhausted => an approvalRequests row MUST be
114
+ // committed. Convex mutations are single transactions: throwing after
115
+ // `ctx.db.insert` would roll the insert back along with everything
116
+ // else, silently destroying the very approval request the guard is
117
+ // supposed to leave behind. So this path returns `{ ok: false }`
118
+ // instead of throwing -- the approval row survives, and the caller
119
+ // (engine/wakeup) is REQUIRED to check `.ok` and treat `false` as a
120
+ // hard refusal. There is no code path that lets the action proceed
121
+ // past an exhausted mandate; the only way authority is restored is an
122
+ // explicit human `resolveApproval("approved")`.
123
+ export const spend = mutation({
124
+ args: {
125
+ orgId: v.string(),
126
+ agentId: v.id("agents"),
127
+ scope: v.string(),
128
+ cost: v.number(),
129
+ reason: v.string(),
130
+ },
131
+ returns: v.object({
132
+ ok: v.boolean(),
133
+ approvalId: v.optional(v.id("approvalRequests")),
134
+ }),
135
+ handler: async (ctx, args) => {
136
+ const orgId = requireOrgId(args.orgId);
137
+ const agent = await ctx.db.get(args.agentId);
138
+ assertSameOrg(orgId, agent, "Agent not found");
139
+ return await spendCore(ctx, orgId, args);
140
+ },
141
+ });
142
+ // spendInternal — SAME choke point as `spend`, INTERNAL ONLY (unreachable
143
+ // from any client). This is the ONE function `engine/wakeup.enqueue` (an
144
+ // `action`) calls, in its OWN transaction, before it may ever call
145
+ // `enqueueInternal` to put work in the pool -- see the wiring note in
146
+ // component/engine/wakeup.ts. Trusting an `orgId` argument here is exactly
147
+ // as safe as `engine/wakeup.enqueueInternal` trusting one: the only code
148
+ // that can reach an `internal.*` function is other server code, never a
149
+ // client SDK. The public `spend` above is completely untouched.
150
+ export const spendInternal = internalMutation({
151
+ args: {
152
+ orgId: v.string(),
153
+ agentId: v.id("agents"),
154
+ scope: v.string(),
155
+ cost: v.number(),
156
+ reason: v.string(),
157
+ },
158
+ returns: v.object({
159
+ ok: v.boolean(),
160
+ approvalId: v.optional(v.id("approvalRequests")),
161
+ }),
162
+ handler: async (ctx, args) => {
163
+ const agent = await ctx.db.get(args.agentId);
164
+ assertSameOrg(args.orgId, agent, "Agent not found");
165
+ return await spendCore(ctx, args.orgId, args);
166
+ },
167
+ });
168
+ async function spendCore(ctx, orgId, args) {
169
+ if (args.cost <= 0) {
170
+ throw new Error("Validation: cost must be positive");
171
+ }
172
+ const mandate = await ctx.db
173
+ .query("mandates")
174
+ .withIndex("by_org_agent_scope", (q) => q.eq("orgId", orgId).eq("agentId", args.agentId).eq("scope", args.scope))
175
+ .unique();
176
+ if (!mandate) {
177
+ // fails-closed: no data => no authority. Never silently unlimited.
178
+ throw new Error(`Mandate refused: no mandate configured for scope "${args.scope}"`);
179
+ }
180
+ const key = `${orgId}:${args.agentId}:${args.scope}`;
181
+ const status = await rateLimiter.limit(ctx, "mandateSpend", {
182
+ key,
183
+ count: args.cost,
184
+ config: {
185
+ kind: mandate.kind,
186
+ rate: mandate.rate,
187
+ period: mandate.period,
188
+ capacity: mandate.capacity,
189
+ },
190
+ });
191
+ if (!status.ok) {
192
+ // Overrun => a human approval request is created and PERSISTED
193
+ // (returning instead of throwing so the transaction commits it). This
194
+ // is the ONLY consequence of an overrun -- there is no code path that
195
+ // spends past an exhausted mandate.
196
+ const approvalId = await ctx.db.insert("approvalRequests", {
197
+ orgId,
198
+ agentId: args.agentId,
199
+ scope: args.scope,
200
+ requestedCost: args.cost,
201
+ reason: args.reason,
202
+ status: "pending",
203
+ createdAt: Date.now(),
204
+ });
205
+ return { ok: false, approvalId };
206
+ }
207
+ // Rollup via sharded-counter (internal research notes: no canary dependency for a
208
+ // guardrail). One shard bucket per org+agent+scope key.
209
+ await usageCounter.add(ctx, key, args.cost);
210
+ return { ok: true };
211
+ }
212
+ export const listApprovals = query({
213
+ args: { orgId: v.string(), agentId: v.optional(v.id("agents")) },
214
+ returns: v.array(v.object({
215
+ _id: v.id("approvalRequests"),
216
+ _creationTime: v.number(),
217
+ orgId: v.string(),
218
+ agentId: v.id("agents"),
219
+ scope: v.string(),
220
+ requestedCost: v.number(),
221
+ reason: v.string(),
222
+ status: v.union(v.literal("pending"), v.literal("approved"), v.literal("denied")),
223
+ createdAt: v.number(),
224
+ resolvedAt: v.optional(v.number()),
225
+ resolvedBy: v.optional(v.string()),
226
+ })),
227
+ handler: async (ctx, args) => {
228
+ const orgId = requireOrgId(args.orgId);
229
+ const rows = await ctx.db
230
+ .query("approvalRequests")
231
+ .withIndex("by_org", (q) => q.eq("orgId", orgId))
232
+ .collect();
233
+ return args.agentId
234
+ ? rows.filter((r) => r.agentId === args.agentId)
235
+ : rows;
236
+ },
237
+ });
238
+ // resolveApproval — the ONLY way an overrun ever becomes usable authority
239
+ // again: an explicit human decision, recorded, never inferred.
240
+ export const resolveApproval = mutation({
241
+ args: {
242
+ orgId: v.string(),
243
+ approvalId: v.id("approvalRequests"),
244
+ decision: v.union(v.literal("approved"), v.literal("denied")),
245
+ resolvedBy: v.string(),
246
+ },
247
+ returns: v.null(),
248
+ handler: async (ctx, args) => {
249
+ const orgId = requireOrgId(args.orgId);
250
+ const existing = await ctx.db.get(args.approvalId);
251
+ assertSameOrg(orgId, existing, "Approval request not found");
252
+ await ctx.db.patch(args.approvalId, {
253
+ status: args.decision,
254
+ resolvedAt: Date.now(),
255
+ resolvedBy: args.resolvedBy,
256
+ });
257
+ return null;
258
+ },
259
+ });
260
+ // usageCount — the rollup readout (sharded-counter), for dashboards/audits.
261
+ export const usageCount = query({
262
+ args: { orgId: v.string(), agentId: v.id("agents"), scope: v.string() },
263
+ returns: v.number(),
264
+ handler: async (ctx, args) => {
265
+ const orgId = requireOrgId(args.orgId);
266
+ const agent = await ctx.db.get(args.agentId);
267
+ assertSameOrg(orgId, agent, "Agent not found");
268
+ const key = `${orgId}:${args.agentId}:${args.scope}`;
269
+ return await usageCounter.count(ctx, key);
270
+ },
271
+ });
@@ -0,0 +1,36 @@
1
+ export declare const startRun: import("convex/server").RegisteredMutation<"public", {
2
+ orgId: string;
3
+ workflowId: string;
4
+ awaitedEventName: string;
5
+ timeoutMs: number;
6
+ }, Promise<import("convex/values").GenericId<"sandboxRuns">>>;
7
+ export declare const attachSandboxId: import("convex/server").RegisteredMutation<"public", {
8
+ orgId: string;
9
+ sandboxId: string;
10
+ sandboxRunId: import("convex/values").GenericId<"sandboxRuns">;
11
+ }, Promise<null>>;
12
+ export declare const getStatus: import("convex/server").RegisteredQuery<"public", {
13
+ orgId: string;
14
+ sandboxRunId: import("convex/values").GenericId<"sandboxRuns">;
15
+ }, Promise<{
16
+ status: "completed" | "pending" | "failed" | "timedOut";
17
+ resultSummary: string | undefined;
18
+ }>>;
19
+ export declare const deadLetterMalformed: import("convex/server").RegisteredMutation<"public", {
20
+ orgId?: string | undefined;
21
+ reason: string;
22
+ deliveryId: string;
23
+ rawBody: string;
24
+ }, Promise<null>>;
25
+ export declare const processCallback: import("convex/server").RegisteredMutation<"public", {
26
+ resultSummary?: string | undefined;
27
+ orgId: string;
28
+ status: "completed" | "failed";
29
+ sandboxRunId: string;
30
+ deliveryId: string;
31
+ }, Promise<{
32
+ outcome: "woke" | "duplicate" | "dead-lettered";
33
+ }>>;
34
+ export declare const timeoutInternal: import("convex/server").RegisteredMutation<"internal", {
35
+ sandboxRunId: import("convex/values").GenericId<"sandboxRuns">;
36
+ }, Promise<null>>;