@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.
Files changed (108) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/build/src/builder-flow-runtime.js +10 -5
  3. package/context/contracts/verification-contract.md +6 -0
  4. package/context/scripts/telemetry/lib/config.sh +29 -0
  5. package/dist/base/build/package.json +1 -1
  6. package/dist/base/build/src/builder-flow-runtime.js +10 -5
  7. package/dist/base/context/contracts/verification-contract.md +6 -0
  8. package/dist/base/context/scripts/telemetry/lib/config.sh +29 -0
  9. package/dist/base/evals/acceptance/prove-capture-teeth.sh +24 -0
  10. package/dist/base/evals/integration/test_effective_backlog_settings.sh +118 -15
  11. package/dist/base/evals/integration/test_liveness_console_relay.sh +91 -1
  12. package/dist/base/evals/integration/test_liveness_heartbeat.sh +66 -0
  13. package/dist/base/install.sh +1 -1
  14. package/dist/base/scripts/hooks/claude-telemetry-hook.js +6 -0
  15. package/dist/base/scripts/hooks/lib/codex-exit-code.js +75 -23
  16. package/dist/base/scripts/hooks/lib/liveness-heartbeat.js +42 -1
  17. package/dist/base/scripts/hooks/lib/liveness-write.js +109 -9
  18. package/dist/base/scripts/liveness/relay.sh +23 -11
  19. package/dist/base/scripts/telemetry/lib/config.sh +29 -0
  20. package/dist/claude-code/build/package.json +1 -1
  21. package/dist/claude-code/build/src/builder-flow-runtime.js +10 -5
  22. package/dist/claude-code/context/contracts/verification-contract.md +6 -0
  23. package/dist/claude-code/context/scripts/telemetry/lib/config.sh +29 -0
  24. package/dist/claude-code/evals/acceptance/prove-capture-teeth.sh +24 -0
  25. package/dist/claude-code/evals/integration/test_effective_backlog_settings.sh +118 -15
  26. package/dist/claude-code/evals/integration/test_liveness_console_relay.sh +91 -1
  27. package/dist/claude-code/evals/integration/test_liveness_heartbeat.sh +66 -0
  28. package/dist/claude-code/install.sh +1 -1
  29. package/dist/claude-code/scripts/hooks/claude-telemetry-hook.js +6 -0
  30. package/dist/claude-code/scripts/hooks/lib/codex-exit-code.js +75 -23
  31. package/dist/claude-code/scripts/hooks/lib/liveness-heartbeat.js +42 -1
  32. package/dist/claude-code/scripts/hooks/lib/liveness-write.js +109 -9
  33. package/dist/claude-code/scripts/liveness/relay.sh +23 -11
  34. package/dist/claude-code/scripts/telemetry/lib/config.sh +29 -0
  35. package/dist/codex/build/package.json +1 -1
  36. package/dist/codex/build/src/builder-flow-runtime.js +10 -5
  37. package/dist/codex/context/contracts/verification-contract.md +6 -0
  38. package/dist/codex/context/scripts/telemetry/lib/config.sh +29 -0
  39. package/dist/codex/evals/acceptance/prove-capture-teeth.sh +24 -0
  40. package/dist/codex/evals/integration/test_effective_backlog_settings.sh +118 -15
  41. package/dist/codex/evals/integration/test_liveness_console_relay.sh +91 -1
  42. package/dist/codex/evals/integration/test_liveness_heartbeat.sh +66 -0
  43. package/dist/codex/install.sh +1 -1
  44. package/dist/codex/scripts/hooks/claude-telemetry-hook.js +6 -0
  45. package/dist/codex/scripts/hooks/lib/codex-exit-code.js +75 -23
  46. package/dist/codex/scripts/hooks/lib/liveness-heartbeat.js +42 -1
  47. package/dist/codex/scripts/hooks/lib/liveness-write.js +109 -9
  48. package/dist/codex/scripts/liveness/relay.sh +23 -11
  49. package/dist/codex/scripts/telemetry/lib/config.sh +29 -0
  50. package/dist/kiro/build/package.json +1 -1
  51. package/dist/kiro/build/src/builder-flow-runtime.js +10 -5
  52. package/dist/kiro/context/contracts/verification-contract.md +6 -0
  53. package/dist/kiro/context/scripts/telemetry/lib/config.sh +29 -0
  54. package/dist/kiro/evals/acceptance/prove-capture-teeth.sh +24 -0
  55. package/dist/kiro/evals/integration/test_effective_backlog_settings.sh +118 -15
  56. package/dist/kiro/evals/integration/test_liveness_console_relay.sh +91 -1
  57. package/dist/kiro/evals/integration/test_liveness_heartbeat.sh +66 -0
  58. package/dist/kiro/install.sh +1 -1
  59. package/dist/kiro/scripts/hooks/claude-telemetry-hook.js +6 -0
  60. package/dist/kiro/scripts/hooks/lib/codex-exit-code.js +75 -23
  61. package/dist/kiro/scripts/hooks/lib/liveness-heartbeat.js +42 -1
  62. package/dist/kiro/scripts/hooks/lib/liveness-write.js +109 -9
  63. package/dist/kiro/scripts/liveness/relay.sh +23 -11
  64. package/dist/kiro/scripts/telemetry/lib/config.sh +29 -0
  65. package/dist/opencode/build/package.json +1 -1
  66. package/dist/opencode/build/src/builder-flow-runtime.js +10 -5
  67. package/dist/opencode/context/contracts/verification-contract.md +6 -0
  68. package/dist/opencode/context/scripts/telemetry/lib/config.sh +29 -0
  69. package/dist/opencode/evals/acceptance/prove-capture-teeth.sh +24 -0
  70. package/dist/opencode/evals/integration/test_effective_backlog_settings.sh +118 -15
  71. package/dist/opencode/evals/integration/test_liveness_console_relay.sh +91 -1
  72. package/dist/opencode/evals/integration/test_liveness_heartbeat.sh +66 -0
  73. package/dist/opencode/install.sh +1 -1
  74. package/dist/opencode/scripts/hooks/claude-telemetry-hook.js +6 -0
  75. package/dist/opencode/scripts/hooks/lib/codex-exit-code.js +75 -23
  76. package/dist/opencode/scripts/hooks/lib/liveness-heartbeat.js +42 -1
  77. package/dist/opencode/scripts/hooks/lib/liveness-write.js +109 -9
  78. package/dist/opencode/scripts/liveness/relay.sh +23 -11
  79. package/dist/opencode/scripts/telemetry/lib/config.sh +29 -0
  80. package/dist/pi/build/package.json +1 -1
  81. package/dist/pi/build/src/builder-flow-runtime.js +10 -5
  82. package/dist/pi/context/contracts/verification-contract.md +6 -0
  83. package/dist/pi/context/scripts/telemetry/lib/config.sh +29 -0
  84. package/dist/pi/evals/acceptance/prove-capture-teeth.sh +24 -0
  85. package/dist/pi/evals/integration/test_effective_backlog_settings.sh +118 -15
  86. package/dist/pi/evals/integration/test_liveness_console_relay.sh +91 -1
  87. package/dist/pi/evals/integration/test_liveness_heartbeat.sh +66 -0
  88. package/dist/pi/install.sh +1 -1
  89. package/dist/pi/scripts/hooks/claude-telemetry-hook.js +6 -0
  90. package/dist/pi/scripts/hooks/lib/codex-exit-code.js +75 -23
  91. package/dist/pi/scripts/hooks/lib/liveness-heartbeat.js +42 -1
  92. package/dist/pi/scripts/hooks/lib/liveness-write.js +109 -9
  93. package/dist/pi/scripts/liveness/relay.sh +23 -11
  94. package/dist/pi/scripts/telemetry/lib/config.sh +29 -0
  95. package/evals/acceptance/prove-capture-teeth.sh +24 -0
  96. package/evals/integration/test_effective_backlog_settings.sh +118 -15
  97. package/evals/integration/test_liveness_console_relay.sh +91 -1
  98. package/evals/integration/test_liveness_heartbeat.sh +66 -0
  99. package/package.json +2 -1
  100. package/scripts/hooks/claude-telemetry-hook.js +6 -0
  101. package/scripts/hooks/lib/codex-exit-code.js +75 -23
  102. package/scripts/hooks/lib/liveness-heartbeat.js +42 -1
  103. package/scripts/hooks/lib/liveness-write.js +109 -9
  104. package/scripts/liveness/relay.sh +23 -11
  105. package/scripts/telemetry/lib/config.sh +29 -0
  106. package/src/builder-flow-runtime.ts +10 -5
  107. package/src/cli/builder-flow-runtime.test.mjs +55 -2
  108. package/src/cli/codex-exit-code.test.mjs +217 -2
@@ -7,26 +7,36 @@
7
7
  # never forked). The Console side ingests this record type and projects the fleet view + runs the
8
8
  # janitor (console repo #125); this script is only the flow-agents EMIT half.
9
9
  #
10
- # STRICTLY OPTIONAL and local-first (ADR 0012 §5): a no-op unless FLOW_AGENTS_CONSOLE_LIVENESS_RELAY
11
- # is enabled AND a console endpoint is configured. Best-effort throughout it must NEVER block, slow,
12
- # or fail the local liveness write that already happened before this was invoked. Every failure path
13
- # is a quiet `exit 0`.
10
+ # STRICTLY OPTIONAL and local-first (ADR 0012 §5): a no-op unless the liveness relay is enabled AND a
11
+ # console endpoint is configured. Enablement is conf-driven (#567, parity with economics #469): an
12
+ # operator sets `console_liveness_relay=1` in the console conf (or it defaults on once a console url
13
+ # resolves) — NOT an env var. config.sh (sourced below) is the authoritative, trust-gated decision
14
+ # and also supplies the endpoint/token/tenant from the conf. Best-effort throughout — it must NEVER
15
+ # block, slow, or fail the local liveness write that already happened before this was invoked. Every
16
+ # failure path is a quiet `exit 0`.
14
17
  #
15
18
  # Invoked (fully detached, best-effort) from scripts/hooks/lib/liveness-write.js after the durable
16
19
  # local append. Usage: relay.sh '<liveness-event-json>'
17
20
  set -uo pipefail
18
21
 
19
- # Opt-in gate — off by default.
20
- case "${FLOW_AGENTS_CONSOLE_LIVENESS_RELAY:-}" in
21
- 1 | true | TRUE | yes | on) ;;
22
- *) exit 0 ;;
23
- esac
24
-
25
22
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" || exit 0
26
23
 
27
24
  # transport.sh sources ${TELEMETRY_DIR}/lib/redact.sh at load — point it at the telemetry lib dir,
28
25
  # then reuse the SAME console POST core the telemetry mirror uses.
29
26
  export TELEMETRY_DIR="${TELEMETRY_DIR:-$SCRIPT_DIR/../telemetry}"
27
+
28
+ # Source config.sh FIRST (best-effort, like economics-record.sh) so the conf resolves enablement
29
+ # (FLOW_AGENTS_CONSOLE_LIVENESS_RELAY) and the endpoint/token/tenant — trust-gated for default-path
30
+ # confs. This is what makes conf-only operation work with NO env var set.
31
+ # shellcheck source=/dev/null
32
+ [[ -f "$TELEMETRY_DIR/lib/config.sh" ]] && source "$TELEMETRY_DIR/lib/config.sh" 2>/dev/null || true
33
+
34
+ # Opt-in gate — off by default, now honoring the conf-derived value config.sh just resolved.
35
+ case "${FLOW_AGENTS_CONSOLE_LIVENESS_RELAY:-}" in
36
+ 1 | true | TRUE | yes | on) ;;
37
+ *) exit 0 ;;
38
+ esac
39
+
30
40
  [[ -f "$TELEMETRY_DIR/lib/transport.sh" ]] || exit 0
31
41
  # shellcheck source=/dev/null
32
42
  source "$TELEMETRY_DIR/lib/transport.sh" 2>/dev/null || exit 0
@@ -73,7 +83,9 @@ record="$(printf '%s' "$event_json" | jq -c '{
73
83
  ttlSeconds: (.ttlSeconds // null),
74
84
  host: (.host // null),
75
85
  branch: (.branch // null),
76
- artifact_dir: (.artifact_dir // .artifactDir // null)
86
+ artifact_dir: (.artifact_dir // .artifactDir // null),
87
+ source: (.source // null),
88
+ activity: (.activity // null)
77
89
  }' 2>/dev/null)" || exit 0
78
90
  [[ -z "$record" || "$record" == "null" ]] && exit 0
79
91
 
@@ -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
 
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@kontourai/flow-agents",
3
- "version": "4.1.0",
3
+ "version": "4.2.1",
4
4
  "type": "module"
5
5
  }
@@ -572,7 +572,7 @@ async function bundleGateEvidence(bundle, gate, state, subject, projectRoot, man
572
572
  throw new BuilderBuildRunInputError("evidence.claims.metadata.gate_claim.route_reason", `is not declared by gate ${String(gate.id ?? "<unknown>")}`);
573
573
  }
574
574
  if (String(gate.id) === "verify-gate" && relevant.some((claim) => claim.claimType === "builder.verify.tests" && claim.value === "pass")) {
575
- await assertVerifiedTestsTrust(bundle.claims, projectRoot);
575
+ await assertVerifiedTestsTrust(relevant, projectRoot);
576
576
  }
577
577
  return { failed, routeReason, expectationIds, visitEnteredAt: enteredAt };
578
578
  }
@@ -602,8 +602,8 @@ function timestampAtOrAfter(value, boundary) {
602
602
  const parsed = parseTimestamp(value);
603
603
  return parsed !== null && parsed >= boundary;
604
604
  }
605
- async function assertVerifiedTestsTrust(claims, projectRoot) {
606
- const testClaims = claims.filter((claim) => isRecord(claim)
605
+ async function assertVerifiedTestsTrust(currentGateClaims, projectRoot) {
606
+ const testClaims = currentGateClaims.filter((claim) => isRecord(claim)
607
607
  && claim.claimType === "builder.verify.tests"
608
608
  && claim.value === "pass"
609
609
  && isRecord(claim.metadata)
@@ -611,7 +611,12 @@ async function assertVerifiedTestsTrust(claims, projectRoot) {
611
611
  && claim.metadata.gate_claim.expectation_id === "tests-evidence");
612
612
  if (testClaims.length === 0)
613
613
  throw new BuilderBuildRunInputError("evidence.tests", "is missing a passing tests-evidence claim");
614
- const liveCritiques = claims.filter((claim) => isRecord(claim)
614
+ // A route-back starts a new gate visit and therefore a new critique generation. Historical
615
+ // reviewer slices remain in the bundle and manifest for audit, but only critiques acquired
616
+ // during this visit describe the implementation snapshot currently being verified. Within a
617
+ // visit every live reviewer slice still participates, so changing reviewers cannot bury a
618
+ // disputed finding.
619
+ const liveCritiques = currentGateClaims.filter((claim) => isRecord(claim)
615
620
  && isRecord(claim.metadata)
616
621
  && claim.metadata.origin === "critique"
617
622
  && typeof claim.metadata.superseded_by !== "string");
@@ -627,7 +632,7 @@ async function assertVerifiedTestsTrust(claims, projectRoot) {
627
632
  await Promise.all(artifacts.map((artifact) => assertReviewedArtifactDigest(artifact, projectRoot)));
628
633
  assertReviewedWorkspaceSnapshot(claim, artifacts, projectRoot);
629
634
  }));
630
- const criteria = claims.filter((claim) => isRecord(claim) && isRecord(claim.metadata) && claim.metadata.origin === "acceptance");
635
+ const criteria = currentGateClaims.filter((claim) => isRecord(claim) && isRecord(claim.metadata) && claim.metadata.origin === "acceptance");
631
636
  if (criteria.length === 0 || criteria.some((claim) => {
632
637
  const criterion = isRecord(claim.metadata.criterion) ? claim.metadata.criterion : null;
633
638
  return claim.value !== "pass" || !criterion || !Array.isArray(criterion.evidence_refs) || criterion.evidence_refs.length === 0;
@@ -20,6 +20,12 @@ Verifiers and reviewers do not modify source code. They may run commands, inspec
20
20
 
21
21
  Verifiers write evidence and acceptance status **through** `record-evidence` (directly, or via the orchestrating skill), never by hand-authoring `evidence.json`, `acceptance.json`, or `trust.bundle`. Those bespoke sidecars were retired as the source of truth by ADR 0010 Phase 4c; the `trust.bundle` is the sole verification artifact, and only the sidecar writer performs the evidence classification (`evidenceType`/`method` derivation and manifest reconciliation) the CI trust anchor depends on (ADR 0020). `config-protection.js` blocks direct tool writes to these gate files by design. If the sidecar writer is unavailable, record the exact gap instead of writing the files by hand.
22
22
 
23
+ ### Critique Generations Across Route-Backs
24
+
25
+ Each visit to a verification gate establishes a critique generation. Critiques acquired during that visit must target the current implementation snapshot, and every live reviewer slice in the generation participates in the gate decision. A disputed finding therefore cannot be hidden by adding a different reviewer.
26
+
27
+ When Flow routes back and later re-enters verification, a fresh critique generation is required. Older reviewer slices remain immutable audit history in the trust bundle and evidence manifest, but their prior snapshot hashes are not reinterpreted as reviews of the new implementation. A reviewer handoff must use the public critique writer; it must never require actor impersonation or direct trust-bundle edits.
28
+
23
29
  ## Mutation Testing Runs In A Scratch Copy
24
30
 
25
31
  Mutation-testing tools (Stryker or equivalent) **must** run against a scratch/throwaway copy of the working tree, never the live working tree. They deliberately introduce defects to measure test-suite sensitivity; running them in place risks leaving mutated source, corrupting the checkout, or tripping the gate/anchor on injected failures. Copy the tree to a temporary directory (or a git worktree/clone) and run the mutation tool there; discard it afterward.
@@ -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
- SCRIPT="$ROOT/scripts/effective-backlog-settings.js"
6
- PROJECT="$ROOT/context/settings/backlog-provider-settings.json"
7
- GLOBAL="$ROOT/evals/fixtures/backlog-provider-settings/global-default.json"
8
- OVERRIDE="$ROOT/evals/fixtures/backlog-provider-settings/project-override.json"
9
- EMPTY="$(mktemp)"
10
- trap 'rm -f "$EMPTY"' EXIT
11
- printf '{"schema_version":"1.0","projects":[]}\n' > "$EMPTY"
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 Backlog Settings ==="
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
- default_result="$(cd "$ROOT" && npm run --silent effective-backlog-settings -- --repo-path . --json)"
24
- [[ "$(printf '%s' "$default_result" | json_value status)" == "configured" ]] && pass "default project settings resolve through npm command" || fail "default project settings resolve through npm command"
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 "$SCRIPT" --repo-path "$ROOT" --project-settings "$PROJECT" --global-settings "$EMPTY" --json)"
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 "$SCRIPT" --repo-path "$ROOT" --project-settings "$OVERRIDE" --global-settings "$GLOBAL" --json)"
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 "$SCRIPT" --repo-path "$ROOT" --project-settings "$EMPTY" --global-settings "$EMPTY" --json 2>/dev/null)"
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 backlog settings checks passed"
156
+ echo "Effective provider settings checks passed"
54
157
  else
55
- echo "Effective backlog settings checks failed: $errors"
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."