@scotthuang/agent-knock-knock 0.2.39 → 0.2.41
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/CHANGELOG.md +16 -0
- package/README.md +32 -0
- package/dist/src/approval-policy.d.ts +46 -0
- package/dist/src/approval-policy.js +321 -0
- package/dist/src/approval-policy.js.map +1 -0
- package/dist/src/cli.js +165 -11
- package/dist/src/cli.js.map +1 -1
- package/dist/src/openclaw-plugin.js +34 -0
- package/dist/src/openclaw-plugin.js.map +1 -1
- package/openclaw.plugin.json +52 -0
- package/package.json +1 -1
package/dist/src/cli.js
CHANGED
|
@@ -145,12 +145,11 @@ function runInstallOpenClaw(options) {
|
|
|
145
145
|
const skillDest = expandHome(options.skillPath ?? "~/.openclaw/skills/agent-knock-knock/SKILL.md");
|
|
146
146
|
const steps = [];
|
|
147
147
|
if (!skillOnly) {
|
|
148
|
-
|
|
149
|
-
label: "openclaw plugins install"
|
|
150
|
-
});
|
|
148
|
+
const pluginInstall = installOpenClawPlugin(openclawBin, root);
|
|
151
149
|
steps.push({
|
|
152
150
|
name: "plugin_installed",
|
|
153
|
-
path: root
|
|
151
|
+
path: root,
|
|
152
|
+
mode: pluginInstall.mode
|
|
154
153
|
});
|
|
155
154
|
runCheckedCommand(openclawBin, ["plugins", "enable", "agent-knock-knock"], {
|
|
156
155
|
label: "openclaw plugins enable"
|
|
@@ -185,6 +184,27 @@ function runInstallOpenClaw(options) {
|
|
|
185
184
|
: "Agent Knock Knock is installed. Try: AKK list"
|
|
186
185
|
});
|
|
187
186
|
}
|
|
187
|
+
function installOpenClawPlugin(openclawBin, root) {
|
|
188
|
+
const linked = spawnSync(openclawBin, ["plugins", "install", "--link", root], {
|
|
189
|
+
encoding: "utf8",
|
|
190
|
+
maxBuffer: 1024 * 1024 * 10
|
|
191
|
+
});
|
|
192
|
+
if (linked.error) {
|
|
193
|
+
throw new Error(`openclaw plugins install failed to start: ${linked.error.message}`);
|
|
194
|
+
}
|
|
195
|
+
if (linked.status === 0) {
|
|
196
|
+
return { mode: "linked" };
|
|
197
|
+
}
|
|
198
|
+
const failure = cleanProcessText(linked.stderr || linked.stdout)
|
|
199
|
+
?? `openclaw plugins install exited with status ${linked.status}`;
|
|
200
|
+
if (!/plugin already exists:/i.test(failure)) {
|
|
201
|
+
throw new Error(failure);
|
|
202
|
+
}
|
|
203
|
+
runCheckedCommand(openclawBin, ["plugins", "install", "--force", root], {
|
|
204
|
+
label: "openclaw plugins replace"
|
|
205
|
+
});
|
|
206
|
+
return { mode: "replaced" };
|
|
207
|
+
}
|
|
188
208
|
function runDoctor(options) {
|
|
189
209
|
const commands = ["node", "openclaw", "acpx", "codex", "claude", "cursor"];
|
|
190
210
|
const checks = commands.map((commandName) => executableCheck(commandName));
|
|
@@ -613,18 +633,21 @@ function detectCodexApprovalPrompt(screen) {
|
|
|
613
633
|
if (key !== "y") {
|
|
614
634
|
return {
|
|
615
635
|
approvable: false,
|
|
616
|
-
reason: `primary approval shortcut is ${key}, not y
|
|
636
|
+
reason: `primary approval shortcut is ${key}, not y`,
|
|
637
|
+
...approvalCandidateFromPrompt(prompt.marker, prompt.region)
|
|
617
638
|
};
|
|
618
639
|
}
|
|
619
640
|
return {
|
|
620
641
|
approvable: true,
|
|
621
642
|
key,
|
|
622
|
-
label: match[1].trim()
|
|
643
|
+
label: match[1].trim(),
|
|
644
|
+
...approvalCandidateFromPrompt(prompt.marker, prompt.region)
|
|
623
645
|
};
|
|
624
646
|
}
|
|
625
647
|
return {
|
|
626
648
|
approvable: false,
|
|
627
|
-
reason: "no primary approve option with a shortcut was detected"
|
|
649
|
+
reason: "no primary approve option with a shortcut was detected",
|
|
650
|
+
...approvalCandidateFromPrompt(prompt.marker, prompt.region)
|
|
628
651
|
};
|
|
629
652
|
}
|
|
630
653
|
function isCodexApprovalPromptVisible(screen) {
|
|
@@ -639,9 +662,12 @@ function codexApprovalPromptRegion(screen) {
|
|
|
639
662
|
];
|
|
640
663
|
const lines = screen.split(/\r?\n/);
|
|
641
664
|
let markerIndex = -1;
|
|
665
|
+
let matchedMarker = "";
|
|
642
666
|
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
643
|
-
|
|
667
|
+
const marker = approvalMarkers.find((candidate) => lines[index].includes(candidate));
|
|
668
|
+
if (marker) {
|
|
644
669
|
markerIndex = index;
|
|
670
|
+
matchedMarker = marker;
|
|
645
671
|
break;
|
|
646
672
|
}
|
|
647
673
|
}
|
|
@@ -661,9 +687,40 @@ function codexApprovalPromptRegion(screen) {
|
|
|
661
687
|
}
|
|
662
688
|
return {
|
|
663
689
|
visible: true,
|
|
664
|
-
region: regionLines.join("\n")
|
|
690
|
+
region: regionLines.join("\n"),
|
|
691
|
+
marker: matchedMarker
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
function approvalCandidateFromPrompt(marker, region) {
|
|
695
|
+
const promptKind = marker === "Would you like to run the following command?"
|
|
696
|
+
? "run_command"
|
|
697
|
+
: marker === "Would you like to make the following edits?"
|
|
698
|
+
? "file_edit"
|
|
699
|
+
: marker === "Would you like to grant these permissions?"
|
|
700
|
+
? "grant_permissions"
|
|
701
|
+
: "unknown";
|
|
702
|
+
return {
|
|
703
|
+
promptKind,
|
|
704
|
+
command: promptKind === "run_command" ? commandFromApprovalRegion(region) : undefined
|
|
665
705
|
};
|
|
666
706
|
}
|
|
707
|
+
function commandFromApprovalRegion(region) {
|
|
708
|
+
const lines = region.split(/\r?\n/);
|
|
709
|
+
const commandStart = lines.findIndex((line) => /^\s*\$\s+/u.test(line));
|
|
710
|
+
if (commandStart < 0) {
|
|
711
|
+
return undefined;
|
|
712
|
+
}
|
|
713
|
+
const parts = [];
|
|
714
|
+
for (let index = commandStart; index < lines.length; index += 1) {
|
|
715
|
+
const line = lines[index];
|
|
716
|
+
if (index > commandStart && (!line.trim() || /^[\s›]*\d+\.\s+/u.test(line) || /Press enter to confirm/u.test(line))) {
|
|
717
|
+
break;
|
|
718
|
+
}
|
|
719
|
+
parts.push(index === commandStart ? line.replace(/^\s*\$\s+/u, "").trim() : line.trim());
|
|
720
|
+
}
|
|
721
|
+
const command = parts.filter(Boolean).join(" ").trim();
|
|
722
|
+
return command ? redactString(command) : undefined;
|
|
723
|
+
}
|
|
667
724
|
function isPostApprovalActivityLine(line) {
|
|
668
725
|
const trimmed = line.trim();
|
|
669
726
|
if (!trimmed) {
|
|
@@ -1616,6 +1673,8 @@ async function listStateForTerminal(terminalControl, options) {
|
|
|
1616
1673
|
approvable: approval.approvable,
|
|
1617
1674
|
key: approval.approvable ? approval.key : undefined,
|
|
1618
1675
|
label: approval.approvable ? approval.label : undefined,
|
|
1676
|
+
prompt_kind: approval.promptKind,
|
|
1677
|
+
command: approval.command,
|
|
1619
1678
|
reason: approval.approvable ? undefined : approval.reason,
|
|
1620
1679
|
screen_excerpt: blocked ? screenExcerpt(screen, 1000) : undefined
|
|
1621
1680
|
},
|
|
@@ -1832,6 +1891,8 @@ async function terminalStatusForControl(terminalControl, options) {
|
|
|
1832
1891
|
approvable: approval.approvable,
|
|
1833
1892
|
key: approval.approvable ? approval.key : undefined,
|
|
1834
1893
|
label: approval.approvable ? approval.label : undefined,
|
|
1894
|
+
prompt_kind: approval.promptKind,
|
|
1895
|
+
command: approval.command,
|
|
1835
1896
|
reason: approval.approvable ? undefined : approval.reason
|
|
1836
1897
|
},
|
|
1837
1898
|
screen: {
|
|
@@ -1866,11 +1927,29 @@ function terminalBridgeApprovalFingerprint({ terminalControl, terminalStatus })
|
|
|
1866
1927
|
target: terminalControl.target,
|
|
1867
1928
|
key: approval.key,
|
|
1868
1929
|
label: approval.label,
|
|
1930
|
+
prompt_kind: approval.prompt_kind,
|
|
1931
|
+
command: approval.command,
|
|
1869
1932
|
excerpt: screen.excerpt
|
|
1870
1933
|
}))
|
|
1871
1934
|
.digest("hex")
|
|
1872
1935
|
.slice(0, 16);
|
|
1873
1936
|
}
|
|
1937
|
+
function terminalBridgeApprovalFingerprintForScreen({ terminalControl, screen, approval }) {
|
|
1938
|
+
return terminalBridgeApprovalFingerprint({
|
|
1939
|
+
terminalControl,
|
|
1940
|
+
terminalStatus: {
|
|
1941
|
+
approval_state: {
|
|
1942
|
+
key: approval.key,
|
|
1943
|
+
label: approval.label,
|
|
1944
|
+
prompt_kind: approval.promptKind,
|
|
1945
|
+
command: approval.command
|
|
1946
|
+
},
|
|
1947
|
+
screen: {
|
|
1948
|
+
excerpt: screenExcerpt(screen)
|
|
1949
|
+
}
|
|
1950
|
+
}
|
|
1951
|
+
});
|
|
1952
|
+
}
|
|
1874
1953
|
function terminalBridgeApprovalInstructions({ conversation, terminalControl, terminalStatus }) {
|
|
1875
1954
|
const approval = isRecord(terminalStatus?.approval_state) ? terminalStatus.approval_state : {};
|
|
1876
1955
|
const screen = isRecord(terminalStatus?.screen) ? terminalStatus.screen : {};
|
|
@@ -2372,6 +2451,38 @@ async function runApprove(options) {
|
|
|
2372
2451
|
});
|
|
2373
2452
|
return;
|
|
2374
2453
|
}
|
|
2454
|
+
const expectedFingerprint = stringValue(options.expectedApprovalFingerprint);
|
|
2455
|
+
const actualFingerprint = terminalBridgeApprovalFingerprintForScreen({ terminalControl, screen, approval });
|
|
2456
|
+
const autoApproved = options.autoApproved === true;
|
|
2457
|
+
const policyRuleId = stringValue(options.policyRuleId);
|
|
2458
|
+
const policyFingerprint = stringValue(options.policyFingerprint);
|
|
2459
|
+
if (expectedFingerprint && actualFingerprint !== expectedFingerprint) {
|
|
2460
|
+
if (autoApproved) {
|
|
2461
|
+
appendEvent(logPath, {
|
|
2462
|
+
ts: new Date().toISOString(),
|
|
2463
|
+
conversation_id: conversation.conversation_id,
|
|
2464
|
+
event: "terminal_auto_approval_decision",
|
|
2465
|
+
action: "rejected",
|
|
2466
|
+
reason: "approval fingerprint changed before execution",
|
|
2467
|
+
terminal_control: terminalControl,
|
|
2468
|
+
expected_fingerprint: expectedFingerprint,
|
|
2469
|
+
actual_fingerprint: actualFingerprint,
|
|
2470
|
+
policy_rule_id: policyRuleId,
|
|
2471
|
+
policy_fingerprint: policyFingerprint
|
|
2472
|
+
});
|
|
2473
|
+
}
|
|
2474
|
+
printJson({
|
|
2475
|
+
conversation,
|
|
2476
|
+
approved: false,
|
|
2477
|
+
blocked: true,
|
|
2478
|
+
reason: "approval fingerprint changed before execution",
|
|
2479
|
+
terminal_control: terminalControl,
|
|
2480
|
+
expected_approval_fingerprint: expectedFingerprint,
|
|
2481
|
+
actual_approval_fingerprint: actualFingerprint,
|
|
2482
|
+
screen_excerpt: screenExcerpt(screen)
|
|
2483
|
+
});
|
|
2484
|
+
return;
|
|
2485
|
+
}
|
|
2375
2486
|
await provider.sendKeys(terminalControl.target, [approval.key], {
|
|
2376
2487
|
socketPath: terminalControl.socketPath
|
|
2377
2488
|
});
|
|
@@ -2381,13 +2492,33 @@ async function runApprove(options) {
|
|
|
2381
2492
|
event: "terminal_approval_send",
|
|
2382
2493
|
terminal_control: terminalControl,
|
|
2383
2494
|
key: approval.key,
|
|
2384
|
-
label: approval.label
|
|
2495
|
+
label: approval.label,
|
|
2496
|
+
approval_fingerprint: actualFingerprint,
|
|
2497
|
+
auto_approved: autoApproved,
|
|
2498
|
+
policy_rule_id: policyRuleId,
|
|
2499
|
+
policy_fingerprint: policyFingerprint
|
|
2385
2500
|
});
|
|
2501
|
+
if (autoApproved) {
|
|
2502
|
+
appendEvent(logPath, {
|
|
2503
|
+
ts: new Date().toISOString(),
|
|
2504
|
+
conversation_id: conversation.conversation_id,
|
|
2505
|
+
event: "terminal_auto_approval_decision",
|
|
2506
|
+
action: "approved",
|
|
2507
|
+
terminal_control: terminalControl,
|
|
2508
|
+
approval_fingerprint: actualFingerprint,
|
|
2509
|
+
policy_rule_id: policyRuleId,
|
|
2510
|
+
policy_fingerprint: policyFingerprint
|
|
2511
|
+
});
|
|
2512
|
+
}
|
|
2386
2513
|
runtimeLog("info", "terminal_approval_send", {
|
|
2387
2514
|
conversation_id: conversation.conversation_id,
|
|
2388
2515
|
terminal_target: terminalControl.target,
|
|
2389
2516
|
key: approval.key,
|
|
2390
|
-
label: approval.label
|
|
2517
|
+
label: approval.label,
|
|
2518
|
+
approval_fingerprint: actualFingerprint,
|
|
2519
|
+
auto_approved: autoApproved,
|
|
2520
|
+
policy_rule_id: policyRuleId,
|
|
2521
|
+
policy_fingerprint: policyFingerprint
|
|
2391
2522
|
});
|
|
2392
2523
|
const nativeTakeoverForUpdate = isRecord(conversation.native_session_takeover)
|
|
2393
2524
|
? { ...conversation.native_session_takeover }
|
|
@@ -2434,6 +2565,9 @@ async function runApprove(options) {
|
|
|
2434
2565
|
terminal_control: terminalControl,
|
|
2435
2566
|
key: approval.key,
|
|
2436
2567
|
label: approval.label,
|
|
2568
|
+
approval_fingerprint: actualFingerprint,
|
|
2569
|
+
auto_approved: autoApproved,
|
|
2570
|
+
policy_rule_id: policyRuleId,
|
|
2437
2571
|
monitor_pid: bridgeMonitor?.pid ?? null
|
|
2438
2572
|
});
|
|
2439
2573
|
}
|
|
@@ -3667,6 +3801,12 @@ async function runTerminalBridgeMonitor(options) {
|
|
|
3667
3801
|
terminal_control: terminalControl,
|
|
3668
3802
|
terminal_status: terminalStatus,
|
|
3669
3803
|
approval_fingerprint: fingerprint,
|
|
3804
|
+
approval_candidate: terminalBridgeApprovalCandidate({
|
|
3805
|
+
executor,
|
|
3806
|
+
terminalControl,
|
|
3807
|
+
terminalStatus,
|
|
3808
|
+
fingerprint
|
|
3809
|
+
}),
|
|
3670
3810
|
approve_command: `AKK approve ${notification.conversation.conversation_id}`,
|
|
3671
3811
|
deny_command: `AKK cancel ${notification.conversation.conversation_id}`,
|
|
3672
3812
|
approve_tool: "agent_knock_knock_approve",
|
|
@@ -3794,6 +3934,20 @@ async function runTerminalBridgeMonitor(options) {
|
|
|
3794
3934
|
sleepSync(pollIntervalMs);
|
|
3795
3935
|
}
|
|
3796
3936
|
}
|
|
3937
|
+
function terminalBridgeApprovalCandidate({ executor, terminalControl, terminalStatus, fingerprint }) {
|
|
3938
|
+
const approval = isRecord(terminalStatus?.approval_state) ? terminalStatus.approval_state : {};
|
|
3939
|
+
if (approval.approvable !== true) {
|
|
3940
|
+
return undefined;
|
|
3941
|
+
}
|
|
3942
|
+
return {
|
|
3943
|
+
agent: executor.kind,
|
|
3944
|
+
kind: stringValue(approval.prompt_kind) ?? "unknown",
|
|
3945
|
+
command: stringValue(approval.command),
|
|
3946
|
+
cwd: terminalControl.currentPath,
|
|
3947
|
+
fingerprint,
|
|
3948
|
+
terminal_target: terminalControl.target
|
|
3949
|
+
};
|
|
3950
|
+
}
|
|
3797
3951
|
async function terminalBridgeCodexContext({ conversation, nativeTakeover, options }) {
|
|
3798
3952
|
const provider = createAgentSessionProvider("codex", options);
|
|
3799
3953
|
const nativeSessionId = stringValue(nativeTakeover?.["native_session_id"]);
|