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.
@@ -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
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cclaw-cli",
3
- "version": "0.48.34",
3
+ "version": "0.48.35",
4
4
  "description": "Installer-first flow toolkit for coding agents",
5
5
  "type": "module",
6
6
  "bin": {