snow-flow 10.0.185 → 10.0.186-dev.682
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/bin/index.js.map +9 -9
- package/bin/worker.js.map +7 -7
- package/mcp/servicenow-unified.js +116 -116
- package/package.json +1 -1
- package/parsers-config.ts +2 -1
- package/src/bun/index.ts +10 -9
- package/src/cli/cmd/agent.ts +3 -3
- package/src/cli/cmd/auth.ts +46 -0
- package/src/cli/cmd/import.ts +2 -2
- package/src/cli/cmd/session.ts +9 -12
- package/src/cli/cmd/tui/component/prompt/autocomplete.tsx +2 -1
- package/src/cli/cmd/tui/component/prompt/index.tsx +19 -6
- package/src/cli/cmd/tui/component/spinner.tsx +24 -0
- package/src/cli/cmd/tui/context/exit.tsx +1 -1
- package/src/cli/cmd/tui/routes/home.tsx +16 -2
- package/src/cli/cmd/tui/routes/session/index.tsx +122 -53
- package/src/cli/cmd/tui/routes/session/permission.tsx +9 -1
- package/src/cli/cmd/tui/routes/session/sidebar.tsx +9 -1
- package/src/cli/cmd/tui/thread.ts +4 -1
- package/src/cli/cmd/tui/ui/dialog-export-options.tsx +1 -1
- package/src/cli/cmd/tui/util/clipboard.ts +3 -3
- package/src/cli/cmd/tui/worker.ts +6 -1
- package/src/config/config.ts +28 -0
- package/src/context/context-db.ts +437 -0
- package/src/format/formatter.ts +14 -5
- package/src/global/index.ts +3 -4
- package/src/mcp/index.ts +7 -2
- package/src/mcp/oauth-callback.ts +7 -15
- package/src/mcp/oauth-provider.ts +34 -3
- package/src/project/project.ts +8 -4
- package/src/provider/models.ts +1 -1
- package/src/provider/provider.ts +88 -9
- package/src/provider/transform.ts +7 -2
- package/src/servicenow/servicenow-mcp-unified/tools/agile/snow_agile_capacity_plan.ts +20 -7
- package/src/servicenow/servicenow-mcp-unified/tools/agile/snow_agile_retrospective.ts +6 -8
- package/src/servicenow/servicenow-mcp-unified/tools/agile/snow_agile_sprint_manage.ts +46 -28
- package/src/servicenow/servicenow-mcp-unified/tools/agile/snow_agile_team_manage.ts +53 -41
- package/src/servicenow/servicenow-mcp-unified/tools/agile/snow_agile_velocity_report.ts +8 -1
- package/src/servicenow/servicenow-mcp-unified/tools/automation/snow_schedule_script_job.ts +388 -243
- package/src/session/compaction.ts +126 -23
- package/src/session/message-v2.ts +33 -10
- package/src/session/processor.ts +29 -17
- package/src/session/prompt.ts +34 -6
- package/src/share/share-next.ts +2 -2
- package/src/shell/shell.ts +2 -1
- package/src/tool/edit.ts +15 -1
- package/src/tool/registry.ts +9 -1
- package/src/tool/truncation.ts +17 -0
- package/src/tool/websearch.ts +1 -1
- package/src/tool/websearch.txt +2 -2
- package/src/tool/write.ts +3 -4
- package/src/util/filesystem.ts +36 -7
- package/src/util/keybind.ts +1 -1
- package/src/util/log.ts +8 -5
- package/src/util/token.ts +28 -0
- package/test/cli/plugin-auth-picker.test.ts +120 -0
- package/test/fixture/fixture.ts +3 -0
- package/test/mcp/oauth-auto-connect.test.ts +197 -0
- package/test/project/project.test.ts +47 -0
- package/test/provider/provider.test.ts +2 -0
- package/test/provider/transform.test.ts +32 -0
- package/test/tool/edit.test.ts +679 -0
|
@@ -1,22 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* snow_schedule_script_job -
|
|
2
|
+
* snow_schedule_script_job - Execute server-side JavaScript on ServiceNow
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Primary: synchronous execution via Scripted REST API (~1-3s)
|
|
5
|
+
* Fallback: scheduled job if endpoint unavailable
|
|
6
|
+
* Auto-deploys the executor endpoint on first use.
|
|
6
7
|
*
|
|
7
|
-
*
|
|
8
|
-
* 1. Creates a scheduled job in sysauto_script table
|
|
9
|
-
* 2. Attempts to create a sys_trigger to run it immediately
|
|
10
|
-
* 3. Polls for results via sys_properties (max 30 seconds)
|
|
11
|
-
* 4. If trigger fails or scheduler is slow, returns executed=false
|
|
12
|
-
*
|
|
13
|
-
* When executed=false is returned:
|
|
14
|
-
* - The script job WAS created successfully
|
|
15
|
-
* - You need to manually run it: System Scheduler > Scheduled Jobs
|
|
16
|
-
* - Or wait for the scheduler to pick it up
|
|
17
|
-
*
|
|
18
|
-
* ⚠️ CRITICAL: ALL SCRIPTS MUST BE ES5 ONLY!
|
|
19
|
-
* ServiceNow runs on Rhino engine - no const/let/arrow functions/template literals.
|
|
8
|
+
* ES5 only! ServiceNow runs on Rhino engine.
|
|
20
9
|
*/
|
|
21
10
|
|
|
22
11
|
import { MCPToolDefinition, ServiceNowContext, ToolResult } from "../../shared/types.js"
|
|
@@ -24,18 +13,88 @@ import { getAuthenticatedClient } from "../../shared/auth.js"
|
|
|
24
13
|
import { createSuccessResult, createErrorResult, SnowFlowError, ErrorType } from "../../shared/error-handler.js"
|
|
25
14
|
import crypto from "crypto"
|
|
26
15
|
|
|
16
|
+
const ENDPOINT_SERVICE_ID = "snow_flow_exec"
|
|
17
|
+
const ENDPOINT_PATH = "/execute"
|
|
18
|
+
|
|
19
|
+
const deployed = new Map<string, boolean>()
|
|
20
|
+
|
|
21
|
+
const OPERATION_SCRIPT = `(function process(request, response) {
|
|
22
|
+
var body = request.body.data;
|
|
23
|
+
var script = body.script;
|
|
24
|
+
var id = body.execution_id || gs.generateGUID();
|
|
25
|
+
|
|
26
|
+
if (!script) {
|
|
27
|
+
response.setStatus(400);
|
|
28
|
+
response.setBody({ success: false, error: 'No script provided' });
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
var output = [];
|
|
33
|
+
var result = null;
|
|
34
|
+
var error = null;
|
|
35
|
+
var startTime = new GlideDateTime();
|
|
36
|
+
|
|
37
|
+
var origPrint = gs.print;
|
|
38
|
+
var origInfo = gs.info;
|
|
39
|
+
var origWarn = gs.warn;
|
|
40
|
+
var origError = gs.error;
|
|
41
|
+
|
|
42
|
+
gs.print = function(msg) {
|
|
43
|
+
var m = String(msg);
|
|
44
|
+
output.push({ level: 'print', message: m });
|
|
45
|
+
origPrint.call(gs, m);
|
|
46
|
+
};
|
|
47
|
+
gs.info = function(msg) {
|
|
48
|
+
var m = String(msg);
|
|
49
|
+
output.push({ level: 'info', message: m });
|
|
50
|
+
origInfo.call(gs, m);
|
|
51
|
+
};
|
|
52
|
+
gs.warn = function(msg) {
|
|
53
|
+
var m = String(msg);
|
|
54
|
+
output.push({ level: 'warn', message: m });
|
|
55
|
+
origWarn.call(gs, m);
|
|
56
|
+
};
|
|
57
|
+
gs.error = function(msg) {
|
|
58
|
+
var m = String(msg);
|
|
59
|
+
output.push({ level: 'error', message: m });
|
|
60
|
+
origError.call(gs, m);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
result = GlideEvaluator.evaluateString(script);
|
|
65
|
+
} catch (e) {
|
|
66
|
+
error = e.toString();
|
|
67
|
+
if (e.stack) error = error + '\\nStack: ' + e.stack;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
gs.print = origPrint;
|
|
71
|
+
gs.info = origInfo;
|
|
72
|
+
gs.warn = origWarn;
|
|
73
|
+
gs.error = origError;
|
|
74
|
+
|
|
75
|
+
var endTime = new GlideDateTime();
|
|
76
|
+
var execMs = Math.abs(GlideDateTime.subtract(startTime, endTime).getNumericValue());
|
|
77
|
+
|
|
78
|
+
response.setStatus(error ? 500 : 200);
|
|
79
|
+
response.setBody({
|
|
80
|
+
execution_id: id,
|
|
81
|
+
success: error === null,
|
|
82
|
+
result: result,
|
|
83
|
+
error: error,
|
|
84
|
+
output: output,
|
|
85
|
+
execution_time_ms: execMs
|
|
86
|
+
});
|
|
87
|
+
})(request, response);`
|
|
88
|
+
|
|
27
89
|
export const toolDefinition: MCPToolDefinition = {
|
|
28
90
|
name: "snow_schedule_script_job",
|
|
29
91
|
description:
|
|
30
|
-
"
|
|
31
|
-
// Metadata for tool discovery (not sent to LLM)
|
|
92
|
+
"Execute server-side JavaScript on ServiceNow. Primary: synchronous execution via Scripted REST API (~1-3s). Fallback: scheduled job if endpoint unavailable. Auto-deploys the executor endpoint on first use. ES5 only (Rhino engine)!",
|
|
32
93
|
category: "automation",
|
|
33
94
|
subcategory: "scheduled-jobs",
|
|
34
95
|
use_cases: ["automation", "scripts", "scheduled-jobs", "debugging", "verification"],
|
|
35
96
|
complexity: "advanced",
|
|
36
97
|
frequency: "high",
|
|
37
|
-
|
|
38
|
-
// Permission enforcement
|
|
39
98
|
permission: "write",
|
|
40
99
|
allowedRoles: ["developer", "admin"],
|
|
41
100
|
inputSchema: {
|
|
@@ -43,7 +102,7 @@ export const toolDefinition: MCPToolDefinition = {
|
|
|
43
102
|
properties: {
|
|
44
103
|
script: {
|
|
45
104
|
type: "string",
|
|
46
|
-
description: "
|
|
105
|
+
description: "ES5 ONLY! JavaScript code to execute (no const/let/arrows/templates - Rhino engine)",
|
|
47
106
|
},
|
|
48
107
|
description: {
|
|
49
108
|
type: "string",
|
|
@@ -57,7 +116,7 @@ export const toolDefinition: MCPToolDefinition = {
|
|
|
57
116
|
},
|
|
58
117
|
timeout: {
|
|
59
118
|
type: "number",
|
|
60
|
-
description: "Timeout in milliseconds for polling execution results",
|
|
119
|
+
description: "Timeout in milliseconds for polling execution results (fallback mode only)",
|
|
61
120
|
default: 30000,
|
|
62
121
|
},
|
|
63
122
|
validate_es5: {
|
|
@@ -72,7 +131,7 @@ export const toolDefinition: MCPToolDefinition = {
|
|
|
72
131
|
},
|
|
73
132
|
autoConfirm: {
|
|
74
133
|
type: "boolean",
|
|
75
|
-
description: "
|
|
134
|
+
description: "Skip user confirmation even if requireConfirmation would normally be required",
|
|
76
135
|
default: false,
|
|
77
136
|
},
|
|
78
137
|
allowDataModification: {
|
|
@@ -89,55 +148,48 @@ export const toolDefinition: MCPToolDefinition = {
|
|
|
89
148
|
},
|
|
90
149
|
}
|
|
91
150
|
|
|
92
|
-
export async function execute(args:
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
} = args
|
|
104
|
-
|
|
105
|
-
// ES5 validation (warning only, does not block execution)
|
|
106
|
-
const es5Warnings: string[] = []
|
|
151
|
+
export async function execute(args: Record<string, unknown>, context: ServiceNowContext): Promise<ToolResult> {
|
|
152
|
+
const script = args.script as string
|
|
153
|
+
const description = (args.description as string) || "Script scheduled via snow_schedule_script_job"
|
|
154
|
+
const timeout = (args.timeout as number) || 30000
|
|
155
|
+
const validate = args.validate_es5 !== false
|
|
156
|
+
const confirmation = args.requireConfirmation === true
|
|
157
|
+
const auto = args.autoConfirm === true
|
|
158
|
+
const modification = args.allowDataModification === true
|
|
159
|
+
const user = args.runAsUser as string | undefined
|
|
160
|
+
|
|
161
|
+
const warnings: string[] = []
|
|
107
162
|
|
|
108
163
|
try {
|
|
109
|
-
if (
|
|
110
|
-
const
|
|
111
|
-
if (!
|
|
112
|
-
|
|
113
|
-
`Script contains ES6+ syntax (${
|
|
164
|
+
if (validate) {
|
|
165
|
+
const validation = validateES5(script)
|
|
166
|
+
if (!validation.valid) {
|
|
167
|
+
warnings.push(
|
|
168
|
+
`Script contains ES6+ syntax (${validation.violations.map((v) => v.type).join(", ")}). This may cause runtime errors in ServiceNow's Rhino engine. Consider using ES5 syntax.`,
|
|
114
169
|
)
|
|
115
170
|
}
|
|
116
171
|
}
|
|
117
172
|
|
|
118
|
-
|
|
119
|
-
const securityAnalysis = analyzeScriptSecurity(script)
|
|
173
|
+
const security = analyzeScriptSecurity(script)
|
|
120
174
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
// Return confirmation request
|
|
124
|
-
const confirmationPrompt = generateConfirmationPrompt({
|
|
175
|
+
if (confirmation && !auto) {
|
|
176
|
+
const prompt = generateConfirmationPrompt({
|
|
125
177
|
script,
|
|
126
178
|
description,
|
|
127
|
-
runAsUser,
|
|
128
|
-
allowDataModification,
|
|
129
|
-
securityAnalysis,
|
|
179
|
+
runAsUser: user,
|
|
180
|
+
allowDataModification: modification,
|
|
181
|
+
securityAnalysis: security,
|
|
130
182
|
})
|
|
131
183
|
|
|
132
184
|
return createSuccessResult(
|
|
133
185
|
{
|
|
134
186
|
requires_confirmation: true,
|
|
135
|
-
confirmation_prompt:
|
|
187
|
+
confirmation_prompt: prompt,
|
|
136
188
|
script_to_execute: script,
|
|
137
189
|
execution_context: {
|
|
138
|
-
runAsUser:
|
|
139
|
-
allowDataModification,
|
|
140
|
-
securityLevel:
|
|
190
|
+
runAsUser: user || "current",
|
|
191
|
+
allowDataModification: modification,
|
|
192
|
+
securityLevel: security.riskLevel,
|
|
141
193
|
},
|
|
142
194
|
next_step: "Call snow_confirm_script_execution with userConfirmed=true to execute",
|
|
143
195
|
},
|
|
@@ -147,105 +199,198 @@ export async function execute(args: any, context: ServiceNowContext): Promise<To
|
|
|
147
199
|
)
|
|
148
200
|
}
|
|
149
201
|
|
|
150
|
-
// Execute the script
|
|
151
202
|
return await executeScript(
|
|
152
|
-
{
|
|
153
|
-
script,
|
|
154
|
-
description,
|
|
155
|
-
timeout,
|
|
156
|
-
securityAnalysis,
|
|
157
|
-
autoConfirm,
|
|
158
|
-
es5Warnings,
|
|
159
|
-
},
|
|
203
|
+
{ script, description, timeout, securityAnalysis: security, autoConfirm: auto, es5Warnings: warnings },
|
|
160
204
|
context,
|
|
161
205
|
)
|
|
162
|
-
} catch (error:
|
|
206
|
+
} catch (error: unknown) {
|
|
207
|
+
const err = error as Error
|
|
163
208
|
return createErrorResult(
|
|
164
|
-
|
|
165
|
-
?
|
|
166
|
-
: new SnowFlowError(ErrorType.UNKNOWN_ERROR,
|
|
209
|
+
err instanceof SnowFlowError
|
|
210
|
+
? err
|
|
211
|
+
: new SnowFlowError(ErrorType.UNKNOWN_ERROR, err.message, { originalError: err }),
|
|
167
212
|
)
|
|
168
213
|
}
|
|
169
214
|
}
|
|
170
215
|
|
|
171
|
-
async function
|
|
216
|
+
async function ensureEndpoint(context: ServiceNowContext): Promise<boolean> {
|
|
217
|
+
if (deployed.get(context.instanceUrl)) return true
|
|
218
|
+
|
|
219
|
+
const client = await getAuthenticatedClient(context)
|
|
220
|
+
|
|
221
|
+
const check = await client.get("/api/now/table/sys_ws_definition", {
|
|
222
|
+
params: {
|
|
223
|
+
sysparm_query: `service_id=${ENDPOINT_SERVICE_ID}`,
|
|
224
|
+
sysparm_fields: "sys_id",
|
|
225
|
+
sysparm_limit: 1,
|
|
226
|
+
},
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
const existing = check.data?.result?.[0]
|
|
230
|
+
if (!existing) {
|
|
231
|
+
const svc = await client.post("/api/now/table/sys_ws_definition", {
|
|
232
|
+
name: "Snow-Flow Script Executor",
|
|
233
|
+
service_id: ENDPOINT_SERVICE_ID,
|
|
234
|
+
short_description: "Synchronous script execution endpoint for Snow-Flow",
|
|
235
|
+
active: true,
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
const svcId = svc.data?.result?.sys_id
|
|
239
|
+
if (!svcId) return false
|
|
240
|
+
|
|
241
|
+
const res = await client.post("/api/now/table/sys_ws_operation", {
|
|
242
|
+
name: "Execute Script",
|
|
243
|
+
web_service_definition: svcId,
|
|
244
|
+
http_method: "POST",
|
|
245
|
+
relative_path: ENDPOINT_PATH,
|
|
246
|
+
operation_script: OPERATION_SCRIPT,
|
|
247
|
+
active: true,
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
if (!res.data?.result?.sys_id) return false
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const ping = await client
|
|
254
|
+
.post(`/api/${ENDPOINT_SERVICE_ID}${ENDPOINT_PATH}`, {
|
|
255
|
+
script: "'pong'",
|
|
256
|
+
execution_id: "deploy_verify",
|
|
257
|
+
})
|
|
258
|
+
.catch(() => null)
|
|
259
|
+
|
|
260
|
+
const ok = ping?.data?.result?.success === true
|
|
261
|
+
if (ok) deployed.set(context.instanceUrl, true)
|
|
262
|
+
return ok
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
async function executeViaSyncApi(
|
|
266
|
+
params: {
|
|
267
|
+
script: string
|
|
268
|
+
executionId: string
|
|
269
|
+
description: string
|
|
270
|
+
securityAnalysis: Record<string, unknown>
|
|
271
|
+
autoConfirm: boolean
|
|
272
|
+
es5Warnings: string[]
|
|
273
|
+
},
|
|
274
|
+
context: ServiceNowContext,
|
|
275
|
+
): Promise<ToolResult | null> {
|
|
276
|
+
const client = await getAuthenticatedClient(context)
|
|
277
|
+
|
|
278
|
+
const response = await client
|
|
279
|
+
.post(`/api/${ENDPOINT_SERVICE_ID}${ENDPOINT_PATH}`, {
|
|
280
|
+
script: params.script,
|
|
281
|
+
execution_id: params.executionId,
|
|
282
|
+
})
|
|
283
|
+
.catch((err: { response?: { status?: number } }) => {
|
|
284
|
+
if (err.response?.status === 404 || err.response?.status === 403) return null
|
|
285
|
+
throw err
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
if (!response) return null
|
|
289
|
+
|
|
290
|
+
const data = response.data?.result
|
|
291
|
+
if (!data) return null
|
|
292
|
+
|
|
293
|
+
const organized = {
|
|
294
|
+
print: (data.output || [])
|
|
295
|
+
.filter((o: { level: string }) => o.level === "print")
|
|
296
|
+
.map((o: { message: string }) => o.message),
|
|
297
|
+
info: (data.output || [])
|
|
298
|
+
.filter((o: { level: string }) => o.level === "info")
|
|
299
|
+
.map((o: { message: string }) => o.message),
|
|
300
|
+
warn: (data.output || [])
|
|
301
|
+
.filter((o: { level: string }) => o.level === "warn")
|
|
302
|
+
.map((o: { message: string }) => o.message),
|
|
303
|
+
error: (data.output || [])
|
|
304
|
+
.filter((o: { level: string }) => o.level === "error")
|
|
305
|
+
.map((o: { message: string }) => o.message),
|
|
306
|
+
success: data.success,
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const result: Record<string, unknown> = {
|
|
310
|
+
executed: true,
|
|
311
|
+
success: data.success,
|
|
312
|
+
result: data.result,
|
|
313
|
+
error: data.error,
|
|
314
|
+
output: organized,
|
|
315
|
+
raw_output: data.output,
|
|
316
|
+
execution_time_ms: data.execution_time_ms,
|
|
317
|
+
execution_id: params.executionId,
|
|
318
|
+
auto_confirmed: params.autoConfirm,
|
|
319
|
+
security_analysis: params.securityAnalysis,
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (params.es5Warnings.length > 0) {
|
|
323
|
+
result.warnings = params.es5Warnings
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return createSuccessResult(result, {
|
|
327
|
+
script_length: params.script.length,
|
|
328
|
+
method: "sync_rest_api",
|
|
329
|
+
description: params.description,
|
|
330
|
+
})
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
async function executeViaScheduler(
|
|
172
334
|
params: {
|
|
173
335
|
script: string
|
|
174
336
|
description: string
|
|
175
337
|
timeout: number
|
|
176
|
-
securityAnalysis:
|
|
338
|
+
securityAnalysis: Record<string, unknown>
|
|
177
339
|
autoConfirm: boolean
|
|
178
340
|
es5Warnings: string[]
|
|
179
341
|
},
|
|
180
342
|
context: ServiceNowContext,
|
|
181
343
|
): Promise<ToolResult> {
|
|
182
|
-
const { script, description, timeout, securityAnalysis, autoConfirm, es5Warnings } = params
|
|
183
|
-
|
|
184
344
|
const client = await getAuthenticatedClient(context)
|
|
185
345
|
|
|
186
|
-
|
|
187
|
-
const escapeForJS = (str: string) =>
|
|
346
|
+
const escape = (str: string) =>
|
|
188
347
|
str.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/\n/g, "\\n").replace(/\r/g, "\\r")
|
|
189
348
|
|
|
190
|
-
// Create unique execution ID for tracking
|
|
191
349
|
const executionId = `exec_${Date.now()}_${crypto.randomBytes(6).toString("hex")}`
|
|
192
|
-
const
|
|
350
|
+
const marker = `SNOW_FLOW_EXEC_${executionId}`
|
|
193
351
|
|
|
194
|
-
|
|
195
|
-
const wrappedScript = `
|
|
196
|
-
// Snow-Flow Script Execution - ID: ${executionId}
|
|
197
|
-
// Description: ${escapeForJS(description)}
|
|
352
|
+
const wrapped = `
|
|
198
353
|
var __sfOutput = [];
|
|
199
354
|
var __sfStartTime = new GlideDateTime();
|
|
200
355
|
var __sfResult = null;
|
|
201
356
|
var __sfError = null;
|
|
202
357
|
|
|
203
|
-
// Store original gs methods
|
|
204
358
|
var __sfOrigPrint = gs.print;
|
|
205
359
|
var __sfOrigInfo = gs.info;
|
|
206
360
|
var __sfOrigWarn = gs.warn;
|
|
207
361
|
var __sfOrigError = gs.error;
|
|
208
362
|
|
|
209
|
-
// Override gs methods to capture output
|
|
210
363
|
gs.print = function(msg) {
|
|
211
364
|
var m = String(msg);
|
|
212
365
|
__sfOutput.push({level: 'print', message: m, timestamp: new GlideDateTime().getDisplayValue()});
|
|
213
366
|
__sfOrigPrint(m);
|
|
214
367
|
};
|
|
215
|
-
|
|
216
368
|
gs.info = function(msg) {
|
|
217
369
|
var m = String(msg);
|
|
218
370
|
__sfOutput.push({level: 'info', message: m, timestamp: new GlideDateTime().getDisplayValue()});
|
|
219
371
|
__sfOrigInfo(m);
|
|
220
372
|
};
|
|
221
|
-
|
|
222
373
|
gs.warn = function(msg) {
|
|
223
374
|
var m = String(msg);
|
|
224
375
|
__sfOutput.push({level: 'warn', message: m, timestamp: new GlideDateTime().getDisplayValue()});
|
|
225
376
|
__sfOrigWarn(m);
|
|
226
377
|
};
|
|
227
|
-
|
|
228
378
|
gs.error = function(msg) {
|
|
229
379
|
var m = String(msg);
|
|
230
380
|
__sfOutput.push({level: 'error', message: m, timestamp: new GlideDateTime().getDisplayValue()});
|
|
231
381
|
__sfOrigError(m);
|
|
232
382
|
};
|
|
233
383
|
|
|
234
|
-
// Execute the user script
|
|
235
384
|
try {
|
|
236
385
|
gs.info('=== Snow-Flow Script Execution Started ===');
|
|
237
|
-
gs.info('Description: ${
|
|
238
|
-
|
|
386
|
+
gs.info('Description: ${escape(params.description)}');
|
|
239
387
|
__sfResult = (function() {
|
|
240
|
-
${script}
|
|
388
|
+
${params.script}
|
|
241
389
|
})();
|
|
242
|
-
|
|
243
390
|
gs.info('=== Snow-Flow Script Execution Completed ===');
|
|
244
|
-
|
|
245
391
|
if (__sfResult !== undefined && __sfResult !== null) {
|
|
246
392
|
gs.info('Script returned: ' + (typeof __sfResult === 'object' ? JSON.stringify(__sfResult) : String(__sfResult)));
|
|
247
393
|
}
|
|
248
|
-
|
|
249
394
|
} catch(e) {
|
|
250
395
|
__sfError = e.toString();
|
|
251
396
|
gs.error('=== Snow-Flow Script Execution Failed ===');
|
|
@@ -255,17 +400,14 @@ try {
|
|
|
255
400
|
}
|
|
256
401
|
}
|
|
257
402
|
|
|
258
|
-
// Restore original gs methods
|
|
259
403
|
gs.print = __sfOrigPrint;
|
|
260
404
|
gs.info = __sfOrigInfo;
|
|
261
405
|
gs.warn = __sfOrigWarn;
|
|
262
406
|
gs.error = __sfOrigError;
|
|
263
407
|
|
|
264
|
-
// Calculate execution time
|
|
265
408
|
var __sfEndTime = new GlideDateTime();
|
|
266
409
|
var __sfExecTimeMs = Math.abs(GlideDateTime.subtract(__sfStartTime, __sfEndTime).getNumericValue());
|
|
267
410
|
|
|
268
|
-
// Build result object
|
|
269
411
|
var __sfResultObj = {
|
|
270
412
|
executionId: '${executionId}',
|
|
271
413
|
success: __sfError === null,
|
|
@@ -276,109 +418,89 @@ var __sfResultObj = {
|
|
|
276
418
|
completedAt: __sfEndTime.getDisplayValue()
|
|
277
419
|
};
|
|
278
420
|
|
|
279
|
-
|
|
280
|
-
gs.
|
|
281
|
-
gs.info('${outputMarker}:DONE');
|
|
421
|
+
gs.setProperty('${marker}', JSON.stringify(__sfResultObj));
|
|
422
|
+
gs.info('${marker}:DONE');
|
|
282
423
|
`
|
|
283
424
|
|
|
284
|
-
|
|
285
|
-
const jobName = `Snow-Flow Exec - ${executionId}`
|
|
425
|
+
const name = `Snow-Flow Exec - ${executionId}`
|
|
286
426
|
|
|
287
|
-
const
|
|
288
|
-
name
|
|
289
|
-
script:
|
|
427
|
+
const job = await client.post("/api/now/table/sysauto_script", {
|
|
428
|
+
name,
|
|
429
|
+
script: wrapped,
|
|
290
430
|
active: true,
|
|
291
431
|
run_type: "on_demand",
|
|
292
432
|
conditional: false,
|
|
293
433
|
})
|
|
294
434
|
|
|
295
|
-
if (!
|
|
435
|
+
if (!job.data?.result?.sys_id) {
|
|
296
436
|
throw new SnowFlowError(ErrorType.SERVICENOW_API_ERROR, "Failed to create scheduled script job", {
|
|
297
|
-
details:
|
|
437
|
+
details: job.data,
|
|
298
438
|
})
|
|
299
439
|
}
|
|
300
440
|
|
|
301
|
-
const
|
|
441
|
+
const jobId = job.data.result.sys_id
|
|
302
442
|
|
|
303
|
-
// Step 2: Create sys_trigger to execute immediately
|
|
304
443
|
const now = new Date()
|
|
305
|
-
const
|
|
306
|
-
const
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
name
|
|
311
|
-
next_action:
|
|
312
|
-
trigger_type: 0,
|
|
313
|
-
state: 0,
|
|
444
|
+
const trigger = new Date(now.getTime() + 2000)
|
|
445
|
+
const triggerStr = trigger.toISOString().replace("T", " ").substring(0, 19)
|
|
446
|
+
|
|
447
|
+
await client
|
|
448
|
+
.post("/api/now/table/sys_trigger", {
|
|
449
|
+
name,
|
|
450
|
+
next_action: triggerStr,
|
|
451
|
+
trigger_type: 0,
|
|
452
|
+
state: 0,
|
|
314
453
|
document: "sysauto_script",
|
|
315
|
-
document_key:
|
|
454
|
+
document_key: jobId,
|
|
316
455
|
claimed_by: "",
|
|
317
456
|
system_id: "snow-flow",
|
|
318
457
|
})
|
|
319
|
-
|
|
320
|
-
// If trigger creation fails, job won't auto-execute
|
|
321
|
-
// Continue anyway - we'll check results
|
|
322
|
-
}
|
|
458
|
+
.catch(() => {})
|
|
323
459
|
|
|
324
|
-
|
|
325
|
-
const
|
|
326
|
-
let result:
|
|
327
|
-
let attempts = 0
|
|
328
|
-
const maxAttempts = Math.ceil(timeout / 2000)
|
|
460
|
+
const start = Date.now()
|
|
461
|
+
const max = Math.ceil(params.timeout / 2000)
|
|
462
|
+
let result: Record<string, unknown> | null = null
|
|
329
463
|
|
|
330
|
-
|
|
331
|
-
attempts++
|
|
464
|
+
for (let i = 0; i < max && Date.now() - start < params.timeout; i++) {
|
|
332
465
|
await new Promise((resolve) => setTimeout(resolve, 2000))
|
|
333
466
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
const propResponse = await client.get("/api/now/table/sys_properties", {
|
|
467
|
+
const prop = await client
|
|
468
|
+
.get("/api/now/table/sys_properties", {
|
|
337
469
|
params: {
|
|
338
|
-
sysparm_query: `name=${
|
|
470
|
+
sysparm_query: `name=${marker}`,
|
|
339
471
|
sysparm_fields: "value,sys_id",
|
|
340
472
|
sysparm_limit: 1,
|
|
341
473
|
},
|
|
342
474
|
})
|
|
475
|
+
.catch(() => null)
|
|
476
|
+
|
|
477
|
+
const entry = prop?.data?.result?.[0]
|
|
478
|
+
if (!entry?.value) continue
|
|
343
479
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
// Delete the property after reading
|
|
349
|
-
const propSysId = propResponse.data.result[0].sys_id
|
|
350
|
-
if (propSysId) {
|
|
351
|
-
await client.delete(`/api/now/table/sys_properties/${propSysId}`).catch(() => {})
|
|
352
|
-
}
|
|
353
|
-
break
|
|
354
|
-
} catch (parseErr) {
|
|
355
|
-
// Continue polling
|
|
356
|
-
}
|
|
480
|
+
try {
|
|
481
|
+
result = JSON.parse(entry.value) as Record<string, unknown>
|
|
482
|
+
if (entry.sys_id) {
|
|
483
|
+
await client.delete(`/api/now/table/sys_properties/${entry.sys_id}`).catch(() => {})
|
|
357
484
|
}
|
|
358
|
-
|
|
359
|
-
|
|
485
|
+
break
|
|
486
|
+
} catch {
|
|
487
|
+
continue
|
|
360
488
|
}
|
|
361
489
|
}
|
|
362
490
|
|
|
363
|
-
// Step 4: Format and return results
|
|
364
491
|
if (result) {
|
|
365
|
-
|
|
366
|
-
try {
|
|
367
|
-
await client.delete(`/api/now/table/sysauto_script/${jobSysId}`)
|
|
368
|
-
} catch (cleanupError) {
|
|
369
|
-
// Ignore cleanup errors
|
|
370
|
-
}
|
|
492
|
+
await client.delete(`/api/now/table/sysauto_script/${jobId}`).catch(() => {})
|
|
371
493
|
|
|
372
|
-
|
|
494
|
+
const output = (result.output || []) as Array<{ level: string; message: string }>
|
|
373
495
|
const organized = {
|
|
374
|
-
print:
|
|
375
|
-
info:
|
|
376
|
-
warn:
|
|
377
|
-
error:
|
|
496
|
+
print: output.filter((o) => o.level === "print").map((o) => o.message),
|
|
497
|
+
info: output.filter((o) => o.level === "info").map((o) => o.message),
|
|
498
|
+
warn: output.filter((o) => o.level === "warn").map((o) => o.message),
|
|
499
|
+
error: output.filter((o) => o.level === "error").map((o) => o.message),
|
|
378
500
|
success: result.success,
|
|
379
501
|
}
|
|
380
502
|
|
|
381
|
-
const
|
|
503
|
+
const data: Record<string, unknown> = {
|
|
382
504
|
executed: true,
|
|
383
505
|
success: result.success,
|
|
384
506
|
result: result.result,
|
|
@@ -387,51 +509,77 @@ gs.info('${outputMarker}:DONE');
|
|
|
387
509
|
raw_output: result.output,
|
|
388
510
|
execution_time_ms: result.executionTimeMs,
|
|
389
511
|
execution_id: executionId,
|
|
390
|
-
auto_confirmed: autoConfirm,
|
|
391
|
-
security_analysis: securityAnalysis,
|
|
512
|
+
auto_confirmed: params.autoConfirm,
|
|
513
|
+
security_analysis: params.securityAnalysis,
|
|
514
|
+
fallback_warning: "Executed via scheduler fallback. The sync REST API endpoint could not be reached.",
|
|
392
515
|
}
|
|
393
516
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
successData.warnings = es5Warnings
|
|
517
|
+
if (params.es5Warnings.length > 0) {
|
|
518
|
+
data.warnings = params.es5Warnings
|
|
397
519
|
}
|
|
398
520
|
|
|
399
|
-
return createSuccessResult(
|
|
521
|
+
return createSuccessResult(data, {
|
|
400
522
|
script_length: params.script.length,
|
|
401
523
|
method: "sysauto_script_with_trigger",
|
|
402
|
-
description,
|
|
524
|
+
description: params.description,
|
|
403
525
|
})
|
|
404
|
-
}
|
|
405
|
-
// Script was saved but execution couldn't be confirmed
|
|
406
|
-
// DO NOT delete the job - user may want to trigger it manually
|
|
407
|
-
const pendingData: any = {
|
|
408
|
-
executed: false,
|
|
409
|
-
execution_id: executionId,
|
|
410
|
-
scheduled_job_sys_id: jobSysId,
|
|
411
|
-
job_name: jobName,
|
|
412
|
-
auto_confirmed: autoConfirm,
|
|
413
|
-
security_analysis: securityAnalysis,
|
|
414
|
-
message:
|
|
415
|
-
"Script was saved as scheduled job but automatic execution could not be confirmed. The sys_trigger may not have been created (permissions) or the scheduler has not yet picked it up.",
|
|
416
|
-
action_required: `Navigate to System Scheduler > Scheduled Jobs and run: ${jobName}`,
|
|
417
|
-
manual_url: `${context.instanceUrl}/sysauto_script.do?sys_id=${jobSysId}`,
|
|
418
|
-
}
|
|
526
|
+
}
|
|
419
527
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
528
|
+
const pending: Record<string, unknown> = {
|
|
529
|
+
executed: false,
|
|
530
|
+
execution_id: executionId,
|
|
531
|
+
scheduled_job_sys_id: jobId,
|
|
532
|
+
job_name: name,
|
|
533
|
+
auto_confirmed: params.autoConfirm,
|
|
534
|
+
security_analysis: params.securityAnalysis,
|
|
535
|
+
message:
|
|
536
|
+
"Script was saved as scheduled job but automatic execution could not be confirmed. The sys_trigger may not have been created (permissions) or the scheduler has not yet picked it up.",
|
|
537
|
+
action_required: `Navigate to System Scheduler > Scheduled Jobs and run: ${name}`,
|
|
538
|
+
manual_url: `${context.instanceUrl}/sysauto_script.do?sys_id=${jobId}`,
|
|
539
|
+
fallback_warning: "Both sync REST API and scheduler fallback failed to confirm execution.",
|
|
540
|
+
}
|
|
424
541
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
method: "scheduled_job_pending",
|
|
428
|
-
description,
|
|
429
|
-
})
|
|
542
|
+
if (params.es5Warnings.length > 0) {
|
|
543
|
+
pending.warnings = params.es5Warnings
|
|
430
544
|
}
|
|
545
|
+
|
|
546
|
+
return createSuccessResult(pending, {
|
|
547
|
+
script_length: params.script.length,
|
|
548
|
+
method: "scheduled_job_pending",
|
|
549
|
+
description: params.description,
|
|
550
|
+
})
|
|
431
551
|
}
|
|
432
552
|
|
|
433
|
-
function
|
|
434
|
-
|
|
553
|
+
async function executeScript(
|
|
554
|
+
params: {
|
|
555
|
+
script: string
|
|
556
|
+
description: string
|
|
557
|
+
timeout: number
|
|
558
|
+
securityAnalysis: Record<string, unknown>
|
|
559
|
+
autoConfirm: boolean
|
|
560
|
+
es5Warnings: string[]
|
|
561
|
+
},
|
|
562
|
+
context: ServiceNowContext,
|
|
563
|
+
): Promise<ToolResult> {
|
|
564
|
+
const executionId = `exec_${Date.now()}_${crypto.randomBytes(6).toString("hex")}`
|
|
565
|
+
|
|
566
|
+
const syncResult = await executeViaSyncApi({ ...params, executionId }, context)
|
|
567
|
+
if (syncResult) return syncResult
|
|
568
|
+
|
|
569
|
+
const ok = await ensureEndpoint(context).catch(() => false)
|
|
570
|
+
if (ok) {
|
|
571
|
+
const retry = await executeViaSyncApi({ ...params, executionId }, context)
|
|
572
|
+
if (retry) return retry
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
return executeViaScheduler(params, context)
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function validateES5(code: string): {
|
|
579
|
+
valid: boolean
|
|
580
|
+
violations: Array<{ type: string; line: number; code: string; fix: string }>
|
|
581
|
+
} {
|
|
582
|
+
const violations: Array<{ type: string; line: number; code: string; fix: string }> = []
|
|
435
583
|
|
|
436
584
|
const patterns = [
|
|
437
585
|
{ regex: /\b(const|let)\s+/g, type: "const/let", fix: "Use 'var'" },
|
|
@@ -442,57 +590,55 @@ function validateES5(code: string): { valid: boolean; violations: any[] } {
|
|
|
442
590
|
{ regex: /class\s+\w+/g, type: "class", fix: "Use function constructor" },
|
|
443
591
|
]
|
|
444
592
|
|
|
445
|
-
|
|
593
|
+
for (const pattern of patterns) {
|
|
446
594
|
let match
|
|
447
|
-
while ((match = regex.exec(code)) !== null) {
|
|
595
|
+
while ((match = pattern.regex.exec(code)) !== null) {
|
|
448
596
|
violations.push({
|
|
449
|
-
type,
|
|
597
|
+
type: pattern.type,
|
|
450
598
|
line: code.substring(0, match.index).split("\n").length,
|
|
451
599
|
code: match[0],
|
|
452
|
-
fix,
|
|
600
|
+
fix: pattern.fix,
|
|
453
601
|
})
|
|
454
602
|
}
|
|
455
|
-
}
|
|
603
|
+
}
|
|
456
604
|
|
|
457
605
|
return { valid: violations.length === 0, violations }
|
|
458
606
|
}
|
|
459
607
|
|
|
460
|
-
function analyzeScriptSecurity(script: string):
|
|
608
|
+
function analyzeScriptSecurity(script: string): Record<string, unknown> {
|
|
461
609
|
const analysis = {
|
|
462
|
-
riskLevel: "LOW",
|
|
610
|
+
riskLevel: "LOW" as string,
|
|
463
611
|
warnings: [] as string[],
|
|
464
612
|
dataOperations: [] as string[],
|
|
465
613
|
systemAccess: [] as string[],
|
|
466
614
|
}
|
|
467
615
|
|
|
468
|
-
const
|
|
469
|
-
|
|
470
|
-
const
|
|
616
|
+
const modification = [/\.insert\(\)/gi, /\.update\(\)/gi, /\.deleteRecord\(\)/gi, /\.setValue\(/gi]
|
|
617
|
+
const system = [/gs\.getUser\(\)/gi, /gs\.getUserID\(\)/gi, /gs\.hasRole\(/gi, /gs\.executeNow\(/gi]
|
|
618
|
+
const dangerous = [/eval\(/gi, /new Function\(/gi, /\.setWorkflow\(/gi]
|
|
471
619
|
|
|
472
|
-
const
|
|
473
|
-
|
|
474
|
-
dataModificationPatterns.forEach((pattern) => {
|
|
620
|
+
for (const pattern of modification) {
|
|
475
621
|
const matches = script.match(pattern)
|
|
476
622
|
if (matches) {
|
|
477
623
|
analysis.dataOperations.push(...matches)
|
|
478
624
|
if (analysis.riskLevel === "LOW") analysis.riskLevel = "MEDIUM"
|
|
479
625
|
}
|
|
480
|
-
}
|
|
626
|
+
}
|
|
481
627
|
|
|
482
|
-
|
|
628
|
+
for (const pattern of system) {
|
|
483
629
|
const matches = script.match(pattern)
|
|
484
630
|
if (matches) {
|
|
485
631
|
analysis.systemAccess.push(...matches)
|
|
486
632
|
}
|
|
487
|
-
}
|
|
633
|
+
}
|
|
488
634
|
|
|
489
|
-
|
|
635
|
+
for (const pattern of dangerous) {
|
|
490
636
|
const matches = script.match(pattern)
|
|
491
637
|
if (matches) {
|
|
492
638
|
analysis.warnings.push(`Potentially dangerous operation detected: ${matches[0]}`)
|
|
493
639
|
analysis.riskLevel = "HIGH"
|
|
494
640
|
}
|
|
495
|
-
}
|
|
641
|
+
}
|
|
496
642
|
|
|
497
643
|
if (script.includes("while") && (script.includes(".next()") || script.includes(".hasNext()"))) {
|
|
498
644
|
analysis.warnings.push("Script contains loops that may process many records")
|
|
@@ -502,52 +648,51 @@ function analyzeScriptSecurity(script: string): any {
|
|
|
502
648
|
return analysis
|
|
503
649
|
}
|
|
504
650
|
|
|
505
|
-
function generateConfirmationPrompt(
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
651
|
+
function generateConfirmationPrompt(ctx: {
|
|
652
|
+
script: string
|
|
653
|
+
description: string
|
|
654
|
+
runAsUser: string | undefined
|
|
655
|
+
allowDataModification: boolean
|
|
656
|
+
securityAnalysis: Record<string, unknown>
|
|
657
|
+
}): string {
|
|
658
|
+
const risk = ctx.securityAnalysis.riskLevel as string
|
|
659
|
+
const emoji = risk === "HIGH" ? "RED" : risk === "MEDIUM" ? "YELLOW" : "GREEN"
|
|
660
|
+
const ops = ctx.securityAnalysis.dataOperations as string[]
|
|
661
|
+
const access = ctx.securityAnalysis.systemAccess as string[]
|
|
662
|
+
const warns = ctx.securityAnalysis.warnings as string[]
|
|
514
663
|
|
|
515
664
|
return `
|
|
516
|
-
|
|
665
|
+
SCRIPT EXECUTION REQUEST
|
|
517
666
|
|
|
518
|
-
|
|
667
|
+
Description: ${ctx.description}
|
|
519
668
|
|
|
520
|
-
|
|
669
|
+
Security Risk Level: ${emoji} ${risk}
|
|
521
670
|
|
|
522
|
-
|
|
523
|
-
|
|
671
|
+
Run as User: ${ctx.runAsUser || "Current User"}
|
|
672
|
+
Data Modification: ${ctx.allowDataModification ? "ALLOWED" : "READ-ONLY"}
|
|
524
673
|
|
|
525
|
-
|
|
526
|
-
${
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
: ""
|
|
530
|
-
}
|
|
531
|
-
${securityAnalysis.systemAccess.length > 0 ? `🔧 System Access: ${securityAnalysis.systemAccess.join(", ")}` : ""}
|
|
532
|
-
${securityAnalysis.warnings.length > 0 ? `⚠️ Warnings: ${securityAnalysis.warnings.join(", ")}` : ""}
|
|
674
|
+
Script Analysis:
|
|
675
|
+
${ops.length > 0 ? `Data Operations Detected: ${ops.join(", ")}` : ""}
|
|
676
|
+
${access.length > 0 ? `System Access: ${access.join(", ")}` : ""}
|
|
677
|
+
${warns.length > 0 ? `Warnings: ${warns.join(", ")}` : ""}
|
|
533
678
|
|
|
534
|
-
|
|
679
|
+
Script to Execute:
|
|
535
680
|
\`\`\`javascript
|
|
536
|
-
${script}
|
|
681
|
+
${ctx.script}
|
|
537
682
|
\`\`\`
|
|
538
683
|
|
|
539
|
-
|
|
684
|
+
Impact: This script will run in ServiceNow's server-side JavaScript context with full API access.
|
|
540
685
|
|
|
541
|
-
|
|
686
|
+
Do you want to proceed with executing this script?
|
|
542
687
|
|
|
543
688
|
Reply with:
|
|
544
|
-
-
|
|
545
|
-
-
|
|
546
|
-
-
|
|
689
|
+
- YES - Execute the script
|
|
690
|
+
- NO - Cancel execution
|
|
691
|
+
- MODIFY - Make changes before execution
|
|
547
692
|
|
|
548
|
-
|
|
693
|
+
Only proceed if you understand what this script does and trust its source!
|
|
549
694
|
`.trim()
|
|
550
695
|
}
|
|
551
696
|
|
|
552
|
-
export const version = "
|
|
697
|
+
export const version = "3.0.0"
|
|
553
698
|
export const author = "Snow-Flow SDK"
|