@scotthuang/agent-knock-knock 0.12.28 → 0.12.30

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 (67) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/src/claude-injected-paste-proof.d.ts +59 -0
  3. package/dist/src/claude-injected-paste-proof.js +153 -0
  4. package/dist/src/claude-injected-paste-proof.js.map +1 -0
  5. package/dist/src/claude-terminal-agent-adapter.js +20 -1
  6. package/dist/src/claude-terminal-agent-adapter.js.map +1 -1
  7. package/dist/src/cli-core.js +50 -35
  8. package/dist/src/cli-core.js.map +1 -1
  9. package/dist/src/codex-terminal-agent-adapter.d.ts +2 -1
  10. package/dist/src/codex-terminal-agent-adapter.js +40 -4
  11. package/dist/src/codex-terminal-agent-adapter.js.map +1 -1
  12. package/dist/src/host-adapter.d.ts +1 -1
  13. package/dist/src/host-adapter.js +1 -1
  14. package/dist/src/host-bridge-tools.js +2 -2
  15. package/dist/src/openclaw-plugin-command-adapter.js +155 -24
  16. package/dist/src/openclaw-plugin-command-adapter.js.map +1 -1
  17. package/dist/src/openclaw-plugin-helpers.d.ts +2 -0
  18. package/dist/src/openclaw-plugin-helpers.js +28 -7
  19. package/dist/src/openclaw-plugin-helpers.js.map +1 -1
  20. package/dist/src/openclaw-plugin-schemas.d.ts +104 -0
  21. package/dist/src/openclaw-plugin-schemas.js +108 -0
  22. package/dist/src/openclaw-plugin-schemas.js.map +1 -1
  23. package/dist/src/openclaw-private-authority-offers.d.ts +2 -0
  24. package/dist/src/openclaw-private-authority-offers.js +12 -0
  25. package/dist/src/openclaw-private-authority-offers.js.map +1 -1
  26. package/dist/src/terminal-agent-adapter.d.ts +34 -0
  27. package/dist/src/terminal-agent-adapter.js +54 -0
  28. package/dist/src/terminal-agent-adapter.js.map +1 -1
  29. package/dist/src/terminal-agent-bridge.d.ts +76 -1
  30. package/dist/src/terminal-agent-bridge.js +873 -153
  31. package/dist/src/terminal-agent-bridge.js.map +1 -1
  32. package/dist/src/terminal-command-cli-adapter.d.ts +2 -1
  33. package/dist/src/terminal-command-cli-adapter.js +46 -8
  34. package/dist/src/terminal-command-cli-adapter.js.map +1 -1
  35. package/dist/src/terminal-dispatch-execution.js +15 -0
  36. package/dist/src/terminal-dispatch-execution.js.map +1 -1
  37. package/dist/src/terminal-identity-authority-cli-adapter.d.ts +1 -0
  38. package/dist/src/terminal-identity-authority-cli-adapter.js +10 -1
  39. package/dist/src/terminal-identity-authority-cli-adapter.js.map +1 -1
  40. package/dist/src/terminal-interaction-authority.d.ts +33 -0
  41. package/dist/src/terminal-interaction-authority.js +3 -0
  42. package/dist/src/terminal-interaction-authority.js.map +1 -0
  43. package/dist/src/terminal-interaction-cli-adapter.d.ts +66 -0
  44. package/dist/src/terminal-interaction-cli-adapter.js +354 -0
  45. package/dist/src/terminal-interaction-cli-adapter.js.map +1 -0
  46. package/dist/src/terminal-interaction-protocol.d.ts +111 -0
  47. package/dist/src/terminal-interaction-protocol.js +432 -0
  48. package/dist/src/terminal-interaction-protocol.js.map +1 -0
  49. package/dist/src/terminal-list-renderer.js +31 -2
  50. package/dist/src/terminal-list-renderer.js.map +1 -1
  51. package/dist/src/terminal-monitor-cli-adapter.js +12 -3
  52. package/dist/src/terminal-monitor-cli-adapter.js.map +1 -1
  53. package/dist/src/terminal-monitor-decision-policy.d.ts +1 -1
  54. package/dist/src/terminal-monitor-state-cli-adapter.js +15 -1
  55. package/dist/src/terminal-monitor-state-cli-adapter.js.map +1 -1
  56. package/dist/src/terminal-mutation-cli-runtime.d.ts +44 -0
  57. package/dist/src/terminal-mutation-cli-runtime.js +41 -0
  58. package/dist/src/terminal-mutation-cli-runtime.js.map +1 -0
  59. package/dist/src/terminal-questionnaire-adapter.d.ts +84 -0
  60. package/dist/src/terminal-questionnaire-adapter.js +677 -0
  61. package/dist/src/terminal-questionnaire-adapter.js.map +1 -0
  62. package/docs/host-bridge-profiles.md +2 -2
  63. package/docs/quickstart-herdr.md +1 -1
  64. package/docs/quickstart-tmux.md +3 -1
  65. package/openclaw.plugin.json +4 -0
  66. package/package.json +1 -1
  67. package/templates/openclaw-skills/agent-knock-knock/SKILL.md +11 -8
@@ -43,7 +43,7 @@ export interface HostAdapter {
43
43
  disposeContext(authority: object): void;
44
44
  }
45
45
  /**
46
- * Adapt AKK's established slash command and 16 semantic tools to a native Host.
46
+ * Adapt AKK's established slash command and 17 semantic tools to a native Host.
47
47
  *
48
48
  * Public metadata is captured exactly once. Executable registries are created
49
49
  * lazily per exact Host authority object so private approvals, action offers,
@@ -9,7 +9,7 @@ const METADATA_CONTEXT = Object.freeze({
9
9
  sessionId: "host-adapter:metadata"
10
10
  });
11
11
  /**
12
- * Adapt AKK's established slash command and 16 semantic tools to a native Host.
12
+ * Adapt AKK's established slash command and 17 semantic tools to a native Host.
13
13
  *
14
14
  * Public metadata is captured exactly once. Executable registries are created
15
15
  * lazily per exact Host authority object so private approvals, action offers,
@@ -97,8 +97,8 @@ export function createHostBridgeToolRegistry(options) {
97
97
  if (!commandDescriptor) {
98
98
  throw new Error("host bridge did not capture the akk command");
99
99
  }
100
- if (descriptors.length !== 16) {
101
- throw new Error(`host bridge expected 16 semantic tools, received ${descriptors.length}`);
100
+ if (descriptors.length !== 17) {
101
+ throw new Error(`host bridge expected 17 semantic tools, received ${descriptors.length}`);
102
102
  }
103
103
  const listed = Object.freeze([...descriptors]);
104
104
  const capturedCommand = commandDescriptor;
@@ -3,10 +3,12 @@ import { AsyncLocalStorage } from "node:async_hooks";
3
3
  import { createHash, randomUUID } from "node:crypto";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { executorDefinitionForKind } from "./executors.js";
6
+ import { isTerminalApprovalDecision } from "./terminal-agent-adapter.js";
6
7
  import { isRecord, nonBlankString as stringValue } from "./value-guards.js";
7
8
  import { AKK_CALLBACK_METHOD, akkUsageText, buildAkkCommandCliArgs, formatAkkListCommandResult, formatAkkRespondCommandResult, formatAkkTerminalWatchHint, formatAkkThreadsCommandResult, formatAkkThreadTransitionCommandResult, formatAkkUnwatchCommandResult, formatAkkWatchCommandResult, formatAkkWatchStatusCommandResult, isAkkModelFacingDiagnosticField, isAkkModelFacingPrivateAuthorityField, isAkkNativeSubmissionAccepted, isAkkThreadTransitionSuccess, normalizeAkkModelFacingFieldName, parseAkkCommand, resolvePluginStoreDir, sanitizeAkkModelFacingDiagnosticText, sanitizeAkkModelFacingLegacyAuthorityInstructionText } from "./openclaw-plugin-helpers.js";
8
- import { approveParameters, cancelParameters, closeParameters, listParameters, listResumableThreadsParameters, nativeInspectParameters, newThreadParameters, reconcileBindingParameters, renewParameters, respondParameters, resumeThreadParameters, retryCallbackParameters, sendParameters, statusParameters, unwatchParameters, watchParameters } from "./openclaw-plugin-schemas.js";
9
- import { consumeOpenClawPrivateAuthorityOffer, openClawApprovalAuthorityOfferKey, rememberOpenClawPrivateAuthorityOffer } from "./openclaw-private-authority-offers.js";
9
+ import { approveParameters, cancelParameters, closeParameters, listParameters, listResumableThreadsParameters, nativeInspectParameters, newThreadParameters, reconcileBindingParameters, renewParameters, respondInteractionParameters, respondParameters, resumeThreadParameters, retryCallbackParameters, sendParameters, statusParameters, unwatchParameters, watchParameters } from "./openclaw-plugin-schemas.js";
10
+ import { consumeOpenClawPrivateAuthorityOffer, openClawApprovalAuthorityOfferKey, openClawInteractionAuthorityOfferKey, rememberOpenClawPrivateAuthorityOffer } from "./openclaw-private-authority-offers.js";
11
+ import { TERMINAL_INTERACTION_LIMITS, validateTerminalInteractionProjection, validateTerminalInteractionResponse } from "./terminal-interaction-protocol.js";
10
12
  const MAX_DISPLAYED_RESUME_SNAPSHOTS = 512;
11
13
  const OPENCLAW_HANDOFF_AUTHORITY_KIND = "handoff";
12
14
  const OPENCLAW_RECONCILE_BINDING_AUTHORITY_KIND = "reconcile_binding";
@@ -272,6 +274,7 @@ export function registerOpenClawCommands(api, displayedResumeSnapshots) {
272
274
  const result = await runHostAwareCli(api, buildStatusCliArgs(api, isRecord(params) ? params : {}));
273
275
  const rendered = toolResult(result);
274
276
  rememberDisplayedApprovalOffer(api, toolContext?.sessionKey, toolContext?.sessionId, result);
277
+ rememberDisplayedInteractionOffer(api, toolContext?.sessionKey, toolContext?.sessionId, result);
275
278
  return rendered;
276
279
  }
277
280
  catch (error) {
@@ -324,9 +327,18 @@ export function registerOpenClawCommands(api, displayedResumeSnapshots) {
324
327
  return args;
325
328
  }
326
329
  });
330
+ registerCliTool(api, {
331
+ name: "agent_knock_knock_respond_interaction",
332
+ description: "Answer exactly one current native questionnaire step shown by agent_knock_knock_status in this controller conversation. Supply only the exact turn_id, interaction_id, and typed semantic answer using advertised question_id and option_id values. AKK consumes the displayed private offer and revalidates its prompt, expiry, owner, and terminal authority before any input. Raw keys, menu indexes, rendered labels, fingerprints, versions, and terminal commands are never accepted. An uncertain response must never be retried blindly.",
333
+ parameters: respondInteractionParameters,
334
+ buildArgs: (params, toolContext) => buildPrivateInteractionResponseArgs(api, params, {
335
+ sessionKey: requiredOpenClawSessionKey(toolContext?.sessionKey),
336
+ sessionId: requiredOpenClawSessionId(toolContext?.sessionId)
337
+ })
338
+ });
327
339
  registerCliTool(api, {
328
340
  name: "agent_knock_knock_approve",
329
- description: "Manually approve the current exact permission request only after the user reviews and explicitly confirms it. Use turn_id for a managed Turn or terminal_id for a separately advertised terminal-scoped approval. AKK privately refreshes the approval prompt and terminal authority, then the CLI recaptures the same prompt under lock before sending keys. Never retry an interrupted approval blindly. This tool cannot enable automatic approval.",
341
+ description: "Dispatch one closed semantic decision for the current exact permission request only after the user reviews and explicitly chooses it. decision defaults to approve_once for compatibility; reject is available only on a managed Turn when the adapter proves a safe native reject choice. Use turn_id for a managed Turn or terminal_id for a separately advertised approve_once-only terminal action. AKK privately refreshes the prompt and authority, then recaptures it under lock. Raw keys, indexes, and labels are not accepted. Never retry an interrupted decision blindly.",
330
342
  parameters: approveParameters,
331
343
  buildArgs: (params, toolContext) => buildPrivateApprovalArgs(api, params, {
332
344
  sessionKey: requiredOpenClawSessionKey(toolContext?.sessionKey),
@@ -459,7 +471,10 @@ async function handleAkkCommand(api, ctx, displayedResumeSnapshots) {
459
471
  return await handleAkkLifecycleCommand(api, ctx, parsed, config, displayedResumeSnapshots);
460
472
  }
461
473
  const args = parsed.action === "approve"
462
- ? await buildPrivateApprovalArgs(api, { turn_id: parsed.turnId }, {
474
+ ? await buildPrivateApprovalArgs(api, {
475
+ turn_id: parsed.turnId,
476
+ decision: parsed.decision
477
+ }, {
463
478
  sessionKey: requiredOpenClawSessionKey(ctx.sessionKey),
464
479
  sessionId: requiredOpenClawSessionId(ctx.sessionId)
465
480
  })
@@ -1261,13 +1276,45 @@ function rememberDisplayedApprovalOffer(api, sessionKeyValue, sessionIdValue, re
1261
1276
  const sessionId = stringValue(sessionIdValue);
1262
1277
  if (!sessionKey || !sessionId || !isRecord(result))
1263
1278
  return;
1264
- const fingerprints = currentApprovalFingerprints(result);
1265
- if (fingerprints.length !== 1)
1279
+ const decisionFingerprints = currentApprovalDecisionFingerprints(result);
1280
+ const fingerprint = decisionFingerprints.approve_once;
1281
+ if (!fingerprint)
1266
1282
  return;
1267
1283
  const target = approvalTargetFromStatus(result);
1268
1284
  if (!target)
1269
1285
  return;
1270
- rememberOpenClawPrivateAuthorityOffer(api, openClawApprovalAuthorityOfferKey(sessionKey, sessionId, target), { fingerprint: fingerprints[0] });
1286
+ rememberOpenClawPrivateAuthorityOffer(api, openClawApprovalAuthorityOfferKey(sessionKey, sessionId, target), {
1287
+ fingerprint,
1288
+ decision_fingerprints: decisionFingerprints
1289
+ });
1290
+ }
1291
+ function rememberDisplayedInteractionOffer(api, sessionKeyValue, sessionIdValue, result) {
1292
+ const sessionKey = stringValue(sessionKeyValue);
1293
+ const sessionId = stringValue(sessionIdValue);
1294
+ if (!sessionKey || !sessionId || !isRecord(result))
1295
+ return;
1296
+ const terminalStatus = isRecord(result.terminal_status)
1297
+ ? result.terminal_status
1298
+ : undefined;
1299
+ const fingerprint = stringValue(terminalStatus?.interaction_prompt_fingerprint);
1300
+ if (!isExactInteractionFingerprint(fingerprint))
1301
+ return;
1302
+ let interactionState;
1303
+ try {
1304
+ interactionState = validateTerminalInteractionProjection(terminalStatus?.interaction_state);
1305
+ }
1306
+ catch {
1307
+ return;
1308
+ }
1309
+ if (interactionState.state !== "pending" ||
1310
+ interactionState.capabilities.respond !== true ||
1311
+ publicTurnIdentity(result).turnId !== interactionState.turn_id) {
1312
+ return;
1313
+ }
1314
+ rememberOpenClawPrivateAuthorityOffer(api, openClawInteractionAuthorityOfferKey(sessionKey, sessionId, interactionState.turn_id, interactionState.interaction_id), {
1315
+ fingerprint,
1316
+ interaction_state: interactionState
1317
+ });
1271
1318
  }
1272
1319
  function approvalTargetFromStatus(result) {
1273
1320
  const conversationId = stringValue(result.conversation_id);
@@ -1340,27 +1387,43 @@ function authoritativeHandoffActionArguments(terminals) {
1340
1387
  : [];
1341
1388
  });
1342
1389
  }
1343
- function currentApprovalFingerprint(result) {
1344
- const unique = isRecord(result) ? currentApprovalFingerprints(result) : [];
1345
- if (unique.length !== 1) {
1346
- throw new Error("the current status does not contain one exact approvable prompt; refresh status and ask the user to review it again");
1390
+ function currentApprovalFingerprint(result, decision) {
1391
+ const fingerprint = isRecord(result)
1392
+ ? currentApprovalDecisionFingerprints(result)[decision]
1393
+ : undefined;
1394
+ if (!fingerprint) {
1395
+ throw new Error(`the current status does not contain one exact ${decision} choice; refresh status and ask the user to review it again`);
1347
1396
  }
1348
- return unique[0];
1397
+ return fingerprint;
1349
1398
  }
1350
1399
  async function buildPrivateApprovalArgs(api, params, { sessionKey, sessionId }) {
1351
1400
  const config = isRecord(api.pluginConfig) ? api.pluginConfig : {};
1352
1401
  const turnId = stringValue(params.turn_id);
1353
1402
  const terminalId = stringValue(params.terminal_id);
1403
+ const decisionValue = params.decision ?? "approve_once";
1404
+ if (!isTerminalApprovalDecision(decisionValue)) {
1405
+ throw new Error("decision must be one of: approve_once, reject");
1406
+ }
1407
+ const decision = decisionValue;
1354
1408
  if (Boolean(turnId) === Boolean(terminalId)) {
1355
1409
  throw new Error("approve requires exactly one of turn_id or terminal_id");
1356
1410
  }
1411
+ if (terminalId && decision !== "approve_once") {
1412
+ throw new Error("terminal-scoped approval supports approve_once only; reject requires an exact managed Turn");
1413
+ }
1357
1414
  const target = terminalId
1358
1415
  ? { type: "terminal_id", id: terminalId }
1359
1416
  : { type: "turn_id", id: requiredString(turnId, "turn_id") };
1360
1417
  const offered = consumeOpenClawPrivateAuthorityOffer(api, openClawApprovalAuthorityOfferKey(sessionKey, sessionId, target));
1361
- const offeredFingerprint = stringValue(offered?.fingerprint) ??
1418
+ const offeredDecisions = isRecord(offered?.decision_fingerprints)
1419
+ ? offered.decision_fingerprints
1420
+ : undefined;
1421
+ const offeredFingerprint = stringValue(offeredDecisions?.[decision]) ??
1422
+ (decision === "approve_once" ? stringValue(offered?.fingerprint) : undefined) ??
1362
1423
  (isRecord(offered?.args)
1363
- ? stringValue(offered.args.expected_approval_fingerprint)
1424
+ ? decision === "approve_once"
1425
+ ? stringValue(offered.args.expected_approval_fingerprint)
1426
+ : undefined
1364
1427
  : undefined);
1365
1428
  if (!isExactApprovalFingerprint(offeredFingerprint)) {
1366
1429
  throw new Error("approve requires a current approval request shown by agent_knock_knock_status in this controller conversation; refresh status, ask the user to review it, and explicitly confirm again");
@@ -1387,10 +1450,11 @@ async function buildPrivateApprovalArgs(api, params, { sessionKey, sessionId })
1387
1450
  else {
1388
1451
  args.push("--turn", requiredString(turnId, "turn_id"));
1389
1452
  }
1390
- const currentFingerprint = currentApprovalFingerprint(await runHostAwareCli(api, statusArgs));
1453
+ const currentFingerprint = currentApprovalFingerprint(await runHostAwareCli(api, statusArgs), decision);
1391
1454
  if (currentFingerprint !== offeredFingerprint) {
1392
1455
  throw new Error("the approval request changed after it was shown; refresh AKK status, ask the user to review the current request, and explicitly confirm again");
1393
1456
  }
1457
+ args.push("--decision", decision);
1394
1458
  args.push("--expected-approval-fingerprint", currentFingerprint);
1395
1459
  if (terminalId) {
1396
1460
  args.push("--expected-terminal-token", requiredString(action?.expected_terminal_token, "current internal terminal approval authority"));
@@ -1398,10 +1462,43 @@ async function buildPrivateApprovalArgs(api, params, { sessionKey, sessionId })
1398
1462
  pushOptional(args, "--store-dir", resolvePluginStoreDir(config));
1399
1463
  return args;
1400
1464
  }
1465
+ function buildPrivateInteractionResponseArgs(api, params, { sessionKey, sessionId }) {
1466
+ const config = isRecord(api.pluginConfig) ? api.pluginConfig : {};
1467
+ const turnId = requiredTerminalInteractionIdentifier(params.turn_id, "turn_id");
1468
+ const interactionId = requiredTerminalInteractionIdentifier(params.interaction_id, "interaction_id");
1469
+ const offered = consumeOpenClawPrivateAuthorityOffer(api, openClawInteractionAuthorityOfferKey(sessionKey, sessionId, turnId, interactionId));
1470
+ const fingerprint = stringValue(offered?.fingerprint);
1471
+ if (!isExactInteractionFingerprint(fingerprint) ||
1472
+ offered?.interaction_state === undefined) {
1473
+ throw new Error("respond_interaction requires a current pending interaction shown by agent_knock_knock_status in this controller conversation; refresh status, review the current questions, and respond again");
1474
+ }
1475
+ const projection = validateTerminalInteractionProjection(offered.interaction_state);
1476
+ const response = validateTerminalInteractionResponse(params, projection);
1477
+ const args = [
1478
+ "respond-interaction",
1479
+ "--turn",
1480
+ response.turn_id,
1481
+ "--interaction",
1482
+ response.interaction_id,
1483
+ "--response-json",
1484
+ JSON.stringify(response),
1485
+ "--expected-interaction-fingerprint",
1486
+ fingerprint,
1487
+ "--expected-interaction-expires-at",
1488
+ projection.expires_at,
1489
+ "--openclaw-session",
1490
+ sessionKey
1491
+ ];
1492
+ pushOptional(args, "--store-dir", resolvePluginStoreDir(config));
1493
+ return args;
1494
+ }
1401
1495
  function isExactApprovalFingerprint(value) {
1402
1496
  return typeof value === "string" && /^[a-f0-9]{64}$/u.test(value);
1403
1497
  }
1404
- function currentApprovalFingerprints(result) {
1498
+ function isExactInteractionFingerprint(value) {
1499
+ return typeof value === "string" && /^[a-f0-9]{64}$/u.test(value);
1500
+ }
1501
+ function currentApprovalDecisionFingerprints(result) {
1405
1502
  const terminalStatus = isRecord(result.terminal_status)
1406
1503
  ? result.terminal_status
1407
1504
  : undefined;
@@ -1411,14 +1508,31 @@ function currentApprovalFingerprints(result) {
1411
1508
  ? terminalStatus.approval_state
1412
1509
  : undefined
1413
1510
  ];
1414
- return [...new Set(states.flatMap((state) => {
1415
- const fingerprint = state?.approvable === true
1416
- ? stringValue(state.fingerprint)
1417
- : undefined;
1418
- return fingerprint && /^[a-f0-9]{64}$/u.test(fingerprint)
1419
- ? [fingerprint]
1420
- : [];
1421
- }))];
1511
+ const found = new Map();
1512
+ const remember = (decision, value) => {
1513
+ if (!isExactApprovalFingerprint(value))
1514
+ return;
1515
+ const fingerprints = found.get(decision) ?? new Set();
1516
+ fingerprints.add(value);
1517
+ found.set(decision, fingerprints);
1518
+ };
1519
+ for (const state of states) {
1520
+ const fingerprint = state?.approvable === true
1521
+ ? stringValue(state.fingerprint)
1522
+ : undefined;
1523
+ remember("approve_once", fingerprint);
1524
+ if (state?.approvable !== true || !Array.isArray(state.choices))
1525
+ continue;
1526
+ for (const choice of state.choices) {
1527
+ if (isRecord(choice) &&
1528
+ isTerminalApprovalDecision(choice.decision)) {
1529
+ remember(choice.decision, choice.fingerprint);
1530
+ }
1531
+ }
1532
+ }
1533
+ return Object.fromEntries([...found.entries()].flatMap(([decision, fingerprints]) => fingerprints.size === 1
1534
+ ? [[decision, [...fingerprints][0]]]
1535
+ : []));
1422
1536
  }
1423
1537
  function sanitizeModelFacingValue(value, parentKey, path = []) {
1424
1538
  if (Array.isArray(value)) {
@@ -1449,6 +1563,15 @@ function sanitizeModelFacingValue(value, parentKey, path = []) {
1449
1563
  (key === "fingerprint" && parentKey === "approval_state")) {
1450
1564
  continue;
1451
1565
  }
1566
+ if (key === "interaction_state") {
1567
+ try {
1568
+ output[key] = validateTerminalInteractionProjection(item);
1569
+ }
1570
+ catch {
1571
+ continue;
1572
+ }
1573
+ continue;
1574
+ }
1452
1575
  output[key] = sanitizeModelFacingValue(item, key, [...path, key]);
1453
1576
  }
1454
1577
  if (output.tool === "agent_knock_knock_send" && isRecord(output.arguments)) {
@@ -2000,6 +2123,14 @@ function requiredOpenClawSessionKey(value) {
2000
2123
  function requiredOpenClawSessionId(value) {
2001
2124
  return requiredString(value, "Controller conversation incarnation for this confirmed action");
2002
2125
  }
2126
+ function requiredTerminalInteractionIdentifier(value, name) {
2127
+ const identifier = requiredString(value, name);
2128
+ if (identifier.length > TERMINAL_INTERACTION_LIMITS.maxIdentifierLength ||
2129
+ !/^[A-Za-z0-9][A-Za-z0-9._:-]*$/u.test(identifier)) {
2130
+ throw new Error(`${name} must be an exact safe interaction identifier`);
2131
+ }
2132
+ return identifier;
2133
+ }
2003
2134
  function parseJson(text) {
2004
2135
  try {
2005
2136
  return JSON.parse(text);