@dg-kit/safety 1.14.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 (38) hide show
  1. package/README.md +16 -0
  2. package/dist/contracts.d.ts +44 -0
  3. package/dist/contracts.d.ts.map +1 -0
  4. package/dist/contracts.js +9 -0
  5. package/dist/contracts.js.map +1 -0
  6. package/dist/default-policies.d.ts +36 -0
  7. package/dist/default-policies.d.ts.map +1 -0
  8. package/dist/default-policies.js +333 -0
  9. package/dist/default-policies.js.map +1 -0
  10. package/dist/device-command-queue.d.ts +60 -0
  11. package/dist/device-command-queue.d.ts.map +1 -0
  12. package/dist/device-command-queue.js +105 -0
  13. package/dist/device-command-queue.js.map +1 -0
  14. package/dist/index.d.ts +9 -0
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/index.js +17 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/lifecycle-guard.d.ts +30 -0
  19. package/dist/lifecycle-guard.d.ts.map +1 -0
  20. package/dist/lifecycle-guard.js +59 -0
  21. package/dist/lifecycle-guard.js.map +1 -0
  22. package/dist/policy-engine.d.ts +45 -0
  23. package/dist/policy-engine.d.ts.map +1 -0
  24. package/dist/policy-engine.js +31 -0
  25. package/dist/policy-engine.js.map +1 -0
  26. package/dist/safety-acceptance.d.ts +27 -0
  27. package/dist/safety-acceptance.d.ts.map +1 -0
  28. package/dist/safety-acceptance.js +64 -0
  29. package/dist/safety-acceptance.js.map +1 -0
  30. package/dist/safety-bus.d.ts +156 -0
  31. package/dist/safety-bus.d.ts.map +1 -0
  32. package/dist/safety-bus.js +169 -0
  33. package/dist/safety-bus.js.map +1 -0
  34. package/dist/safety-notice-content.d.ts +37 -0
  35. package/dist/safety-notice-content.d.ts.map +1 -0
  36. package/dist/safety-notice-content.js +72 -0
  37. package/dist/safety-notice-content.js.map +1 -0
  38. package/package.json +38 -0
package/README.md ADDED
@@ -0,0 +1,16 @@
1
+ # @dg-kit/safety
2
+
3
+ The shared device safety chain for DG-Lab integrations. It provides strength and burst policy
4
+ enforcement, cold-start clamps, serialized command execution, emergency-stop preemption, device
5
+ leases, lifecycle guards, and the cross-module safety bus used by 0xNuller.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install @dg-kit/safety
11
+ ```
12
+
13
+ Safety policy is enforced before commands reach a transport. Applications should still expose a
14
+ clear stop control and should not treat account identity as permission to control a device.
15
+
16
+ The package is ESM-only and exports its public API from the package root.
@@ -0,0 +1,44 @@
1
+ import type { DeviceCommand } from '@dg-kit/core';
2
+ /** Command origin. Used for policy/permission decisions and for routing replies back to the original channel. */
3
+ export type SourceType = 'web' | 'qq' | 'telegram' | 'cli' | 'api' | 'system' | 'sensor';
4
+ export interface ActionContext {
5
+ sessionId: string;
6
+ sourceType: SourceType;
7
+ sourceUserId?: string;
8
+ sourceUserName?: string;
9
+ sourceChannelId?: string;
10
+ traceId: string;
11
+ }
12
+ export type PermissionDecision = {
13
+ type: 'approve-once';
14
+ } | {
15
+ type: 'approve-scoped';
16
+ expiresAt?: number;
17
+ } | {
18
+ type: 'deny';
19
+ reason?: string;
20
+ };
21
+ export type PolicyDecision = {
22
+ type: 'allow';
23
+ } | {
24
+ type: 'deny';
25
+ reason: string;
26
+ } | {
27
+ type: 'clamp';
28
+ command: DeviceCommand;
29
+ reason: string;
30
+ } | {
31
+ type: 'require-confirm';
32
+ reason: string;
33
+ };
34
+ export interface PermissionRequest {
35
+ context: ActionContext;
36
+ toolName: string;
37
+ toolDisplayName?: string;
38
+ summary: string;
39
+ args: Record<string, unknown>;
40
+ }
41
+ export interface PermissionService {
42
+ request(input: PermissionRequest): Promise<PermissionDecision>;
43
+ }
44
+ //# sourceMappingURL=contracts.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"contracts.d.ts","sourceRoot":"","sources":["../src/contracts.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAElD,iHAAiH;AACjH,MAAM,MAAM,UAAU,GAAG,KAAK,GAAG,IAAI,GAAG,UAAU,GAAG,KAAK,GAAG,KAAK,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAEzF,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,UAAU,CAAC;IACvB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,MAAM,kBAAkB,GAC1B;IAAE,IAAI,EAAE,cAAc,CAAA;CAAE,GACxB;IAAE,IAAI,EAAE,gBAAgB,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,GAC9C;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAEtC,MAAM,MAAM,cAAc,GACtB;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE,GACjB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAChC;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,aAAa,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACzD;IAAE,IAAI,EAAE,iBAAiB,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAEhD,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,aAAa,CAAC;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC/B;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,CAAC,KAAK,EAAE,iBAAiB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAAC;CAChE"}
@@ -0,0 +1,9 @@
1
+ // Safety-chain contracts.
2
+ //
3
+ // Before the merge these types existed twice: in @dg-agent/core and in
4
+ // apps/voice/src/lib/types.ts (whose file header described itself as a
5
+ // "mirror slice" of the former). They describe how a caller drives the
6
+ // safety chain, not the BLE protocol itself — hence this package rather
7
+ // than @dg-kit/core.
8
+ export {};
9
+ //# sourceMappingURL=contracts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"contracts.js","sourceRoot":"","sources":["../src/contracts.ts"],"names":[],"mappings":"AAAA,0BAA0B;AAC1B,EAAE;AACF,uEAAuE;AACvE,uEAAuE;AACvE,uEAAuE;AACvE,wEAAwE;AACxE,qBAAqB"}
@@ -0,0 +1,36 @@
1
+ import type { OpossumPolicyRule } from './policy-engine.js';
2
+ import type { PolicyRule } from './policy-engine.js';
3
+ export declare const DEFAULT_MAX_COLD_START_STRENGTH = 10;
4
+ export declare const DEFAULT_MAX_ADJUST_STEP = 10;
5
+ export declare const DEFAULT_MAX_BURST_DURATION_MS = 5000;
6
+ export declare const DEFAULT_MAX_OPOSSUM_COLD_START_INTENSITY = 10;
7
+ export declare const DEFAULT_MAX_OPOSSUM_ADJUST_STEP = 10;
8
+ export declare const DEFAULT_USER_MAX_OPOSSUM_INTENSITY = 50;
9
+ export interface DefaultPolicyOptions {
10
+ maxStrengthA?: number;
11
+ maxStrengthB?: number;
12
+ maxColdStartStrength?: number;
13
+ maxAdjustStep?: number;
14
+ maxBurstDurationMs?: number;
15
+ maxBurstStrengthAbsolute?: number;
16
+ maxBurstStrengthRelative?: number;
17
+ }
18
+ export declare function createDefaultPolicyRules(options?: DefaultPolicyOptions): PolicyRule[];
19
+ export interface DefaultOpossumPolicyOptions {
20
+ maxColdStartIntensity?: number;
21
+ maxAdjustStep?: number;
22
+ /** User absolute intensity cap per channel — mirrors Coyote's maxStrengthA/B. */
23
+ maxIntensityA?: number;
24
+ maxIntensityB?: number;
25
+ }
26
+ /**
27
+ * Mirrors `createDefaultPolicyRules` in spirit: require-connection first,
28
+ * then a cold-start intensity clamp, then a step-adjust clamp, then a
29
+ * permission gate. Deliberately narrower than Coyote's rule set — no
30
+ * per-channel hardware limit (Opossum has no `limitA`/`limitB` concept).
31
+ * `vibrateBurst` shares the same per-channel intensity ceiling as
32
+ * `vibrateStart` rather than growing Coyote-style burst-specific
33
+ * absolute/relative caps.
34
+ */
35
+ export declare function createDefaultOpossumPolicyRules(options?: DefaultOpossumPolicyOptions): OpossumPolicyRule[];
36
+ //# sourceMappingURL=default-policies.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"default-policies.d.ts","sourceRoot":"","sources":["../src/default-policies.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAC5D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAErD,eAAO,MAAM,+BAA+B,KAAK,CAAC;AAClD,eAAO,MAAM,uBAAuB,KAAK,CAAC;AAC1C,eAAO,MAAM,6BAA6B,OAAQ,CAAC;AAQnD,eAAO,MAAM,wCAAwC,KAAK,CAAC;AAC3D,eAAO,MAAM,+BAA+B,KAAK,CAAC;AAElD,eAAO,MAAM,kCAAkC,KAAK,CAAC;AAErD,MAAM,WAAW,oBAAoB;IACnC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAG5B,wBAAwB,CAAC,EAAE,MAAM,CAAC;IAClC,wBAAwB,CAAC,EAAE,MAAM,CAAC;CACnC;AAMD,wBAAgB,wBAAwB,CAAC,OAAO,GAAE,oBAAyB,GAAG,UAAU,EAAE,CA6JzF;AA8CD,MAAM,WAAW,2BAA2B;IAC1C,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,iFAAiF;IACjF,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAMD;;;;;;;;GAQG;AACH,wBAAgB,+BAA+B,CAC7C,OAAO,GAAE,2BAAgC,GACxC,iBAAiB,EAAE,CAwGrB"}
@@ -0,0 +1,333 @@
1
+ export const DEFAULT_MAX_COLD_START_STRENGTH = 10;
2
+ export const DEFAULT_MAX_ADJUST_STEP = 10;
3
+ export const DEFAULT_MAX_BURST_DURATION_MS = 5_000;
4
+ const MAX_ADJUST_STEP_LIMIT = 200;
5
+ const MAX_BURST_DURATION_LIMIT_MS = 20_000;
6
+ const DEFAULT_USER_MAX_STRENGTH = 50;
7
+ // Opossum intensity is also a 0-200 range (see OpossumCommand doc comment in
8
+ // @dg-kit/core), so its safety caps default to the same magnitude as
9
+ // Coyote's cold-start / step-adjust caps rather than inventing new numbers.
10
+ export const DEFAULT_MAX_OPOSSUM_COLD_START_INTENSITY = 10;
11
+ export const DEFAULT_MAX_OPOSSUM_ADJUST_STEP = 10;
12
+ // Absolute per-channel ceiling, mirroring Coyote's DEFAULT_USER_MAX_STRENGTH.
13
+ export const DEFAULT_USER_MAX_OPOSSUM_INTENSITY = 50;
14
+ function requiresConfirmation(command) {
15
+ return command.type !== 'stop' && command.type !== 'emergencyStop';
16
+ }
17
+ export function createDefaultPolicyRules(options = {}) {
18
+ const maxStrengthA = normalizeStrengthLimit(options.maxStrengthA);
19
+ const maxStrengthB = normalizeStrengthLimit(options.maxStrengthB);
20
+ const maxColdStartStrength = normalizeColdStartStrengthLimit(options.maxColdStartStrength);
21
+ const maxAdjustStep = normalizeAdjustStepLimit(options.maxAdjustStep);
22
+ const maxBurstDurationMs = normalizeBurstDurationLimit(options.maxBurstDurationMs);
23
+ const maxBurstStrengthAbsolute = normalizeOptionalCap(options.maxBurstStrengthAbsolute);
24
+ const maxBurstStrengthRelative = normalizeOptionalCap(options.maxBurstStrengthRelative);
25
+ return [
26
+ {
27
+ name: 'require-device-connection',
28
+ evaluate({ deviceState }) {
29
+ if (!deviceState.connected) {
30
+ return { type: 'deny', reason: '设备未连接' };
31
+ }
32
+ return null;
33
+ },
34
+ },
35
+ {
36
+ name: 'soft-start',
37
+ evaluate({ command, deviceState }) {
38
+ if (command.type !== 'start')
39
+ return null;
40
+ const current = command.channel === 'A' ? deviceState.strengthA : deviceState.strengthB;
41
+ if (current > 0)
42
+ return null;
43
+ if (command.strength <= maxColdStartStrength)
44
+ return null;
45
+ return {
46
+ type: 'clamp',
47
+ command: { ...command, strength: maxColdStartStrength },
48
+ reason: `冷启动强度上限为 ${maxColdStartStrength}`,
49
+ };
50
+ },
51
+ },
52
+ {
53
+ name: 'user-strength-cap',
54
+ evaluate({ command, deviceState }) {
55
+ switch (command.type) {
56
+ case 'start': {
57
+ const effectiveLimit = getEffectiveLimit(command.channel, deviceState, {
58
+ A: maxStrengthA,
59
+ B: maxStrengthB,
60
+ });
61
+ if (command.strength <= effectiveLimit)
62
+ return null;
63
+ return {
64
+ type: 'clamp',
65
+ command: { ...command, strength: effectiveLimit },
66
+ reason: `${command.channel} 通道强度上限为 ${effectiveLimit}`,
67
+ };
68
+ }
69
+ case 'burst': {
70
+ const effectiveLimit = getEffectiveLimit(command.channel, deviceState, {
71
+ A: maxStrengthA,
72
+ B: maxStrengthB,
73
+ });
74
+ if (command.strength <= effectiveLimit)
75
+ return null;
76
+ return {
77
+ type: 'clamp',
78
+ command: { ...command, strength: effectiveLimit },
79
+ reason: `${command.channel} 通道 burst 强度上限为 ${effectiveLimit}`,
80
+ };
81
+ }
82
+ case 'adjustStrength': {
83
+ const current = command.channel === 'A' ? deviceState.strengthA : deviceState.strengthB;
84
+ const effectiveLimit = getEffectiveLimit(command.channel, deviceState, {
85
+ A: maxStrengthA,
86
+ B: maxStrengthB,
87
+ });
88
+ const target = clamp(current + command.delta, 0, effectiveLimit);
89
+ const clampedDelta = target - current;
90
+ if (clampedDelta === command.delta)
91
+ return null;
92
+ return {
93
+ type: 'clamp',
94
+ command: { ...command, delta: clampedDelta },
95
+ reason: `调整后的强度需遵守 ${command.channel} 通道上限 ${effectiveLimit}`,
96
+ };
97
+ }
98
+ default:
99
+ return null;
100
+ }
101
+ },
102
+ },
103
+ {
104
+ name: 'burst-strength-cap',
105
+ evaluate({ command, deviceState }) {
106
+ if (command.type !== 'burst')
107
+ return null;
108
+ if (maxBurstStrengthAbsolute === 0 && maxBurstStrengthRelative === 0)
109
+ return null;
110
+ const current = command.channel === 'A' ? deviceState.strengthA : deviceState.strengthB;
111
+ let cap = Number.POSITIVE_INFINITY;
112
+ const reasons = [];
113
+ if (maxBurstStrengthAbsolute > 0 && command.strength > maxBurstStrengthAbsolute) {
114
+ cap = Math.min(cap, maxBurstStrengthAbsolute);
115
+ reasons.push(`突增绝对强度上限 ${maxBurstStrengthAbsolute}`);
116
+ }
117
+ if (maxBurstStrengthRelative > 0) {
118
+ const relativeCap = current + maxBurstStrengthRelative;
119
+ if (command.strength > relativeCap) {
120
+ cap = Math.min(cap, relativeCap);
121
+ reasons.push(`突增相对强度上限 +${maxBurstStrengthRelative}(当前 ${current} → 不超过 ${relativeCap})`);
122
+ }
123
+ }
124
+ if (!Number.isFinite(cap))
125
+ return null;
126
+ return {
127
+ type: 'clamp',
128
+ command: { ...command, strength: cap },
129
+ reason: reasons.join(';'),
130
+ };
131
+ },
132
+ },
133
+ {
134
+ name: 'step-adjust',
135
+ evaluate({ command }) {
136
+ if (command.type !== 'adjustStrength')
137
+ return null;
138
+ if (Math.abs(command.delta) <= maxAdjustStep)
139
+ return null;
140
+ return {
141
+ type: 'clamp',
142
+ command: {
143
+ ...command,
144
+ delta: Math.sign(command.delta || 1) * maxAdjustStep,
145
+ },
146
+ reason: `单次调节幅度上限为 ±${maxAdjustStep}`,
147
+ };
148
+ },
149
+ },
150
+ {
151
+ name: 'burst-duration',
152
+ evaluate({ command }) {
153
+ if (command.type !== 'burst')
154
+ return null;
155
+ if (command.durationMs <= maxBurstDurationMs)
156
+ return null;
157
+ return {
158
+ type: 'clamp',
159
+ command: {
160
+ ...command,
161
+ durationMs: maxBurstDurationMs,
162
+ },
163
+ reason: `Burst 时长上限为 ${maxBurstDurationMs}ms`,
164
+ };
165
+ },
166
+ },
167
+ {
168
+ name: 'permission-gate',
169
+ evaluate({ command }) {
170
+ if (!requiresConfirmation(command))
171
+ return null;
172
+ return {
173
+ type: 'require-confirm',
174
+ reason: '该操作会修改设备状态,需要先获取权限',
175
+ };
176
+ },
177
+ },
178
+ ];
179
+ }
180
+ function normalizeStrengthLimit(value) {
181
+ const raw = typeof value === 'number' ? value : DEFAULT_USER_MAX_STRENGTH;
182
+ return clamp(raw, 0, 200);
183
+ }
184
+ function normalizeColdStartStrengthLimit(value) {
185
+ const raw = typeof value === 'number' ? value : DEFAULT_MAX_COLD_START_STRENGTH;
186
+ return clamp(raw, 0, 200);
187
+ }
188
+ function normalizeAdjustStepLimit(value) {
189
+ const raw = typeof value === 'number' ? value : DEFAULT_MAX_ADJUST_STEP;
190
+ return clamp(raw, 1, MAX_ADJUST_STEP_LIMIT);
191
+ }
192
+ function normalizeBurstDurationLimit(value) {
193
+ const raw = typeof value === 'number' ? value : DEFAULT_MAX_BURST_DURATION_MS;
194
+ return clamp(raw, 100, MAX_BURST_DURATION_LIMIT_MS);
195
+ }
196
+ // 0 means "disabled" for the burst-specific caps. Negative input is
197
+ // coerced to 0 so a misconfigured slider can't widen the cap into the
198
+ // negative-strength territory.
199
+ function normalizeOptionalCap(value) {
200
+ if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0)
201
+ return 0;
202
+ return Math.min(Math.round(value), 200);
203
+ }
204
+ function getEffectiveLimit(channel, deviceState, userLimits) {
205
+ const hardware = channel === 'A' ? deviceState.limitA : deviceState.limitB;
206
+ const user = userLimits[channel];
207
+ return Math.min(hardware, user);
208
+ }
209
+ function clamp(value, min, max) {
210
+ const number = Number(value);
211
+ if (!Number.isFinite(number))
212
+ return min;
213
+ return Math.max(min, Math.min(max, Math.round(number)));
214
+ }
215
+ function opossumRequiresConfirmation(command) {
216
+ return command.type !== 'vibrateStop';
217
+ }
218
+ /**
219
+ * Mirrors `createDefaultPolicyRules` in spirit: require-connection first,
220
+ * then a cold-start intensity clamp, then a step-adjust clamp, then a
221
+ * permission gate. Deliberately narrower than Coyote's rule set — no
222
+ * per-channel hardware limit (Opossum has no `limitA`/`limitB` concept).
223
+ * `vibrateBurst` shares the same per-channel intensity ceiling as
224
+ * `vibrateStart` rather than growing Coyote-style burst-specific
225
+ * absolute/relative caps.
226
+ */
227
+ export function createDefaultOpossumPolicyRules(options = {}) {
228
+ const maxColdStartIntensity = normalizeOpossumIntensityLimit(options.maxColdStartIntensity, DEFAULT_MAX_OPOSSUM_COLD_START_INTENSITY);
229
+ const maxAdjustStep = normalizeOpossumAdjustStepLimit(options.maxAdjustStep);
230
+ const maxIntensityA = normalizeOpossumIntensityLimit(options.maxIntensityA, DEFAULT_USER_MAX_OPOSSUM_INTENSITY);
231
+ const maxIntensityB = normalizeOpossumIntensityLimit(options.maxIntensityB, DEFAULT_USER_MAX_OPOSSUM_INTENSITY);
232
+ return [
233
+ {
234
+ name: 'require-opossum-connection',
235
+ evaluate({ deviceState }) {
236
+ if (!deviceState.connected) {
237
+ return { type: 'deny', reason: '设备未连接' };
238
+ }
239
+ return null;
240
+ },
241
+ },
242
+ {
243
+ name: 'opossum-cold-start',
244
+ evaluate({ command, deviceState }) {
245
+ if (command.type !== 'vibrateStart')
246
+ return null;
247
+ const current = command.channel === 'A' ? deviceState.intensityA : deviceState.intensityB;
248
+ if (current > 0)
249
+ return null;
250
+ if (command.intensity <= maxColdStartIntensity)
251
+ return null;
252
+ return {
253
+ type: 'clamp',
254
+ command: { ...command, intensity: maxColdStartIntensity },
255
+ reason: `负鼠冷启动强度上限为 ${maxColdStartIntensity}`,
256
+ };
257
+ },
258
+ },
259
+ {
260
+ // Mirrors Coyote's 'user-strength-cap': an absolute per-channel
261
+ // ceiling the AI can never exceed regardless of how many small
262
+ // vibrate_adjust steps it stacks up. Without this, cold-start +
263
+ // step-adjust alone bound each individual call but not the
264
+ // accumulated total — the AI could still walk intensity up to the
265
+ // protocol max (200) one step at a time.
266
+ name: 'opossum-user-intensity-cap',
267
+ evaluate({ command, deviceState }) {
268
+ if (command.type === 'vibrateSetPattern')
269
+ return null;
270
+ const effectiveLimit = command.channel === 'A' ? maxIntensityA : maxIntensityB;
271
+ if (command.type === 'vibrateStart' || command.type === 'vibrateBurst') {
272
+ if (command.intensity <= effectiveLimit)
273
+ return null;
274
+ return {
275
+ type: 'clamp',
276
+ command: { ...command, intensity: effectiveLimit },
277
+ reason: `负鼠 ${command.channel} 通道强度上限为 ${effectiveLimit}`,
278
+ };
279
+ }
280
+ if (command.type === 'vibrateAdjust') {
281
+ const current = command.channel === 'A' ? deviceState.intensityA : deviceState.intensityB;
282
+ const target = clamp(current + command.delta, 0, effectiveLimit);
283
+ const clampedDelta = target - current;
284
+ if (clampedDelta === command.delta)
285
+ return null;
286
+ return {
287
+ type: 'clamp',
288
+ command: { ...command, delta: clampedDelta },
289
+ reason: `调整后的强度需遵守负鼠 ${command.channel} 通道上限 ${effectiveLimit}`,
290
+ };
291
+ }
292
+ return null;
293
+ },
294
+ },
295
+ {
296
+ name: 'opossum-step-adjust',
297
+ evaluate({ command }) {
298
+ if (command.type !== 'vibrateAdjust')
299
+ return null;
300
+ if (Math.abs(command.delta) <= maxAdjustStep)
301
+ return null;
302
+ return {
303
+ type: 'clamp',
304
+ command: {
305
+ ...command,
306
+ delta: Math.sign(command.delta || 1) * maxAdjustStep,
307
+ },
308
+ reason: `负鼠单次调节幅度上限为 ±${maxAdjustStep}`,
309
+ };
310
+ },
311
+ },
312
+ {
313
+ name: 'opossum-permission-gate',
314
+ evaluate({ command }) {
315
+ if (!opossumRequiresConfirmation(command))
316
+ return null;
317
+ return {
318
+ type: 'require-confirm',
319
+ reason: '该操作会修改设备状态,需要先获取权限',
320
+ };
321
+ },
322
+ },
323
+ ];
324
+ }
325
+ function normalizeOpossumIntensityLimit(value, fallback) {
326
+ const raw = typeof value === 'number' ? value : fallback;
327
+ return clamp(raw, 0, 200);
328
+ }
329
+ function normalizeOpossumAdjustStepLimit(value) {
330
+ const raw = typeof value === 'number' ? value : DEFAULT_MAX_OPOSSUM_ADJUST_STEP;
331
+ return clamp(raw, 1, MAX_ADJUST_STEP_LIMIT);
332
+ }
333
+ //# sourceMappingURL=default-policies.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"default-policies.js","sourceRoot":"","sources":["../src/default-policies.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,60 @@
1
+ import type { DeviceClient, OpossumCommand } from '@dg-kit/core';
2
+ import type { DeviceCommand, DeviceCommandResult } from '@dg-kit/core';
3
+ import type { OpossumClient, OpossumCommandResult } from '@dg-kit/protocol';
4
+ export interface PriorityInterrupt<TCommand, TResult> {
5
+ /** Which command(s) should bypass the queue and run immediately. */
6
+ matches(command: TCommand): boolean;
7
+ /** How to actually run a matched command (typically not via `execute()`). */
8
+ run(command: TCommand): Promise<TResult>;
9
+ /** Result for a task that was already queued when a later interrupt fired. */
10
+ skippedResult(): Promise<TResult>;
11
+ }
12
+ export interface SerialCommandQueueOptions<TCommand, TResult> {
13
+ execute(command: TCommand): Promise<TResult>;
14
+ /** Omit for a plain FIFO queue with no priority-interrupt concept. */
15
+ priorityInterrupt?: PriorityInterrupt<TCommand, TResult>;
16
+ }
17
+ /**
18
+ * Runs commands against a device one at a time, so concurrent tool calls
19
+ * can't race each other's writes — a rejected command doesn't jam the
20
+ * queue, the next enqueued command still runs.
21
+ *
22
+ * An optional `priorityInterrupt` lets one command "type" (Coyote's
23
+ * `emergencyStop`) skip the line entirely: it bumps a generation counter
24
+ * and runs immediately via `run()` rather than `execute()`, and any
25
+ * already-queued-but-not-yet-run task notices its generation is stale and
26
+ * resolves with `skippedResult()` instead of actually executing. Extracted
27
+ * from `DeviceCommandQueue` (which needs the interrupt) and reused as a
28
+ * plain FIFO by `OpossumCommandQueue` (which doesn't — see its own doc
29
+ * comment for why).
30
+ */
31
+ export declare class SerialCommandQueue<TCommand, TResult> {
32
+ private readonly options;
33
+ private tail;
34
+ private generation;
35
+ constructor(options: SerialCommandQueueOptions<TCommand, TResult>);
36
+ enqueue(command: TCommand): Promise<TResult>;
37
+ }
38
+ export declare class DeviceCommandQueue {
39
+ private readonly device;
40
+ private readonly queue;
41
+ constructor(device: DeviceClient);
42
+ enqueue(command: DeviceCommand): Promise<DeviceCommandResult>;
43
+ }
44
+ /**
45
+ * Serializes Opossum vibration commands the same way `DeviceCommandQueue`
46
+ * serializes Coyote commands, so concurrent `vibrate_*` tool calls can't
47
+ * race each other's writes. No `emergencyStop`-style generation bump is
48
+ * needed here: Opossum has no command analogous to Coyote's `emergencyStop`
49
+ * variant in its own command union (`vibrate_stop` already exists for that),
50
+ * and the runtime-wide panic button calls `OpossumClient.emergencyStop()`
51
+ * directly (see `AgentRuntime.emergencyStop`), bypassing the queue exactly
52
+ * like Coyote's does.
53
+ */
54
+ export declare class OpossumCommandQueue {
55
+ private readonly device;
56
+ private readonly queue;
57
+ constructor(device: OpossumClient);
58
+ enqueue(command: OpossumCommand): Promise<OpossumCommandResult>;
59
+ }
60
+ //# sourceMappingURL=device-command-queue.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"device-command-queue.d.ts","sourceRoot":"","sources":["../src/device-command-queue.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACjE,OAAO,KAAK,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AACvE,OAAO,KAAK,EAAE,aAAa,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAE5E,MAAM,WAAW,iBAAiB,CAAC,QAAQ,EAAE,OAAO;IAClD,oEAAoE;IACpE,OAAO,CAAC,OAAO,EAAE,QAAQ,GAAG,OAAO,CAAC;IACpC,6EAA6E;IAC7E,GAAG,CAAC,OAAO,EAAE,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACzC,8EAA8E;IAC9E,aAAa,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;CACnC;AAED,MAAM,WAAW,yBAAyB,CAAC,QAAQ,EAAE,OAAO;IAC1D,OAAO,CAAC,OAAO,EAAE,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC7C,sEAAsE;IACtE,iBAAiB,CAAC,EAAE,iBAAiB,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;CAC1D;AAED;;;;;;;;;;;;;GAaG;AACH,qBAAa,kBAAkB,CAAC,QAAQ,EAAE,OAAO;IAInC,OAAO,CAAC,QAAQ,CAAC,OAAO;IAHpC,OAAO,CAAC,IAAI,CAAoC;IAChD,OAAO,CAAC,UAAU,CAAK;gBAEM,OAAO,EAAE,yBAAyB,CAAC,QAAQ,EAAE,OAAO,CAAC;IAE5E,OAAO,CAAC,OAAO,EAAE,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC;CA0CnD;AAED,qBAAa,kBAAkB;IAGjB,OAAO,CAAC,QAAQ,CAAC,MAAM;IAFnC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAyD;gBAElD,MAAM,EAAE,YAAY;IAoB3C,OAAO,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,mBAAmB,CAAC;CAGpE;AAED;;;;;;;;;GASG;AACH,qBAAa,mBAAmB;IAGlB,OAAO,CAAC,QAAQ,CAAC,MAAM;IAFnC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA2D;gBAEpD,MAAM,EAAE,aAAa;IAM5C,OAAO,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,oBAAoB,CAAC;CAGtE"}
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Runs commands against a device one at a time, so concurrent tool calls
3
+ * can't race each other's writes — a rejected command doesn't jam the
4
+ * queue, the next enqueued command still runs.
5
+ *
6
+ * An optional `priorityInterrupt` lets one command "type" (Coyote's
7
+ * `emergencyStop`) skip the line entirely: it bumps a generation counter
8
+ * and runs immediately via `run()` rather than `execute()`, and any
9
+ * already-queued-but-not-yet-run task notices its generation is stale and
10
+ * resolves with `skippedResult()` instead of actually executing. Extracted
11
+ * from `DeviceCommandQueue` (which needs the interrupt) and reused as a
12
+ * plain FIFO by `OpossumCommandQueue` (which doesn't — see its own doc
13
+ * comment for why).
14
+ */
15
+ export class SerialCommandQueue {
16
+ options;
17
+ tail = Promise.resolve();
18
+ generation = 0;
19
+ constructor(options) {
20
+ this.options = options;
21
+ }
22
+ async enqueue(command) {
23
+ const interrupt = this.options.priorityInterrupt;
24
+ if (interrupt?.matches(command)) {
25
+ this.generation += 1;
26
+ return interrupt.run(command);
27
+ }
28
+ const generation = this.generation;
29
+ const task = this.tail.then(async () => {
30
+ if (interrupt && generation !== this.generation) {
31
+ return interrupt.skippedResult();
32
+ }
33
+ const startedAt = this.generation;
34
+ const result = await this.options.execute(command);
35
+ // An emergency stop happened *while the command was executing*: it was
36
+ // already in flight, so the generation check can't catch it — the check
37
+ // runs before execute, and the stop runs concurrently. Its write lands
38
+ // after the stop, so the device halts and then jumps back to the
39
+ // previous strength. Observed exactly in that order (stop → in-flight
40
+ // +10 lands → strength 10).
41
+ //
42
+ // A packet already sent cannot be recalled; what we can do is stop
43
+ // again right after. Emergency stop must be idempotent, so a duplicate
44
+ // stop is safe — a missed one is not.
45
+ if (interrupt && startedAt !== this.generation) {
46
+ await interrupt.run(command);
47
+ return interrupt.skippedResult();
48
+ }
49
+ return result;
50
+ });
51
+ this.tail = task.then(() => undefined, () => undefined);
52
+ return task;
53
+ }
54
+ }
55
+ export class DeviceCommandQueue {
56
+ device;
57
+ queue;
58
+ constructor(device) {
59
+ this.device = device;
60
+ this.queue = new SerialCommandQueue({
61
+ execute: (command) => this.device.execute(command),
62
+ priorityInterrupt: {
63
+ matches: (command) => command.type === 'emergencyStop',
64
+ run: async () => {
65
+ await this.device.emergencyStop();
66
+ return {
67
+ state: await this.device.getState(),
68
+ notes: ['queue-drained-by-emergency-stop'],
69
+ };
70
+ },
71
+ skippedResult: async () => ({
72
+ state: await this.device.getState(),
73
+ notes: ['skipped-after-priority-interrupt'],
74
+ }),
75
+ },
76
+ });
77
+ }
78
+ async enqueue(command) {
79
+ return this.queue.enqueue(command);
80
+ }
81
+ }
82
+ /**
83
+ * Serializes Opossum vibration commands the same way `DeviceCommandQueue`
84
+ * serializes Coyote commands, so concurrent `vibrate_*` tool calls can't
85
+ * race each other's writes. No `emergencyStop`-style generation bump is
86
+ * needed here: Opossum has no command analogous to Coyote's `emergencyStop`
87
+ * variant in its own command union (`vibrate_stop` already exists for that),
88
+ * and the runtime-wide panic button calls `OpossumClient.emergencyStop()`
89
+ * directly (see `AgentRuntime.emergencyStop`), bypassing the queue exactly
90
+ * like Coyote's does.
91
+ */
92
+ export class OpossumCommandQueue {
93
+ device;
94
+ queue;
95
+ constructor(device) {
96
+ this.device = device;
97
+ this.queue = new SerialCommandQueue({
98
+ execute: (command) => this.device.execute(command),
99
+ });
100
+ }
101
+ async enqueue(command) {
102
+ return this.queue.enqueue(command);
103
+ }
104
+ }
105
+ //# sourceMappingURL=device-command-queue.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"device-command-queue.js","sourceRoot":"","sources":["../src/device-command-queue.ts"],"names":[],"mappings":"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"}