@xpufx/paseo-top 0.4.4 → 0.4.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.
- package/OVERVIEW.md +18 -0
- package/client/host-ui.tsx +184 -0
- package/client/multi-host-view.tsx +57 -43
- package/client/multi-host.ts +36 -13
- package/client/pill-labels.ts +94 -1
- package/client/pill.tsx +692 -616
- package/client/resources-query.ts +1 -1
- package/client/settings-events.ts +1 -1
- package/client/settings-ui.tsx +86 -9
- package/client/surface.tsx +308 -276
- package/client/telemetry-copy.ts +8 -2
- package/client/telemetry.tsx +372 -321
- package/client/turn-panel.tsx +126 -104
- package/client/vendor/paseo-plugin-helper/{host.ts → core/host.ts} +9 -3
- package/client/vendor/paseo-plugin-helper/core/index.ts +76 -0
- package/client/vendor/paseo-plugin-helper/core/plugin-load-count.ts +89 -0
- package/client/vendor/paseo-plugin-helper/{query-refresh.ts → core/query-refresh.ts} +2 -2
- package/client/vendor/paseo-plugin-helper/{query.ts → core/query.ts} +2 -2
- package/client/vendor/paseo-plugin-helper/{settings.ts → core/settings.ts} +1 -1
- package/client/vendor/paseo-plugin-helper/{shared-settings.ts → core/shared-settings.ts} +2 -2
- package/client/vendor/paseo-plugin-helper/{utils → lifecycle}/clipboard.ts +2 -2
- package/client/vendor/paseo-plugin-helper/lifecycle/flair.ts +26 -0
- package/client/vendor/paseo-plugin-helper/{theme/color-utils.ts → lifecycle/host-color.ts} +95 -38
- package/client/vendor/paseo-plugin-helper/lifecycle/host-theme.tsx +109 -0
- package/client/vendor/paseo-plugin-helper/lifecycle/index.ts +23 -0
- package/client/vendor/paseo-plugin-helper/lifecycle/multi-host.ts +66 -0
- package/client/vendor/paseo-plugin-helper/{panel.tsx → lifecycle/panel.tsx} +7 -7
- package/client/vendor/paseo-plugin-helper/{pill.tsx → lifecycle/pill.tsx} +51 -18
- package/client/vendor/paseo-plugin-helper/lifecycle/providers.tsx +46 -0
- package/client/vendor/paseo-plugin-helper/lifecycle/scroll-owner.ts +26 -0
- package/client/vendor/paseo-plugin-helper/{surface.tsx → lifecycle/surface.tsx} +6 -6
- package/client/vendor/paseo-plugin-helper/ui/color.ts +1 -0
- package/client/vendor/paseo-plugin-helper/ui/content.tsx +1326 -0
- package/client/vendor/paseo-plugin-helper/ui/controls.tsx +1018 -0
- package/client/vendor/paseo-plugin-helper/{components/DataTable.tsx → ui/data.tsx} +50 -58
- package/client/vendor/paseo-plugin-helper/ui/index.ts +44 -0
- package/client/vendor/paseo-plugin-helper/ui/layout.tsx +263 -0
- package/client/vendor/paseo-plugin-helper/ui/modal.tsx +111 -0
- package/client/vendor/paseo-plugin-helper/ui/plugin-reload-banner.tsx +119 -0
- package/client/vendor/paseo-plugin-helper/{settings-screen.tsx → ui/settings-screen.tsx} +3 -3
- package/client/vendor/paseo-plugin-helper/ui/settings.ts +36 -0
- package/client/vendor/paseo-plugin-helper/ui/text.tsx +821 -0
- package/client/vendor/paseo-plugin-helper/ui/theme.tsx +1 -0
- package/client/vendor/permission-audit/client.tsx +706 -0
- package/client/vendor/permission-audit/index.tsx +1 -0
- package/index.client.tsx +58 -37
- package/index.server.ts +256 -234
- package/npm-shrinkwrap.json +44 -0
- package/package.json +8 -4
- package/paseo-plugin.json +2 -0
- package/screenshots/1-paseo-top-modal-system.jpg +0 -0
- package/screenshots/2-paseo-top-modal-workspace.jpg +0 -0
- package/screenshots/3-paseo-top-modal-settings.jpg +0 -0
- package/screenshots/4-paseo-top-modal-about.jpg +0 -0
- package/screenshots/paseo-top-dashboard.jpg +0 -0
- package/screenshots/paseo-top-general-view.jpg +0 -0
- package/server/resources.ts +98 -0
- package/server/vendor/paseo-plugin-helper/custom-pills.ts +70 -0
- package/server/vendor/paseo-plugin-helper/index.ts +2 -0
- package/server/vendor/paseo-plugin-helper/logger.ts +5 -16
- package/server/vendor/paseo-plugin-helper/rpc-guard.ts +2 -2
- package/server/vendor/paseo-plugin-helper/storage-guard.ts +294 -0
- package/server/vendor/paseo-plugin-helper/storage.ts +36 -2
- package/server/vendor/paseo-plugin-helper/telemetry.ts +486 -0
- package/server/vendor/paseo-plugin-helper/tickets.ts +18 -67
- package/server/vendor/permission-audit/index.ts +1 -0
- package/server/vendor/permission-audit/server.ts +839 -0
- package/shared/helper-version.ts +1 -1
- package/shared/multi-host.ts +153 -159
- package/shared/resources.ts +145 -0
- package/shared/vendor/paseo-plugin-helper/async.ts +14 -7
- package/shared/vendor/paseo-plugin-helper/features.ts +244 -0
- package/shared/vendor/paseo-plugin-helper/forge.ts +27 -0
- package/shared/vendor/paseo-plugin-helper/index.ts +1 -0
- package/shared/vendor/paseo-plugin-helper/tickets.ts +122 -28
- package/shared/vendor/permission-audit/filter.ts +61 -0
- package/shared/vendor/permission-audit/index.ts +2 -0
- package/shared/vendor/permission-audit/shared.ts +119 -0
- package/shared/version.ts +1 -1
- package/client/vendor/paseo-plugin-helper/components/AboutSection.tsx +0 -493
- package/client/vendor/paseo-plugin-helper/components/AttentionBeacon.tsx +0 -250
- package/client/vendor/paseo-plugin-helper/components/Badge.tsx +0 -168
- package/client/vendor/paseo-plugin-helper/components/Button.tsx +0 -204
- package/client/vendor/paseo-plugin-helper/components/Card.tsx +0 -257
- package/client/vendor/paseo-plugin-helper/components/CodeBlock.tsx +0 -196
- package/client/vendor/paseo-plugin-helper/components/Collapsible.tsx +0 -277
- package/client/vendor/paseo-plugin-helper/components/CommandBox.tsx +0 -172
- package/client/vendor/paseo-plugin-helper/components/CopyButton.tsx +0 -180
- package/client/vendor/paseo-plugin-helper/components/EmptyState.tsx +0 -97
- package/client/vendor/paseo-plugin-helper/components/HighlightedText.tsx +0 -70
- package/client/vendor/paseo-plugin-helper/components/InlineButton.tsx +0 -88
- package/client/vendor/paseo-plugin-helper/components/InteractiveRow.tsx +0 -142
- package/client/vendor/paseo-plugin-helper/components/KeyValue.tsx +0 -446
- package/client/vendor/paseo-plugin-helper/components/MetricGauge.tsx +0 -247
- package/client/vendor/paseo-plugin-helper/components/ProgressBar.tsx +0 -117
- package/client/vendor/paseo-plugin-helper/components/Responsive.tsx +0 -53
- package/client/vendor/paseo-plugin-helper/components/SearchInput.tsx +0 -132
- package/client/vendor/paseo-plugin-helper/components/SectionHeader.tsx +0 -87
- package/client/vendor/paseo-plugin-helper/components/Select.tsx +0 -283
- package/client/vendor/paseo-plugin-helper/components/StatusDot.tsx +0 -80
- package/client/vendor/paseo-plugin-helper/components/Tabs.tsx +0 -319
- package/client/vendor/paseo-plugin-helper/components/TextInput.tsx +0 -150
- package/client/vendor/paseo-plugin-helper/components/Toggle.tsx +0 -163
- package/client/vendor/paseo-plugin-helper/components/TruncatedText.tsx +0 -157
- package/client/vendor/paseo-plugin-helper/components/index.ts +0 -26
- package/client/vendor/paseo-plugin-helper/custom-pills.tsx +0 -225
- package/client/vendor/paseo-plugin-helper/forge-icon.tsx +0 -79
- package/client/vendor/paseo-plugin-helper/icon.tsx +0 -39
- package/client/vendor/paseo-plugin-helper/index.ts +0 -30
- package/client/vendor/paseo-plugin-helper/layout/ActionBar.tsx +0 -49
- package/client/vendor/paseo-plugin-helper/layout/FormRow.tsx +0 -103
- package/client/vendor/paseo-plugin-helper/layout/Grid.tsx +0 -65
- package/client/vendor/paseo-plugin-helper/layout/ModalBody.tsx +0 -368
- package/client/vendor/paseo-plugin-helper/layout/ModalContent.tsx +0 -51
- package/client/vendor/paseo-plugin-helper/layout/Row.tsx +0 -39
- package/client/vendor/paseo-plugin-helper/layout/Stack.tsx +0 -39
- package/client/vendor/paseo-plugin-helper/layout/index.ts +0 -7
- package/client/vendor/paseo-plugin-helper/styles/index.ts +0 -1
- package/client/vendor/paseo-plugin-helper/styles/recipes.ts +0 -550
- package/client/vendor/paseo-plugin-helper/theme/flair.ts +0 -76
- package/client/vendor/paseo-plugin-helper/theme/host-variables.ts +0 -121
- package/client/vendor/paseo-plugin-helper/theme/index.ts +0 -7
- package/client/vendor/paseo-plugin-helper/theme/provider.tsx +0 -178
- package/client/vendor/paseo-plugin-helper/theme/responsive.ts +0 -213
- package/client/vendor/paseo-plugin-helper/theme/tokens.ts +0 -200
- package/client/vendor/paseo-plugin-helper/theme/useResponsive.ts +0 -57
- package/client/vendor/paseo-plugin-helper/tickets.tsx +0 -766
- /package/client/vendor/paseo-plugin-helper/{command-center.ts → core/command-center.ts} +0 -0
- /package/client/vendor/paseo-plugin-helper/{snapshot.ts → core/snapshot.ts} +0 -0
- /package/client/vendor/paseo-plugin-helper/{utils → lifecycle}/haptics.ts +0 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@xpufx/paseo-top",
|
|
3
|
+
"version": "0.4.7",
|
|
4
|
+
"lockfileVersion": 2,
|
|
5
|
+
"requires": true,
|
|
6
|
+
"packages": {
|
|
7
|
+
"": {
|
|
8
|
+
"name": "@xpufx/paseo-top",
|
|
9
|
+
"version": "0.4.7",
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@xpufx/paseo-plugin-ui": "^0.1.0"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"node_modules/@xpufx/paseo-plugin-ui": {
|
|
16
|
+
"version": "0.1.0",
|
|
17
|
+
"resolved": "https://registry.npmjs.org/@xpufx/paseo-plugin-ui/-/paseo-plugin-ui-0.1.0.tgz",
|
|
18
|
+
"integrity": "sha512-HHRFBoLX11Dl01oOOp7z7xcvF01oavn9RPnBo5UaDafZ7kMmiNSLcOIm3jz5P7q9g+mt8iT9Pjmu3R6vzLlVog==",
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"peerDependencies": {
|
|
21
|
+
"@getpaseo/plugin": ">=0.9.0",
|
|
22
|
+
"@xpufx/backend-tickets": ">=0.1.0",
|
|
23
|
+
"react": ">=18.0.0",
|
|
24
|
+
"react-native": ">=0.70.0"
|
|
25
|
+
},
|
|
26
|
+
"peerDependenciesMeta": {
|
|
27
|
+
"@getpaseo/plugin": {
|
|
28
|
+
"optional": false
|
|
29
|
+
},
|
|
30
|
+
"@xpufx/backend-tickets": {
|
|
31
|
+
"optional": true
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@xpufx/paseo-plugin-ui": {
|
|
38
|
+
"version": "0.1.0",
|
|
39
|
+
"resolved": "https://registry.npmjs.org/@xpufx/paseo-plugin-ui/-/paseo-plugin-ui-0.1.0.tgz",
|
|
40
|
+
"integrity": "sha512-HHRFBoLX11Dl01oOOp7z7xcvF01oavn9RPnBo5UaDafZ7kMmiNSLcOIm3jz5P7q9g+mt8iT9Pjmu3R6vzLlVog==",
|
|
41
|
+
"requires": {}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
package/package.json
CHANGED
|
@@ -2,16 +2,17 @@
|
|
|
2
2
|
"name": "@xpufx/paseo-top",
|
|
3
3
|
"private": false,
|
|
4
4
|
"license": "MIT",
|
|
5
|
-
"version": "0.4.
|
|
6
|
-
"dependencies": {
|
|
7
|
-
"permission-audit": "*"
|
|
8
|
-
},
|
|
5
|
+
"version": "0.4.7",
|
|
9
6
|
"scripts": {
|
|
10
7
|
"stamp": "node -e \"import('./server/vendor/paseo-plugin-helper/version.ts').then(m => m.stampVersion({ targetFile: 'shared/version.ts' }))\"",
|
|
11
8
|
"typecheck": "tsc --noEmit",
|
|
12
9
|
"test": "npm run typecheck && vitest run --config client/vitest.config.ts"
|
|
13
10
|
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@xpufx/paseo-plugin-ui": "^0.1.0"
|
|
13
|
+
},
|
|
14
14
|
"devDependencies": {
|
|
15
|
+
"paseo-plugin-ui-testing": "*",
|
|
15
16
|
"@getpaseo/plugin": "^0.9.0",
|
|
16
17
|
"@tanstack/react-query": "^5.90.11",
|
|
17
18
|
"@types/react": "^19.0.10",
|
|
@@ -25,8 +26,11 @@
|
|
|
25
26
|
},
|
|
26
27
|
"files": [
|
|
27
28
|
"paseo-plugin.json",
|
|
29
|
+
"npm-shrinkwrap.json",
|
|
28
30
|
"README.md",
|
|
31
|
+
"OVERVIEW.md",
|
|
29
32
|
"LICENSE",
|
|
33
|
+
"screenshots",
|
|
30
34
|
"index.client.tsx",
|
|
31
35
|
"index.server.ts",
|
|
32
36
|
"client",
|
package/paseo-plugin.json
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/server/resources.ts
CHANGED
|
@@ -29,6 +29,91 @@ import {
|
|
|
29
29
|
} from "../shared/resources";
|
|
30
30
|
import { PLUGIN_VERSION } from "../shared/version";
|
|
31
31
|
|
|
32
|
+
// --- Velocity & Context Pressure helpers -------------------------------------
|
|
33
|
+
|
|
34
|
+
/** Thresholds for context pressure levels. */
|
|
35
|
+
const CONTEXT_NORMAL_UPPER = 60; // <60% → normal
|
|
36
|
+
const CONTEXT_WARNING_UPPER = 75; // 60-74% → warning, >=75% → high
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Compute live generation velocity (tokens per second).
|
|
40
|
+
*
|
|
41
|
+
* Formula: (outputTokens + reasoningTokens) / (durationMs / 1000)
|
|
42
|
+
*
|
|
43
|
+
* Returns 0 for:
|
|
44
|
+
* - zero or missing duration (< 50 ms threshold to avoid skewed averages)
|
|
45
|
+
* - missing output tokens
|
|
46
|
+
* - interrupted/canceled turns (prevents skewed averages on partial generations)
|
|
47
|
+
*/
|
|
48
|
+
export function computeTokensPerSecond(
|
|
49
|
+
outputTokens: number | undefined,
|
|
50
|
+
durationMs: number | undefined,
|
|
51
|
+
outcomeKind: string,
|
|
52
|
+
): number | undefined {
|
|
53
|
+
if (outputTokens == null || !Number.isFinite(outputTokens)) return undefined;
|
|
54
|
+
if (outputTokens <= 0) return 0;
|
|
55
|
+
|
|
56
|
+
const dur = durationMs ?? undefined;
|
|
57
|
+
if (dur == null || !Number.isFinite(dur) || dur < 50) return 0;
|
|
58
|
+
|
|
59
|
+
// Interrupted/canceled turns with partial data yield 0 TPS.
|
|
60
|
+
if (outcomeKind === "canceled" || outcomeKind === "failed") return 0;
|
|
61
|
+
|
|
62
|
+
const durationSec = dur / 1000;
|
|
63
|
+
const rate = outputTokens / durationSec;
|
|
64
|
+
if (!Number.isFinite(rate) || rate <= 0) return 0;
|
|
65
|
+
return Math.round(rate * 100) / 100; // round to 2dp
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Context pressure level based on utilization percentage. */
|
|
69
|
+
type ContextPressureLevel = "normal" | "warning" | "high";
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Classify context pressure into discrete levels:
|
|
73
|
+
* - "normal": utilization < 60%
|
|
74
|
+
* - "warning": 60–74%
|
|
75
|
+
* - "high": >= 75% (amnesia risk)
|
|
76
|
+
*
|
|
77
|
+
* Returns undefined when context window info is absent.
|
|
78
|
+
*/
|
|
79
|
+
export function classifyContextPressure(
|
|
80
|
+
contextUsedTokens: number | undefined,
|
|
81
|
+
contextMaxTokens: number | undefined,
|
|
82
|
+
): { level: ContextPressureLevel; pct: number } | undefined {
|
|
83
|
+
if (
|
|
84
|
+
contextUsedTokens == null ||
|
|
85
|
+
contextMaxTokens == null ||
|
|
86
|
+
contextMaxTokens <= 0
|
|
87
|
+
) {
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const pct = Math.min(100, Math.round((contextUsedTokens / contextMaxTokens) * 100));
|
|
92
|
+
|
|
93
|
+
let level: ContextPressureLevel;
|
|
94
|
+
if (pct >= CONTEXT_WARNING_UPPER) {
|
|
95
|
+
level = "high";
|
|
96
|
+
} else if (pct >= CONTEXT_NORMAL_UPPER) {
|
|
97
|
+
level = "warning";
|
|
98
|
+
} else {
|
|
99
|
+
level = "normal";
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return { level, pct };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Spread context pressure fields into the telemetry object. */
|
|
106
|
+
function computeContextPressure(
|
|
107
|
+
contextUsedTokens: number | undefined,
|
|
108
|
+
contextMaxTokens: number | undefined,
|
|
109
|
+
): Pick<TopTimelineTelemetryData, "contextPressureLevel" | "contextUtilizationPct"> {
|
|
110
|
+
const result = classifyContextPressure(contextUsedTokens, contextMaxTokens);
|
|
111
|
+
return {
|
|
112
|
+
contextPressureLevel: result?.level,
|
|
113
|
+
contextUtilizationPct: result?.pct,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
32
117
|
export const log = createPluginLogger("top", { version: PLUGIN_VERSION });
|
|
33
118
|
|
|
34
119
|
log.info("Initialized host system resources monitor", { version: PLUGIN_VERSION });
|
|
@@ -437,6 +522,10 @@ export async function handleGetSystemResources(input?: {
|
|
|
437
522
|
.filter((state) => effectiveEnabled.has(state.id)),
|
|
438
523
|
lastTurn: lastTurnTelemetry,
|
|
439
524
|
liveUsage: lastLiveUsage,
|
|
525
|
+
// Velocity data from the most recent turn telemetry.
|
|
526
|
+
tokensPerSecond: lastTurnTelemetry?.tokensPerSecond,
|
|
527
|
+
contextPressureLevel: lastTurnTelemetry?.contextPressureLevel,
|
|
528
|
+
contextUtilizationPct: lastTurnTelemetry?.contextUtilizationPct,
|
|
440
529
|
};
|
|
441
530
|
}
|
|
442
531
|
|
|
@@ -778,6 +867,15 @@ export async function collectTurnTelemetry(
|
|
|
778
867
|
contextUsedTokens,
|
|
779
868
|
contextMaxTokens,
|
|
780
869
|
costUsd,
|
|
870
|
+
// Live generation velocity: (output + reasoning) / durationSec.
|
|
871
|
+
// Guard against zero-duration, missing fields, and interrupted turns.
|
|
872
|
+
tokensPerSecond: computeTokensPerSecond(
|
|
873
|
+
outputTokens,
|
|
874
|
+
durationMs,
|
|
875
|
+
outcome.kind,
|
|
876
|
+
),
|
|
877
|
+
// Context pressure from the most recent turn's utilization.
|
|
878
|
+
...computeContextPressure(contextUsedTokens, contextMaxTokens),
|
|
781
879
|
};
|
|
782
880
|
lastTurnTelemetry = data;
|
|
783
881
|
return data;
|
|
@@ -11,6 +11,9 @@ import {
|
|
|
11
11
|
parseNumericPillValue,
|
|
12
12
|
formatPillDisplay,
|
|
13
13
|
} from "../../../shared/vendor/paseo-plugin-helper/custom-pills";
|
|
14
|
+
import type { FeatureModule, FeatureDisposer } from "../../../shared/vendor/paseo-plugin-helper/features";
|
|
15
|
+
import type { PluginRpcContract } from "../../../shared/vendor/paseo-plugin-helper/rpc";
|
|
16
|
+
|
|
14
17
|
|
|
15
18
|
|
|
16
19
|
/**
|
|
@@ -342,3 +345,70 @@ export class CustomPillPoller {
|
|
|
342
345
|
}
|
|
343
346
|
}
|
|
344
347
|
}
|
|
348
|
+
|
|
349
|
+
export interface CustomPillsFeatureOptions {
|
|
350
|
+
id?: string;
|
|
351
|
+
configDir?: string;
|
|
352
|
+
logger?: PluginLogger;
|
|
353
|
+
poller?: CustomPillPoller;
|
|
354
|
+
contracts?: {
|
|
355
|
+
get?: PluginRpcContract;
|
|
356
|
+
list?: PluginRpcContract;
|
|
357
|
+
run?: PluginRpcContract;
|
|
358
|
+
};
|
|
359
|
+
handlers?: {
|
|
360
|
+
get?: (input?: any) => Promise<any> | any;
|
|
361
|
+
list?: (input?: any) => Promise<any> | any;
|
|
362
|
+
run?: (input: any) => Promise<any> | any;
|
|
363
|
+
};
|
|
364
|
+
onMount?: (poller: CustomPillPoller) => void;
|
|
365
|
+
startPoller?: boolean;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Creates a reusable FeatureModule for declarative custom metric pills (#933).
|
|
370
|
+
*/
|
|
371
|
+
export function createCustomPillsFeature<TServerContext = any, TClientContext = any>(
|
|
372
|
+
options: CustomPillsFeatureOptions = {},
|
|
373
|
+
): FeatureModule<TServerContext, TClientContext> {
|
|
374
|
+
const poller = options.poller ?? new CustomPillPoller({
|
|
375
|
+
configDir: options.configDir,
|
|
376
|
+
logger: options.logger,
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
const contractsRecord: Record<string, PluginRpcContract> = {};
|
|
380
|
+
if (options.contracts?.get) contractsRecord.get = options.contracts.get;
|
|
381
|
+
if (options.contracts?.list) contractsRecord.list = options.contracts.list;
|
|
382
|
+
if (options.contracts?.run) contractsRecord.run = options.contracts.run;
|
|
383
|
+
|
|
384
|
+
return {
|
|
385
|
+
id: options.id || "custom-pills",
|
|
386
|
+
contracts: contractsRecord,
|
|
387
|
+
contributeServer(server: any) {
|
|
388
|
+
if (options.startPoller !== false) {
|
|
389
|
+
void poller.start();
|
|
390
|
+
}
|
|
391
|
+
options.onMount?.(poller);
|
|
392
|
+
|
|
393
|
+
if (typeof server?.handle === "function") {
|
|
394
|
+
if (options.contracts?.get && options.handlers?.get) {
|
|
395
|
+
server.handle(options.contracts.get, options.handlers.get);
|
|
396
|
+
}
|
|
397
|
+
if (options.contracts?.list && options.handlers?.list) {
|
|
398
|
+
server.handle(options.contracts.list, options.handlers.list);
|
|
399
|
+
}
|
|
400
|
+
if (options.contracts?.run && options.handlers?.run) {
|
|
401
|
+
server.handle(options.contracts.run, options.handlers.run);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
let stopped = false;
|
|
406
|
+
return () => {
|
|
407
|
+
if (stopped) return;
|
|
408
|
+
stopped = true;
|
|
409
|
+
poller.stop();
|
|
410
|
+
};
|
|
411
|
+
},
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
|
|
@@ -46,28 +46,17 @@ export function isProductionEnv(env: NodeJS.ProcessEnv = process.env): boolean {
|
|
|
46
46
|
return (env.NODE_ENV ?? "").trim().toLowerCase() === "production";
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
/**
|
|
50
|
-
* True only when `NODE_ENV` is explicitly a development value
|
|
51
|
-
* (`development`/`dev`). Unset, empty, `production`, and anything else
|
|
52
|
-
* (e.g. `test`) all count as quiet, so a shipped plugin defaults to info
|
|
53
|
-
* without any env var set.
|
|
54
|
-
*/
|
|
55
|
-
export function isDevelopmentEnv(env: NodeJS.ProcessEnv = process.env): boolean {
|
|
56
|
-
const nodeEnv = (env.NODE_ENV ?? "").trim().toLowerCase();
|
|
57
|
-
return nodeEnv === "development" || nodeEnv === "dev";
|
|
58
|
-
}
|
|
59
|
-
|
|
60
49
|
/**
|
|
61
50
|
* Default rule (documented for operators):
|
|
62
51
|
* explicit `PASEO_PLUGIN_LOG_LEVEL`/`PASEO_LOG_LEVEL`/`PASEO_DEBUG` always wins;
|
|
63
|
-
* otherwise
|
|
64
|
-
*
|
|
65
|
-
*
|
|
52
|
+
* otherwise `info` in every environment — including `NODE_ENV=development`/`dev`
|
|
53
|
+
* and when no env var is set at all. Debug requires an explicit level; the
|
|
54
|
+
* environment name alone never lifts the floor.
|
|
66
55
|
*/
|
|
67
56
|
export function resolveDefaultMinLevel(
|
|
68
57
|
env: NodeJS.ProcessEnv = process.env,
|
|
69
58
|
): LogLevel {
|
|
70
|
-
return resolveMinLevelFromEnv(env) ??
|
|
59
|
+
return resolveMinLevelFromEnv(env) ?? "info";
|
|
71
60
|
}
|
|
72
61
|
|
|
73
62
|
export interface PluginLoggerOptions {
|
|
@@ -90,7 +79,7 @@ export interface PluginLoggerOptions {
|
|
|
90
79
|
|
|
91
80
|
/**
|
|
92
81
|
* Minimum log level to print. Defaults to resolveDefaultMinLevel():
|
|
93
|
-
* info unless
|
|
82
|
+
* info unless an explicit level is set via the environment.
|
|
94
83
|
*/
|
|
95
84
|
minLevel?: LogLevel;
|
|
96
85
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { withTimeout,
|
|
1
|
+
import { withTimeout, isTimeoutError } from "../../../shared/vendor/paseo-plugin-helper/async";
|
|
2
2
|
|
|
3
3
|
export interface RpcGuardOptions<T> {
|
|
4
4
|
timeoutMs?: number;
|
|
@@ -40,7 +40,7 @@ export function guardRpcHandler<TInput, TOutput>(
|
|
|
40
40
|
"RPC handler",
|
|
41
41
|
);
|
|
42
42
|
} catch (err) {
|
|
43
|
-
if (err
|
|
43
|
+
if (isTimeoutError(err)) {
|
|
44
44
|
options.onTimeout?.({ timeoutMs, inflight });
|
|
45
45
|
}
|
|
46
46
|
throw err;
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import type { PathLike } from "node:fs";
|
|
3
|
+
import fsp from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import os from "node:os";
|
|
6
|
+
import { resolvePaseoHome } from "./storage";
|
|
7
|
+
|
|
8
|
+
export class ScopedStorageViolationError extends Error {
|
|
9
|
+
readonly code = "ERR_SCOPED_STORAGE_VIOLATION";
|
|
10
|
+
readonly attemptedPath: string;
|
|
11
|
+
readonly allowedRoots: string[];
|
|
12
|
+
|
|
13
|
+
constructor(message: string, attemptedPath: string, allowedRoots: string[]) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.name = "ScopedStorageViolationError";
|
|
16
|
+
this.attemptedPath = attemptedPath;
|
|
17
|
+
this.allowedRoots = allowedRoots;
|
|
18
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface InstallStorageGuardOptions {
|
|
23
|
+
pluginId: string;
|
|
24
|
+
namespace?: string; // defaults to "plugin-data/xpufx"
|
|
25
|
+
baseDir?: string;
|
|
26
|
+
allowTmp?: boolean; // defaults to true
|
|
27
|
+
mode?: "enforce" | "warn"; // defaults to "enforce"
|
|
28
|
+
onViolation?: (violation: { path: string; operation: string; allowedRoots: string[] }) => void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function resolvePathString(target: PathLike | number): string | null {
|
|
32
|
+
if (typeof target === "string") {
|
|
33
|
+
return path.resolve(target);
|
|
34
|
+
}
|
|
35
|
+
if (Buffer.isBuffer(target)) {
|
|
36
|
+
return path.resolve(target.toString("utf8"));
|
|
37
|
+
}
|
|
38
|
+
if (target instanceof URL) {
|
|
39
|
+
if (target.protocol === "file:") {
|
|
40
|
+
const pathname = decodeURIComponent(target.pathname);
|
|
41
|
+
return path.resolve(pathname);
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function isPathWithinRoot(targetPath: string, rootDir: string): boolean {
|
|
49
|
+
const resolvedTarget = path.resolve(targetPath);
|
|
50
|
+
const resolvedRoot = path.resolve(rootDir);
|
|
51
|
+
|
|
52
|
+
if (resolvedTarget === resolvedRoot) {
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const relative = path.relative(resolvedRoot, resolvedTarget);
|
|
57
|
+
return !relative.startsWith("..") && !path.isAbsolute(relative);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function installStorageGuard(options: InstallStorageGuardOptions): () => void {
|
|
61
|
+
const pluginId = options.pluginId;
|
|
62
|
+
const namespace = options.namespace ?? "plugin-data/xpufx";
|
|
63
|
+
const allowTmp = options.allowTmp ?? true;
|
|
64
|
+
const mode = options.mode ?? "enforce";
|
|
65
|
+
|
|
66
|
+
const pluginStorageDir = options.baseDir
|
|
67
|
+
? path.resolve(options.baseDir, pluginId)
|
|
68
|
+
: path.resolve(resolvePaseoHome(), namespace, pluginId);
|
|
69
|
+
const allowedRoots: string[] = [pluginStorageDir];
|
|
70
|
+
|
|
71
|
+
if (allowTmp) {
|
|
72
|
+
allowedRoots.push(path.resolve(os.tmpdir()));
|
|
73
|
+
allowedRoots.push(path.resolve(process.cwd(), ".tmp"));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function validateTarget(target: PathLike | number, operation: string): void {
|
|
77
|
+
const resolved = resolvePathString(target);
|
|
78
|
+
if (!resolved) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const isAllowed = allowedRoots.some((root) => isPathWithinRoot(resolved, root));
|
|
83
|
+
if (!isAllowed) {
|
|
84
|
+
options.onViolation?.({
|
|
85
|
+
path: resolved,
|
|
86
|
+
operation,
|
|
87
|
+
allowedRoots: [...allowedRoots],
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const message = `Scoped storage violation: write operation '${operation}' on '${resolved}' is outside permitted scopes: [${allowedRoots.join(", ")}]`;
|
|
91
|
+
|
|
92
|
+
if (mode === "enforce") {
|
|
93
|
+
throw new ScopedStorageViolationError(message, resolved, [...allowedRoots]);
|
|
94
|
+
} else {
|
|
95
|
+
console.warn(`[WARN] ${message}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Backup originals
|
|
101
|
+
const origWriteFile = fs.writeFile;
|
|
102
|
+
const origWriteFileSync = fs.writeFileSync;
|
|
103
|
+
const origAppendFile = fs.appendFile;
|
|
104
|
+
const origAppendFileSync = fs.appendFileSync;
|
|
105
|
+
const origMkdir = fs.mkdir;
|
|
106
|
+
const origMkdirSync = fs.mkdirSync;
|
|
107
|
+
const origRmdir = fs.rmdir;
|
|
108
|
+
const origRmdirSync = fs.rmdirSync;
|
|
109
|
+
const origUnlink = fs.unlink;
|
|
110
|
+
const origUnlinkSync = fs.unlinkSync;
|
|
111
|
+
const origRm = fs.rm;
|
|
112
|
+
const origRmSync = fs.rmSync;
|
|
113
|
+
const origRename = fs.rename;
|
|
114
|
+
const origRenameSync = fs.renameSync;
|
|
115
|
+
const origCopyFile = fs.copyFile;
|
|
116
|
+
const origCopyFileSync = fs.copyFileSync;
|
|
117
|
+
const origCreateWriteStream = fs.createWriteStream;
|
|
118
|
+
|
|
119
|
+
const origPromisesWriteFile = fsp.writeFile;
|
|
120
|
+
const origPromisesAppendFile = fsp.appendFile;
|
|
121
|
+
const origPromisesMkdir = fsp.mkdir;
|
|
122
|
+
const origPromisesRmdir = fsp.rmdir;
|
|
123
|
+
const origPromisesUnlink = fsp.unlink;
|
|
124
|
+
const origPromisesRm = fsp.rm;
|
|
125
|
+
const origPromisesRename = fsp.rename;
|
|
126
|
+
const origPromisesCopyFile = fsp.copyFile;
|
|
127
|
+
|
|
128
|
+
// Intercept node:fs sync & async
|
|
129
|
+
const anyFs = fs as any;
|
|
130
|
+
|
|
131
|
+
anyFs.writeFile = function (file: PathLike | number, ...args: unknown[]) {
|
|
132
|
+
validateTarget(file, "writeFile");
|
|
133
|
+
return Reflect.apply(origWriteFile, fs, [file, ...args]);
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
anyFs.writeFileSync = function (file: PathLike | number, ...args: unknown[]) {
|
|
137
|
+
validateTarget(file, "writeFileSync");
|
|
138
|
+
return Reflect.apply(origWriteFileSync, fs, [file, ...args]);
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
anyFs.appendFile = function (file: PathLike | number, ...args: unknown[]) {
|
|
142
|
+
validateTarget(file, "appendFile");
|
|
143
|
+
return Reflect.apply(origAppendFile, fs, [file, ...args]);
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
anyFs.appendFileSync = function (file: PathLike | number, ...args: unknown[]) {
|
|
147
|
+
validateTarget(file, "appendFileSync");
|
|
148
|
+
return Reflect.apply(origAppendFileSync, fs, [file, ...args]);
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
anyFs.mkdir = function (pathArg: PathLike, ...args: unknown[]) {
|
|
152
|
+
validateTarget(pathArg, "mkdir");
|
|
153
|
+
return Reflect.apply(origMkdir, fs, [pathArg, ...args]);
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
anyFs.mkdirSync = function (pathArg: PathLike, ...args: unknown[]) {
|
|
157
|
+
validateTarget(pathArg, "mkdirSync");
|
|
158
|
+
return Reflect.apply(origMkdirSync, fs, [pathArg, ...args]);
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
anyFs.rmdir = function (pathArg: PathLike, ...args: unknown[]) {
|
|
162
|
+
validateTarget(pathArg, "rmdir");
|
|
163
|
+
return Reflect.apply(origRmdir, fs, [pathArg, ...args]);
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
anyFs.rmdirSync = function (pathArg: PathLike, ...args: unknown[]) {
|
|
167
|
+
validateTarget(pathArg, "rmdirSync");
|
|
168
|
+
return Reflect.apply(origRmdirSync, fs, [pathArg, ...args]);
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
anyFs.unlink = function (pathArg: PathLike, ...args: unknown[]) {
|
|
172
|
+
validateTarget(pathArg, "unlink");
|
|
173
|
+
return Reflect.apply(origUnlink, fs, [pathArg, ...args]);
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
anyFs.unlinkSync = function (pathArg: PathLike, ...args: unknown[]) {
|
|
177
|
+
validateTarget(pathArg, "unlinkSync");
|
|
178
|
+
return Reflect.apply(origUnlinkSync, fs, [pathArg, ...args]);
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
anyFs.rm = function (pathArg: PathLike, ...args: unknown[]) {
|
|
182
|
+
validateTarget(pathArg, "rm");
|
|
183
|
+
return Reflect.apply(origRm, fs, [pathArg, ...args]);
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
anyFs.rmSync = function (pathArg: PathLike, ...args: unknown[]) {
|
|
187
|
+
validateTarget(pathArg, "rmSync");
|
|
188
|
+
return Reflect.apply(origRmSync, fs, [pathArg, ...args]);
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
anyFs.rename = function (oldPath: PathLike, newPath: PathLike, ...args: unknown[]) {
|
|
192
|
+
validateTarget(oldPath, "rename:source");
|
|
193
|
+
validateTarget(newPath, "rename:destination");
|
|
194
|
+
return Reflect.apply(origRename, fs, [oldPath, newPath, ...args]);
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
anyFs.renameSync = function (oldPath: PathLike, newPath: PathLike, ...args: unknown[]) {
|
|
198
|
+
validateTarget(oldPath, "renameSync:source");
|
|
199
|
+
validateTarget(newPath, "renameSync:destination");
|
|
200
|
+
return Reflect.apply(origRenameSync, fs, [oldPath, newPath, ...args]);
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
anyFs.copyFile = function (src: PathLike, dest: PathLike, ...args: unknown[]) {
|
|
204
|
+
validateTarget(dest, "copyFile:destination");
|
|
205
|
+
return Reflect.apply(origCopyFile, fs, [src, dest, ...args]);
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
anyFs.copyFileSync = function (src: PathLike, dest: PathLike, ...args: unknown[]) {
|
|
209
|
+
validateTarget(dest, "copyFileSync:destination");
|
|
210
|
+
return Reflect.apply(origCopyFileSync, fs, [src, dest, ...args]);
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
anyFs.createWriteStream = function (pathArg: PathLike, ...args: unknown[]) {
|
|
214
|
+
validateTarget(pathArg, "createWriteStream");
|
|
215
|
+
return Reflect.apply(origCreateWriteStream, fs, [pathArg, ...args]);
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
// Intercept node:fs/promises
|
|
219
|
+
const anyFsp = fsp as any;
|
|
220
|
+
|
|
221
|
+
anyFsp.writeFile = async function (file: PathLike | number | fsp.FileHandle, ...args: unknown[]) {
|
|
222
|
+
if (typeof file === "string" || Buffer.isBuffer(file) || file instanceof URL) {
|
|
223
|
+
validateTarget(file as unknown as PathLike, "fsp.writeFile");
|
|
224
|
+
}
|
|
225
|
+
return Reflect.apply(origPromisesWriteFile, fsp, [file, ...args]);
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
anyFsp.appendFile = async function (file: PathLike | number | fsp.FileHandle, ...args: unknown[]) {
|
|
229
|
+
if (typeof file === "string" || Buffer.isBuffer(file) || file instanceof URL) {
|
|
230
|
+
validateTarget(file as unknown as PathLike, "fsp.appendFile");
|
|
231
|
+
}
|
|
232
|
+
return Reflect.apply(origPromisesAppendFile, fsp, [file, ...args]);
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
anyFsp.mkdir = async function (pathArg: PathLike, ...args: unknown[]) {
|
|
236
|
+
validateTarget(pathArg, "fsp.mkdir");
|
|
237
|
+
return Reflect.apply(origPromisesMkdir, fsp, [pathArg, ...args]);
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
anyFsp.rmdir = async function (pathArg: PathLike, ...args: unknown[]) {
|
|
241
|
+
validateTarget(pathArg, "fsp.rmdir");
|
|
242
|
+
return Reflect.apply(origPromisesRmdir, fsp, [pathArg, ...args]);
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
anyFsp.unlink = async function (pathArg: PathLike, ...args: unknown[]) {
|
|
246
|
+
validateTarget(pathArg, "fsp.unlink");
|
|
247
|
+
return Reflect.apply(origPromisesUnlink, fsp, [pathArg, ...args]);
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
anyFsp.rm = async function (pathArg: PathLike, ...args: unknown[]) {
|
|
251
|
+
validateTarget(pathArg, "fsp.rm");
|
|
252
|
+
return Reflect.apply(origPromisesRm, fsp, [pathArg, ...args]);
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
anyFsp.rename = async function (oldPath: PathLike, newPath: PathLike) {
|
|
256
|
+
validateTarget(oldPath, "fsp.rename:source");
|
|
257
|
+
validateTarget(newPath, "fsp.rename:destination");
|
|
258
|
+
return Reflect.apply(origPromisesRename, fsp, [oldPath, newPath]);
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
anyFsp.copyFile = async function (src: PathLike, dest: PathLike, ...args: unknown[]) {
|
|
262
|
+
validateTarget(dest, "fsp.copyFile:destination");
|
|
263
|
+
return Reflect.apply(origPromisesCopyFile, fsp, [src, dest, ...args]);
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
return function uninstallStorageGuard(): void {
|
|
267
|
+
fs.writeFile = origWriteFile;
|
|
268
|
+
fs.writeFileSync = origWriteFileSync;
|
|
269
|
+
fs.appendFile = origAppendFile;
|
|
270
|
+
fs.appendFileSync = origAppendFileSync;
|
|
271
|
+
fs.mkdir = origMkdir;
|
|
272
|
+
fs.mkdirSync = origMkdirSync;
|
|
273
|
+
fs.rmdir = origRmdir;
|
|
274
|
+
fs.rmdirSync = origRmdirSync;
|
|
275
|
+
fs.unlink = origUnlink;
|
|
276
|
+
fs.unlinkSync = origUnlinkSync;
|
|
277
|
+
fs.rm = origRm;
|
|
278
|
+
fs.rmSync = origRmSync;
|
|
279
|
+
fs.rename = origRename;
|
|
280
|
+
fs.renameSync = origRenameSync;
|
|
281
|
+
fs.copyFile = origCopyFile;
|
|
282
|
+
fs.copyFileSync = origCopyFileSync;
|
|
283
|
+
fs.createWriteStream = origCreateWriteStream;
|
|
284
|
+
|
|
285
|
+
fsp.writeFile = origPromisesWriteFile;
|
|
286
|
+
fsp.appendFile = origPromisesAppendFile;
|
|
287
|
+
fsp.mkdir = origPromisesMkdir;
|
|
288
|
+
fsp.rmdir = origPromisesRmdir;
|
|
289
|
+
fsp.unlink = origPromisesUnlink;
|
|
290
|
+
fsp.rm = origPromisesRm;
|
|
291
|
+
fsp.rename = origPromisesRename;
|
|
292
|
+
fsp.copyFile = origPromisesCopyFile;
|
|
293
|
+
};
|
|
294
|
+
}
|