@loadmill/droid-cua 2.17.0 → 2.19.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 (33) hide show
  1. package/README.md +18 -0
  2. package/build/index.js +16 -1
  3. package/build/src/browser/playwright/actions.js +17 -8
  4. package/build/src/cli/headless-execution-config.js +6 -0
  5. package/build/src/cli/headless-report.js +113 -0
  6. package/build/src/cli/headless-runner.js +40 -6
  7. package/build/src/commands/help.js +1 -0
  8. package/build/src/commands/run.js +2 -1
  9. package/build/src/core/execution-engine.js +125 -2
  10. package/build/src/core/prompts.js +1 -1
  11. package/build/src/core/report/generate-html-report.js +88 -0
  12. package/build/src/core/report/parse-run-report.js +222 -0
  13. package/build/src/core/report/render-html-report.js +317 -0
  14. package/build/src/core/report/report-file-name.js +31 -0
  15. package/build/src/core/report/run-log-model.js +146 -0
  16. package/build/src/core/report/run-report-model.js +143 -0
  17. package/build/src/core/secrets.js +116 -0
  18. package/build/src/core/tool-handlers/close-current-tab.js +3 -3
  19. package/build/src/core/tool-handlers/navigate.js +3 -3
  20. package/build/src/core/tool-handlers/preview-click.js +6 -6
  21. package/build/src/core/tool-handlers/type-sensitive-text.js +62 -0
  22. package/build/src/core/tool-handlers/utils.js +3 -3
  23. package/build/src/device/android/actions.js +13 -8
  24. package/build/src/device/cloud/actions.js +17 -8
  25. package/build/src/device/ios/actions.js +17 -8
  26. package/build/src/device/openai.js +17 -0
  27. package/build/src/device/sensitive-action.js +61 -0
  28. package/build/src/modes/design-mode-ink.js +3 -1
  29. package/build/src/modes/design-mode.js +3 -1
  30. package/build/src/modes/execution-mode.js +17 -0
  31. package/build/src/prompts/design.js +4 -3
  32. package/build/src/prompts/execution.js +15 -1
  33. package/package.json +1 -1
@@ -0,0 +1,116 @@
1
+ import path from "node:path";
2
+ import { readFile } from "node:fs/promises";
3
+ import { parse as parseDotenv } from "dotenv";
4
+ export const SECRETS_FILE_NAME = ".secrets";
5
+ export function normalizeSecretKeys(keys) {
6
+ if (!Array.isArray(keys))
7
+ return [];
8
+ return keys
9
+ .map((key) => (typeof key === "string" ? key.trim() : ""))
10
+ .filter(Boolean)
11
+ .sort((a, b) => a.localeCompare(b));
12
+ }
13
+ function sortedKeys(secretsByKey = {}) {
14
+ return normalizeSecretKeys(Object.keys(secretsByKey));
15
+ }
16
+ function mergeSecretMaps(...maps) {
17
+ const merged = {};
18
+ for (const map of maps) {
19
+ if (!map || typeof map !== "object")
20
+ continue;
21
+ for (const [key, value] of Object.entries(map)) {
22
+ if (!key)
23
+ continue;
24
+ merged[key] = String(value ?? "");
25
+ }
26
+ }
27
+ return merged;
28
+ }
29
+ export function getDefaultSecretsPath(basePath = process.cwd()) {
30
+ return path.join(basePath, SECRETS_FILE_NAME);
31
+ }
32
+ export function parseSecretsOverrideString(rawValue) {
33
+ if (rawValue == null || rawValue === "") {
34
+ return {};
35
+ }
36
+ if (typeof rawValue !== "string") {
37
+ throw new Error("--secrets must be a comma-separated KEY=value string.");
38
+ }
39
+ const parsed = {};
40
+ for (const rawEntry of rawValue.split(",")) {
41
+ const entry = rawEntry.trim();
42
+ if (!entry)
43
+ continue;
44
+ const separatorIndex = entry.indexOf("=");
45
+ if (separatorIndex < 0) {
46
+ throw new Error("--secrets entries must use KEY=value format.");
47
+ }
48
+ const key = entry.slice(0, separatorIndex).trim();
49
+ if (!key) {
50
+ throw new Error("--secrets entries must include a non-empty key.");
51
+ }
52
+ parsed[key] = entry.slice(separatorIndex + 1);
53
+ }
54
+ return parsed;
55
+ }
56
+ export async function loadSecretsFile(filePath) {
57
+ if (!filePath) {
58
+ return {
59
+ status: "missing",
60
+ secretsByKey: {},
61
+ path: null,
62
+ message: null,
63
+ };
64
+ }
65
+ try {
66
+ const content = await readFile(filePath, "utf-8");
67
+ return {
68
+ status: "loaded",
69
+ secretsByKey: parseDotenv(content),
70
+ path: filePath,
71
+ message: null,
72
+ };
73
+ }
74
+ catch (error) {
75
+ if (error?.code === "ENOENT") {
76
+ return {
77
+ status: "missing",
78
+ secretsByKey: {},
79
+ path: filePath,
80
+ message: null,
81
+ };
82
+ }
83
+ const message = error instanceof Error ? error.message : "Unknown file read error.";
84
+ return {
85
+ status: "failed",
86
+ secretsByKey: {},
87
+ path: filePath,
88
+ message,
89
+ };
90
+ }
91
+ }
92
+ /**
93
+ * @param {Object} [options]
94
+ * @param {string|null} [options.secretsPath] Path to a `.secrets` file. Pass `null`
95
+ * to skip file loading entirely (resolves to no file-based secrets).
96
+ * @param {string|null} [options.cliSecrets] Comma-separated KEY=value overrides.
97
+ */
98
+ export async function resolveExecutionSecrets({ secretsPath = getDefaultSecretsPath(), cliSecrets = null, } = {}) {
99
+ const fileResult = await loadSecretsFile(secretsPath);
100
+ if (fileResult.status === "failed") {
101
+ throw new Error(`Could not read secrets file ${fileResult.path}: ${fileResult.message}`);
102
+ }
103
+ const cliSecretsByKey = parseSecretsOverrideString(cliSecrets);
104
+ const secretsByKey = mergeSecretMaps(fileResult.secretsByKey, cliSecretsByKey);
105
+ const availableSecretKeys = sortedKeys(secretsByKey);
106
+ return {
107
+ secretsByKey,
108
+ availableSecretKeys,
109
+ sourceMetadata: {
110
+ filePath: fileResult.path,
111
+ fileStatus: fileResult.status,
112
+ fileKeyCount: sortedKeys(fileResult.secretsByKey).length,
113
+ cliKeyCount: sortedKeys(cliSecretsByKey).length,
114
+ },
115
+ };
116
+ }
@@ -2,7 +2,7 @@ import { normalizeBrowserInfoForSession } from "../../browser/normalize.js";
2
2
  import { CAPTURE_SOURCES } from "../capture-sources.js";
3
3
  import { printCliOutput } from "../../utils/console-output.js";
4
4
  import { updateBrowserSessionInfo } from "./browser-session.js";
5
- import { buildFunctionCallOutputItem, buildPreviewOutputContent, buildTextToolOutputItem, } from "./utils.js";
5
+ import { buildFunctionCallOutputItem, buildToolOutputContent, buildTextToolOutputItem, } from "./utils.js";
6
6
  export async function handleCloseCurrentTabToolCall({ engine, callId, context = null, stepContext = null }) {
7
7
  const session = engine.session;
8
8
  if (session?.targetKind !== "browser" && session?.deviceInfo?.platform !== "browser") {
@@ -44,9 +44,9 @@ export async function handleCloseCurrentTabToolCall({ engine, callId, context =
44
44
  });
45
45
  return {
46
46
  kind: "continue",
47
- outputItem: buildFunctionCallOutputItem(callId, buildPreviewOutputContent({
47
+ outputItem: buildFunctionCallOutputItem(callId, buildToolOutputContent({
48
48
  text: result.message,
49
- annotatedScreenshotBase64: screenshotBase64,
49
+ imageBase64: screenshotBase64,
50
50
  })),
51
51
  currentScreenshot,
52
52
  };
@@ -3,7 +3,7 @@ import { buildExecutionNavigationRecoveryPrompt } from "../../prompts/execution.
3
3
  import { emitDesktopDebug } from "../../utils/desktop-debug.js";
4
4
  import { printCliOutput } from "../../utils/console-output.js";
5
5
  import { updateBrowserSessionInfo } from "./browser-session.js";
6
- import { buildDebugContext, buildFunctionCallOutputItem, buildPreviewOutputContent, buildTextToolOutputItem, isDesignContext, parseStringToolArgument, } from "./utils.js";
6
+ import { buildDebugContext, buildFunctionCallOutputItem, buildToolOutputContent, buildTextToolOutputItem, isDesignContext, parseStringToolArgument, } from "./utils.js";
7
7
  export async function handleNavigateToolCall({ engine, callId, argumentsText, context = null, stepContext = null }) {
8
8
  const session = engine.session;
9
9
  const parsed = parseStringToolArgument(argumentsText, "target", "navigate");
@@ -69,9 +69,9 @@ export async function handleNavigateToolCall({ engine, callId, argumentsText, co
69
69
  const screenshotBase64 = await engine.getScreenshotAsBase64(session.deviceId, session.deviceInfo);
70
70
  return {
71
71
  kind: "continue",
72
- outputItem: buildFunctionCallOutputItem(callId, buildPreviewOutputContent({
72
+ outputItem: buildFunctionCallOutputItem(callId, buildToolOutputContent({
73
73
  text: `Navigation complete: ${normalizedUrl}`,
74
- annotatedScreenshotBase64: screenshotBase64,
74
+ imageBase64: screenshotBase64,
75
75
  })),
76
76
  };
77
77
  }
@@ -1,7 +1,7 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { createAnnotatedScreenshotFromBase64 } from "../../utils/run-screenshot-recorder.js";
3
3
  import { CAPTURE_SOURCES } from "../capture-sources.js";
4
- import { buildFunctionCallOutputItem, buildPreviewOutputContent, } from "./utils.js";
4
+ import { buildFunctionCallOutputItem, buildToolOutputContent, } from "./utils.js";
5
5
  function buildPreviewClickValidationError(argumentsText) {
6
6
  if (!argumentsText || typeof argumentsText !== "string") {
7
7
  return "Invalid preview_click arguments: expected a JSON object with numeric x and y.";
@@ -42,7 +42,7 @@ export async function handlePreviewClick({ engine, callId, argumentsText, contex
42
42
  const session = engine.session;
43
43
  const parsedArguments = parsePreviewClickArguments(argumentsText);
44
44
  if (!parsedArguments.ok) {
45
- return continueWith(buildFunctionCallOutputItem(callId, buildPreviewOutputContent({ text: parsedArguments.message })));
45
+ return continueWith(buildFunctionCallOutputItem(callId, buildToolOutputContent({ text: parsedArguments.message })));
46
46
  }
47
47
  try {
48
48
  const screenshotBase64 = await engine.getScreenshotAsBase64(session.deviceId, session.deviceInfo);
@@ -52,7 +52,7 @@ export async function handlePreviewClick({ engine, callId, argumentsText, contex
52
52
  style: "preview",
53
53
  });
54
54
  if (!previewResult?.screenshotBase64 || !previewResult?.marker) {
55
- return continueWith(buildFunctionCallOutputItem(callId, buildPreviewOutputContent({
55
+ return continueWith(buildFunctionCallOutputItem(callId, buildToolOutputContent({
56
56
  text: "preview_click failed: could not annotate the current screenshot.",
57
57
  })));
58
58
  }
@@ -99,13 +99,13 @@ export async function handlePreviewClick({ engine, callId, argumentsText, contex
99
99
  },
100
100
  });
101
101
  }
102
- return continueWith(buildFunctionCallOutputItem(callId, buildPreviewOutputContent({
102
+ return continueWith(buildFunctionCallOutputItem(callId, buildToolOutputContent({
103
103
  text: previewText,
104
- annotatedScreenshotBase64: previewResult.screenshotBase64,
104
+ imageBase64: previewResult.screenshotBase64,
105
105
  })));
106
106
  }
107
107
  catch (error) {
108
- return continueWith(buildFunctionCallOutputItem(callId, buildPreviewOutputContent({
108
+ return continueWith(buildFunctionCallOutputItem(callId, buildToolOutputContent({
109
109
  text: `preview_click failed: ${error instanceof Error ? error.message : "unable to capture or annotate the current screen."}`,
110
110
  })));
111
111
  }
@@ -0,0 +1,62 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { CAPTURE_SOURCES } from "../capture-sources.js";
3
+ import { buildFunctionCallOutputItem, buildToolOutputContent, buildTextToolOutputItem, parseStringToolArgument, } from "./utils.js";
4
+ export async function handleTypeSensitiveTextToolCall({ engine, callId, argumentsText, context = null, stepContext = null }) {
5
+ const parsed = parseStringToolArgument(argumentsText, "key", "type_sensitive_text");
6
+ if (!parsed.ok) {
7
+ return {
8
+ kind: "continue",
9
+ outputItem: buildTextToolOutputItem(callId, parsed.message),
10
+ };
11
+ }
12
+ const key = parsed.value;
13
+ const secretsByKey = engine.secretsByKey && typeof engine.secretsByKey === "object" ? engine.secretsByKey : {};
14
+ if (!Object.prototype.hasOwnProperty.call(secretsByKey, key)) {
15
+ return {
16
+ kind: "continue",
17
+ outputItem: buildTextToolOutputItem(callId, `Secret key not found: ${key}`),
18
+ };
19
+ }
20
+ const action = {
21
+ type: "type",
22
+ text: String(secretsByKey[key] ?? ""),
23
+ sensitive: true,
24
+ sensitiveKey: key,
25
+ };
26
+ try {
27
+ const result = await engine.handleModelAction(engine.session.deviceId, action, engine.session.deviceInfo.scale, {
28
+ ...context,
29
+ stepId: stepContext?.stepId,
30
+ instructionIndex: stepContext?.instructionIndex,
31
+ eventId: randomUUID(),
32
+ });
33
+ if (result?.success === false) {
34
+ return {
35
+ kind: "fail",
36
+ error: `Sensitive typing failed for ${key}.`,
37
+ };
38
+ }
39
+ }
40
+ catch {
41
+ return {
42
+ kind: "fail",
43
+ error: `Sensitive typing failed for ${key}.`,
44
+ };
45
+ }
46
+ const screenshotBase64 = await engine.getScreenshotAsBase64(engine.session.deviceId, engine.session.deviceInfo);
47
+ const currentScreenshot = await engine.recordScreenshot(screenshotBase64, {
48
+ runId: context?.runId,
49
+ sessionId: context?.sessionId,
50
+ stepId: stepContext?.stepId,
51
+ instructionIndex: stepContext?.instructionIndex,
52
+ captureSource: CAPTURE_SOURCES.CALL_OUTPUT_POST_ACTION,
53
+ });
54
+ return {
55
+ kind: "continue",
56
+ outputItem: buildFunctionCallOutputItem(callId, buildToolOutputContent({
57
+ text: `Typed secret: ${key}`,
58
+ imageBase64: screenshotBase64,
59
+ })),
60
+ currentScreenshot,
61
+ };
62
+ }
@@ -45,12 +45,12 @@ export function buildFunctionCallOutputItem(callId, output) {
45
45
  export function buildTextToolOutputItem(callId, text) {
46
46
  return buildFunctionCallOutputItem(callId, [{ type: "input_text", text }]);
47
47
  }
48
- export function buildPreviewOutputContent({ text, annotatedScreenshotBase64 = null }) {
48
+ export function buildToolOutputContent({ text, imageBase64 = null }) {
49
49
  const content = [{ type: "input_text", text }];
50
- if (annotatedScreenshotBase64) {
50
+ if (imageBase64) {
51
51
  content.push({
52
52
  type: "input_image",
53
- image_url: `data:image/png;base64,${annotatedScreenshotBase64}`,
53
+ image_url: `data:image/png;base64,${imageBase64}`,
54
54
  detail: "high",
55
55
  });
56
56
  }
@@ -3,6 +3,7 @@ import { emitDesktopDebug, truncateForDebug } from "../../utils/desktop-debug.js
3
3
  import { getConfiguredStepDelayMs } from "../../utils/step-delay.js";
4
4
  import { buildActionEventMeta } from "../action-event-meta.js";
5
5
  import { buildMobileKeypressPayload, getAndroidAdbTransport, normalizeMobileKeypress } from "../mobile-keypress.js";
6
+ import { actionForErrorLog, buildActionErrorOutput, buildDebugTextField, buildSensitiveDebugFields, buildTypeActionPayload, buildTypeActionText, isSensitiveAction, } from "../sensitive-action.js";
6
7
  import { resolveScrollGesture } from "../scroll-gesture.js";
7
8
  import { execAdb } from "./tools.js";
8
9
  function adbShell(deviceId, command) {
@@ -34,7 +35,8 @@ export async function handleModelAction(deviceId, action, scale = 1.0, context =
34
35
  y1,
35
36
  x2,
36
37
  y2,
37
- text: typeof text === "string" ? truncateForDebug(text, 300) : undefined,
38
+ text: buildDebugTextField(action, truncateForDebug),
39
+ ...buildSensitiveDebugFields(action),
38
40
  keyCount: Array.isArray(keys) ? keys.length : 0,
39
41
  pathPoints: Array.isArray(path) ? path.length : 0
40
42
  });
@@ -79,7 +81,7 @@ export async function handleModelAction(deviceId, action, scale = 1.0, context =
79
81
  }
80
82
  break;
81
83
  case "type":
82
- addOutput({ type: 'action', text: `Typing text: ${text}`, ...meta({ text }) });
84
+ addOutput({ type: 'action', text: buildTypeActionText(action, text), ...meta(buildTypeActionPayload(action, text)) });
83
85
  const escapedText = text.replace(/(["\\$`])/g, "\\$1").replace(/ /g, "%s");
84
86
  await adbShell(deviceId, `input text "${escapedText}"`);
85
87
  break;
@@ -104,13 +106,15 @@ export async function handleModelAction(deviceId, action, scale = 1.0, context =
104
106
  default:
105
107
  addOutput({ type: 'info', text: `Unknown action: ${JSON.stringify(action)}` });
106
108
  }
109
+ return { success: true };
107
110
  }
108
111
  catch (error) {
112
+ const outputError = buildActionErrorOutput(action, error);
109
113
  // Log full error details to file
110
114
  logger.error('Action execution error', {
111
- action,
112
- message: error.message,
113
- stack: error.stack
115
+ action: actionForErrorLog(action),
116
+ message: outputError.message,
117
+ stack: isSensitiveAction(action) ? undefined : error.stack
114
118
  });
115
119
  emitDesktopDebug("device.error", "device", {
116
120
  runId: context?.runId,
@@ -121,12 +125,12 @@ export async function handleModelAction(deviceId, action, scale = 1.0, context =
121
125
  platform: "android",
122
126
  operation: "action.execute",
123
127
  actionType: action?.type,
124
- message: error.message
128
+ message: outputError.message
125
129
  });
126
130
  // Show user-friendly error message
127
131
  addOutput({
128
132
  type: 'error',
129
- text: `Error executing action: ${error.message}`,
133
+ text: outputError.text,
130
134
  eventType: 'error',
131
135
  actionType: action?.type,
132
136
  runId: context?.runId,
@@ -134,10 +138,11 @@ export async function handleModelAction(deviceId, action, scale = 1.0, context =
134
138
  instructionIndex: context?.instructionIndex,
135
139
  payload: {
136
140
  eventId: context?.eventId,
137
- message: error.message,
141
+ message: outputError.message,
138
142
  platform: 'android'
139
143
  }
140
144
  });
141
145
  addOutput({ type: 'info', text: 'Full error details have been logged to the debug log.' });
146
+ return { success: false, error: outputError.message };
142
147
  }
143
148
  }
@@ -2,6 +2,7 @@ import { logger } from "../../utils/logger.js";
2
2
  import { emitDesktopDebug, truncateForDebug } from "../../utils/desktop-debug.js";
3
3
  import { getConfiguredStepDelayMs } from "../../utils/step-delay.js";
4
4
  import { buildActionEventMeta } from "../action-event-meta.js";
5
+ import { actionForErrorLog, buildActionErrorOutput, buildDebugTextField, buildSensitiveDebugFields, buildTypeActionPayload, buildTypeActionText, isSensitiveAction, } from "../sensitive-action.js";
5
6
  import { buildMobileKeypressPayload, getCloudAndroidTransport, getIosTransport, normalizeMobileKeypress } from "../mobile-keypress.js";
6
7
  import { getActiveSession, getDevicePixelRatio } from "./connection.js";
7
8
  import { resolveScrollGesture } from "../scroll-gesture.js";
@@ -33,7 +34,8 @@ export async function handleModelAction(deviceId, action, scale = 1.0, context =
33
34
  deviceId,
34
35
  actionType: action?.type,
35
36
  scale,
36
- text: typeof action?.text === "string" ? truncateForDebug(action.text, 300) : undefined,
37
+ text: buildDebugTextField(action, truncateForDebug),
38
+ ...buildSensitiveDebugFields(action),
37
39
  keyCount: Array.isArray(action?.keys) ? action.keys.length : 0,
38
40
  pathPoints: Array.isArray(action?.path) ? action.path.length : 0
39
41
  });
@@ -48,7 +50,11 @@ export async function handleModelAction(deviceId, action, scale = 1.0, context =
48
50
  break;
49
51
  }
50
52
  case "type": {
51
- addOutput({ type: "action", text: `Typing text: ${action.text}`, ...meta({ text: action.text }) });
53
+ addOutput({
54
+ type: "action",
55
+ text: buildTypeActionText(action, action.text),
56
+ ...meta(buildTypeActionPayload(action, action.text))
57
+ });
52
58
  await session.client.type(session.sessionId, action.text);
53
59
  break;
54
60
  }
@@ -133,12 +139,14 @@ export async function handleModelAction(deviceId, action, scale = 1.0, context =
133
139
  default:
134
140
  addOutput({ type: "info", text: `Unknown action: ${JSON.stringify(action)}` });
135
141
  }
142
+ return { success: true };
136
143
  }
137
144
  catch (error) {
145
+ const outputError = buildActionErrorOutput(action, error);
138
146
  logger.error("Cloud action execution error", {
139
- action,
140
- message: error.message,
141
- stack: error.stack,
147
+ action: actionForErrorLog(action),
148
+ message: outputError.message,
149
+ stack: isSensitiveAction(action) ? undefined : error.stack,
142
150
  providerId: session.providerId,
143
151
  platform
144
152
  });
@@ -152,11 +160,11 @@ export async function handleModelAction(deviceId, action, scale = 1.0, context =
152
160
  source: session.providerId,
153
161
  operation: "action.execute",
154
162
  actionType: action?.type,
155
- message: error.message
163
+ message: outputError.message
156
164
  });
157
165
  addOutput({
158
166
  type: "error",
159
- text: `Error executing action: ${error.message}`,
167
+ text: outputError.text,
160
168
  eventType: "error",
161
169
  actionType: action?.type,
162
170
  runId: context?.runId,
@@ -164,11 +172,12 @@ export async function handleModelAction(deviceId, action, scale = 1.0, context =
164
172
  instructionIndex: context?.instructionIndex,
165
173
  payload: {
166
174
  eventId: context?.eventId,
167
- message: error.message,
175
+ message: outputError.message,
168
176
  platform,
169
177
  source: session.providerId
170
178
  }
171
179
  });
172
180
  addOutput({ type: "info", text: "Full error details have been logged to the debug log." });
181
+ return { success: false, error: outputError.message };
173
182
  }
174
183
  }
@@ -6,6 +6,7 @@
6
6
  import * as appium from "./appium-client.js";
7
7
  import { buildActionEventMeta } from "../action-event-meta.js";
8
8
  import { buildMobileKeypressPayload, getIosTransport, normalizeMobileKeypress } from "../mobile-keypress.js";
9
+ import { actionForErrorLog, buildActionErrorOutput, buildDebugTextField, buildSensitiveDebugFields, buildTypeActionPayload, buildTypeActionText, isSensitiveAction, } from "../sensitive-action.js";
9
10
  import { getActiveSession, getDevicePixelRatio } from "./connection.js";
10
11
  import { logger } from "../../utils/logger.js";
11
12
  import { emitDesktopDebug, truncateForDebug } from "../../utils/desktop-debug.js";
@@ -41,7 +42,8 @@ export async function handleModelAction(simulatorId, action, scale = 1.0, contex
41
42
  simulatorId,
42
43
  actionType: action?.type,
43
44
  scale,
44
- text: typeof action?.text === "string" ? truncateForDebug(action.text, 300) : undefined,
45
+ text: buildDebugTextField(action, truncateForDebug),
46
+ ...buildSensitiveDebugFields(action),
45
47
  keyCount: Array.isArray(action?.keys) ? action.keys.length : 0,
46
48
  pathPoints: Array.isArray(action?.path) ? action.path.length : 0
47
49
  });
@@ -59,7 +61,11 @@ export async function handleModelAction(simulatorId, action, scale = 1.0, contex
59
61
  break;
60
62
  }
61
63
  case "type": {
62
- addOutput({ type: "action", text: `Typing text: ${action.text}`, ...meta({ text: action.text }) });
64
+ addOutput({
65
+ type: "action",
66
+ text: buildTypeActionText(action, action.text),
67
+ ...meta(buildTypeActionPayload(action, action.text))
68
+ });
63
69
  await appium.type(session.sessionId, action.text);
64
70
  break;
65
71
  }
@@ -138,12 +144,14 @@ export async function handleModelAction(simulatorId, action, scale = 1.0, contex
138
144
  default:
139
145
  addOutput({ type: "info", text: `Unknown action: ${JSON.stringify(action)}` });
140
146
  }
147
+ return { success: true };
141
148
  }
142
149
  catch (error) {
150
+ const outputError = buildActionErrorOutput(action, error);
143
151
  logger.error("iOS action execution error", {
144
- action,
145
- message: error.message,
146
- stack: error.stack,
152
+ action: actionForErrorLog(action),
153
+ message: outputError.message,
154
+ stack: isSensitiveAction(action) ? undefined : error.stack,
147
155
  });
148
156
  emitDesktopDebug("device.error", "device", {
149
157
  runId: context?.runId,
@@ -154,11 +162,11 @@ export async function handleModelAction(simulatorId, action, scale = 1.0, contex
154
162
  platform: "ios",
155
163
  operation: "action.execute",
156
164
  actionType: action?.type,
157
- message: error.message
165
+ message: outputError.message
158
166
  });
159
167
  addOutput({
160
168
  type: "error",
161
- text: `Error executing action: ${error.message}`,
169
+ text: outputError.text,
162
170
  eventType: "error",
163
171
  actionType: action?.type,
164
172
  runId: context?.runId,
@@ -166,10 +174,11 @@ export async function handleModelAction(simulatorId, action, scale = 1.0, contex
166
174
  instructionIndex: context?.instructionIndex,
167
175
  payload: {
168
176
  eventId: context?.eventId,
169
- message: error.message,
177
+ message: outputError.message,
170
178
  platform: "ios"
171
179
  }
172
180
  });
173
181
  addOutput({ type: "info", text: "Full error details have been logged to the debug log." });
182
+ return { success: false, error: outputError.message };
174
183
  }
175
184
  }
@@ -107,6 +107,20 @@ const loadmillTool = {
107
107
  additionalProperties: false,
108
108
  },
109
109
  };
110
+ const typeSensitiveTextTool = {
111
+ type: "function",
112
+ name: "type_sensitive_text",
113
+ description: "Type a secret value by key without exposing the value to the model. Use this only for secret keys listed in the prompt.",
114
+ strict: true,
115
+ parameters: {
116
+ type: "object",
117
+ properties: {
118
+ key: { type: "string" },
119
+ },
120
+ required: ["key"],
121
+ additionalProperties: false,
122
+ },
123
+ };
110
124
  const testDesignCompleteTool = {
111
125
  type: "function",
112
126
  name: "test_design_complete",
@@ -156,6 +170,9 @@ export function buildFunctionTools({ aimAssistMode = DEFAULT_AIM_ASSIST_MODE, to
156
170
  if (toolCapabilities.loadmill) {
157
171
  tools.push(loadmillTool);
158
172
  }
173
+ if (toolCapabilities.typeSensitiveText) {
174
+ tools.push(typeSensitiveTextTool);
175
+ }
159
176
  if (toolCapabilities.testDesignComplete) {
160
177
  tools.push(testDesignCompleteTool);
161
178
  }
@@ -0,0 +1,61 @@
1
+ export function isSensitiveAction(action) {
2
+ return action?.sensitive === true;
3
+ }
4
+ export function getSensitiveKey(action) {
5
+ return typeof action?.sensitiveKey === "string" && action.sensitiveKey.trim()
6
+ ? action.sensitiveKey.trim()
7
+ : "secret";
8
+ }
9
+ export function buildTypeActionText(action, text) {
10
+ if (isSensitiveAction(action)) {
11
+ return `Typing secret: ${getSensitiveKey(action)}`;
12
+ }
13
+ return `Typing text: ${text}`;
14
+ }
15
+ export function buildTypeActionPayload(action, text) {
16
+ if (isSensitiveAction(action)) {
17
+ return {
18
+ sensitive: true,
19
+ secretKey: getSensitiveKey(action),
20
+ };
21
+ }
22
+ return { text };
23
+ }
24
+ export function buildDebugTextField(action, truncate) {
25
+ if (isSensitiveAction(action)) {
26
+ return undefined;
27
+ }
28
+ return typeof action?.text === "string" ? truncate(action.text, 300) : undefined;
29
+ }
30
+ export function buildSensitiveDebugFields(action) {
31
+ if (!isSensitiveAction(action)) {
32
+ return {};
33
+ }
34
+ return {
35
+ sensitive: true,
36
+ secretKey: getSensitiveKey(action),
37
+ };
38
+ }
39
+ export function actionForErrorLog(action) {
40
+ if (!isSensitiveAction(action)) {
41
+ return action;
42
+ }
43
+ return {
44
+ type: action?.type,
45
+ sensitive: true,
46
+ secretKey: getSensitiveKey(action),
47
+ };
48
+ }
49
+ export function buildActionErrorOutput(action, error) {
50
+ if (isSensitiveAction(action)) {
51
+ const key = getSensitiveKey(action);
52
+ return {
53
+ text: `Error typing secret: ${key}`,
54
+ message: `Sensitive typing failed for ${key}.`,
55
+ };
56
+ }
57
+ return {
58
+ text: `Error executing action: ${error.message}`,
59
+ message: error.message,
60
+ };
61
+ }
@@ -33,7 +33,8 @@ export class DesignModeInk {
33
33
  async start() {
34
34
  const addOutput = this.context.addOutput;
35
35
  // Set design mode system prompt
36
- const designPrompt = buildDesignModePrompt(this.session.deviceInfo, {});
36
+ const availableSecretKeys = this.engine.getAvailableSecretKeys();
37
+ const designPrompt = buildDesignModePrompt(this.session.deviceInfo, {}, "", { availableSecretKeys });
37
38
  this.baseDesignPrompt = designPrompt;
38
39
  this.session.setSystemPrompt(designPrompt);
39
40
  this.engine.toolCapabilities = {
@@ -41,6 +42,7 @@ export class DesignModeInk {
41
42
  navigate: this.session?.deviceInfo?.platform === "browser",
42
43
  closeCurrentTab: this.session?.deviceInfo?.platform === "browser",
43
44
  loadmill: false,
45
+ typeSensitiveText: this.engine.hasSecrets(),
44
46
  testDesignComplete: true,
45
47
  };
46
48
  this.engine.baseSystemPrompt = designPrompt;
@@ -30,7 +30,8 @@ export class DesignMode {
30
30
  */
31
31
  async start(context) {
32
32
  // Set design mode system prompt
33
- const designPrompt = buildDesignModePrompt(this.session.deviceInfo, {});
33
+ const availableSecretKeys = this.engine.getAvailableSecretKeys();
34
+ const designPrompt = buildDesignModePrompt(this.session.deviceInfo, {}, "", { availableSecretKeys });
34
35
  this.baseDesignPrompt = designPrompt;
35
36
  this.session.setSystemPrompt(designPrompt);
36
37
  this.engine.toolCapabilities = {
@@ -38,6 +39,7 @@ export class DesignMode {
38
39
  navigate: this.session?.deviceInfo?.platform === "browser",
39
40
  closeCurrentTab: this.session?.deviceInfo?.platform === "browser",
40
41
  loadmill: false,
42
+ typeSensitiveText: this.engine.hasSecrets(),
41
43
  testDesignComplete: true,
42
44
  };
43
45
  this.engine.baseSystemPrompt = designPrompt;