@khoavu882/doflow 2.0.0-beta.4 → 2.0.0-beta.5

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 (101) hide show
  1. package/core/.antigravity-plugin/plugin.json +0 -1
  2. package/core/.claude-plugin/plugin.json +1 -1
  3. package/core/.codex-plugin/plugin.json +1 -1
  4. package/core/.plugin/plugin.json +1 -1
  5. package/core/harnesses/claude/hooks/mcp-tool-guard.sh +7 -17
  6. package/core/harnesses/claude/hooks/pre-bash-guard.sh +7 -17
  7. package/core/harnesses/claude/hooks/pre-implement-gate.sh +7 -18
  8. package/core/harnesses/claude/hooks/session-start.sh +5 -3
  9. package/core/harnesses/claude/hooks/stop-check.sh +5 -3
  10. package/core/harnesses/claude/hooks/subagent-audit.sh +5 -5
  11. package/core/harnesses/claude/hooks/user-prompt-submit.sh +5 -3
  12. package/core/harnesses/codex/hooks/mcp-tool-guard.sh +7 -15
  13. package/core/harnesses/codex/hooks/pre-bash-guard.sh +7 -15
  14. package/core/harnesses/codex/hooks/pre-implement-gate.sh +7 -16
  15. package/core/harnesses/codex/hooks/session-start.sh +5 -3
  16. package/core/harnesses/codex/hooks/stop-check.sh +6 -5
  17. package/core/harnesses/codex/hooks/subagent-audit.sh +17 -6
  18. package/core/harnesses/codex/hooks/user-prompt-submit.sh +5 -4
  19. package/core/harnesses/gemini/hooks/mcp-tool-guard.sh +7 -18
  20. package/core/harnesses/gemini/hooks/pre-bash-guard.sh +7 -18
  21. package/core/harnesses/gemini/hooks/pre-implement-gate.sh +7 -19
  22. package/core/harnesses/gemini/hooks/session-start.sh +5 -3
  23. package/core/harnesses/kiro/hooks/mcp-tool-guard.sh +20 -5
  24. package/core/harnesses/kiro/hooks/pre-bash-guard.sh +20 -5
  25. package/core/harnesses/kiro/hooks/pre-implement-gate.sh +20 -5
  26. package/core/harnesses/kiro/hooks/session-start.sh +11 -5
  27. package/core/harnesses/kiro/hooks/stop-check.sh +21 -30
  28. package/core/harnesses/pi/extensions/doflow-guard.js +159 -0
  29. package/core/harnesses/shared/hooks/ENVELOPE_MIGRATION.md +27 -11
  30. package/core/harnesses/shared/hooks/adapters/antigravity.js +14 -57
  31. package/core/harnesses/shared/hooks/bridge/bridge.sh +184 -0
  32. package/core/harnesses/shared/hooks/bridge/envelope.jq +60 -0
  33. package/core/harnesses/shared/hooks/bridge/respond.sh +62 -0
  34. package/core/harnesses/shared/hooks/policies/capture-failure.sh +4 -2
  35. package/core/harnesses/shared/hooks/policies/contract.sh +75 -0
  36. package/core/harnesses/shared/hooks/policies/deny-json.sh +39 -4
  37. package/core/harnesses/shared/hooks/policies/lib.sh +4 -2
  38. package/core/harnesses/shared/hooks/policies/mcp-tool-guard.sh +46 -34
  39. package/core/harnesses/shared/hooks/policies/pre-bash-guard.sh +75 -25
  40. package/core/harnesses/shared/hooks/policies/pre-implementation-gate.sh +67 -73
  41. package/core/harnesses/shared/hooks/policies/session-context.sh +37 -7
  42. package/core/harnesses/shared/hooks/policies/stop-check.sh +42 -15
  43. package/core/harnesses/shared/hooks/policies/subagent-audit.sh +35 -5
  44. package/core/harnesses/shared/hooks/policies/user-prompt-submit.sh +40 -7
  45. package/core/harnesses/shared/hooks/stream-hook-runner.js +69 -181
  46. package/core/registry/assets.json +19 -0
  47. package/core/registry/contracts.json +2 -2
  48. package/core/registry/harnesses.json +196 -10
  49. package/core/registry/lifecycle.json +2626 -7
  50. package/core/registry/storage.json +24 -1
  51. package/core/shared/scripts/doflow/bin/doflow-run +10 -6
  52. package/core/shared/skills/do-design/SKILL.md +6 -6
  53. package/core/shared/skills/do-execute-plan/SKILL.md +11 -10
  54. package/core/shared/skills/do-execute-plan/references/readiness_gate.md +2 -0
  55. package/core/shared/skills/do-implement/SKILL.md +3 -2
  56. package/core/shared/skills/do-plan/SKILL.md +7 -8
  57. package/package.json +1 -1
  58. package/src/adapters/antigravity/index.js +83 -47
  59. package/src/adapters/claude/index.js +15 -1
  60. package/src/adapters/codex/hooks.js +40 -5
  61. package/src/adapters/codex/index.js +52 -9
  62. package/src/adapters/copy-tree.js +4 -4
  63. package/src/adapters/gemini/hooks.js +57 -16
  64. package/src/adapters/gemini/index.js +57 -18
  65. package/src/adapters/kiro/index.js +15 -1
  66. package/src/adapters/pi/hooks.js +119 -0
  67. package/src/adapters/pi/index.js +34 -9
  68. package/src/cli/commands/install.js +1 -1
  69. package/src/cli/commands/update.js +1 -1
  70. package/src/cli/index.js +17 -5
  71. package/src/cli/runtime-commands.js +51 -12
  72. package/src/lifecycle/coverage.js +57 -0
  73. package/src/lifecycle/hook-step.js +61 -0
  74. package/src/lifecycle/index.js +28 -14
  75. package/src/registry/index.js +136 -7
  76. package/src/runtime/candidate.js +505 -0
  77. package/src/runtime/claims.js +14 -5
  78. package/src/runtime/cli-result.js +11 -4
  79. package/src/runtime/cli.js +35 -268
  80. package/src/runtime/context/budget.js +203 -0
  81. package/src/runtime/context/compose.js +1367 -0
  82. package/src/runtime/context/delivery-store.js +102 -0
  83. package/src/runtime/context/mandatory.js +594 -0
  84. package/src/runtime/context/pointer.js +142 -0
  85. package/src/runtime/context-pack.js +1029 -244
  86. package/src/runtime/evidence-fields.js +39 -0
  87. package/src/runtime/evidence-ledger.js +7 -0
  88. package/src/runtime/freshness.js +44 -7
  89. package/src/runtime/outcome.js +1 -1
  90. package/src/runtime/readiness.js +83 -13
  91. package/src/runtime/retrieval-plan.js +422 -73
  92. package/src/runtime/retrieval-record.js +223 -0
  93. package/src/runtime/scaffold/artifacts.js +3 -0
  94. package/src/runtime/source-version.js +289 -0
  95. package/src/runtime/task-attempts.js +1 -1
  96. package/src/runtime/task-scope.js +28 -3
  97. package/src/runtime/task-state.js +72 -7
  98. package/core/.antigravity-plugin/hooks.json +0 -39
  99. package/core/harnesses/shared/hooks/adapters/arg-fields.js +0 -19
  100. package/core/harnesses/shared/hooks/adapters/gemini.js +0 -114
  101. package/core/harnesses/shared/hooks/hooks.json +0 -47
@@ -6,7 +6,7 @@
6
6
  # example its pattern file is missing). It exists only to be sourced, in a subshell with all output
7
7
  # discarded, from inside a branch where the policy already fails open:
8
8
  #
9
- # ( . "$(dirname "$0")/capture-failure.sh"; doflow_capture_failure pre-bash-guard patterns-missing ) >/dev/null 2>&1 || true
9
+ # ( . "<policy-dir>/capture-failure.sh"; doflow_capture_failure pre-bash-guard patterns-missing ) >/dev/null 2>&1 || true
10
10
  #
11
11
  # The subshell keeps anything this file does away from the policy, which runs with `set -uo pipefail`,
12
12
  # does not source lib.sh and would change exit codes under `set -e`. So this file:
@@ -15,6 +15,8 @@
15
15
  # - reads no stdin and writes nothing to stdout or stderr;
16
16
  # - never rotates the file (rotation is done by the Node writers only, DEC-019);
17
17
  # - never blocks, never changes the policy's decision, and swallows every error.
18
+
19
+ case ${BASH_SOURCE[0]} in */*) _DOFLOW_CAPTURE_FAILURE_DIR=${BASH_SOURCE[0]%/*} ;; *) _DOFLOW_CAPTURE_FAILURE_DIR=. ;; esac
18
20
  # The line holds only the policy name and the point name from DoFlow's own code, the working
19
21
  # directory, the harness name and the install version: never the tool call, its arguments or stdin.
20
22
  # It is a no-op when no failure home can be resolved (HOME unset, or XDG_CONFIG_HOME relative) and
@@ -54,7 +56,7 @@ doflow_capture_failure() {
54
56
  [[ "$harness" =~ ^[A-Za-z0-9._-]{1,40}$ ]] || harness="none"
55
57
 
56
58
  # script_version of the nearest install manifest: walking up from this file, then the global one.
57
- here="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd -P)" || here=""
59
+ here="$(CDPATH= cd -P -- "$_DOFLOW_CAPTURE_FAILURE_DIR" 2>/dev/null && pwd -P)" || here=""
58
60
  dir="$here"
59
61
  while [ -n "$dir" ] && [ -z "$version" ]; do
60
62
  if [ -f "$dir/.doflow/.install-manifest.json" ]; then
@@ -0,0 +1,75 @@
1
+ #!/usr/bin/env bash
2
+ # contract.sh — shared decision and envelope contract for canonical policies.
3
+ #
4
+ # This file is sourced by policy scripts. It sets no shell options, installs no traps, reads no
5
+ # stdin, and keeps decision recording independent of jq.
6
+
7
+ doflow_decision_init() {
8
+ DF_POLICY="${1:-}"
9
+ DF_POLICY_VERSION="${2:-0}"
10
+ doflow_decide allow allow
11
+ }
12
+
13
+ doflow_decide() {
14
+ local LC_ALL=C
15
+ local outcome="${1:-allow}" code="${2:-invalid-code}" fd="${DOFLOW_DECISION_FD:-}"
16
+ case "$code" in
17
+ [a-z]*)
18
+ case "$code" in
19
+ *[!a-z0-9:._-]*) code=invalid-code ;;
20
+ *) [ "${#code}" -le 40 ] || code=invalid-code ;;
21
+ esac
22
+ ;;
23
+ *) code=invalid-code ;;
24
+ esac
25
+ case "$fd" in
26
+ 3|4|5|6|7|8|9)
27
+ # A caller may set the descriptor name without passing an open fd. Keep
28
+ # that broken handoff silent and harmless, including under `set -e`.
29
+ { printf '\036{"v":1,"policy":"%s","policyVersion":%s,"outcome":"%s","code":"%s"}\n' \
30
+ "${DF_POLICY:-}" "${DF_POLICY_VERSION:-0}" "$outcome" "$code" >&"$fd"; } 2>/dev/null || :
31
+ ;;
32
+ esac
33
+ }
34
+
35
+ doflow_read_envelope() {
36
+ local assignments
37
+ if [ -z "${INPUT:-}" ]; then
38
+ DF_ENV_CLASS=missing
39
+ DF_ENV_CODE=envelope-missing
40
+ DF_ENV_HARNESS=
41
+ DF_ENV_EVENT=
42
+ DF_ENV_OP=
43
+ DF_ENV_TOOL=
44
+ DF_ENV_PATHS=
45
+ DF_ENV_COMMAND=
46
+ DF_ENV_PATCH=
47
+ DF_ENV_ROOT=
48
+ DF_ENV_TASK=
49
+ return 0
50
+ fi
51
+ assignments=$(printf '%s' "${INPUT:-}" | jq -sr '
52
+ (.[0]) as $root |
53
+ ($root | if type == "object" then . else {} end) as $object |
54
+ ($object.doflow_event // {}) as $event |
55
+ ($event | if type == "object" then . else {} end) as $fields |
56
+ def field($key): ($fields[$key] // "") | if type == "string" then . else "" end;
57
+ def shellvar($name; $value): ($name + "=" + ($value | @sh));
58
+ (if ($root | type) != "object" or (($event | type) != "object") then "missing"
59
+ elif ($event | has("version") | not) then "missing"
60
+ elif ($event.version | type) == "number" and $event.version == 1 then "v1"
61
+ else "unsupported" end) as $class |
62
+ [shellvar("DF_ENV_CLASS"; $class),
63
+ shellvar("DF_ENV_CODE"; (if $class == "missing" then "envelope-missing" else "" end)),
64
+ shellvar("DF_ENV_HARNESS"; field("harness")),
65
+ shellvar("DF_ENV_EVENT"; field("event")),
66
+ shellvar("DF_ENV_OP"; field("operation")),
67
+ shellvar("DF_ENV_TOOL"; field("tool")),
68
+ shellvar("DF_ENV_PATHS"; ([$fields.paths[]? | select(type == "string")] | join("\n"))),
69
+ shellvar("DF_ENV_COMMAND"; field("command")),
70
+ shellvar("DF_ENV_PATCH"; field("patch")),
71
+ shellvar("DF_ENV_ROOT"; field("projectRoot")),
72
+ shellvar("DF_ENV_TASK"; field("taskId"))][]
73
+ ' 2>/dev/null) || return 1
74
+ eval "$assignments"
75
+ }
@@ -8,22 +8,57 @@
8
8
  # the front door right there, before it ever reached the code below that emits
9
9
  # the deny JSON. Sourcing this file must never change the caller's shell options.
10
10
  #
11
- # Usage: source "$(dirname "$0")/../../shared/hooks/policies/deny-json.sh"
11
+ # Usage: source "<policy-dir>/deny-json.sh"
12
+
13
+ case ${BASH_SOURCE[0]} in */*) _DOFLOW_DENY_JSON_DIR=${BASH_SOURCE[0]%/*} ;; *) _DOFLOW_DENY_JSON_DIR=. ;; esac
14
+
15
+ # Escape a string as a JSON string body without external tools. Work bytewise so
16
+ # every control byte is handled the same way regardless of the caller's locale.
17
+ doflow_json_escape() {
18
+ local LC_ALL=C input="${1-}" output="" char ordinal i
19
+ for ((i = 0; i < ${#input}; i++)); do
20
+ char="${input:i:1}"
21
+ case "$char" in
22
+ '\') output="$output\\\\" ;;
23
+ '"') output="$output\\\"" ;;
24
+ $'\n') output="$output\\n" ;;
25
+ $'\r') output="$output\\r" ;;
26
+ $'\t') output="$output\\t" ;;
27
+ *)
28
+ printf -v ordinal '%d' "'${char}"
29
+ if [ "$ordinal" -ge 32 ]; then output="$output$char"; fi
30
+ ;;
31
+ esac
32
+ done
33
+ printf '%s' "$output"
34
+ }
35
+
36
+ _doflow_json_reason() {
37
+ printf '"%s"' "$(doflow_json_escape "$1")"
38
+ }
12
39
 
13
40
  # Claude/Codex shape: nested under hookSpecificOutput.permissionDecision.
14
41
  emit_pretooluse_deny_nested() {
15
42
  local reason="$1"
16
- jq -n --arg reason "$reason" '{
43
+ if command -v jq >/dev/null 2>&1; then
44
+ jq -n --arg reason "$reason" '{
17
45
  hookSpecificOutput: {
18
46
  hookEventName: "PreToolUse",
19
47
  permissionDecision: "deny",
20
48
  permissionDecisionReason: $reason
21
49
  }
22
- }'
50
+ }'
51
+ else
52
+ printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":%s}}\n' "$(_doflow_json_reason "$reason")"
53
+ fi
23
54
  }
24
55
 
25
56
  # Gemini shape: flat {decision, reason} (packages/core/src/hooks/types.ts).
26
57
  emit_pretooluse_deny_flat() {
27
58
  local reason="$1"
28
- jq -n --arg reason "$reason" '{decision: "deny", reason: $reason}'
59
+ if command -v jq >/dev/null 2>&1; then
60
+ jq -n --arg reason "$reason" '{decision: "deny", reason: $reason}'
61
+ else
62
+ printf '{"decision":"deny","reason":%s}\n' "$(_doflow_json_reason "$reason")"
63
+ fi
29
64
  }
@@ -1,13 +1,15 @@
1
1
  #!/usr/bin/env bash
2
2
  # lib.sh — shared constants and helpers for the Canonical Policy Library
3
3
  #
4
- # Usage: source "$(dirname "$0")/lib.sh"
4
+ # Usage: source "<policy-dir>/lib.sh"
5
5
  #
6
6
  # All hooks must:
7
7
  # 1. source this file at the top
8
8
  # 2. call require_jq immediately after
9
9
  # 3. capture stdin once: INPUT=$(cat)
10
10
  # 4. use: json_field "$INPUT" ".field_name"
11
+
12
+ case ${BASH_SOURCE[0]} in */*) _DOFLOW_LIB_DIR=${BASH_SOURCE[0]%/*} ;; *) _DOFLOW_LIB_DIR=. ;; esac
11
13
  #
12
14
  # Deployment:
13
15
  # This file is installed by the doflow CLI and overwritten on update — do
@@ -327,7 +329,7 @@ storage_upkeep_async() {
327
329
  # Background the fully redirected subshell; an inner `&` leaves this shell
328
330
  # holding the hook's captured stdout open until the dispatcher exits. The
329
331
  # storage verb takes its project from the working directory: run it at the root.
330
- ( cd "$root" 2>/dev/null && nohup "$run" storage --action upkeep </dev/null >/dev/null 2>&1 ) >/dev/null 2>&1 &
332
+ ( cd "$root" 2>/dev/null && nohup "$run" storage --action upkeep </dev/null >/dev/null 2>&1 ) >/dev/null 2>&1 3>&- 5>&- &
331
333
  return 0
332
334
  done
333
335
  return 0
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env bash
2
+ case $0 in */*) _DOFLOW_POLDIR=${0%/*} ;; *) _DOFLOW_POLDIR=. ;; esac
2
3
  # mcp-tool-guard.sh — Canonical Policy Library: PreToolUse(mcp_*) hook
3
4
  #
4
5
  # Intercepts every MCP tool call and blocks matches against mcp-policy.conf, the
@@ -13,19 +14,9 @@
13
14
  # Canonical Policy Script Contract (design.md §4):
14
15
  # env DOFLOW_PROJECT_DIR, DOFLOW_AGENT (both optional; not required to
15
16
  # evaluate this policy)
16
- # stdin a PreToolUse-shaped JSON payload; tool name is read from the union
17
- # of field names harnesses use (tool_name/tool/toolName/name), since
18
- # a native front door (Claude/Codex/Kiro) execs this script directly
19
- # with no payload translation of its own.
20
- # exit 0 = allow, non-zero = deny with the reason on stderr.
21
- #
22
- # Reconciliation note (022-normalize-hooks, Phase A): MCP tool-name
23
- # vocabularies differ per harness — Claude/Codex/Kiro use a double-underscore
24
- # "mcp__<server>__<tool>" convention, Gemini documents a single-underscore
25
- # "mcp_<server>_<tool>" convention. A single-underscore prefix check ("^mcp_")
26
- # matches both conventions (the double-underscore form is a superset match of
27
- # the single-underscore prefix), so this script uses that one generic check
28
- # instead of hardcoding either harness's exact convention.
17
+ # stdin a native payload with envelope v1 under doflow_event; only the
18
+ # envelope's operation and tool fields are used.
19
+ # exit exit 2 + doflow_decide deny = deny; other faults per IC-011.
29
20
  #
30
21
  # mcp-policy.conf's shape (TAB-separated regex<TAB>reason, '#'-comments,
31
22
  # blank lines skipped) is identical across every harness that ships one today
@@ -34,36 +25,56 @@
34
25
 
35
26
  set -uo pipefail
36
27
 
37
- command -v jq >/dev/null 2>&1 || exit 0 # no jq -> cannot evaluate -> allow
38
-
39
28
  INPUT=$(cat)
29
+ if [ -f "$_DOFLOW_POLDIR/contract.sh" ]; then
30
+ . "$_DOFLOW_POLDIR/contract.sh"
31
+ else
32
+ doflow_decision_init() { :; }
33
+ doflow_decide() { :; }
34
+ doflow_read_envelope() { DF_ENV_CLASS=missing; DF_ENV_CODE=contract-missing; }
35
+ ( . "$_DOFLOW_POLDIR/capture-failure.sh"; doflow_capture_failure mcp-tool-guard contract-missing ) >/dev/null 2>&1 || true
36
+ if [[ "${DOFLOW_AGENT:-}" =~ ^[a-z]+$ ]]; then
37
+ printf '[mcp-tool-guard] DoFlow hooks out of step; run: doflow update -t %s\n' "$DOFLOW_AGENT" >&2
38
+ else
39
+ printf '[mcp-tool-guard] DoFlow hooks out of step; run: doflow update\n' >&2
40
+ fi
41
+ doflow_decide deny contract-missing
42
+ exit 2
43
+ fi
44
+ doflow_decision_init mcp-tool-guard 1
45
+ command -v jq >/dev/null 2>&1 || {
46
+ doflow_decide error dependency-missing:jq
47
+ ( . "$_DOFLOW_POLDIR/capture-failure.sh"; doflow_capture_failure mcp-tool-guard dependency-missing:jq ) >/dev/null 2>&1 || true
48
+ exit 0
49
+ }
50
+ DF_ENV_CLASS=missing DF_ENV_CODE=envelope-missing
51
+ doflow_read_envelope || true
40
52
 
41
53
  TOOL_NAME=""
42
- for field in '.tool_name' '.tool' '.toolName' '.name'; do
43
- TOOL_NAME=$(printf '%s' "$INPUT" | jq -r "${field} // empty" 2>/dev/null)
44
- [ -n "$TOOL_NAME" ] && break
45
- done
46
-
47
- # Fast exit for non-MCP tool events (also covers "tool name unresolved").
48
- # "mcp_" (single underscore) matches both Claude/Codex/Kiro's "mcp__" and
49
- # Gemini's "mcp_" naming conventions. Matched case-insensitively: the caller
50
- # (stream-hook-runner.js's MCP_TOOL_PATTERN) classifies case-insensitively
51
- # before ever routing here, so this check must not be stricter than the
52
- # classification that decided to invoke it (022-code-review finding).
53
- shopt -s nocasematch 2>/dev/null || true
54
- case "$TOOL_NAME" in
55
- mcp_*) ;;
56
- *) exit 0 ;;
57
- esac
58
- shopt -u nocasematch 2>/dev/null || true
54
+ if [ "$DF_ENV_CLASS" = v1 ]; then
55
+ [ "$DF_ENV_OP" = mcp ] || exit 0
56
+ TOOL_NAME=$DF_ENV_TOOL
57
+ elif [ "$DF_ENV_CLASS" = unsupported ]; then
58
+ doflow_decide unsupported envelope-version
59
+ ( . "$_DOFLOW_POLDIR/capture-failure.sh"; doflow_capture_failure mcp-tool-guard unsupported:envelope-version ) >/dev/null 2>&1 || true
60
+ exit 0
61
+ else
62
+ if [ "${DF_ENV_CODE:-envelope-missing}" != contract-missing ]; then
63
+ ( . "$_DOFLOW_POLDIR/capture-failure.sh"; doflow_capture_failure mcp-tool-guard envelope-missing ) >/dev/null 2>&1 || true
64
+ fi
65
+ if [[ "${DOFLOW_AGENT:-}" =~ ^[a-z]+$ ]]; then printf '[mcp-tool-guard] DoFlow hooks out of step; run: doflow update -t %s\n' "$DOFLOW_AGENT" >&2; else printf '[mcp-tool-guard] DoFlow hooks out of step; run: doflow update\n' >&2; fi
66
+ doflow_decide deny "${DF_ENV_CODE:-envelope-missing}"
67
+ exit 2
68
+ fi
59
69
 
60
- POLICY_FILE="$(dirname "$0")/mcp-policy.conf"
70
+ POLICY_FILE="$_DOFLOW_POLDIR/mcp-policy.conf"
61
71
 
62
72
  # If policy file is missing, allow everything (fail open — don't block the agent).
63
73
  if [ ! -f "$POLICY_FILE" ]; then
64
74
  # The policy file ships with DoFlow, so a missing one means a broken install: record it on this
65
75
  # machine (feature 046, IC-018) and fail open exactly as before. Subshell, output discarded.
66
- ( . "$(dirname "$0")/capture-failure.sh"; doflow_capture_failure mcp-tool-guard policy-file-missing ) >/dev/null 2>&1 || true
76
+ ( . "$_DOFLOW_POLDIR/capture-failure.sh"; doflow_capture_failure mcp-tool-guard policy-file-missing ) >/dev/null 2>&1 || true
77
+ doflow_decide error mcp-policy-missing
67
78
  exit 0
68
79
  fi
69
80
 
@@ -83,6 +94,7 @@ while IFS=$'\t' read -r pattern reason || [ -n "$pattern" ]; do
83
94
 
84
95
  if [ "$matched" = "true" ]; then
85
96
  echo "[mcp-tool-guard] ${reason:-MCP tool call blocked by mcp-tool-guard} (tool: $TOOL_NAME)" >&2
97
+ doflow_decide deny blocked-pattern
86
98
  exit 2
87
99
  fi
88
100
  done < "$POLICY_FILE"
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env bash
2
+ case $0 in */*) _DOFLOW_POLDIR=${0%/*} ;; *) _DOFLOW_POLDIR=. ;; esac
2
3
  # pre-bash-guard.sh — Canonical Policy Library: PreToolUse(shell-command) hook
3
4
  #
4
5
  # Intercepts every shell-command tool call and blocks dangerous commands by
@@ -10,14 +11,9 @@
10
11
  # Canonical Policy Script Contract (design.md §4):
11
12
  # env DOFLOW_PROJECT_DIR, DOFLOW_AGENT (both optional; not required to
12
13
  # evaluate this policy)
13
- # stdin a PreToolUse-shaped JSON payload; tool name is read from the union
14
- # of field names harnesses use, and matched against the union of
15
- # known shell-command tool names (Bash/bash — Claude/Codex;
16
- # run_shell_command — Gemini; run_command — the generic name
17
- # stream-hook-runner.js's own COMMAND_TOOL_PATTERN already
18
- # recognized), since a native front door (Claude/Codex/Kiro) execs
19
- # this script directly with no payload translation of its own.
20
- # exit 0 = allow, non-zero = deny with the reason on stderr.
14
+ # stdin a native payload with envelope v1 under doflow_event; only the
15
+ # envelope's operation, tool and command fields are used.
16
+ # exit exit 2 + doflow_decide deny = deny; other faults per IC-011.
21
17
  #
22
18
  # Reconciliation note (022-normalize-hooks, Phase A / D1): this also absorbs
23
19
  # stream-hook-runner.js's inline DESTRUCTIVE_COMMAND_PATTERN
@@ -38,29 +34,54 @@
38
34
 
39
35
  set -uo pipefail
40
36
 
41
- command -v jq >/dev/null 2>&1 || exit 0 # no jq -> cannot evaluate -> allow
42
-
43
37
  INPUT=$(cat)
38
+ if [ -f "$_DOFLOW_POLDIR/contract.sh" ]; then
39
+ . "$_DOFLOW_POLDIR/contract.sh"
40
+ else
41
+ doflow_decision_init() { :; }
42
+ doflow_decide() { :; }
43
+ doflow_read_envelope() { DF_ENV_CLASS=missing; DF_ENV_CODE=contract-missing; }
44
+ ( . "$_DOFLOW_POLDIR/capture-failure.sh"; doflow_capture_failure pre-bash-guard contract-missing ) >/dev/null 2>&1 || true
45
+ if [[ "${DOFLOW_AGENT:-}" =~ ^[a-z]+$ ]]; then
46
+ printf '[pre-bash-guard] DoFlow hooks out of step; run: doflow update -t %s\n' "$DOFLOW_AGENT" >&2
47
+ else
48
+ printf '[pre-bash-guard] DoFlow hooks out of step; run: doflow update\n' >&2
49
+ fi
50
+ doflow_decide deny contract-missing
51
+ exit 2
52
+ fi
53
+ doflow_decision_init pre-bash-guard 1
54
+ command -v jq >/dev/null 2>&1 || {
55
+ doflow_decide error dependency-missing:jq
56
+ ( . "$_DOFLOW_POLDIR/capture-failure.sh"; doflow_capture_failure pre-bash-guard dependency-missing:jq ) >/dev/null 2>&1 || true
57
+ exit 0
58
+ }
59
+ DF_ENV_CLASS=missing DF_ENV_CODE=envelope-missing
60
+ doflow_read_envelope || true
44
61
 
45
62
  TOOL_NAME=""
46
- for field in '.tool_name' '.tool' '.toolName' '.name'; do
47
- TOOL_NAME=$(printf '%s' "$INPUT" | jq -r "${field} // empty" 2>/dev/null)
48
- [ -n "$TOOL_NAME" ] && break
49
- done
50
-
51
- # Fast exit for non-shell-command tool events (union of every known
52
- # shell-command tool name across harnesses, case-insensitive).
53
- case "$TOOL_NAME" in
54
- Bash|bash|run_shell_command|run_command) ;;
55
- *) exit 0 ;;
56
- esac
57
-
58
- COMMAND=$(printf '%s' "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
63
+ COMMAND=""
64
+ if [ "$DF_ENV_CLASS" = v1 ]; then
65
+ [ "$DF_ENV_OP" = command ] || exit 0
66
+ TOOL_NAME=$DF_ENV_TOOL
67
+ COMMAND=$DF_ENV_COMMAND
68
+ elif [ "$DF_ENV_CLASS" = unsupported ]; then
69
+ doflow_decide unsupported envelope-version
70
+ ( . "$_DOFLOW_POLDIR/capture-failure.sh"; doflow_capture_failure pre-bash-guard unsupported:envelope-version ) >/dev/null 2>&1 || true
71
+ exit 0
72
+ else
73
+ if [ "${DF_ENV_CODE:-envelope-missing}" != contract-missing ]; then
74
+ ( . "$_DOFLOW_POLDIR/capture-failure.sh"; doflow_capture_failure pre-bash-guard envelope-missing ) >/dev/null 2>&1 || true
75
+ fi
76
+ if [[ "${DOFLOW_AGENT:-}" =~ ^[a-z]+$ ]]; then printf '[pre-bash-guard] DoFlow hooks out of step; run: doflow update -t %s\n' "$DOFLOW_AGENT" >&2; else printf '[pre-bash-guard] DoFlow hooks out of step; run: doflow update\n' >&2; fi
77
+ doflow_decide deny "${DF_ENV_CODE:-envelope-missing}"
78
+ exit 2
79
+ fi
59
80
 
60
81
  # Nothing to check if command is empty
61
82
  [ -z "$COMMAND" ] && exit 0
62
83
 
63
- PATTERNS_FILE="$(dirname "$0")/blocked-patterns.conf"
84
+ PATTERNS_FILE="$_DOFLOW_POLDIR/blocked-patterns.conf"
64
85
 
65
86
  # ASCII-only patterns: the C locale keeps bash's substring/pattern operations
66
87
  # byte-wise (much faster than multibyte-aware in a UTF-8 locale).
@@ -268,17 +289,21 @@ if [ ! -f "$PATTERNS_FILE" ]; then
268
289
  # The pattern file ships with DoFlow, so a missing one means a broken install: record it on this
269
290
  # machine (feature 046, IC-018) and go on exactly as before. Subshell, output discarded: nothing
270
291
  # the helper does can reach this policy's options, variables, output or exit status.
271
- ( . "$(dirname "$0")/capture-failure.sh"; doflow_capture_failure pre-bash-guard patterns-missing ) >/dev/null 2>&1 || true
292
+ ( . "$_DOFLOW_POLDIR/capture-failure.sh"; doflow_capture_failure pre-bash-guard patterns-missing ) >/dev/null 2>&1 || true
293
+ doflow_decide error patterns-missing
272
294
  if _floor_rm_hits '/+\*{0,2}'; then
273
295
  echo "[pre-bash-guard] Catastrophic delete blocked — recursive rm of the root directory (/)" >&2
296
+ doflow_decide deny catastrophic-rm-root
274
297
  exit 2
275
298
  fi
276
299
  if _floor_rm_hits '(~[A-Za-z_][A-Za-z0-9_.-]*|~|\$HOME|\$\{HOME\})(/\*{0,2})?'; then
277
300
  echo "[pre-bash-guard] Catastrophic delete blocked — recursive rm of the home directory" >&2
301
+ doflow_decide deny catastrophic-rm-home
278
302
  exit 2
279
303
  fi
280
304
  if _floor_rm_hits '/(Users|home|etc|usr|bin|sbin|var|opt|System|Library|Applications|Volumes|private|root|boot|lib|dev|proc)(/\*{0,2})?'; then
281
305
  echo "[pre-bash-guard] Catastrophic delete blocked — recursive rm of a system directory" >&2
306
+ doflow_decide deny catastrophic-rm-system
282
307
  exit 2
283
308
  fi
284
309
  # No patterns file beyond the hardcoded floor — allow everything else
@@ -286,6 +311,30 @@ if [ ! -f "$PATTERNS_FILE" ]; then
286
311
  exit 0
287
312
  fi
288
313
 
314
+ # One combined ERE cheaply rules out commands that match no live pattern. A
315
+ # grep match or regex error falls through to the original per-line loop, which
316
+ # still owns the reason and optional exclude semantics.
317
+ _anchored_patterns="" _full_patterns="" _has_backreference=false
318
+ while IFS=$'\t' read -r pattern _reason _exclude || [ -n "$pattern" ]; do
319
+ [ -z "$pattern" ] && continue
320
+ case "$pattern" in \#*) continue ;; esac
321
+ if [[ "$pattern" =~ \\[1-9] ]]; then _has_backreference=true; fi
322
+ case "$pattern" in
323
+ '(^|'*) _anchored_patterns="${_anchored_patterns:+$_anchored_patterns|}($pattern)" ;;
324
+ *) _full_patterns="${_full_patterns:+$_full_patterns|}($pattern)" ;;
325
+ esac
326
+ done < "$PATTERNS_FILE"
327
+ _anchored_status=1 _full_status=1
328
+ if [ -n "$_anchored_patterns" ]; then
329
+ grep -qiE -- "$_anchored_patterns" <<<"$SHELL_TEXT" 2>/dev/null
330
+ _anchored_status=$?
331
+ fi
332
+ if [ -n "$_full_patterns" ]; then
333
+ grep -qiE -- "$_full_patterns" <<<"$COMMAND" 2>/dev/null
334
+ _full_status=$?
335
+ fi
336
+ if [ "$_anchored_status" -eq 1 ] && [ "$_full_status" -eq 1 ] && [ "$_has_backreference" != true ]; then exit 0; fi
337
+
289
338
  # ── Pattern matching ──────────────────────────────────────────────────────────
290
339
 
291
340
  while IFS=$'\t' read -r pattern reason exclude || [ -n "$pattern" ]; do
@@ -320,6 +369,7 @@ while IFS=$'\t' read -r pattern reason exclude || [ -n "$pattern" ]; do
320
369
 
321
370
  if [ "$matched" = "true" ]; then
322
371
  echo "[pre-bash-guard] ${reason:-Command blocked by pre-bash-guard}" >&2
372
+ doflow_decide deny blocked-pattern
323
373
  exit 2
324
374
  fi
325
375
  done < "$PATTERNS_FILE"