@cat-factory/server 0.215.0 → 0.217.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/dist/index.d.ts +1 -0
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +1 -0
  4. package/dist/index.js.map +1 -1
  5. package/dist/modules/publicApi/PublicDecisionController.d.ts.map +1 -1
  6. package/dist/modules/publicApi/PublicDecisionController.js +41 -322
  7. package/dist/modules/publicApi/PublicDecisionController.js.map +1 -1
  8. package/dist/modules/publicApi/decisions/approvalRoutes.d.ts +4 -0
  9. package/dist/modules/publicApi/decisions/approvalRoutes.d.ts.map +1 -0
  10. package/dist/modules/publicApi/decisions/approvalRoutes.js +96 -0
  11. package/dist/modules/publicApi/decisions/approvalRoutes.js.map +1 -0
  12. package/dist/modules/publicApi/decisions/dialogueRoutes.d.ts +5 -0
  13. package/dist/modules/publicApi/decisions/dialogueRoutes.d.ts.map +1 -0
  14. package/dist/modules/publicApi/decisions/dialogueRoutes.js +161 -0
  15. package/dist/modules/publicApi/decisions/dialogueRoutes.js.map +1 -0
  16. package/dist/modules/publicApi/decisions/gateRoutes.d.ts +5 -0
  17. package/dist/modules/publicApi/decisions/gateRoutes.d.ts.map +1 -0
  18. package/dist/modules/publicApi/decisions/gateRoutes.js +120 -0
  19. package/dist/modules/publicApi/decisions/gateRoutes.js.map +1 -0
  20. package/dist/modules/publicApi/decisions/projection.d.ts +62 -0
  21. package/dist/modules/publicApi/decisions/projection.d.ts.map +1 -0
  22. package/dist/modules/publicApi/decisions/projection.js +489 -0
  23. package/dist/modules/publicApi/decisions/projection.js.map +1 -0
  24. package/dist/modules/publicApi/decisions/requirementsRoutes.d.ts +11 -0
  25. package/dist/modules/publicApi/decisions/requirementsRoutes.d.ts.map +1 -0
  26. package/dist/modules/publicApi/decisions/requirementsRoutes.js +91 -0
  27. package/dist/modules/publicApi/decisions/requirementsRoutes.js.map +1 -0
  28. package/dist/modules/publicApi/decisions/scope.d.ts +107 -0
  29. package/dist/modules/publicApi/decisions/scope.d.ts.map +1 -0
  30. package/dist/modules/publicApi/decisions/scope.js +142 -0
  31. package/dist/modules/publicApi/decisions/scope.js.map +1 -0
  32. package/dist/modules/publicApi/publicApiAdmission.d.ts +9 -5
  33. package/dist/modules/publicApi/publicApiAdmission.d.ts.map +1 -1
  34. package/dist/modules/publicApi/publicApiAdmission.js +13 -5
  35. package/dist/modules/publicApi/publicApiAdmission.js.map +1 -1
  36. package/dist/persistence/rpc-allowlist.d.ts.map +1 -1
  37. package/dist/persistence/rpc-allowlist.js +5 -0
  38. package/dist/persistence/rpc-allowlist.js.map +1 -1
  39. package/dist/runtime/spendAlerts.d.ts +24 -0
  40. package/dist/runtime/spendAlerts.d.ts.map +1 -0
  41. package/dist/runtime/spendAlerts.js +173 -0
  42. package/dist/runtime/spendAlerts.js.map +1 -0
  43. package/package.json +9 -9
@@ -0,0 +1,173 @@
1
+ import { distinctAccountIds, spendThresholdCardContent } from '@cat-factory/orchestration';
2
+ import { describeError } from '@cat-factory/kernel';
3
+ import { mergeSpendAlertStates, spendAlertEscalated, spendAlertFiring } from '@cat-factory/spend';
4
+ // Runtime-neutral spend-alert sweep: the PROACTIVE half of the spend safeguard, shared by both
5
+ // facades' periodic sweeps (the Worker's cron `scheduled` handler and the Node `setInterval`
6
+ // sweeper) exactly like `sweepPlatformHealth`.
7
+ //
8
+ // The safeguard itself only ever speaks once the money is gone: `isOverBudget` pauses a run at
9
+ // the ceiling and a `budget_paused` card appears. That is the worst possible moment to learn a
10
+ // budget was running out, so this sweep evaluates each workspace's (and its account's) forward
11
+ // position (burn rate, month-end projection) and raises a `budget_threshold` card while there
12
+ // is still time to raise the limit or stop a runaway.
13
+ //
14
+ // Nothing here can gate anything. The forecast is advisory by construction (see
15
+ // `@cat-factory/spend`'s `forecast.logic.ts`), so a projection bug costs a wrong card and never
16
+ // a paused or unpaused run.
17
+ //
18
+ // TWO reads serve the whole deployment: the ledger aggregates are batched per scope set, and the
19
+ // per-workspace notified state is one chunked `listLatestByType`. A point read per workspace
20
+ // would be the banned N+1, run every few minutes across every tenant.
21
+ /**
22
+ * How often a pass runs on both facades. Spend is a slow signal: the burn rate is measured over
23
+ * a week and a budget is monthly, so a tighter cadence would buy nothing and cost four aggregate
24
+ * queries per tenant set. Shared rather than per-facade so the Worker's stateless cron window and
25
+ * the Node timer cannot drift into meaning different things.
26
+ */
27
+ export const SPEND_ALERT_INTERVAL_MS = 15 * 60_000;
28
+ /**
29
+ * One pass. Returns how many workspaces a card was raised on.
30
+ *
31
+ * A no-op when the notifications module is not wired (tests, mothership local nodes). Unlike the
32
+ * platform-health sweep there is no opt-in flag: a budget is something a deployment configured on
33
+ * purpose, and a safeguard that warns only when an operator remembered to switch the warning on
34
+ * is the silent pause this exists to replace.
35
+ *
36
+ * Best-effort per workspace: one workspace's failed raise is logged and skipped, never aborting
37
+ * the rest. This sweep must not become the silent background failure it exists to prevent.
38
+ */
39
+ export async function sweepSpendAlerts(container, now, logger) {
40
+ const notifications = container.notifications;
41
+ if (!notifications)
42
+ return { raised: 0 };
43
+ const workspaces = await container.workspaceService.list(null);
44
+ if (workspaces.length === 0)
45
+ return { raised: 0 };
46
+ const workspaceIds = workspaces.map((ws) => ws.id);
47
+ const spend = container.spendService;
48
+ // The account tier is evaluated ONCE per account and fanned out to its workspaces, matching
49
+ // the scope its rollup is computed at. Legacy null-account boards contribute nothing here and
50
+ // are still forecast on their own workspace tier.
51
+ const [byWorkspace, byAccount, lastNotified] = await Promise.all([
52
+ spend.forecastWorkspaces(workspaceIds, now),
53
+ spend.forecastAccounts(distinctAccountIds(workspaces), now),
54
+ notifications.service.listLatestByType(workspaceIds, 'budget_threshold'),
55
+ ]);
56
+ let raised = 0;
57
+ for (const workspace of workspaces) {
58
+ try {
59
+ if (await settleWorkspace({ container, logger }, { workspace, byWorkspace, byAccount, lastNotified })) {
60
+ raised += 1;
61
+ }
62
+ }
63
+ catch (error) {
64
+ logger?.warn('spend-alerts: failed to evaluate workspace', {
65
+ scope: 'spend-alerts',
66
+ workspaceId: workspace.id,
67
+ ...describeError(error),
68
+ });
69
+ }
70
+ }
71
+ return { raised };
72
+ }
73
+ /**
74
+ * Evaluate ONE workspace and raise its card when the alert state has ESCALATED. Returns whether
75
+ * a card was raised.
76
+ *
77
+ * There is deliberately no clearing counterpart. A crossed threshold does not un-cross in any
78
+ * way worth telling anyone about (spend only rises within a period), and the card auto-expires
79
+ * with the period: the next period's first crossing raises a new one because the payload's
80
+ * `budgetPeriodStart` differs. Auto-dismissing on a fall-back would only be possible for the
81
+ * projection, and would make the card flicker as the burn rate wobbles around the limit.
82
+ */
83
+ async function settleWorkspace(deps, input) {
84
+ const { workspace, byWorkspace, byAccount, lastNotified } = input;
85
+ // Built workspace-tier-first, because the alert list is part of the card's dedup identity: an
86
+ // order that depended on which tier happened to be evaluated first would re-deliver the card on
87
+ // a refactor that changed nothing a human can see.
88
+ const firing = [
89
+ { tier: 'workspace', forecast: byWorkspace.get(workspace.id) },
90
+ {
91
+ tier: 'account',
92
+ forecast: workspace.accountId ? byAccount.get(workspace.accountId) : undefined,
93
+ },
94
+ ].filter(isFiring);
95
+ if (firing.length === 0)
96
+ return false;
97
+ // The WORST firing tier owns the HEADLINE. A workspace holds at most one open block-less card
98
+ // per type, so the two tiers cannot each have one; leading with the worse of them is what keeps
99
+ // the card honest when both fire.
100
+ const worst = [...firing].sort(bySeverity)[0];
101
+ const alerts = firing.map(({ tier, forecast }) => ({
102
+ tier,
103
+ threshold: forecast.alert.threshold,
104
+ projectedOverrun: forecast.alert.projectedOverrun,
105
+ }));
106
+ // The ESCALATION decision is made on the FOLD of every firing tier, not on the worst one: the
107
+ // card being compared against lists them all, so only a fold-versus-fold comparison sees a
108
+ // SECOND tier starting to fire beside an unchanged first one.
109
+ const next = mergeSpendAlertStates(worst.forecast.alert.periodStart, alerts);
110
+ const previous = readNotifiedState(lastNotified.get(workspace.id));
111
+ if (!spendAlertEscalated(previous, next))
112
+ return false;
113
+ const { title, body } = spendThresholdCardContent(alerts, {
114
+ tier: worst.tier,
115
+ costLimit: worst.forecast.costLimit,
116
+ currency: worst.forecast.currency,
117
+ threshold: worst.forecast.alert.threshold,
118
+ });
119
+ await deps.container.notifications?.service.raise(workspace.id, {
120
+ type: 'budget_threshold',
121
+ blockId: null,
122
+ executionId: null,
123
+ title,
124
+ body,
125
+ payload: {
126
+ budgetPeriodStart: worst.forecast.alert.periodStart,
127
+ budgetAlerts: alerts,
128
+ },
129
+ });
130
+ deps.logger?.info('spend-alerts: raised a budget threshold card', {
131
+ scope: 'spend-alerts',
132
+ workspaceId: workspace.id,
133
+ // The tiers and their STATE, never the amounts: a log line naming a workspace's spend puts a
134
+ // per-tenant financial figure into the deployment's operational log.
135
+ tiers: alerts.map((a) => a.tier).join(','),
136
+ // The FOLD, matching what the escalation test acted on: logging the worst tier's own signals
137
+ // would omit the account overrun that is sometimes the sole reason this card was raised.
138
+ threshold: next.threshold,
139
+ projectedOverrun: next.projectedOverrun,
140
+ });
141
+ return true;
142
+ }
143
+ /** Narrowing predicate: a tier whose forecast is present AND firing. */
144
+ function isFiring(entry) {
145
+ return entry.forecast != null && spendAlertFiring(entry.forecast.alert);
146
+ }
147
+ /**
148
+ * Worst-first: a crossed threshold outranks a bare projection, and a higher crossed threshold
149
+ * outranks a lower one. Total, so the sort is deterministic when both tiers are in the same state.
150
+ */
151
+ function bySeverity(a, b) {
152
+ return (b.forecast.alert.threshold ?? 0) - (a.forecast.alert.threshold ?? 0);
153
+ }
154
+ /**
155
+ * The state a workspace was last notified at, read back off its most recent card.
156
+ *
157
+ * The card row IS the sweep's alert store: it is persisted, shared by every replica, and
158
+ * survives a restart, which an in-process "already notified" flag would not on a multi-node
159
+ * deployment. A card with no period stamped (nothing was ever raised, or a card written before
160
+ * this payload existed) reads as "never notified", so the next firing state escalates, which is the safe
161
+ * direction, since the cost is one extra card rather than a missed warning.
162
+ *
163
+ * Folded through the SAME `mergeSpendAlertStates` the sweep folds this pass's tiers with, so the
164
+ * two sides of the escalation test can never come to mean different things.
165
+ */
166
+ function readNotifiedState(card) {
167
+ const payload = card?.payload;
168
+ const periodStart = payload?.budgetPeriodStart;
169
+ if (typeof periodStart !== 'number')
170
+ return null;
171
+ return mergeSpendAlertStates(periodStart, payload?.budgetAlerts ?? []);
172
+ }
173
+ //# sourceMappingURL=spendAlerts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"spendAlerts.js","sourceRoot":"","sources":["../../src/runtime/spendAlerts.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cat-factory/server",
3
- "version": "0.215.0",
3
+ "version": "0.217.0",
4
4
  "description": "Runtime-neutral HTTP layer for the Agent Architecture Board: the Hono controllers, middleware (auth/authz/CORS/error), request helpers and the gateway seams shared by every deployment facade (Cloudflare Worker, Node service).",
5
5
  "repository": {
6
6
  "type": "git",
@@ -30,14 +30,14 @@
30
30
  "hono": "^4.12.33",
31
31
  "pino": "^10.3.1",
32
32
  "valibot": "^1.4.2",
33
- "@cat-factory/agents": "0.110.7",
34
- "@cat-factory/contracts": "0.236.0",
35
- "@cat-factory/integrations": "0.126.2",
36
- "@cat-factory/kernel": "0.236.0",
37
- "@cat-factory/mcp-server": "0.6.0",
38
- "@cat-factory/orchestration": "0.203.0",
39
- "@cat-factory/prompt-fragments": "0.15.62",
40
- "@cat-factory/spend": "0.14.14"
33
+ "@cat-factory/agents": "0.110.9",
34
+ "@cat-factory/contracts": "0.238.0",
35
+ "@cat-factory/kernel": "0.237.0",
36
+ "@cat-factory/integrations": "0.127.0",
37
+ "@cat-factory/mcp-server": "0.7.0",
38
+ "@cat-factory/orchestration": "0.205.0",
39
+ "@cat-factory/spend": "0.15.0",
40
+ "@cat-factory/prompt-fragments": "0.15.64"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@types/node": "^26.1.2",