@kontourai/flow-agents 4.1.0 → 4.2.1
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/build/src/builder-flow-runtime.js +10 -5
- package/context/contracts/verification-contract.md +6 -0
- package/context/scripts/telemetry/lib/config.sh +29 -0
- package/dist/base/build/package.json +1 -1
- package/dist/base/build/src/builder-flow-runtime.js +10 -5
- package/dist/base/context/contracts/verification-contract.md +6 -0
- package/dist/base/context/scripts/telemetry/lib/config.sh +29 -0
- package/dist/base/evals/acceptance/prove-capture-teeth.sh +24 -0
- package/dist/base/evals/integration/test_effective_backlog_settings.sh +118 -15
- package/dist/base/evals/integration/test_liveness_console_relay.sh +91 -1
- package/dist/base/evals/integration/test_liveness_heartbeat.sh +66 -0
- package/dist/base/install.sh +1 -1
- package/dist/base/scripts/hooks/claude-telemetry-hook.js +6 -0
- package/dist/base/scripts/hooks/lib/codex-exit-code.js +75 -23
- package/dist/base/scripts/hooks/lib/liveness-heartbeat.js +42 -1
- package/dist/base/scripts/hooks/lib/liveness-write.js +109 -9
- package/dist/base/scripts/liveness/relay.sh +23 -11
- package/dist/base/scripts/telemetry/lib/config.sh +29 -0
- package/dist/claude-code/build/package.json +1 -1
- package/dist/claude-code/build/src/builder-flow-runtime.js +10 -5
- package/dist/claude-code/context/contracts/verification-contract.md +6 -0
- package/dist/claude-code/context/scripts/telemetry/lib/config.sh +29 -0
- package/dist/claude-code/evals/acceptance/prove-capture-teeth.sh +24 -0
- package/dist/claude-code/evals/integration/test_effective_backlog_settings.sh +118 -15
- package/dist/claude-code/evals/integration/test_liveness_console_relay.sh +91 -1
- package/dist/claude-code/evals/integration/test_liveness_heartbeat.sh +66 -0
- package/dist/claude-code/install.sh +1 -1
- package/dist/claude-code/scripts/hooks/claude-telemetry-hook.js +6 -0
- package/dist/claude-code/scripts/hooks/lib/codex-exit-code.js +75 -23
- package/dist/claude-code/scripts/hooks/lib/liveness-heartbeat.js +42 -1
- package/dist/claude-code/scripts/hooks/lib/liveness-write.js +109 -9
- package/dist/claude-code/scripts/liveness/relay.sh +23 -11
- package/dist/claude-code/scripts/telemetry/lib/config.sh +29 -0
- package/dist/codex/build/package.json +1 -1
- package/dist/codex/build/src/builder-flow-runtime.js +10 -5
- package/dist/codex/context/contracts/verification-contract.md +6 -0
- package/dist/codex/context/scripts/telemetry/lib/config.sh +29 -0
- package/dist/codex/evals/acceptance/prove-capture-teeth.sh +24 -0
- package/dist/codex/evals/integration/test_effective_backlog_settings.sh +118 -15
- package/dist/codex/evals/integration/test_liveness_console_relay.sh +91 -1
- package/dist/codex/evals/integration/test_liveness_heartbeat.sh +66 -0
- package/dist/codex/install.sh +1 -1
- package/dist/codex/scripts/hooks/claude-telemetry-hook.js +6 -0
- package/dist/codex/scripts/hooks/lib/codex-exit-code.js +75 -23
- package/dist/codex/scripts/hooks/lib/liveness-heartbeat.js +42 -1
- package/dist/codex/scripts/hooks/lib/liveness-write.js +109 -9
- package/dist/codex/scripts/liveness/relay.sh +23 -11
- package/dist/codex/scripts/telemetry/lib/config.sh +29 -0
- package/dist/kiro/build/package.json +1 -1
- package/dist/kiro/build/src/builder-flow-runtime.js +10 -5
- package/dist/kiro/context/contracts/verification-contract.md +6 -0
- package/dist/kiro/context/scripts/telemetry/lib/config.sh +29 -0
- package/dist/kiro/evals/acceptance/prove-capture-teeth.sh +24 -0
- package/dist/kiro/evals/integration/test_effective_backlog_settings.sh +118 -15
- package/dist/kiro/evals/integration/test_liveness_console_relay.sh +91 -1
- package/dist/kiro/evals/integration/test_liveness_heartbeat.sh +66 -0
- package/dist/kiro/install.sh +1 -1
- package/dist/kiro/scripts/hooks/claude-telemetry-hook.js +6 -0
- package/dist/kiro/scripts/hooks/lib/codex-exit-code.js +75 -23
- package/dist/kiro/scripts/hooks/lib/liveness-heartbeat.js +42 -1
- package/dist/kiro/scripts/hooks/lib/liveness-write.js +109 -9
- package/dist/kiro/scripts/liveness/relay.sh +23 -11
- package/dist/kiro/scripts/telemetry/lib/config.sh +29 -0
- package/dist/opencode/build/package.json +1 -1
- package/dist/opencode/build/src/builder-flow-runtime.js +10 -5
- package/dist/opencode/context/contracts/verification-contract.md +6 -0
- package/dist/opencode/context/scripts/telemetry/lib/config.sh +29 -0
- package/dist/opencode/evals/acceptance/prove-capture-teeth.sh +24 -0
- package/dist/opencode/evals/integration/test_effective_backlog_settings.sh +118 -15
- package/dist/opencode/evals/integration/test_liveness_console_relay.sh +91 -1
- package/dist/opencode/evals/integration/test_liveness_heartbeat.sh +66 -0
- package/dist/opencode/install.sh +1 -1
- package/dist/opencode/scripts/hooks/claude-telemetry-hook.js +6 -0
- package/dist/opencode/scripts/hooks/lib/codex-exit-code.js +75 -23
- package/dist/opencode/scripts/hooks/lib/liveness-heartbeat.js +42 -1
- package/dist/opencode/scripts/hooks/lib/liveness-write.js +109 -9
- package/dist/opencode/scripts/liveness/relay.sh +23 -11
- package/dist/opencode/scripts/telemetry/lib/config.sh +29 -0
- package/dist/pi/build/package.json +1 -1
- package/dist/pi/build/src/builder-flow-runtime.js +10 -5
- package/dist/pi/context/contracts/verification-contract.md +6 -0
- package/dist/pi/context/scripts/telemetry/lib/config.sh +29 -0
- package/dist/pi/evals/acceptance/prove-capture-teeth.sh +24 -0
- package/dist/pi/evals/integration/test_effective_backlog_settings.sh +118 -15
- package/dist/pi/evals/integration/test_liveness_console_relay.sh +91 -1
- package/dist/pi/evals/integration/test_liveness_heartbeat.sh +66 -0
- package/dist/pi/install.sh +1 -1
- package/dist/pi/scripts/hooks/claude-telemetry-hook.js +6 -0
- package/dist/pi/scripts/hooks/lib/codex-exit-code.js +75 -23
- package/dist/pi/scripts/hooks/lib/liveness-heartbeat.js +42 -1
- package/dist/pi/scripts/hooks/lib/liveness-write.js +109 -9
- package/dist/pi/scripts/liveness/relay.sh +23 -11
- package/dist/pi/scripts/telemetry/lib/config.sh +29 -0
- package/evals/acceptance/prove-capture-teeth.sh +24 -0
- package/evals/integration/test_effective_backlog_settings.sh +118 -15
- package/evals/integration/test_liveness_console_relay.sh +91 -1
- package/evals/integration/test_liveness_heartbeat.sh +66 -0
- package/package.json +2 -1
- package/scripts/hooks/claude-telemetry-hook.js +6 -0
- package/scripts/hooks/lib/codex-exit-code.js +75 -23
- package/scripts/hooks/lib/liveness-heartbeat.js +42 -1
- package/scripts/hooks/lib/liveness-write.js +109 -9
- package/scripts/liveness/relay.sh +23 -11
- package/scripts/telemetry/lib/config.sh +29 -0
- package/src/builder-flow-runtime.ts +10 -5
- package/src/cli/builder-flow-runtime.test.mjs +55 -2
- package/src/cli/codex-exit-code.test.mjs +217 -2
|
@@ -109,6 +109,13 @@ FLOW_AGENTS_CONSOLE_ECONOMICS_ENDPOINT_URL="${FLOW_AGENTS_CONSOLE_ECONOMICS_ENDP
|
|
|
109
109
|
# Set (non-empty) only when the config file carries an explicit console_economics_relay key —
|
|
110
110
|
# distinguishes "operator said 0/1" from "key absent" for the default-on rule below.
|
|
111
111
|
console_economics_relay_raw=""
|
|
112
|
+
# Liveness relay (#567): same conf-driven, opt-out-not-silent-off shape as economics (#469) so an
|
|
113
|
+
# operator enables the hosted liveness mirror via console_liveness_relay=1 in the conf, NOT an env
|
|
114
|
+
# var/.profile. A caller-pre-set env var is the starting point; the config key overrides it; absent
|
|
115
|
+
# any key the default-on rule below turns it on once a console telemetry sink resolves.
|
|
116
|
+
FLOW_AGENTS_CONSOLE_LIVENESS_RELAY="${FLOW_AGENTS_CONSOLE_LIVENESS_RELAY:-}"
|
|
117
|
+
FLOW_AGENTS_CONSOLE_LIVENESS_ENDPOINT_URL="${FLOW_AGENTS_CONSOLE_LIVENESS_ENDPOINT_URL:-}"
|
|
118
|
+
console_liveness_relay_raw=""
|
|
112
119
|
# Pricing registry source (consumed by lib/pricing.sh). Explicit file/URL win;
|
|
113
120
|
# otherwise lib/pricing.sh uses the bundled pricing.json offline.
|
|
114
121
|
TELEMETRY_PRICING_FILE="${TELEMETRY_PRICING_FILE:-${FLOW_AGENTS_PRICING_FILE:-}}"
|
|
@@ -164,6 +171,17 @@ if [[ -f "$TELEMETRY_CONFIG_FILE" ]]; then
|
|
|
164
171
|
esac
|
|
165
172
|
;;
|
|
166
173
|
console_economics_endpoint_url) FLOW_AGENTS_CONSOLE_ECONOMICS_ENDPOINT_URL="$value" ;;
|
|
174
|
+
console_liveness_relay)
|
|
175
|
+
case "$(echo "$value" | tr '[:upper:]' '[:lower:]')" in
|
|
176
|
+
1|true|yes|on) console_liveness_relay_raw="1" ;;
|
|
177
|
+
0|false|no|off) console_liveness_relay_raw="0" ;;
|
|
178
|
+
*)
|
|
179
|
+
printf 'warning: config.sh: unrecognized console_liveness_relay value %q; treating as off\n' "$value" >&2
|
|
180
|
+
console_liveness_relay_raw="$value"
|
|
181
|
+
;;
|
|
182
|
+
esac
|
|
183
|
+
;;
|
|
184
|
+
console_liveness_endpoint_url) FLOW_AGENTS_CONSOLE_LIVENESS_ENDPOINT_URL="$value" ;;
|
|
167
185
|
console_pricing_url) TELEMETRY_PRICING_URL="$value" ;;
|
|
168
186
|
pricing_url) TELEMETRY_PRICING_URL="$value" ;;
|
|
169
187
|
pricing_file) TELEMETRY_PRICING_FILE="$value" ;;
|
|
@@ -186,6 +204,17 @@ elif [[ -z "$FLOW_AGENTS_CONSOLE_ECONOMICS_RELAY" \
|
|
|
186
204
|
FLOW_AGENTS_CONSOLE_ECONOMICS_RELAY=1
|
|
187
205
|
fi
|
|
188
206
|
|
|
207
|
+
# Liveness relay default-on rule (#567), identical shape to economics above: an explicit
|
|
208
|
+
# console_liveness_relay config key always wins; otherwise, once a console telemetry sink resolves,
|
|
209
|
+
# the relay defaults ON unless a caller already pre-set FLOW_AGENTS_CONSOLE_LIVENESS_RELAY in the
|
|
210
|
+
# environment. relay.sh's own opt-in gate reads this exact variable.
|
|
211
|
+
if [[ -n "$console_liveness_relay_raw" ]]; then
|
|
212
|
+
FLOW_AGENTS_CONSOLE_LIVENESS_RELAY="$console_liveness_relay_raw"
|
|
213
|
+
elif [[ -z "$FLOW_AGENTS_CONSOLE_LIVENESS_RELAY" \
|
|
214
|
+
&& ( -n "${CONSOLE_TELEMETRY_URL:-}" || -n "${CONSOLE_TELEMETRY_ENDPOINT_URL:-}" ) ]]; then
|
|
215
|
+
FLOW_AGENTS_CONSOLE_LIVENESS_RELAY=1
|
|
216
|
+
fi
|
|
217
|
+
|
|
189
218
|
# Pricing URL is explicit-only (env or config). Do not derive the console
|
|
190
219
|
# pricing endpoint by default; the bundled registry is the reliable offline floor.
|
|
191
220
|
|
|
@@ -246,6 +246,30 @@ codex_banner_teeth(){
|
|
|
246
246
|
else
|
|
247
247
|
_f "Codex banner CALL_ID: did not record pass/exitCode:0 for the call_id-matched entry: $(cat "$log_callid" 2>/dev/null)"
|
|
248
248
|
fi
|
|
249
|
+
|
|
250
|
+
# --- PARALLEL command correlation (#644): current Codex exec_command calls
|
|
251
|
+
# store arguments under `cmd`. With no hook call_id, each PostToolUse event
|
|
252
|
+
# must resolve its exact function_call/output pair by normalized command.
|
|
253
|
+
local proj_parallel; proj_parallel="$(mktemp -d)"
|
|
254
|
+
mkdir -p "$proj_parallel/.kontourai/flow-agents/cap-codex-parallel"
|
|
255
|
+
printf '# Repo\n' > "$proj_parallel/AGENTS.md"
|
|
256
|
+
printf '%s' '{"schema_version":"1.0","task_slug":"cap-codex-parallel","status":"in_progress","phase":"verification","updated_at":"2026-06-23T00:00:00Z"}' > "$proj_parallel/.kontourai/flow-agents/cap-codex-parallel/state.json"
|
|
257
|
+
local rollout_parallel="$proj_parallel/rollout-parallel.jsonl"
|
|
258
|
+
printf '%s\n' '{"timestamp":"2026-06-23T00:00:00Z","type":"response_item","payload":{"type":"function_call","call_id":"call_pass","name":"exec_command","arguments":"{\"cmd\":\"find seed -type f\"}"}}' > "$rollout_parallel"
|
|
259
|
+
printf '%s\n' '{"timestamp":"2026-06-23T00:00:01Z","type":"response_item","payload":{"type":"function_call","call_id":"call_fail","name":"exec_command","arguments":"{\"cmd\":\"cat seed/src/window.js\"}"}}' >> "$rollout_parallel"
|
|
260
|
+
printf '%s\n' '{"timestamp":"2026-06-23T00:00:02Z","type":"response_item","payload":{"type":"function_call_output","call_id":"call_pass","output":"Process exited with code 0\nOutput:\n..."}}' >> "$rollout_parallel"
|
|
261
|
+
printf '%s\n' '{"timestamp":"2026-06-23T00:00:03Z","type":"response_item","payload":{"type":"function_call_output","call_id":"call_fail","output":"Process exited with code 1\nOutput:\n..."}}' >> "$rollout_parallel"
|
|
262
|
+
printf '{"hook_event_name":"PostToolUse","tool_name":"Bash","cwd":"%s","tool_input":{"command":"find seed -type f"},"transcript_path":"%s"}' "$proj_parallel" "$rollout_parallel" \
|
|
263
|
+
| env CODEX_HOME="$home" CLAUDE_PROJECT_DIR="$home" bash -c "$capcmd" >/dev/null 2>&1 || true
|
|
264
|
+
printf '{"hook_event_name":"PostToolUse","tool_name":"Bash","cwd":"%s","tool_input":{"command":"cat seed/src/window.js"},"transcript_path":"%s"}' "$proj_parallel" "$rollout_parallel" \
|
|
265
|
+
| env CODEX_HOME="$home" CLAUDE_PROJECT_DIR="$home" bash -c "$capcmd" >/dev/null 2>&1 || true
|
|
266
|
+
local log_parallel="$proj_parallel/.kontourai/flow-agents/cap-codex-parallel/command-log.jsonl"
|
|
267
|
+
if rg -q '"command":"find seed -type f","observedResult":"pass","exitCode":0' "$log_parallel" 2>/dev/null \
|
|
268
|
+
&& rg -q '"command":"cat seed/src/window.js","observedResult":"fail","exitCode":1' "$log_parallel" 2>/dev/null; then
|
|
269
|
+
_p "Codex banner PARALLEL: cmd correlation keeps each command attached to its own exit code"
|
|
270
|
+
else
|
|
271
|
+
_f "Codex banner PARALLEL: command/result attribution crossed or stayed unresolved: $(cat "$log_parallel" 2>/dev/null)"
|
|
272
|
+
fi
|
|
249
273
|
}
|
|
250
274
|
|
|
251
275
|
run_bundle "Claude Code" "$ROOT/dist/claude-code/install.sh" ".claude/settings.json" "CLAUDE_PROJECT_DIR"
|
|
@@ -2,13 +2,18 @@
|
|
|
2
2
|
set -uo pipefail
|
|
3
3
|
|
|
4
4
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
5
|
+
CLI="$ROOT/build/src/cli.js"
|
|
6
|
+
BACKLOG_SETTINGS="$ROOT/context/settings/backlog-provider-settings.json"
|
|
7
|
+
ASSIGNMENT_SETTINGS="$ROOT/context/settings/assignment-provider-settings.json"
|
|
8
|
+
BACKLOG_GLOBAL="$ROOT/evals/fixtures/backlog-provider-settings/global-default.json"
|
|
9
|
+
BACKLOG_OVERRIDE="$ROOT/evals/fixtures/backlog-provider-settings/project-override.json"
|
|
10
|
+
TMPDIR_EVAL="$(mktemp -d)"
|
|
11
|
+
EMPTY_BACKLOG="$TMPDIR_EVAL/empty-backlog-settings.json"
|
|
12
|
+
EMPTY_ASSIGNMENT="$TMPDIR_EVAL/empty-assignment-settings.json"
|
|
13
|
+
trap 'rm -rf "$TMPDIR_EVAL"' EXIT
|
|
14
|
+
|
|
15
|
+
printf '{"schema_version":"1.0","projects":[]}\n' > "$EMPTY_BACKLOG"
|
|
16
|
+
printf '{"schema_version":"1.0","projects":[]}\n' > "$EMPTY_ASSIGNMENT"
|
|
12
17
|
|
|
13
18
|
errors=0
|
|
14
19
|
pass() { echo " ✓ $1"; }
|
|
@@ -18,17 +23,32 @@ json_value() {
|
|
|
18
23
|
NO_COLOR=1 FORCE_COLOR=0 node -e 'const fs=require("fs"); let cur=JSON.parse(fs.readFileSync(0,"utf8")); for (const part of process.argv[1].split(".")) cur=Array.isArray(cur) ? cur[Number(part)] : cur[part]; console.log(cur);' "$1"
|
|
19
24
|
}
|
|
20
25
|
|
|
21
|
-
echo "=== Effective
|
|
26
|
+
echo "=== Effective provider settings ==="
|
|
27
|
+
|
|
28
|
+
# Build exactly once before calling the actual compiled CLI commands below.
|
|
29
|
+
if (cd "$ROOT" && npm run build --silent); then
|
|
30
|
+
pass "compiled CLI build succeeds"
|
|
31
|
+
else
|
|
32
|
+
fail "compiled CLI build succeeds"
|
|
33
|
+
fi
|
|
22
34
|
|
|
23
|
-
|
|
24
|
-
|
|
35
|
+
if [[ ! -f "$CLI" ]]; then
|
|
36
|
+
fail "compiled CLI is available"
|
|
37
|
+
else
|
|
38
|
+
pass "compiled CLI is available"
|
|
39
|
+
fi
|
|
40
|
+
|
|
41
|
+
echo "--- existing backlog precedence and ask-user behavior ---"
|
|
42
|
+
|
|
43
|
+
default_result="$(node "$CLI" effective-backlog-settings --repo-path "$ROOT" --json 2>/dev/null)"
|
|
44
|
+
[[ "$(printf '%s' "$default_result" | json_value status)" == "configured" ]] && pass "default project settings resolve through compiled CLI" || fail "default project settings resolve through compiled CLI"
|
|
25
45
|
[[ "$(printf '%s' "$default_result" | json_value settings.work_item_provider.kind)" == "github" ]] && pass "default provider kind is github" || fail "default provider kind is github"
|
|
26
46
|
[[ "$(printf '%s' "$default_result" | json_value settings.work_item_provider.repo.owner)" == "kontourai" ]] && pass "default repo owner is kontourai" || fail "default repo owner is kontourai"
|
|
27
47
|
[[ "$(printf '%s' "$default_result" | json_value settings.work_item_provider.repo.name)" == "flow-agents" ]] && pass "default repo name is flow-agents" || fail "default repo name is flow-agents"
|
|
28
48
|
[[ "$(printf '%s' "$default_result" | json_value settings.board_provider.board.number)" == "1" ]] && pass "default project number is 1" || fail "default project number is 1"
|
|
29
49
|
! printf '%s' "$default_result" | rg -q '/build/context/settings/backlog-provider-settings.json' && pass "default settings path is not build-relative" || fail "default settings path is not build-relative"
|
|
30
50
|
|
|
31
|
-
configured="$(node "$
|
|
51
|
+
configured="$(node "$CLI" effective-backlog-settings --repo-path "$ROOT" --project-settings "$BACKLOG_SETTINGS" --global-settings "$EMPTY_BACKLOG" --json)"
|
|
32
52
|
[[ "$(printf '%s' "$configured" | json_value status)" == "configured" ]] && pass "configured repo resolves" || fail "configured repo resolves"
|
|
33
53
|
[[ "$(printf '%s' "$configured" | json_value settings.work_item_provider.kind)" == "github" ]] && pass "provider kind is github" || fail "provider kind is github"
|
|
34
54
|
[[ "$(printf '%s' "$configured" | json_value settings.work_item_provider.repo.owner)" == "kontourai" ]] && pass "repo owner is kontourai" || fail "repo owner is kontourai"
|
|
@@ -38,21 +58,104 @@ printf '%s' "$configured" | rg -F -q '"ready_statuses": [' && pass "filters incl
|
|
|
38
58
|
printf '%s' "$configured" | rg -F -q '"include_labels": []' && pass "filters do not require labels by default" || fail "filters do not require labels by default"
|
|
39
59
|
printf '%s' "$configured" | rg -q '"prefer_finishing_active_work": true' && pass "WIP policy prefers finishing active work" || fail "WIP policy prefers finishing active work"
|
|
40
60
|
|
|
41
|
-
override_result="$(node "$
|
|
61
|
+
override_result="$(node "$CLI" effective-backlog-settings --repo-path "$ROOT" --project-settings "$BACKLOG_OVERRIDE" --global-settings "$BACKLOG_GLOBAL" --json)"
|
|
42
62
|
[[ "$(printf '%s' "$override_result" | json_value source)" == "project" ]] && pass "project settings override global defaults" || fail "project settings override global defaults"
|
|
43
63
|
[[ "$(printf '%s' "$override_result" | json_value settings.board_provider.board.number)" == "1" ]] && pass "project override keeps Project 1" || fail "project override keeps Project 1"
|
|
44
64
|
! printf '%s' "$override_result" | rg -q 'global-ready' && pass "project override replaces global filters" || fail "project override replaces global filters"
|
|
45
65
|
|
|
46
|
-
missing_output="$(node "$
|
|
66
|
+
missing_output="$(node "$CLI" effective-backlog-settings --repo-path "$ROOT" --project-settings "$EMPTY_BACKLOG" --global-settings "$EMPTY_BACKLOG" --json 2>/dev/null)"
|
|
47
67
|
missing_status=$?
|
|
48
68
|
[[ "$missing_status" -eq 2 ]] && pass "missing settings exits with ask-user status" || fail "missing settings exits with ask-user status"
|
|
49
69
|
[[ "$(printf '%s' "$missing_output" | json_value status)" == "ask_user" ]] && pass "missing settings reports ask_user" || fail "missing settings reports ask_user"
|
|
50
70
|
printf '%s' "$missing_output" | rg -q 'WorkItemProvider and BoardProvider' && pass "missing settings names provider roles" || fail "missing settings names provider roles"
|
|
51
71
|
|
|
72
|
+
echo "--- public self-configuration and external multi-repository settings ---"
|
|
73
|
+
|
|
74
|
+
# Product-specific fleet routing belongs in consumer-owned settings. The public package proves
|
|
75
|
+
# that an external settings file can configure multiple repositories without embedding those
|
|
76
|
+
# consumer mappings in Flow Agents source.
|
|
77
|
+
if node --input-type=module - "$ROOT" "$CLI" "$TMPDIR_EVAL/repos" "$BACKLOG_SETTINGS" "$ASSIGNMENT_SETTINGS" "$EMPTY_BACKLOG" "$EMPTY_ASSIGNMENT" <<'NODE'
|
|
78
|
+
import { execFileSync } from "node:child_process";
|
|
79
|
+
import fs from "node:fs";
|
|
80
|
+
import path from "node:path";
|
|
81
|
+
import Ajv2020 from "ajv/dist/2020.js";
|
|
82
|
+
|
|
83
|
+
const [root, cli, reposRoot, publicBacklogPath, publicAssignmentPath, emptyBacklog, emptyAssignment] = process.argv.slice(2);
|
|
84
|
+
const readJson = (file) => JSON.parse(fs.readFileSync(file, "utf8"));
|
|
85
|
+
const assert = (condition, message) => {
|
|
86
|
+
if (!condition) throw new Error(message);
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const backlogSchema = readJson(path.join(root, "schemas/backlog-provider-settings.schema.json"));
|
|
90
|
+
const assignmentSchema = readJson(path.join(root, "schemas/assignment-provider-settings.schema.json"));
|
|
91
|
+
const ajv = new Ajv2020({ allErrors: true });
|
|
92
|
+
const validateBacklog = ajv.compile(backlogSchema);
|
|
93
|
+
const validateAssignment = ajv.compile(assignmentSchema);
|
|
94
|
+
|
|
95
|
+
const publicBacklog = readJson(publicBacklogPath);
|
|
96
|
+
const publicAssignment = readJson(publicAssignmentPath);
|
|
97
|
+
assert(validateBacklog(publicBacklog), `public backlog settings schema validation failed: ${ajv.errorsText(validateBacklog.errors)}`);
|
|
98
|
+
assert(validateAssignment(publicAssignment), `public assignment settings schema validation failed: ${ajv.errorsText(validateAssignment.errors)}`);
|
|
99
|
+
assert(publicBacklog.projects.length === 1 && publicBacklog.projects[0]?.project?.repo?.name === "flow-agents", "public backlog settings must remain self-only");
|
|
100
|
+
assert(publicAssignment.projects.length === 1 && publicAssignment.projects[0]?.project?.repo?.name === "flow-agents", "public assignment settings must remain self-only");
|
|
101
|
+
|
|
102
|
+
const owner = "example-org";
|
|
103
|
+
const targets = ["alpha", "beta"];
|
|
104
|
+
const projectNumber = 42;
|
|
105
|
+
const repoRef = (name) => ({ owner, name, url: `https://github.com/${owner}/${name}` });
|
|
106
|
+
const externalBacklog = {
|
|
107
|
+
schema_version: "1.0",
|
|
108
|
+
projects: targets.map((name) => ({
|
|
109
|
+
project: { repo: repoRef(name) },
|
|
110
|
+
work_item_provider: { role: "WorkItemProvider", kind: "github", repo: repoRef(name), capabilities: ["issues", "labels", "assignees", "pr_links", "comments"] },
|
|
111
|
+
board_provider: { role: "BoardProvider", kind: "github", repo: repoRef(name), board: { type: "github_project", owner, number: projectNumber, url: `https://github.com/orgs/${owner}/projects/${projectNumber}` }, capabilities: ["projects_boards", "status_fields", "custom_fields"] },
|
|
112
|
+
selection: { filters: { issue_state: "open", include_labels: [], ready_statuses: ["ready"], exclude_statuses: ["triage", "in_progress", "blocked", "review", "verification", "done"] }, wip_policy: { prefer_finishing_active_work: true, active_statuses: ["in_progress", "review", "verification"], block_new_work_when_active_count_exceeds: 0 } },
|
|
113
|
+
})),
|
|
114
|
+
};
|
|
115
|
+
const externalAssignment = {
|
|
116
|
+
schema_version: "1.0",
|
|
117
|
+
projects: targets.map((name) => ({
|
|
118
|
+
project: { repo: repoRef(name) },
|
|
119
|
+
provider: { kind: "github", repo: repoRef(name), capabilities: ["assignees", "labels", "comments"] },
|
|
120
|
+
policy: { label_name: "agent:claimed", claim_comment_marker: "<!-- flow-agents:assignment-claim -->", human_assignee_policy: { behavior: "ask_first", idle_threshold_days: 3 }, comment_refresh_on_phase_transition: false },
|
|
121
|
+
})),
|
|
122
|
+
};
|
|
123
|
+
assert(validateBacklog(externalBacklog), `external backlog settings schema validation failed: ${ajv.errorsText(validateBacklog.errors)}`);
|
|
124
|
+
assert(validateAssignment(externalAssignment), `external assignment settings schema validation failed: ${ajv.errorsText(validateAssignment.errors)}`);
|
|
125
|
+
|
|
126
|
+
fs.mkdirSync(reposRoot, { recursive: true });
|
|
127
|
+
const externalBacklogPath = path.join(reposRoot, "backlog-provider-settings.json");
|
|
128
|
+
const externalAssignmentPath = path.join(reposRoot, "assignment-provider-settings.json");
|
|
129
|
+
fs.writeFileSync(externalBacklogPath, JSON.stringify(externalBacklog, null, 2));
|
|
130
|
+
fs.writeFileSync(externalAssignmentPath, JSON.stringify(externalAssignment, null, 2));
|
|
131
|
+
|
|
132
|
+
for (const name of targets) {
|
|
133
|
+
const repoPath = path.join(reposRoot, name);
|
|
134
|
+
fs.mkdirSync(repoPath, { recursive: true });
|
|
135
|
+
fs.writeFileSync(path.join(repoPath, "package.json"), JSON.stringify({ name: `fixture-${name}`, repository: { type: "git", url: `https://github.com/${owner}/${name}.git` } }, null, 2));
|
|
136
|
+
const resolve = (command, globalPath, emptyProject) => JSON.parse(execFileSync(process.execPath, [cli, command, "--repo-path", repoPath, "--project-settings", emptyProject, "--global-settings", globalPath, "--json"], { encoding: "utf8" }));
|
|
137
|
+
const backlogResult = resolve("effective-backlog-settings", externalBacklogPath, emptyBacklog);
|
|
138
|
+
const assignmentResult = resolve("effective-assignment-provider-settings", externalAssignmentPath, emptyAssignment);
|
|
139
|
+
|
|
140
|
+
assert(backlogResult.status === "configured" && backlogResult.source === "global", `${name} backlog settings must resolve from the external file`);
|
|
141
|
+
assert(backlogResult.settings.work_item_provider?.repo?.name === name, `${name} must select its own work item repository`);
|
|
142
|
+
assert(backlogResult.settings.board_provider?.board?.number === projectNumber, `${name} must select the external board`);
|
|
143
|
+
assert(assignmentResult.status === "configured" && assignmentResult.source === "global", `${name} assignment settings must resolve from the external file`);
|
|
144
|
+
assert(assignmentResult.settings.provider?.repo?.name === name, `${name} must select its own assignment repository`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
console.log("Public settings remain self-only and external multi-repository settings resolve through the supported global precedence layer.");
|
|
148
|
+
NODE
|
|
149
|
+
then
|
|
150
|
+
pass "public settings stay self-only and external multi-repository settings resolve"
|
|
151
|
+
else
|
|
152
|
+
fail "public settings stay self-only and external multi-repository settings resolve"
|
|
153
|
+
fi
|
|
154
|
+
|
|
52
155
|
if [[ "$errors" -eq 0 ]]; then
|
|
53
|
-
echo "Effective
|
|
156
|
+
echo "Effective provider settings checks passed"
|
|
54
157
|
else
|
|
55
|
-
echo "Effective
|
|
158
|
+
echo "Effective provider settings checks failed: $errors"
|
|
56
159
|
fi
|
|
57
160
|
|
|
58
161
|
exit "$errors"
|
|
@@ -18,6 +18,13 @@ source "$ROOT/evals/lib/node.sh"
|
|
|
18
18
|
|
|
19
19
|
TMPDIR_EVAL="$(mktemp -d)"
|
|
20
20
|
trap 'rm -rf "$TMPDIR_EVAL"; [[ -n "${STUB_PID:-}" ]] && kill "$STUB_PID" 2>/dev/null' EXIT
|
|
21
|
+
|
|
22
|
+
# Hermetic HOME: the conf-driven relay resolver (#567) reads <bundle-root>/telemetry-console.conf
|
|
23
|
+
# as a resolution slot. Point HOME at an empty dir so these tests never read the running machine's
|
|
24
|
+
# real console conf — enablement here is controlled only by the env flag / an explicit
|
|
25
|
+
# TELEMETRY_CONFIG_FILE, exactly as each case sets up.
|
|
26
|
+
export HOME="$TMPDIR_EVAL/clean-home"
|
|
27
|
+
mkdir -p "$HOME"
|
|
21
28
|
PORT=38795
|
|
22
29
|
RECV="$TMPDIR_EVAL/recv.jsonl"
|
|
23
30
|
|
|
@@ -29,6 +36,12 @@ fail() { echo " [FAIL] $1"; errors=$((errors + 1)); }
|
|
|
29
36
|
# controls the response — "ok" (200) or "fail" (500) — to exercise the relay-failure path.
|
|
30
37
|
start_stub() {
|
|
31
38
|
local mode="$1"
|
|
39
|
+
# Fresh port per stub: the relay POST is detached/async, so a late POST from a PRIOR "on" case
|
|
40
|
+
# could otherwise race into the next case's freshly-cleared RECV on a reused port and cause a
|
|
41
|
+
# spurious "a POST happened" failure in a "no POST" assertion. A new port each time sends any
|
|
42
|
+
# stray prior-case POST to a now-dead port (connection refused), never into this case's RECV.
|
|
43
|
+
PORT=$((PORT + 1))
|
|
44
|
+
ENDPOINT="http://127.0.0.1:${PORT}/records"
|
|
32
45
|
STUB_MODE="$mode" node -e '
|
|
33
46
|
const http=require("http"),fs=require("fs");
|
|
34
47
|
const out=process.argv[1], mode=process.env.STUB_MODE||"ok";
|
|
@@ -67,7 +80,7 @@ echo "=== console liveness relay (#295) ==="
|
|
|
67
80
|
echo "--- 1. relay on+configured: liveness emit mirrors to the Console with Bearer + tenant ---"
|
|
68
81
|
: > "$RECV"; start_stub ok || { fail "stub server did not start"; }
|
|
69
82
|
ROOT_A="$TMPDIR_EVAL/a"
|
|
70
|
-
EVT1='{"type":"claim","subjectId":"relay-subj-1","actor":"claude-code:sessA:host","actor_key":"claude-code:sessA:host","at":"2026-07-05T00:00:00Z","ttlSeconds":1800,"host":"host","branch":"agent/a/relay-subj-1","artifact_dir":"relay-subj-1"}'
|
|
83
|
+
EVT1='{"type":"claim","subjectId":"relay-subj-1","actor":"claude-code:sessA:host","actor_key":"claude-code:sessA:host","at":"2026-07-05T00:00:00Z","ttlSeconds":1800,"host":"host","branch":"agent/a/relay-subj-1","artifact_dir":"relay-subj-1","source":"tool-activity","activity":{"tool":"Bash"}}'
|
|
71
84
|
(
|
|
72
85
|
export FLOW_AGENTS_CONSOLE_LIVENESS_RELAY=1
|
|
73
86
|
export FLOW_AGENTS_CONSOLE_LIVENESS_ENDPOINT_URL="$ENDPOINT"
|
|
@@ -92,6 +105,8 @@ const checks=[
|
|
|
92
105
|
["subjectId", b.subjectId==="relay-subj-1"],
|
|
93
106
|
["actor", b.actor==="claude-code:sessA:host"],
|
|
94
107
|
["branch", b.branch==="agent/a/relay-subj-1"],
|
|
108
|
+
["source tool-activity", b.source==="tool-activity"],
|
|
109
|
+
["activity.tool Bash (richer real-liveness rides to the Console)", !!b.activity && b.activity.tool==="Bash"],
|
|
95
110
|
];
|
|
96
111
|
for(const [name,ok] of checks) console.log((ok?"OK ":"BAD ")+name);
|
|
97
112
|
' "$RECV" > "$TMPDIR_EVAL/checks.txt" 2>&1
|
|
@@ -156,6 +171,81 @@ console.log(hasRawCtl?"RAW":"ESCAPED");
|
|
|
156
171
|
if grep -q ESCAPED "$TMPDIR_EVAL/inj.txt"; then pass "hostile actor/branch control bytes are JSON-escaped, never raw in the POST body (injection discipline)"; else fail "raw control bytes reached the POST body: $(cat "$TMPDIR_EVAL/inj.txt")"; fi
|
|
157
172
|
stop_stub
|
|
158
173
|
|
|
174
|
+
# ─── 6. CONF-DRIVEN enablement (#567): NO env flag, `console_liveness_relay=1` in the conf → POST ─
|
|
175
|
+
echo "--- 6. conf-driven (#567): console_liveness_relay=1 in the conf relays with NO env var ---"
|
|
176
|
+
: > "$RECV"; start_stub ok
|
|
177
|
+
ROOT_F="$TMPDIR_EVAL/f"
|
|
178
|
+
CONF6="$TMPDIR_EVAL/conf6.conf"
|
|
179
|
+
printf 'console_telemetry_url=https://example.invalid\nconsole_liveness_relay=1\n' > "$CONF6"
|
|
180
|
+
(
|
|
181
|
+
unset FLOW_AGENTS_CONSOLE_LIVENESS_RELAY # the whole point: no env var
|
|
182
|
+
export TELEMETRY_CONFIG_FILE="$CONF6"
|
|
183
|
+
export FLOW_AGENTS_CONSOLE_LIVENESS_ENDPOINT_URL="$ENDPOINT" # POST target (stub)
|
|
184
|
+
emit_event "$ROOT_F" '{"type":"claim","subjectId":"conf-on-1","actor":"a","at":"t"}'
|
|
185
|
+
)
|
|
186
|
+
[[ -s "$ROOT_F/liveness/events.jsonl" ]] && pass "local write happened (conf-driven case)" || fail "local write missing (conf-driven)"
|
|
187
|
+
if wait_for_post; then pass "conf-driven relay POSTed with NO env var (console_liveness_relay=1)"; else fail "conf-driven relay did NOT POST (env-var-free enablement broken)"; fi
|
|
188
|
+
stop_stub
|
|
189
|
+
|
|
190
|
+
# ─── 7. CONF-DRIVEN auto-on parity (#469): a console url with no explicit key defaults ON ────────
|
|
191
|
+
echo "--- 7. conf-driven auto-on: a console url with no explicit key relays; explicit =0 suppresses ---"
|
|
192
|
+
: > "$RECV"; start_stub ok
|
|
193
|
+
ROOT_G="$TMPDIR_EVAL/g"
|
|
194
|
+
CONF7="$TMPDIR_EVAL/conf7.conf"
|
|
195
|
+
printf 'console_telemetry_url=https://example.invalid\n' > "$CONF7" # url only, no explicit key
|
|
196
|
+
(
|
|
197
|
+
unset FLOW_AGENTS_CONSOLE_LIVENESS_RELAY
|
|
198
|
+
export TELEMETRY_CONFIG_FILE="$CONF7"
|
|
199
|
+
export FLOW_AGENTS_CONSOLE_LIVENESS_ENDPOINT_URL="$ENDPOINT"
|
|
200
|
+
emit_event "$ROOT_G" '{"type":"claim","subjectId":"auto-on-1","actor":"a","at":"t"}'
|
|
201
|
+
)
|
|
202
|
+
if wait_for_post; then pass "auto-on: a configured console url defaults the relay ON (#469 parity)"; else fail "auto-on did NOT POST (console-url default-on broken)"; fi
|
|
203
|
+
stop_stub
|
|
204
|
+
|
|
205
|
+
: > "$RECV"; start_stub ok
|
|
206
|
+
ROOT_H="$TMPDIR_EVAL/h"
|
|
207
|
+
CONF8="$TMPDIR_EVAL/conf8.conf"
|
|
208
|
+
printf 'console_telemetry_url=https://example.invalid\nconsole_liveness_relay=0\n' > "$CONF8" # explicit off wins
|
|
209
|
+
(
|
|
210
|
+
unset FLOW_AGENTS_CONSOLE_LIVENESS_RELAY
|
|
211
|
+
export TELEMETRY_CONFIG_FILE="$CONF8"
|
|
212
|
+
export FLOW_AGENTS_CONSOLE_LIVENESS_ENDPOINT_URL="$ENDPOINT"
|
|
213
|
+
emit_event "$ROOT_H" '{"type":"claim","subjectId":"conf-off-1","actor":"a","at":"t"}'
|
|
214
|
+
)
|
|
215
|
+
sleep 1
|
|
216
|
+
[[ -s "$ROOT_H/liveness/events.jsonl" ]] && pass "local write happened (explicit conf off)" || fail "local write missing (explicit conf off)"
|
|
217
|
+
[[ ! -s "$RECV" ]] && pass "explicit console_liveness_relay=0 suppresses the relay even with a url (opt-out wins)" || fail "a POST happened despite console_liveness_relay=0"
|
|
218
|
+
stop_stub
|
|
219
|
+
|
|
220
|
+
# ─── 8. EXFIL DEFENSE: an UNTRUSTED (mode 644) default-path conf must NOT relay (trust gate) ─────
|
|
221
|
+
# The cases above drive enablement through TELEMETRY_CONFIG_FILE, which is trusted BY DESIGN (an
|
|
222
|
+
# explicit env override bypasses the mode-600 gate). This case exercises the actual supply-chain
|
|
223
|
+
# defense end-to-end: a conf planted at the user-global default slot (<bundle-root>/…, reached via
|
|
224
|
+
# the hermetic HOME set at the top) pointing at an attacker stub with console_liveness_relay=1, but
|
|
225
|
+
# mode 644 — exactly what a non-operator drop (clone/tarball/local tool) can produce, since git can
|
|
226
|
+
# only ship 644/755, never 600. relay.sh re-sources config.sh, whose telemetry_conf_trusted gate
|
|
227
|
+
# (mode 600 + owner + not-a-symlink) rejects it, so no URL resolves and nothing is POSTed — while
|
|
228
|
+
# the durable local write still happens. NO env flag, NO TELEMETRY_CONFIG_FILE, NO endpoint env:
|
|
229
|
+
# the untrusted conf is the ONLY possible signal, so a POST here would be a real exfil regression.
|
|
230
|
+
echo "--- 8. exfil defense: an untrusted (mode 644) <bundle-root> conf with an attacker url never relays ---"
|
|
231
|
+
: > "$RECV"; start_stub ok
|
|
232
|
+
ROOT_X="$TMPDIR_EVAL/x"
|
|
233
|
+
mkdir -p "<bundle-root>"
|
|
234
|
+
UNTRUSTED_CONF="<bundle-root>/telemetry-console.conf"
|
|
235
|
+
printf 'console_telemetry_url=http://127.0.0.1:%s\nconsole_liveness_relay=1\n' "$PORT" > "$UNTRUSTED_CONF"
|
|
236
|
+
chmod 644 "$UNTRUSTED_CONF"
|
|
237
|
+
(
|
|
238
|
+
unset FLOW_AGENTS_CONSOLE_LIVENESS_RELAY
|
|
239
|
+
unset TELEMETRY_CONFIG_FILE
|
|
240
|
+
unset FLOW_AGENTS_CONSOLE_LIVENESS_ENDPOINT_URL
|
|
241
|
+
emit_event "$ROOT_X" '{"type":"claim","subjectId":"exfil-x","actor":"a","at":"t"}'
|
|
242
|
+
)
|
|
243
|
+
sleep 1
|
|
244
|
+
[[ -s "$ROOT_X/liveness/events.jsonl" ]] && pass "local write happened (untrusted-conf case, local-first intact)" || fail "local write missing (untrusted conf)"
|
|
245
|
+
[[ ! -s "$RECV" ]] && pass "an untrusted (644) default-path conf naming an attacker url NEVER relays (trust gate holds, no exfil)" || fail "EXFIL: a POST reached the attacker stub from an untrusted 644 conf"
|
|
246
|
+
rm -f "$UNTRUSTED_CONF"
|
|
247
|
+
stop_stub
|
|
248
|
+
|
|
159
249
|
echo ""
|
|
160
250
|
if [[ "$errors" -eq 0 ]]; then
|
|
161
251
|
echo "test_liveness_console_relay: all checks passed."
|
|
@@ -704,6 +704,72 @@ else
|
|
|
704
704
|
_fail "maybeEmitHeartbeat did not neutralize an orphan heartbeat as expected: $J1_OUT"
|
|
705
705
|
fi
|
|
706
706
|
|
|
707
|
+
# ─── K. Richer "real liveness": activity descriptor on the emitted heartbeat ────────────────
|
|
708
|
+
echo "--- K. Activity enrichment (tool name on the pulse) ---"
|
|
709
|
+
|
|
710
|
+
# call_heartbeat_activity <scratch_root> <env_json> <now_iso> <activity_json>
|
|
711
|
+
# Like call_heartbeat, but passes an `activity` descriptor through opts so we can prove the emitted
|
|
712
|
+
# heartbeat carries a sanitized activity field (the "what is this agent doing right now" signal).
|
|
713
|
+
call_heartbeat_activity() {
|
|
714
|
+
local root="$1" env_json="$2" now="$3" activity_json="$4"
|
|
715
|
+
MODULE_ARG="$HEARTBEAT_MODULE" ROOT_ARG="$root" ENV_JSON_ARG="$env_json" NOW_ARG="$now" ACTIVITY_ARG="$activity_json" \
|
|
716
|
+
node - <<'NODE'
|
|
717
|
+
const { maybeEmitHeartbeat } = require(process.env.MODULE_ARG);
|
|
718
|
+
const env = JSON.parse(process.env.ENV_JSON_ARG);
|
|
719
|
+
const opts = { cwd: process.env.ROOT_ARG, env, now: process.env.NOW_ARG, activity: JSON.parse(process.env.ACTIVITY_ARG) };
|
|
720
|
+
process.stdout.write(JSON.stringify(maybeEmitHeartbeat(opts)));
|
|
721
|
+
NODE
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
# K1: a clean tool name rides through onto the emitted heartbeat as activity.tool.
|
|
725
|
+
K1_ROOT="$(new_scratch)"
|
|
726
|
+
seed_claim "$K1_ROOT" "k1-subj" "agent-k1" "2026-06-25T12:00:00.000Z" 1800
|
|
727
|
+
K1_RESULT="$(call_heartbeat_activity "$K1_ROOT" '{"FLOW_AGENTS_ACTOR":"agent-k1"}' "2026-06-25T12:01:00.000Z" '{"tool":"Bash"}')"
|
|
728
|
+
K1_LINE="$(tail -n 1 "$(stream_file "$K1_ROOT")" 2>/dev/null)"
|
|
729
|
+
if [[ "$K1_RESULT" == '{"emitted":true}' ]] \
|
|
730
|
+
&& grep -q '"type":"heartbeat"' <<<"$K1_LINE" \
|
|
731
|
+
&& grep -q '"activity":{"tool":"Bash"}' <<<"$K1_LINE"; then
|
|
732
|
+
_pass "the emitted heartbeat carries a sanitized activity.tool (what the agent is doing right now)"
|
|
733
|
+
else
|
|
734
|
+
_fail "the heartbeat did not carry the expected activity.tool: result=$K1_RESULT line=$K1_LINE"
|
|
735
|
+
fi
|
|
736
|
+
|
|
737
|
+
# K2: an MCP-style tool name (double underscores + hyphens) survives sanitizeSegment intact.
|
|
738
|
+
K2_ROOT="$(new_scratch)"
|
|
739
|
+
seed_claim "$K2_ROOT" "k2-subj" "agent-k2" "2026-06-25T12:00:00.000Z" 1800
|
|
740
|
+
call_heartbeat_activity "$K2_ROOT" '{"FLOW_AGENTS_ACTOR":"agent-k2"}' "2026-06-25T12:01:00.000Z" '{"tool":"mcp__claude-in-chrome__navigate"}' >/dev/null
|
|
741
|
+
K2_LINE="$(tail -n 1 "$(stream_file "$K2_ROOT")" 2>/dev/null)"
|
|
742
|
+
if grep -q '"activity":{"tool":"mcp__claude-in-chrome__navigate"}' <<<"$K2_LINE"; then
|
|
743
|
+
_pass "an MCP tool name (double underscores + hyphens) rides through the allowlist sanitizer intact"
|
|
744
|
+
else
|
|
745
|
+
_fail "the MCP tool name was not preserved on the activity descriptor: line=$K2_LINE"
|
|
746
|
+
fi
|
|
747
|
+
|
|
748
|
+
# K3: a hostile tool name (control bytes / injection payload) is stripped to the allowlist charset —
|
|
749
|
+
# never emitted raw into the append-only stream.
|
|
750
|
+
K3_ROOT="$(new_scratch)"
|
|
751
|
+
seed_claim "$K3_ROOT" "k3-subj" "agent-k3" "2026-06-25T12:00:00.000Z" 1800
|
|
752
|
+
call_heartbeat_activity "$K3_ROOT" '{"FLOW_AGENTS_ACTOR":"agent-k3"}' "2026-06-25T12:01:00.000Z" '{"tool":"Ba\"sh\n{\"x\":1}"}' >/dev/null
|
|
753
|
+
K3_LINE="$(tail -n 1 "$(stream_file "$K3_ROOT")" 2>/dev/null)"
|
|
754
|
+
# The allowlist keeps only [A-Za-z0-9_.-], so the payload collapses to "Bashx1".
|
|
755
|
+
if grep -q '"activity":{"tool":"Bashx1"}' <<<"$K3_LINE" && ! grep -q 'Ba"sh' <<<"$K3_LINE"; then
|
|
756
|
+
_pass "a hostile tool name is stripped to the allowlist charset, never emitted raw (injection discipline)"
|
|
757
|
+
else
|
|
758
|
+
_fail "the hostile tool name was not sanitized as expected: line=$K3_LINE"
|
|
759
|
+
fi
|
|
760
|
+
|
|
761
|
+
# K4: no activity supplied → the heartbeat is a plain aliveness pulse with NO activity key (the field
|
|
762
|
+
# is omitted, not emitted empty).
|
|
763
|
+
K4_ROOT="$(new_scratch)"
|
|
764
|
+
seed_claim "$K4_ROOT" "k4-subj" "agent-k4" "2026-06-25T12:00:00.000Z" 1800
|
|
765
|
+
call_heartbeat "$K4_ROOT" '{"FLOW_AGENTS_ACTOR":"agent-k4"}' "2026-06-25T12:01:00.000Z" >/dev/null
|
|
766
|
+
K4_LINE="$(tail -n 1 "$(stream_file "$K4_ROOT")" 2>/dev/null)"
|
|
767
|
+
if grep -q '"type":"heartbeat"' <<<"$K4_LINE" && ! grep -q '"activity"' <<<"$K4_LINE"; then
|
|
768
|
+
_pass "with no activity supplied the heartbeat omits the activity field entirely (no empty-object noise)"
|
|
769
|
+
else
|
|
770
|
+
_fail "the no-activity heartbeat unexpectedly carried an activity field: line=$K4_LINE"
|
|
771
|
+
fi
|
|
772
|
+
|
|
707
773
|
echo ""
|
|
708
774
|
if [[ "$errors" -eq 0 ]]; then
|
|
709
775
|
echo "Liveness heartbeat integration passed."
|
|
@@ -72,7 +72,7 @@ if [[ ! -e "$DEST/CLAUDE.md" && ! -L "$DEST/CLAUDE.md" ]]; then
|
|
|
72
72
|
rm -f "$instruction_tmp"
|
|
73
73
|
fi
|
|
74
74
|
if command -v node >/dev/null 2>&1; then
|
|
75
|
-
node "$DEST/scripts/install-merge.js" --config "$DEST/.claude/settings.json" --managed-hooks "$SRC/.claude/settings.json" --version "4.1
|
|
75
|
+
node "$DEST/scripts/install-merge.js" --config "$DEST/.claude/settings.json" --managed-hooks "$SRC/.claude/settings.json" --version "4.2.1" --install-record "$DEST/.flow-agents/install.json" --runtime "claude-code" || true
|
|
76
76
|
fi
|
|
77
77
|
if [[ ${#CONSOLE_CONFIG_ARGS[@]} -gt 0 || -n "${FLOW_AGENTS_TELEMETRY_SINK:-}" || -n "${FLOW_AGENTS_TELEMETRY_SINKS:-}" || -n "${FLOW_AGENTS_CONSOLE_URL:-}" || -n "${CONSOLE_TELEMETRY_URL:-}" || -n "${CONSOLE_URL:-}" || -n "${FLOW_AGENTS_CONSOLE_TOKEN_FILE:-}" || -n "${CONSOLE_TELEMETRY_TOKEN_FILE:-}" ]]; then
|
|
78
78
|
bash "$DEST/scripts/telemetry/install-console-config.sh" "$DEST/scripts/telemetry/telemetry.conf" "${CONSOLE_CONFIG_ARGS[@]}"
|
|
@@ -105,9 +105,15 @@ async function main() {
|
|
|
105
105
|
let conflict;
|
|
106
106
|
if (canonical === 'postToolUse') {
|
|
107
107
|
try {
|
|
108
|
+
// Richer "real liveness": name the tool this pulse rode in on so the heartbeat says WHAT the
|
|
109
|
+
// agent is doing, not just that it is alive. Claude Code's PostToolUse payload carries the
|
|
110
|
+
// tool name as `tool_name` (fall back to `toolName`); the heartbeat sanitizes it, so a
|
|
111
|
+
// missing/odd value degrades to a plain aliveness pulse rather than an error.
|
|
112
|
+
const toolName = payload.tool_name || payload.toolName;
|
|
108
113
|
const heartbeatResult = require('./lib/liveness-heartbeat').maybeEmitHeartbeat({
|
|
109
114
|
cwd: process.cwd(),
|
|
110
115
|
env: process.env,
|
|
116
|
+
activity: toolName ? { tool: toolName } : undefined,
|
|
111
117
|
});
|
|
112
118
|
conflict = heartbeatResult && heartbeatResult.conflict;
|
|
113
119
|
} catch (err) {
|