podwatch 1.0.2 → 1.1.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.
- package/dist/config-monitor.d.ts +65 -0
- package/dist/config-monitor.d.ts.map +1 -0
- package/dist/config-monitor.js +218 -0
- package/dist/config-monitor.js.map +1 -0
- package/dist/hooks/budget.d.ts +20 -0
- package/dist/hooks/budget.d.ts.map +1 -0
- package/dist/hooks/budget.js +134 -0
- package/dist/hooks/budget.js.map +1 -0
- package/dist/hooks/cost.d.ts +2 -0
- package/dist/hooks/cost.d.ts.map +1 -1
- package/dist/hooks/cost.js +13 -0
- package/dist/hooks/cost.js.map +1 -1
- package/dist/hooks/lifecycle.d.ts.map +1 -1
- package/dist/hooks/lifecycle.js +11 -52
- package/dist/hooks/lifecycle.js.map +1 -1
- package/dist/hooks/security.d.ts +2 -2
- package/dist/hooks/security.d.ts.map +1 -1
- package/dist/hooks/security.js +80 -7
- package/dist/hooks/security.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -5
- package/dist/index.js.map +1 -1
- package/dist/test-helpers/mock-transmitter.d.ts +25 -0
- package/dist/test-helpers/mock-transmitter.d.ts.map +1 -0
- package/dist/test-helpers/mock-transmitter.js +54 -0
- package/dist/test-helpers/mock-transmitter.js.map +1 -0
- package/dist/transmitter.d.ts +11 -0
- package/dist/transmitter.d.ts.map +1 -1
- package/dist/transmitter.js +41 -4
- package/dist/transmitter.js.map +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/updater.d.ts +2 -2
- package/dist/updater.d.ts.map +1 -1
- package/dist/updater.js +3 -3
- package/dist/updater.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config monitor — snapshots and diffs the gateway config for drift/tampering detection.
|
|
3
|
+
*
|
|
4
|
+
* Monitors security-relevant config fields and emits config_change events
|
|
5
|
+
* when drift is detected. Called on startup, every pulse, and gateway_start.
|
|
6
|
+
*
|
|
7
|
+
* Replaces the single-field model tracking that was in lifecycle.ts.
|
|
8
|
+
*/
|
|
9
|
+
export interface MonitoredPath {
|
|
10
|
+
/** Dot-separated path into api.config (e.g. "agents.defaults.model") */
|
|
11
|
+
path: string;
|
|
12
|
+
/** Human-readable label used as prefix in change events */
|
|
13
|
+
label: string;
|
|
14
|
+
/** Keys to redact from snapshots (e.g. apiKey) — prevents secrets in events */
|
|
15
|
+
redactKeys?: string[];
|
|
16
|
+
}
|
|
17
|
+
export declare const MONITORED_PATHS: MonitoredPath[];
|
|
18
|
+
/**
|
|
19
|
+
* Get a nested value from an object by dot-separated path.
|
|
20
|
+
*/
|
|
21
|
+
export declare function deepGet(obj: unknown, path: string): unknown;
|
|
22
|
+
/**
|
|
23
|
+
* Deep clone a value, replacing values at sensitive keys with "***".
|
|
24
|
+
*/
|
|
25
|
+
export declare function deepClone(value: unknown, redactKeys?: string[]): unknown;
|
|
26
|
+
/**
|
|
27
|
+
* Deep equality check for two values.
|
|
28
|
+
*/
|
|
29
|
+
export declare function deepEqual(a: unknown, b: unknown): boolean;
|
|
30
|
+
export interface ConfigChange {
|
|
31
|
+
field: string;
|
|
32
|
+
oldValue: unknown;
|
|
33
|
+
newValue: unknown;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Diff two values recursively. Returns a list of changes with dot-path field names.
|
|
37
|
+
* For arrays, reports the whole array as changed (no element-level diff).
|
|
38
|
+
*/
|
|
39
|
+
export declare function diffValues(oldVal: unknown, newVal: unknown, prefix: string): ConfigChange[];
|
|
40
|
+
/**
|
|
41
|
+
* Extract monitored config sections from the full gateway config.
|
|
42
|
+
* Redacts sensitive keys (apiKey, etc.) so they don't appear in events.
|
|
43
|
+
*/
|
|
44
|
+
export declare function extractMonitoredConfig(config: Record<string, unknown>, paths?: MonitoredPath[]): Record<string, unknown>;
|
|
45
|
+
/**
|
|
46
|
+
* Initialize the config snapshot. Called once on register().
|
|
47
|
+
* Does NOT emit events (baseline capture).
|
|
48
|
+
*/
|
|
49
|
+
export declare function initSnapshot(config: Record<string, unknown>): void;
|
|
50
|
+
/**
|
|
51
|
+
* Check for config changes against the stored snapshot.
|
|
52
|
+
* Emits config_change events for each changed field, then updates snapshot.
|
|
53
|
+
*
|
|
54
|
+
* If no snapshot exists yet, initializes one (no events emitted).
|
|
55
|
+
*/
|
|
56
|
+
export declare function checkConfigChanges(config: Record<string, unknown>): ConfigChange[];
|
|
57
|
+
/**
|
|
58
|
+
* Reset the snapshot (for testing or re-register).
|
|
59
|
+
*/
|
|
60
|
+
export declare function resetSnapshot(): void;
|
|
61
|
+
/**
|
|
62
|
+
* Get the current snapshot (for testing/diagnostics).
|
|
63
|
+
*/
|
|
64
|
+
export declare function getSnapshot(): Record<string, unknown> | null;
|
|
65
|
+
//# sourceMappingURL=config-monitor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config-monitor.d.ts","sourceRoot":"","sources":["../src/config-monitor.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAQH,MAAM,WAAW,aAAa;IAC5B,wEAAwE;IACxE,IAAI,EAAE,MAAM,CAAC;IACb,2DAA2D;IAC3D,KAAK,EAAE,MAAM,CAAC;IACd,+EAA+E;IAC/E,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;CACvB;AAED,eAAO,MAAM,eAAe,EAAE,aAAa,EAQ1C,CAAC;AAMF;;GAEG;AACH,wBAAgB,OAAO,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAS3D;AAED;;GAEG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,OAAO,EAAE,UAAU,CAAC,EAAE,MAAM,EAAE,GAAG,OAAO,CAcxE;AAED;;GAEG;AACH,wBAAgB,SAAS,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,GAAG,OAAO,CAsBzD;AAMD,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,OAAO,CAAC;IAClB,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,YAAY,EAAE,CA6C3F;AAMD;;;GAGG;AACH,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,KAAK,GAAE,aAAa,EAAoB,GACvC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAOzB;AAQD;;;GAGG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAElE;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,YAAY,EAAE,CAgClF;AAED;;GAEG;AACH,wBAAgB,aAAa,IAAI,IAAI,CAEpC;AAED;;GAEG;AACH,wBAAgB,WAAW,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAE5D"}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Config monitor — snapshots and diffs the gateway config for drift/tampering detection.
|
|
4
|
+
*
|
|
5
|
+
* Monitors security-relevant config fields and emits config_change events
|
|
6
|
+
* when drift is detected. Called on startup, every pulse, and gateway_start.
|
|
7
|
+
*
|
|
8
|
+
* Replaces the single-field model tracking that was in lifecycle.ts.
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.MONITORED_PATHS = void 0;
|
|
12
|
+
exports.deepGet = deepGet;
|
|
13
|
+
exports.deepClone = deepClone;
|
|
14
|
+
exports.deepEqual = deepEqual;
|
|
15
|
+
exports.diffValues = diffValues;
|
|
16
|
+
exports.extractMonitoredConfig = extractMonitoredConfig;
|
|
17
|
+
exports.initSnapshot = initSnapshot;
|
|
18
|
+
exports.checkConfigChanges = checkConfigChanges;
|
|
19
|
+
exports.resetSnapshot = resetSnapshot;
|
|
20
|
+
exports.getSnapshot = getSnapshot;
|
|
21
|
+
const transmitter_js_1 = require("./transmitter.js");
|
|
22
|
+
exports.MONITORED_PATHS = [
|
|
23
|
+
{ path: "agents.defaults.model", label: "model" },
|
|
24
|
+
{ path: "agents.defaults.models", label: "models" },
|
|
25
|
+
{ path: "tools.exec", label: "tools.exec" },
|
|
26
|
+
{ path: "tools.elevated", label: "tools.elevated" },
|
|
27
|
+
{ path: "models.providers", label: "providers", redactKeys: ["apiKey", "apiSecret", "secret", "token"] },
|
|
28
|
+
{ path: "plugins", label: "plugins" },
|
|
29
|
+
{ path: "session", label: "session" },
|
|
30
|
+
];
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// Pure utility functions (exported for testing)
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
/**
|
|
35
|
+
* Get a nested value from an object by dot-separated path.
|
|
36
|
+
*/
|
|
37
|
+
function deepGet(obj, path) {
|
|
38
|
+
if (obj == null || typeof obj !== "object")
|
|
39
|
+
return undefined;
|
|
40
|
+
const parts = path.split(".");
|
|
41
|
+
let current = obj;
|
|
42
|
+
for (const part of parts) {
|
|
43
|
+
if (current == null || typeof current !== "object")
|
|
44
|
+
return undefined;
|
|
45
|
+
current = current[part];
|
|
46
|
+
}
|
|
47
|
+
return current;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Deep clone a value, replacing values at sensitive keys with "***".
|
|
51
|
+
*/
|
|
52
|
+
function deepClone(value, redactKeys) {
|
|
53
|
+
if (value == null)
|
|
54
|
+
return value;
|
|
55
|
+
if (typeof value !== "object")
|
|
56
|
+
return value;
|
|
57
|
+
if (Array.isArray(value))
|
|
58
|
+
return value.map((v) => deepClone(v, redactKeys));
|
|
59
|
+
const result = {};
|
|
60
|
+
for (const [k, v] of Object.entries(value)) {
|
|
61
|
+
if (redactKeys?.includes(k) && v != null && v !== "") {
|
|
62
|
+
result[k] = "***";
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
result[k] = deepClone(v, redactKeys);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return result;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Deep equality check for two values.
|
|
72
|
+
*/
|
|
73
|
+
function deepEqual(a, b) {
|
|
74
|
+
if (a === b)
|
|
75
|
+
return true;
|
|
76
|
+
if (a == null && b == null)
|
|
77
|
+
return true;
|
|
78
|
+
if (a == null || b == null)
|
|
79
|
+
return false;
|
|
80
|
+
if (typeof a !== typeof b)
|
|
81
|
+
return false;
|
|
82
|
+
if (typeof a !== "object")
|
|
83
|
+
return false;
|
|
84
|
+
if (Array.isArray(a) !== Array.isArray(b))
|
|
85
|
+
return false;
|
|
86
|
+
if (Array.isArray(a)) {
|
|
87
|
+
const arrB = b;
|
|
88
|
+
if (a.length !== arrB.length)
|
|
89
|
+
return false;
|
|
90
|
+
return a.every((v, i) => deepEqual(v, arrB[i]));
|
|
91
|
+
}
|
|
92
|
+
const objA = a;
|
|
93
|
+
const objB = b;
|
|
94
|
+
const keysA = Object.keys(objA).sort();
|
|
95
|
+
const keysB = Object.keys(objB).sort();
|
|
96
|
+
if (keysA.length !== keysB.length)
|
|
97
|
+
return false;
|
|
98
|
+
if (!keysA.every((k, i) => k === keysB[i]))
|
|
99
|
+
return false;
|
|
100
|
+
return keysA.every((k) => deepEqual(objA[k], objB[k]));
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Diff two values recursively. Returns a list of changes with dot-path field names.
|
|
104
|
+
* For arrays, reports the whole array as changed (no element-level diff).
|
|
105
|
+
*/
|
|
106
|
+
function diffValues(oldVal, newVal, prefix) {
|
|
107
|
+
const changes = [];
|
|
108
|
+
// Both null/undefined — no change
|
|
109
|
+
if (oldVal == null && newVal == null)
|
|
110
|
+
return changes;
|
|
111
|
+
// One is null/undefined — whole path changed
|
|
112
|
+
if (oldVal == null || newVal == null) {
|
|
113
|
+
changes.push({ field: prefix, oldValue: oldVal ?? null, newValue: newVal ?? null });
|
|
114
|
+
return changes;
|
|
115
|
+
}
|
|
116
|
+
// Different types
|
|
117
|
+
if (typeof oldVal !== typeof newVal) {
|
|
118
|
+
changes.push({ field: prefix, oldValue: oldVal, newValue: newVal });
|
|
119
|
+
return changes;
|
|
120
|
+
}
|
|
121
|
+
// Primitives
|
|
122
|
+
if (typeof oldVal !== "object") {
|
|
123
|
+
if (oldVal !== newVal) {
|
|
124
|
+
changes.push({ field: prefix, oldValue: oldVal, newValue: newVal });
|
|
125
|
+
}
|
|
126
|
+
return changes;
|
|
127
|
+
}
|
|
128
|
+
// Arrays — report as whole if different
|
|
129
|
+
if (Array.isArray(oldVal) || Array.isArray(newVal)) {
|
|
130
|
+
if (!deepEqual(oldVal, newVal)) {
|
|
131
|
+
changes.push({ field: prefix, oldValue: oldVal, newValue: newVal });
|
|
132
|
+
}
|
|
133
|
+
return changes;
|
|
134
|
+
}
|
|
135
|
+
// Objects — recurse into each key
|
|
136
|
+
const oldRecord = oldVal;
|
|
137
|
+
const newRecord = newVal;
|
|
138
|
+
const allKeys = new Set([...Object.keys(oldRecord), ...Object.keys(newRecord)]);
|
|
139
|
+
for (const key of allKeys) {
|
|
140
|
+
const childPath = prefix ? `${prefix}.${key}` : key;
|
|
141
|
+
changes.push(...diffValues(oldRecord[key], newRecord[key], childPath));
|
|
142
|
+
}
|
|
143
|
+
return changes;
|
|
144
|
+
}
|
|
145
|
+
// ---------------------------------------------------------------------------
|
|
146
|
+
// Snapshot extraction
|
|
147
|
+
// ---------------------------------------------------------------------------
|
|
148
|
+
/**
|
|
149
|
+
* Extract monitored config sections from the full gateway config.
|
|
150
|
+
* Redacts sensitive keys (apiKey, etc.) so they don't appear in events.
|
|
151
|
+
*/
|
|
152
|
+
function extractMonitoredConfig(config, paths = exports.MONITORED_PATHS) {
|
|
153
|
+
const result = {};
|
|
154
|
+
for (const { path, label, redactKeys } of paths) {
|
|
155
|
+
const value = deepGet(config, path);
|
|
156
|
+
result[label] = deepClone(value, redactKeys);
|
|
157
|
+
}
|
|
158
|
+
return result;
|
|
159
|
+
}
|
|
160
|
+
// ---------------------------------------------------------------------------
|
|
161
|
+
// Stateful config monitor
|
|
162
|
+
// ---------------------------------------------------------------------------
|
|
163
|
+
let snapshot = null;
|
|
164
|
+
/**
|
|
165
|
+
* Initialize the config snapshot. Called once on register().
|
|
166
|
+
* Does NOT emit events (baseline capture).
|
|
167
|
+
*/
|
|
168
|
+
function initSnapshot(config) {
|
|
169
|
+
snapshot = extractMonitoredConfig(config);
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Check for config changes against the stored snapshot.
|
|
173
|
+
* Emits config_change events for each changed field, then updates snapshot.
|
|
174
|
+
*
|
|
175
|
+
* If no snapshot exists yet, initializes one (no events emitted).
|
|
176
|
+
*/
|
|
177
|
+
function checkConfigChanges(config) {
|
|
178
|
+
if (!snapshot) {
|
|
179
|
+
initSnapshot(config);
|
|
180
|
+
return [];
|
|
181
|
+
}
|
|
182
|
+
const current = extractMonitoredConfig(config);
|
|
183
|
+
const allChanges = [];
|
|
184
|
+
for (const { label } of exports.MONITORED_PATHS) {
|
|
185
|
+
const changes = diffValues(snapshot[label], current[label], label);
|
|
186
|
+
for (const change of changes) {
|
|
187
|
+
allChanges.push(change);
|
|
188
|
+
transmitter_js_1.transmitter.enqueue({
|
|
189
|
+
type: "config_change",
|
|
190
|
+
ts: Date.now(),
|
|
191
|
+
field: change.field,
|
|
192
|
+
value: change.newValue,
|
|
193
|
+
previousValue: change.oldValue,
|
|
194
|
+
params: {
|
|
195
|
+
field: change.field,
|
|
196
|
+
value: change.newValue,
|
|
197
|
+
previousValue: change.oldValue,
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
// Update snapshot after emitting
|
|
203
|
+
snapshot = current;
|
|
204
|
+
return allChanges;
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Reset the snapshot (for testing or re-register).
|
|
208
|
+
*/
|
|
209
|
+
function resetSnapshot() {
|
|
210
|
+
snapshot = null;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Get the current snapshot (for testing/diagnostics).
|
|
214
|
+
*/
|
|
215
|
+
function getSnapshot() {
|
|
216
|
+
return snapshot ? { ...snapshot } : null;
|
|
217
|
+
}
|
|
218
|
+
//# sourceMappingURL=config-monitor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config-monitor.js","sourceRoot":"","sources":["../src/config-monitor.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Budget hard stop hooks — before_model_resolve and before_prompt_build.
|
|
3
|
+
*
|
|
4
|
+
* When the server signals a hard stop (budget exceeded + hard stop enabled),
|
|
5
|
+
* these hooks:
|
|
6
|
+
* 1. Try to downgrade to a cheaper model (best-effort)
|
|
7
|
+
* 2. Prepend context telling the LLM to inform the user about the budget limit
|
|
8
|
+
*/
|
|
9
|
+
import type { PodwatchConfig } from "../index.js";
|
|
10
|
+
import type { PluginApi } from "../types.js";
|
|
11
|
+
/**
|
|
12
|
+
* Find the cheapest available model from the gateway config.
|
|
13
|
+
* Returns { model, provider } or null if only one model is configured.
|
|
14
|
+
*/
|
|
15
|
+
export declare function findCheapestModel(config: Record<string, unknown>): {
|
|
16
|
+
model: string;
|
|
17
|
+
provider: string;
|
|
18
|
+
} | null;
|
|
19
|
+
export declare function registerBudgetHooks(api: PluginApi, config: PodwatchConfig): void;
|
|
20
|
+
//# sourceMappingURL=budget.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"budget.d.ts","sourceRoot":"","sources":["../../src/hooks/budget.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AA6B7C;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC9B;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CA0D5C;AAMD,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,SAAS,EAAE,MAAM,EAAE,cAAc,GAAG,IAAI,CA6ChF"}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Budget hard stop hooks — before_model_resolve and before_prompt_build.
|
|
4
|
+
*
|
|
5
|
+
* When the server signals a hard stop (budget exceeded + hard stop enabled),
|
|
6
|
+
* these hooks:
|
|
7
|
+
* 1. Try to downgrade to a cheaper model (best-effort)
|
|
8
|
+
* 2. Prepend context telling the LLM to inform the user about the budget limit
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.findCheapestModel = findCheapestModel;
|
|
12
|
+
exports.registerBudgetHooks = registerBudgetHooks;
|
|
13
|
+
const transmitter_js_1 = require("../transmitter.js");
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Model cost heuristic — cheaper models first
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
/** Known cheap model patterns (ordered cheapest first). */
|
|
18
|
+
const CHEAP_MODEL_PATTERNS = [
|
|
19
|
+
"haiku",
|
|
20
|
+
"flash",
|
|
21
|
+
"mini",
|
|
22
|
+
"nano",
|
|
23
|
+
"lite",
|
|
24
|
+
"sonnet",
|
|
25
|
+
];
|
|
26
|
+
/**
|
|
27
|
+
* Score a model name by cost (lower = cheaper).
|
|
28
|
+
* Known cheap patterns get low scores, unknown models get a high default.
|
|
29
|
+
*/
|
|
30
|
+
function modelCostScore(modelName) {
|
|
31
|
+
const lower = modelName.toLowerCase();
|
|
32
|
+
for (let i = 0; i < CHEAP_MODEL_PATTERNS.length; i++) {
|
|
33
|
+
if (lower.includes(CHEAP_MODEL_PATTERNS[i]))
|
|
34
|
+
return i;
|
|
35
|
+
}
|
|
36
|
+
return 100; // unknown = expensive
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Find the cheapest available model from the gateway config.
|
|
40
|
+
* Returns { model, provider } or null if only one model is configured.
|
|
41
|
+
*/
|
|
42
|
+
function findCheapestModel(config) {
|
|
43
|
+
const candidates = [];
|
|
44
|
+
// Check models.providers for available models
|
|
45
|
+
const providers = config?.models?.providers;
|
|
46
|
+
if (providers && typeof providers === "object") {
|
|
47
|
+
for (const [providerName, providerCfg] of Object.entries(providers)) {
|
|
48
|
+
const cfg = providerCfg;
|
|
49
|
+
const models = cfg.models;
|
|
50
|
+
if (Array.isArray(models)) {
|
|
51
|
+
for (const m of models) {
|
|
52
|
+
if (typeof m === "string") {
|
|
53
|
+
candidates.push({
|
|
54
|
+
model: `${providerName}/${m}`,
|
|
55
|
+
provider: providerName,
|
|
56
|
+
score: modelCostScore(m),
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// Also check agents.defaults.models for configured model aliases
|
|
64
|
+
const agentModels = config?.agents?.defaults?.models;
|
|
65
|
+
if (agentModels && typeof agentModels === "object") {
|
|
66
|
+
for (const modelId of Object.keys(agentModels)) {
|
|
67
|
+
if (typeof modelId === "string") {
|
|
68
|
+
const parts = modelId.split("/");
|
|
69
|
+
const modelName = parts[parts.length - 1] ?? modelId;
|
|
70
|
+
const provider = parts.length > 1 ? parts[0] : "";
|
|
71
|
+
candidates.push({
|
|
72
|
+
model: modelId,
|
|
73
|
+
provider,
|
|
74
|
+
score: modelCostScore(modelName),
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// Deduplicate by model name
|
|
80
|
+
const seen = new Set();
|
|
81
|
+
const unique = candidates.filter((c) => {
|
|
82
|
+
if (seen.has(c.model))
|
|
83
|
+
return false;
|
|
84
|
+
seen.add(c.model);
|
|
85
|
+
return true;
|
|
86
|
+
});
|
|
87
|
+
if (unique.length <= 1)
|
|
88
|
+
return null;
|
|
89
|
+
// Sort by score (cheapest first) and pick the cheapest
|
|
90
|
+
unique.sort((a, b) => a.score - b.score);
|
|
91
|
+
// Get the current primary model to avoid returning the same one
|
|
92
|
+
const primaryModel = config?.agents?.defaults?.model?.primary;
|
|
93
|
+
const cheapest = unique.find((c) => c.model !== primaryModel) ?? unique[0];
|
|
94
|
+
return { model: cheapest.model, provider: cheapest.provider };
|
|
95
|
+
}
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
// Hook registration
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
function registerBudgetHooks(api, config) {
|
|
100
|
+
// -----------------------------------------------------------------------
|
|
101
|
+
// before_model_resolve — downgrade to cheaper model on hard stop
|
|
102
|
+
// -----------------------------------------------------------------------
|
|
103
|
+
api.registerHook("before_model_resolve", async () => {
|
|
104
|
+
if (!config.enableBudgetEnforcement)
|
|
105
|
+
return;
|
|
106
|
+
const budget = transmitter_js_1.transmitter.getCachedBudget();
|
|
107
|
+
if (!budget?.hardStopActive)
|
|
108
|
+
return;
|
|
109
|
+
const cheaper = findCheapestModel(api.config);
|
|
110
|
+
if (!cheaper)
|
|
111
|
+
return; // only one model configured, skip
|
|
112
|
+
return {
|
|
113
|
+
modelOverride: cheaper.model,
|
|
114
|
+
providerOverride: cheaper.provider,
|
|
115
|
+
};
|
|
116
|
+
}, { name: "podwatch-budget-model-resolve" });
|
|
117
|
+
// -----------------------------------------------------------------------
|
|
118
|
+
// before_prompt_build — inject budget warning into prompt
|
|
119
|
+
// -----------------------------------------------------------------------
|
|
120
|
+
api.registerHook("before_prompt_build", async () => {
|
|
121
|
+
if (!config.enableBudgetEnforcement)
|
|
122
|
+
return;
|
|
123
|
+
const budget = transmitter_js_1.transmitter.getCachedBudget();
|
|
124
|
+
if (!budget?.hardStopActive)
|
|
125
|
+
return;
|
|
126
|
+
return {
|
|
127
|
+
prependContext: "BUDGET HARD STOP ACTIVE. Your spending limit has been reached. " +
|
|
128
|
+
"Reply ONLY with a brief message telling the user their budget is exceeded " +
|
|
129
|
+
"and to visit podwatch.app/costs to resume. Do not use any tools. Do not perform any analysis.",
|
|
130
|
+
};
|
|
131
|
+
}, { name: "podwatch-budget-prompt-build" });
|
|
132
|
+
api.logger.info("[podwatch/budget] Hard stop hooks registered");
|
|
133
|
+
}
|
|
134
|
+
//# sourceMappingURL=budget.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"budget.js","sourceRoot":"","sources":["../../src/hooks/budget.ts"],"names":[],"mappings":"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"}
|
package/dist/hooks/cost.d.ts
CHANGED
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
* Dedup strategy: lastSeenIndex tracks how far we've read into event.messages.
|
|
10
10
|
* Each invocation only processes messages from lastSeenIndex onwards, then
|
|
11
11
|
* advances the pointer. Zero memory growth, O(1) bookkeeping.
|
|
12
|
+
*
|
|
13
|
+
* Cost events are correlated per LLM turn (not per tool call) using turn_id.
|
|
12
14
|
*/
|
|
13
15
|
import type { PodwatchConfig } from "../index.js";
|
|
14
16
|
/**
|
package/dist/hooks/cost.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cost.d.ts","sourceRoot":"","sources":["../../src/hooks/cost.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"cost.d.ts","sourceRoot":"","sources":["../../src/hooks/cost.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAclD;;GAEG;AACH,wBAAgB,eAAe,IAAI,IAAI,CAEtC;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAE3D;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CACjC,GAAG,EAAE,GAAG,EACR,MAAM,EAAE,cAAc,EACtB,kBAAkB,EAAE,OAAO,GAC1B,IAAI,CAiFN"}
|
package/dist/hooks/cost.js
CHANGED
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
* Dedup strategy: lastSeenIndex tracks how far we've read into event.messages.
|
|
11
11
|
* Each invocation only processes messages from lastSeenIndex onwards, then
|
|
12
12
|
* advances the pointer. Zero memory growth, O(1) bookkeeping.
|
|
13
|
+
*
|
|
14
|
+
* Cost events are correlated per LLM turn (not per tool call) using turn_id.
|
|
13
15
|
*/
|
|
14
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
17
|
exports._resetCostState = _resetCostState;
|
|
@@ -18,6 +20,13 @@ exports.registerCostHandler = registerCostHandler;
|
|
|
18
20
|
const transmitter_js_1 = require("../transmitter.js");
|
|
19
21
|
// Track how far into event.messages we've already processed — per session
|
|
20
22
|
const lastSeenIndexMap = new Map();
|
|
23
|
+
/**
|
|
24
|
+
* Generate a turn-based correlation ID for cost events.
|
|
25
|
+
* Cost events correlate to LLM turns, not individual tool calls.
|
|
26
|
+
*/
|
|
27
|
+
function generateTurnId() {
|
|
28
|
+
return `turn_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`;
|
|
29
|
+
}
|
|
21
30
|
/**
|
|
22
31
|
* Reset all dedup state (exported for testing).
|
|
23
32
|
*/
|
|
@@ -63,6 +72,9 @@ function registerCostHandler(api, config, diagnosticsEnabled) {
|
|
|
63
72
|
break; // only check the last user message
|
|
64
73
|
}
|
|
65
74
|
}
|
|
75
|
+
// Generate a turn_id for this batch of cost events (per before_agent_start invocation)
|
|
76
|
+
// This links all cost events from the same LLM turn together
|
|
77
|
+
const turnId = generateTurnId();
|
|
66
78
|
for (const msg of newMessages) {
|
|
67
79
|
// Only assistant messages have usage data
|
|
68
80
|
if (msg.role !== "assistant")
|
|
@@ -90,6 +102,7 @@ function registerCostHandler(api, config, diagnosticsEnabled) {
|
|
|
90
102
|
costUsd: costTotal,
|
|
91
103
|
costBreakdown: msg.usage.cost, // full {input, output, cacheRead, cacheWrite, total} object
|
|
92
104
|
durationMs: undefined,
|
|
105
|
+
correlationId: turnId, // Link cost events per turn
|
|
93
106
|
// Tag heartbeat-triggered cost events so the dashboard can distinguish them
|
|
94
107
|
...(isHeartbeat ? { sessionType: "heartbeat" } : {}),
|
|
95
108
|
});
|
package/dist/hooks/cost.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cost.js","sourceRoot":"","sources":["../../src/hooks/cost.ts"],"names":[],"mappings":";AAAA
|
|
1
|
+
{"version":3,"file":"cost.js","sourceRoot":"","sources":["../../src/hooks/cost.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lifecycle.d.ts","sourceRoot":"","sources":["../../src/hooks/lifecycle.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"lifecycle.d.ts","sourceRoot":"","sources":["../../src/hooks/lifecycle.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AA4BlD;;GAEG;AACH,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,cAAc,GAAG,IAAI,CA4HhF"}
|
package/dist/hooks/lifecycle.js
CHANGED
|
@@ -46,6 +46,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
46
46
|
exports.registerLifecycleHandlers = registerLifecycleHandlers;
|
|
47
47
|
const transmitter_js_1 = require("../transmitter.js");
|
|
48
48
|
const scanner_js_1 = require("../scanner.js");
|
|
49
|
+
const config_monitor_js_1 = require("../config-monitor.js");
|
|
49
50
|
const fs = __importStar(require("node:fs"));
|
|
50
51
|
const path = __importStar(require("node:path"));
|
|
51
52
|
const PLUGIN_VERSION = JSON.parse(fs.readFileSync(path.join(__dirname, "..", "..", "package.json"), "utf-8")).version;
|
|
@@ -55,53 +56,7 @@ let scanTimer = null;
|
|
|
55
56
|
let pulseFailureCount = 0;
|
|
56
57
|
const PULSE_FAILURE_THRESHOLD = 3; // Start backoff after N consecutive failures
|
|
57
58
|
const PULSE_MAX_INTERVAL_MS = 3_600_000; // 60 min cap
|
|
58
|
-
// Config change detection
|
|
59
|
-
let knownPrimaryModel = null;
|
|
60
|
-
/**
|
|
61
|
-
* Read the primary model from the OpenClaw gateway config.
|
|
62
|
-
* Handles both string and object shapes for `agents.defaults.model`.
|
|
63
|
-
*/
|
|
64
|
-
function readPrimaryModel(api) {
|
|
65
|
-
try {
|
|
66
|
-
const modelCfg = api.config?.agents?.defaults?.model;
|
|
67
|
-
if (!modelCfg)
|
|
68
|
-
return null;
|
|
69
|
-
if (typeof modelCfg === "string")
|
|
70
|
-
return modelCfg;
|
|
71
|
-
if (typeof modelCfg === "object" && modelCfg.primary)
|
|
72
|
-
return String(modelCfg.primary);
|
|
73
|
-
return null;
|
|
74
|
-
}
|
|
75
|
-
catch {
|
|
76
|
-
return null;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
/**
|
|
80
|
-
* Check if the primary model changed and emit a config_change event if so.
|
|
81
|
-
*/
|
|
82
|
-
function checkModelConfigChange(api) {
|
|
83
|
-
const currentModel = readPrimaryModel(api);
|
|
84
|
-
if (currentModel === knownPrimaryModel)
|
|
85
|
-
return;
|
|
86
|
-
const previousValue = knownPrimaryModel;
|
|
87
|
-
knownPrimaryModel = currentModel;
|
|
88
|
-
// Don't emit on first read if null
|
|
89
|
-
if (currentModel === null && previousValue === null)
|
|
90
|
-
return;
|
|
91
|
-
transmitter_js_1.transmitter.enqueue({
|
|
92
|
-
type: "config_change",
|
|
93
|
-
ts: Date.now(),
|
|
94
|
-
field: "model.primary",
|
|
95
|
-
value: currentModel,
|
|
96
|
-
previousValue,
|
|
97
|
-
// Pass as params so they appear in toolArgs on the server
|
|
98
|
-
params: {
|
|
99
|
-
field: "model.primary",
|
|
100
|
-
value: currentModel,
|
|
101
|
-
previousValue,
|
|
102
|
-
},
|
|
103
|
-
});
|
|
104
|
-
}
|
|
59
|
+
// Config change detection is now handled by config-monitor.ts
|
|
105
60
|
/**
|
|
106
61
|
* Register lifecycle hook handlers.
|
|
107
62
|
*/
|
|
@@ -115,9 +70,9 @@ function registerLifecycleHandlers(api, config) {
|
|
|
115
70
|
const basePulseIntervalMs = config.pulseIntervalMs ?? 300_000;
|
|
116
71
|
// Reset pulse backoff state
|
|
117
72
|
pulseFailureCount = 0;
|
|
118
|
-
//
|
|
119
|
-
|
|
120
|
-
|
|
73
|
+
// Initialize config monitor snapshot (baseline — no events emitted)
|
|
74
|
+
(0, config_monitor_js_1.resetSnapshot)();
|
|
75
|
+
(0, config_monitor_js_1.initSnapshot)(api.config ?? {});
|
|
121
76
|
// Send initial pulse right now
|
|
122
77
|
void sendPulseWithBackoff(endpoint, apiKey, basePulseIntervalMs, api);
|
|
123
78
|
// Initial skill/plugin scan — delayed 30s to let gateway fully settle
|
|
@@ -135,6 +90,10 @@ function registerLifecycleHandlers(api, config) {
|
|
|
135
90
|
// gateway_start — best-effort re-scan (in case it ever fires)
|
|
136
91
|
// -----------------------------------------------------------------------
|
|
137
92
|
api.on("gateway_start", async (event) => {
|
|
93
|
+
// Check config changes on gateway restart (config may have changed)
|
|
94
|
+
if (api.config) {
|
|
95
|
+
(0, config_monitor_js_1.checkConfigChanges)(api.config);
|
|
96
|
+
}
|
|
138
97
|
// Re-run scan as best-effort; pulse is already running
|
|
139
98
|
void runScan(api.config?.agents?.defaults?.workspace);
|
|
140
99
|
}, { name: "podwatch-gateway-start" });
|
|
@@ -206,8 +165,8 @@ function registerLifecycleHandlers(api, config) {
|
|
|
206
165
|
*/
|
|
207
166
|
async function sendPulseWithBackoff(endpoint, apiKey, baseIntervalMs, api) {
|
|
208
167
|
// Check for config changes on each pulse
|
|
209
|
-
if (api) {
|
|
210
|
-
|
|
168
|
+
if (api?.config) {
|
|
169
|
+
(0, config_monitor_js_1.checkConfigChanges)(api.config);
|
|
211
170
|
}
|
|
212
171
|
let success = false;
|
|
213
172
|
try {
|