cclaw-cli 0.48.34 → 0.48.35
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/dist/content/opencode-plugin.js +126 -2
- package/package.json +1 -1
|
@@ -12,6 +12,7 @@ export default function cclawPlugin(ctx) {
|
|
|
12
12
|
const stateDir = join(runtimeDir, "state");
|
|
13
13
|
const logsDir = join(runtimeDir, "logs");
|
|
14
14
|
const pluginLogPath = join(logsDir, "opencode-plugin.log");
|
|
15
|
+
const configPath = join(runtimeDir, "config.yaml");
|
|
15
16
|
const flowStatePath = join(stateDir, "flow-state.json");
|
|
16
17
|
const checkpointPath = join(stateDir, "checkpoint.json");
|
|
17
18
|
const activityPath = join(stateDir, "stage-activity.jsonl");
|
|
@@ -306,6 +307,41 @@ export default function cclawPlugin(ctx) {
|
|
|
306
307
|
lastHookStderr.set(hookName, trimmed);
|
|
307
308
|
}
|
|
308
309
|
|
|
310
|
+
/**
|
|
311
|
+
* A hook process can exit non-zero for two very different reasons:
|
|
312
|
+
* (a) the guard legitimately decided to refuse an operation
|
|
313
|
+
* (strict-mode refusal — this is the *only* case we ever want
|
|
314
|
+
* to surface as a block to the user), or
|
|
315
|
+
* (b) the hook infrastructure itself failed — the runtime crashed,
|
|
316
|
+
* a child binary was missing, stderr is a chunk of yargs help
|
|
317
|
+
* from some unrelated process, timeout, etc.
|
|
318
|
+
*
|
|
319
|
+
* Treating (b) as a block is what produces the "guard blocked
|
|
320
|
+
* tool.execute.before" error on a user who did nothing wrong. The
|
|
321
|
+
* heuristic below trims guaranteed-infra signals so only cleanly
|
|
322
|
+
* structured guard output is eligible for a real strict-mode block.
|
|
323
|
+
*/
|
|
324
|
+
const INFRA_NOISE_PATTERNS = [
|
|
325
|
+
/^\\s*(Usage|Options|Commands|Examples|Positionals|Aliases):/im,
|
|
326
|
+
/^\\s*--[a-z][a-z0-9-]*\\b.*\\[(string|boolean|number|array)\\]/im,
|
|
327
|
+
/\\bcommand (not found|failed)\\b/i,
|
|
328
|
+
/\\bno such file or directory\\b/i,
|
|
329
|
+
/\\bCannot find module\\b/i,
|
|
330
|
+
/\\bThrowsCompletion\\b/,
|
|
331
|
+
/\\b(Reference|Syntax|Type|Range)Error\\b/,
|
|
332
|
+
/^\\s*at [^\\n]+\\([^)]*:\\d+:\\d+\\)/im,
|
|
333
|
+
/^\\s*node:internal\\b/im
|
|
334
|
+
];
|
|
335
|
+
function looksLikeInfrastructureFailure(stderr) {
|
|
336
|
+
if (typeof stderr !== "string") return true;
|
|
337
|
+
const trimmed = stderr.trim();
|
|
338
|
+
if (trimmed.length === 0) return true;
|
|
339
|
+
for (const pattern of INFRA_NOISE_PATTERNS) {
|
|
340
|
+
if (pattern.test(trimmed)) return true;
|
|
341
|
+
}
|
|
342
|
+
return false;
|
|
343
|
+
}
|
|
344
|
+
|
|
309
345
|
async function runHookScript(hookName, payload = {}) {
|
|
310
346
|
const { spawn } = await import("node:child_process");
|
|
311
347
|
const hookRuntimePath = join(root, "${RUNTIME_ROOT}/hooks/run-hook.mjs");
|
|
@@ -406,14 +442,42 @@ export default function cclawPlugin(ctx) {
|
|
|
406
442
|
* still runs through guards.
|
|
407
443
|
*/
|
|
408
444
|
const SAFE_READONLY_TOOLS = new Set([
|
|
445
|
+
// Filesystem / search reads — no state mutation possible.
|
|
409
446
|
"read",
|
|
410
447
|
"glob",
|
|
411
448
|
"grep",
|
|
412
449
|
"list",
|
|
413
450
|
"ls",
|
|
414
451
|
"view",
|
|
452
|
+
"find",
|
|
453
|
+
// Network reads — no local state mutation.
|
|
415
454
|
"webfetch",
|
|
416
|
-
"websearch"
|
|
455
|
+
"websearch",
|
|
456
|
+
// User-facing question / ask tools: they only ask the human for
|
|
457
|
+
// input and cannot touch the filesystem or execute code. Blocking
|
|
458
|
+
// them strands the plugin mid-decision (see OpenCode's \`question\`).
|
|
459
|
+
"question",
|
|
460
|
+
"ask",
|
|
461
|
+
"askuser",
|
|
462
|
+
"askquestion",
|
|
463
|
+
"ask_question",
|
|
464
|
+
"ask_user",
|
|
465
|
+
"ask_user_question",
|
|
466
|
+
"askuserquestion",
|
|
467
|
+
"request_user_input",
|
|
468
|
+
"requestuserinput",
|
|
469
|
+
"prompt",
|
|
470
|
+
// Thinking / scratchpad tools — pure reasoning with no side effects.
|
|
471
|
+
"think",
|
|
472
|
+
"thinking",
|
|
473
|
+
// Todo bookkeeping tools — they write only inside the harness's own
|
|
474
|
+
// session state, not project files, and blocking them breaks agent
|
|
475
|
+
// planning without protecting anything.
|
|
476
|
+
"todo",
|
|
477
|
+
"todoread",
|
|
478
|
+
"todowrite",
|
|
479
|
+
"todo_read",
|
|
480
|
+
"todo_write"
|
|
417
481
|
]);
|
|
418
482
|
|
|
419
483
|
function isSafeReadOnlyTool(payload) {
|
|
@@ -435,6 +499,42 @@ export default function cclawPlugin(ctx) {
|
|
|
435
499
|
return false;
|
|
436
500
|
}
|
|
437
501
|
|
|
502
|
+
/**
|
|
503
|
+
* Strictness derived from (in order of precedence): CCLAW_STRICTNESS
|
|
504
|
+
* env override, \`.cclaw/config.yaml\` key \`strictness\`, or the
|
|
505
|
+
* library default of "advisory". The plugin only ever *blocks* tool
|
|
506
|
+
* execution when strictness resolves to "strict"; in advisory mode
|
|
507
|
+
* guard failures are logged and the tool call proceeds. This mirrors
|
|
508
|
+
* the Ralph-loop / hook-runtime semantics of
|
|
509
|
+
* \`DEFAULT_STRICTNESS = advisory\`, so the plugin can no longer
|
|
510
|
+
* accidentally be the stricter half of a mismatched pair.
|
|
511
|
+
*/
|
|
512
|
+
function readConfigStrictness() {
|
|
513
|
+
try {
|
|
514
|
+
if (!existsSync(configPath)) return "";
|
|
515
|
+
const { readFileSync } = require("node:fs");
|
|
516
|
+
const raw = readFileSync(configPath, "utf8");
|
|
517
|
+
if (typeof raw !== "string" || raw.length === 0) return "";
|
|
518
|
+
const match = raw.match(/^\\s*strictness\\s*:\\s*([A-Za-z0-9_-]+)/m);
|
|
519
|
+
return match && typeof match[1] === "string" ? match[1].trim().toLowerCase() : "";
|
|
520
|
+
} catch {
|
|
521
|
+
return "";
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function resolveStrictness() {
|
|
526
|
+
const envRaw = typeof process.env.CCLAW_STRICTNESS === "string"
|
|
527
|
+
? process.env.CCLAW_STRICTNESS.trim().toLowerCase()
|
|
528
|
+
: "";
|
|
529
|
+
if (envRaw === "strict") return "strict";
|
|
530
|
+
if (envRaw === "advisory" || envRaw === "off" || envRaw === "disabled" || envRaw === "none") {
|
|
531
|
+
return "advisory";
|
|
532
|
+
}
|
|
533
|
+
const fileRaw = readConfigStrictness();
|
|
534
|
+
if (fileRaw === "strict") return "strict";
|
|
535
|
+
return "advisory";
|
|
536
|
+
}
|
|
537
|
+
|
|
438
538
|
/**
|
|
439
539
|
* cclaw considers itself "active" in a project when both the state
|
|
440
540
|
* file and the hook runtime script exist. If either is missing the
|
|
@@ -618,11 +718,35 @@ export default function cclawPlugin(ctx) {
|
|
|
618
718
|
const failed = !promptOk ? "prompt-guard" : "workflow-guard";
|
|
619
719
|
const rawDetail = lastHookStderr.get(failed) || "";
|
|
620
720
|
const detail = rawDetail.length > 0 ? rawDetail.slice(-400) : "(no stderr captured)";
|
|
721
|
+
if (looksLikeInfrastructureFailure(rawDetail)) {
|
|
722
|
+
// Never let a broken hook runtime or misrouted child-process
|
|
723
|
+
// stderr (yargs help, Node crash, ENOENT, timeout) masquerade
|
|
724
|
+
// as a policy block. Log the infra hit and let the user keep
|
|
725
|
+
// working regardless of strictness.
|
|
726
|
+
logToFile(
|
|
727
|
+
"infra: " + failed + " non-zero exit with non-guard stderr — treated as infrastructure failure, tool allowed. " +
|
|
728
|
+
"stderr=" + detail.replace(/\\s+/g, " ").slice(0, 300)
|
|
729
|
+
);
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
const strictness = resolveStrictness();
|
|
733
|
+
if (strictness !== "strict") {
|
|
734
|
+
// Advisory mode (the default) — every guard refusal is a hint,
|
|
735
|
+
// not a hard stop. Users report the "failure" as a log line
|
|
736
|
+
// and keep working. Only \`strictness: strict\` in config.yaml
|
|
737
|
+
// or CCLAW_STRICTNESS=strict upgrades this to a thrown block.
|
|
738
|
+
logToFile(
|
|
739
|
+
"advisory: " + failed + " flagged tool.execute.before (strictness=" +
|
|
740
|
+
strictness + "). detail=" + detail.replace(/\\s+/g, " ").slice(0, 300)
|
|
741
|
+
);
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
621
744
|
throw new Error(
|
|
622
745
|
"cclaw " + failed + " blocked tool.execute.before.\\n" +
|
|
623
746
|
"Reason: " + detail + "\\n" +
|
|
624
747
|
"Diagnose: run \`cclaw doctor\` in project root.\\n" +
|
|
625
|
-
"Bypass (temporary): export CCLAW_DISABLE=1 before starting OpenCode
|
|
748
|
+
"Bypass (temporary): export CCLAW_DISABLE=1 before starting OpenCode,\\n" +
|
|
749
|
+
"or set \`strictness: advisory\` in .cclaw/config.yaml."
|
|
626
750
|
);
|
|
627
751
|
}
|
|
628
752
|
},
|