@kontourai/flow-agents 3.3.0 → 3.4.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 (256) hide show
  1. package/.github/workflows/add-to-project.yml +15 -0
  2. package/.github/workflows/ci.yml +161 -0
  3. package/CHANGELOG.md +48 -0
  4. package/CONTEXT.md +5 -1
  5. package/README.md +19 -8
  6. package/build/src/builder-flow-run-adapter.d.ts +80 -0
  7. package/build/src/builder-flow-run-adapter.js +241 -0
  8. package/build/src/builder-flow-runtime.d.ts +16 -0
  9. package/build/src/builder-flow-runtime.js +290 -0
  10. package/build/src/cli/builder-run.d.ts +1 -0
  11. package/build/src/cli/builder-run.js +27 -0
  12. package/build/src/cli/effective-backlog-settings.js +70 -2
  13. package/build/src/cli/init.d.ts +34 -0
  14. package/build/src/cli/init.js +341 -61
  15. package/build/src/cli/kit.js +55 -12
  16. package/build/src/cli/pull-work-provider.js +346 -5
  17. package/build/src/cli/skill-drift-check.d.ts +1 -0
  18. package/build/src/cli/skill-drift-check.js +165 -0
  19. package/build/src/cli/telemetry-doctor.d.ts +37 -0
  20. package/build/src/cli/telemetry-doctor.js +53 -6
  21. package/build/src/cli/validate-hook-influence.js +37 -7
  22. package/build/src/cli/workflow-sidecar.d.ts +93 -8
  23. package/build/src/cli/workflow-sidecar.js +1175 -158
  24. package/build/src/cli.js +5 -0
  25. package/build/src/flow-kit/validate.d.ts +54 -34
  26. package/build/src/flow-kit/validate.js +237 -26
  27. package/build/src/index.d.ts +2 -0
  28. package/build/src/index.js +1 -0
  29. package/build/src/lib/console-connect-options.d.ts +97 -0
  30. package/build/src/lib/console-connect-options.js +199 -0
  31. package/build/src/lib/console-telemetry-validate.d.ts +49 -0
  32. package/build/src/lib/console-telemetry-validate.js +91 -0
  33. package/build/src/lib/flow-resolver.d.ts +56 -3
  34. package/build/src/lib/flow-resolver.js +151 -11
  35. package/build/src/lib/fs.d.ts +17 -0
  36. package/build/src/lib/fs.js +172 -0
  37. package/build/src/lib/local-artifact-root.d.ts +44 -1
  38. package/build/src/lib/local-artifact-root.js +131 -3
  39. package/build/src/runtime-adapters.d.ts +39 -3
  40. package/build/src/runtime-adapters.js +77 -31
  41. package/build/src/tools/build-universal-bundles.js +40 -2
  42. package/build/src/tools/codex-agent-routing.d.ts +2 -0
  43. package/build/src/tools/codex-agent-routing.js +49 -0
  44. package/build/src/tools/generate-context-map.js +1 -0
  45. package/build/src/tools/validate-source-tree.js +27 -1
  46. package/context/scripts/hooks/lib/kit-catalog.js +235 -0
  47. package/context/scripts/hooks/lib/runnable-command.js +177 -0
  48. package/context/scripts/hooks/stop-goal-fit.js +278 -48
  49. package/context/scripts/hooks/workflow-steering.js +121 -21
  50. package/context/scripts/package.json +3 -0
  51. package/context/scripts/telemetry/install-console-config.sh +25 -4
  52. package/context/scripts/telemetry/lib/config.sh +102 -12
  53. package/context/scripts/telemetry/lib/pricing.sh +50 -0
  54. package/context/scripts/telemetry/lib/session.sh +3 -0
  55. package/context/scripts/telemetry/lib/transport.sh +87 -0
  56. package/context/scripts/telemetry/lib/usage.sh +205 -4
  57. package/context/scripts/telemetry/telemetry.conf +6 -0
  58. package/context/scripts/telemetry/telemetry.sh +48 -0
  59. package/context/settings/workspace-backlog-provider-settings.example.json +48 -0
  60. package/docs/agent-usage-feedback-loop.md +35 -0
  61. package/docs/architecture-engine-and-kits.md +110 -0
  62. package/docs/context-map.md +2 -0
  63. package/docs/decisions/embeddable-engine.md +152 -0
  64. package/docs/decisions/index.md +3 -1
  65. package/docs/decisions/trust-ledger-retention.md +88 -0
  66. package/docs/decisions/workflow-enforcement.md +31 -9
  67. package/docs/fixture-ownership.md +3 -0
  68. package/docs/implementing-trust-reconciliation.md +129 -0
  69. package/docs/index.md +19 -9
  70. package/docs/integrations/flow-agents-console.md +167 -0
  71. package/docs/kit-authoring-guide.md +52 -21
  72. package/docs/spec/builder-flow-runtime.md +80 -0
  73. package/docs/spec/runtime-hook-surface.md +45 -1
  74. package/docs/specs/economics-record-contract.md +270 -0
  75. package/docs/specs/harness-capability-matrix.md +74 -0
  76. package/docs/specs/learning-review-proposals-contract.md +340 -0
  77. package/docs/specs/routing-efficiency-review.md +59 -0
  78. package/docs/verifiable-trust.md +74 -25
  79. package/docs/workflow-usage-guide.md +10 -0
  80. package/evals/acceptance/prove-capture-teeth.sh +132 -0
  81. package/evals/ci/antigaming-suite.sh +1 -0
  82. package/evals/ci/run-baseline.sh +72 -4
  83. package/evals/fixtures/economics/acceptance.json +12 -0
  84. package/evals/fixtures/economics/agents/tool-worker-1/events.jsonl +2 -0
  85. package/evals/fixtures/economics/agents/tool-worker-2/events.jsonl +2 -0
  86. package/evals/fixtures/economics/agents/tool-worker-3/events.jsonl +2 -0
  87. package/evals/fixtures/economics/agents/tool-worker-4/events.jsonl +1 -0
  88. package/evals/fixtures/economics/agents/tool-worker-5/events.jsonl +2 -0
  89. package/evals/fixtures/economics/critique.json +22 -0
  90. package/evals/fixtures/economics/expected-record.json +71 -0
  91. package/evals/fixtures/economics/session-usage-event.json +1 -0
  92. package/evals/fixtures/economics/state.json +11 -0
  93. package/evals/fixtures/economics/transcript.jsonl +3 -0
  94. package/evals/fixtures/hook-influence/cases.json +7 -7
  95. package/evals/fixtures/learning-review-proposals/balanced/economics.jsonl +6 -0
  96. package/evals/fixtures/learning-review-proposals/effect-follow-up/economics.jsonl +5 -0
  97. package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-1/trust.bundle +21 -0
  98. package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-2/trust.bundle +21 -0
  99. package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-3/trust.bundle +21 -0
  100. package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-4/trust.bundle +21 -0
  101. package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-5/trust.bundle +21 -0
  102. package/evals/fixtures/learning-review-proposals/pattern-present/economics.jsonl +6 -0
  103. package/evals/fixtures/learning-review-proposals/pattern-present/expected-aggregates.json +30 -0
  104. package/evals/fixtures/learning-review-proposals/pattern-present/expected-aggregates.md +66 -0
  105. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-1/gate-review.inquiries.json +26 -0
  106. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-1/trust.bundle +21 -0
  107. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-2/gate-review.inquiries.json +26 -0
  108. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-2/trust.bundle +21 -0
  109. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-3/gate-review.inquiries.json +26 -0
  110. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-3/trust.bundle +21 -0
  111. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-4/gate-review.inquiries.json +26 -0
  112. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-4/trust.bundle +21 -0
  113. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-5/trust.bundle +21 -0
  114. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-6/trust.bundle +21 -0
  115. package/evals/fixtures/learning-review-proposals/repeat-window/economics.jsonl +6 -0
  116. package/evals/fixtures/learning-review-proposals/under-threshold/economics.jsonl +3 -0
  117. package/evals/fixtures/telemetry/usage-transcript-sample.jsonl +4 -0
  118. package/evals/fixtures/trust-reconcile-exploits/mcp-degrade.json +42 -0
  119. package/evals/integration/test_builder_entry_enforcement.sh +241 -0
  120. package/evals/integration/test_builder_step_producers.sh +18 -10
  121. package/evals/integration/test_bundle_install.sh +172 -0
  122. package/evals/integration/test_console_tenant_isolation.sh +167 -0
  123. package/evals/integration/test_critique_supersession_roundtrip.sh +4 -1
  124. package/evals/integration/test_dual_emit_flow_step.sh +10 -4
  125. package/evals/integration/test_economics_record.sh +674 -0
  126. package/evals/integration/test_effective_backlog_settings.sh +1 -1
  127. package/evals/integration/test_evidence_capture_hook.sh +17 -2
  128. package/evals/integration/test_exemption_usage_review.sh +198 -0
  129. package/evals/integration/test_fixture_retirement_audit.sh +2 -2
  130. package/evals/integration/test_flow_kit_install_git.sh +83 -0
  131. package/evals/integration/test_flowdef_session_activation.sh +0 -1
  132. package/evals/integration/test_flowdef_session_history_preservation.sh +13 -3
  133. package/evals/integration/test_gate_lockdown.sh +7 -0
  134. package/evals/integration/test_gate_review_inquiry_records.sh +9 -1
  135. package/evals/integration/test_goal_fit_hook.sh +2031 -0
  136. package/evals/integration/test_hook_category_behaviors.sh +8 -1
  137. package/evals/integration/test_hook_influence_cases.sh +25 -1
  138. package/evals/integration/test_install_merge.sh +227 -2
  139. package/evals/integration/test_kit_conformance_levels.sh +6 -6
  140. package/evals/integration/test_learning_review_proposals.sh +329 -0
  141. package/evals/integration/test_liveness_conflict_injection.sh +26 -22
  142. package/evals/integration/test_liveness_console_relay.sh +166 -0
  143. package/evals/integration/test_liveness_heartbeat.sh +17 -17
  144. package/evals/integration/test_liveness_worktree_root.sh +575 -0
  145. package/evals/integration/test_phase_map_and_gate_claim.sh +6 -1
  146. package/evals/integration/test_publish_delivery.sh +331 -1
  147. package/evals/integration/test_pull_work_board.sh +200 -0
  148. package/evals/integration/test_pull_work_provider.sh +1 -1
  149. package/evals/integration/test_record_check.sh +378 -0
  150. package/evals/integration/test_routing_efficiency.sh +71 -0
  151. package/evals/integration/test_runtime_adapter_activation.sh +28 -0
  152. package/evals/integration/test_session_resume_roundtrip.sh +16 -19
  153. package/evals/integration/test_skill_drift_check.sh +870 -0
  154. package/evals/integration/test_telemetry.sh +445 -0
  155. package/evals/integration/test_telemetry_doctor.sh +66 -0
  156. package/evals/integration/test_telemetry_usage_pipeline.sh +228 -0
  157. package/evals/integration/test_trust_reconcile_negatives.sh +30 -13
  158. package/evals/integration/test_trust_reconcile_trailer_diagnostic.sh +247 -0
  159. package/evals/integration/test_usage_cost.sh +61 -0
  160. package/evals/integration/test_workflow_sidecar_writer.sh +1395 -0
  161. package/evals/integration/test_workflow_steering_hook.sh +157 -16
  162. package/evals/integration/test_workspace_settings.sh +176 -0
  163. package/evals/lib/env.sh +26 -0
  164. package/evals/lib/node.sh +8 -0
  165. package/evals/run.sh +29 -0
  166. package/evals/static/test_ci_integration_coverage.sh +115 -0
  167. package/evals/static/test_declared_scope_forms_documented.sh +114 -0
  168. package/evals/static/test_universal_bundles.sh +34 -0
  169. package/evals/static/test_validate_source_kit_asset_scope.sh +259 -0
  170. package/evals/static/test_workflow_skills.sh +1 -1
  171. package/kits/builder/flows/build.flow.json +9 -18
  172. package/kits/builder/flows/publish-learn.flow.json +5 -1
  173. package/kits/builder/kit.json +120 -0
  174. package/kits/builder/skills/deliver/SKILL.md +42 -0
  175. package/kits/builder/skills/evidence-gate/SKILL.md +12 -0
  176. package/kits/builder/skills/execute-plan/SKILL.md +9 -0
  177. package/kits/builder/skills/learning-review/SKILL.md +51 -0
  178. package/kits/builder/skills/plan-work/SKILL.md +17 -20
  179. package/kits/builder/skills/pull-work/SKILL.md +21 -0
  180. package/kits/builder/skills/release-readiness/SKILL.md +12 -0
  181. package/kits/knowledge/kit.json +9 -0
  182. package/kits/veritas-governance/docs/README.md +35 -7
  183. package/kits/veritas-governance/fixtures/exemption-review/mixed-fresh-stale.DECLARED.json +14 -0
  184. package/kits/veritas-governance/kit.json +14 -0
  185. package/kits/veritas-governance/skills/exemption-usage-review/SKILL.md +128 -0
  186. package/kits/veritas-governance/skills/exemption-usage-review/review-exemptions.mjs +231 -0
  187. package/package.json +2 -2
  188. package/packaging/manifest.json +29 -0
  189. package/schemas/backlog-provider-settings.schema.json +13 -0
  190. package/schemas/workflow-state.schema.json +44 -0
  191. package/scripts/README.md +4 -0
  192. package/scripts/check-content-boundary.cjs +8 -1
  193. package/scripts/ci/trust-reconcile.js +136 -0
  194. package/scripts/hooks/codex-hook-adapter.js +77 -2
  195. package/scripts/hooks/evidence-capture.js +38 -5
  196. package/scripts/hooks/lib/codex-exit-code.js +316 -0
  197. package/scripts/hooks/lib/kit-catalog.js +235 -0
  198. package/scripts/hooks/lib/liveness-write.js +28 -1
  199. package/scripts/hooks/lib/local-artifact-paths.js +97 -1
  200. package/scripts/hooks/lib/runnable-command.js +177 -0
  201. package/scripts/hooks/lib/skill-drift.js +350 -0
  202. package/scripts/hooks/stop-goal-fit.js +278 -48
  203. package/scripts/hooks/workflow-steering.js +121 -21
  204. package/scripts/install-codex-home.sh +97 -47
  205. package/scripts/install-merge.js +72 -14
  206. package/scripts/install-owned-files.js +178 -0
  207. package/scripts/liveness/relay.sh +84 -0
  208. package/scripts/telemetry/economics-record.schema.json +145 -0
  209. package/scripts/telemetry/economics-record.sh +331 -0
  210. package/scripts/telemetry/install-console-config.sh +25 -4
  211. package/scripts/telemetry/learning-review-decide.sh +124 -0
  212. package/scripts/telemetry/learning-review-proposals.schema.json +161 -0
  213. package/scripts/telemetry/learning-review-proposals.sh +484 -0
  214. package/scripts/telemetry/lib/config.sh +102 -12
  215. package/scripts/telemetry/lib/pricing.sh +14 -6
  216. package/scripts/telemetry/lib/session.sh +3 -0
  217. package/scripts/telemetry/lib/transport.sh +133 -15
  218. package/scripts/telemetry/lib/usage.sh +121 -28
  219. package/scripts/telemetry/routing-efficiency.sh +0 -0
  220. package/scripts/telemetry/telemetry.conf +6 -0
  221. package/scripts/telemetry/telemetry.sh +48 -0
  222. package/src/builder-flow-run-adapter.ts +357 -0
  223. package/src/builder-flow-runtime.ts +348 -0
  224. package/src/cli/builder-flow-run-adapter.test.mjs +495 -0
  225. package/src/cli/builder-flow-runtime.test.mjs +213 -0
  226. package/src/cli/builder-run.ts +28 -0
  227. package/src/cli/codex-agent-routing.test.mjs +44 -0
  228. package/src/cli/codex-exit-code.test.mjs +207 -0
  229. package/src/cli/console-connect-options.test.mjs +329 -0
  230. package/src/cli/console-telemetry-validate.test.mjs +157 -0
  231. package/src/cli/effective-backlog-settings.ts +68 -2
  232. package/src/cli/flow-resolver-composition.test.mjs +101 -0
  233. package/src/cli/init.test.mjs +161 -0
  234. package/src/cli/init.ts +407 -62
  235. package/src/cli/kit-metadata-security.test.mjs +443 -0
  236. package/src/cli/kit.ts +50 -12
  237. package/src/cli/pull-work-provider.ts +377 -3
  238. package/src/cli/sidecar-pure-helpers.test.mjs +64 -0
  239. package/src/cli/skill-drift-check.ts +196 -0
  240. package/src/cli/telemetry-doctor.test.mjs +53 -0
  241. package/src/cli/telemetry-doctor.ts +50 -7
  242. package/src/cli/validate-hook-influence.ts +37 -6
  243. package/src/cli/workflow-sidecar.ts +1150 -151
  244. package/src/cli.ts +5 -0
  245. package/src/flow-kit/validate.ts +277 -38
  246. package/src/index.ts +19 -0
  247. package/src/lib/console-connect-options.ts +261 -0
  248. package/src/lib/console-telemetry-validate.ts +88 -0
  249. package/src/lib/flow-resolver.ts +153 -10
  250. package/src/lib/fs.ts +160 -0
  251. package/src/lib/local-artifact-root.ts +129 -3
  252. package/src/runtime-adapters.ts +113 -33
  253. package/src/tools/build-universal-bundles.ts +36 -2
  254. package/src/tools/codex-agent-routing.ts +48 -0
  255. package/src/tools/generate-context-map.ts +1 -0
  256. package/src/tools/validate-source-tree.ts +26 -1
@@ -83,6 +83,451 @@ for lib in config.sh session.sh enrich.sh transport.sh redact.sh; do
83
83
  fi
84
84
  done
85
85
 
86
+ # --- 1b. Config file resolution precedence ---
87
+ # scripts/telemetry/lib/config.sh must resolve TELEMETRY_CONFIG_FILE in this
88
+ # order: (1) explicit env always wins; (2) a gitignored per-workspace conf at
89
+ # .kontourai/telemetry-console.conf if present AND operator-trusted
90
+ # (project-specific override); (3) a gitignored user-global conf at
91
+ # ~/.flow-agents/telemetry-console.conf if present AND operator-trusted
92
+ # (one machine-wide install, matching the existing ~/.flow-agents
93
+ # install-home convention); (4) the shipped telemetry.conf default. The
94
+ # shipped conf is tracked and ships into dist bundles, so owner credentials
95
+ # must never land there — hence the per-workspace/per-machine fallbacks.
96
+ # "Operator-trusted" means mode 600 and owned by the current user, since
97
+ # install-console-config.sh always chmod 600s the conf it writes while git
98
+ # can only store 644/755 — never 600 — so a conf smuggled in via
99
+ # clone/tarball/PR/supply-chain cannot pass the gate even at the expected
100
+ # path. Symlinks are rejected outright regardless of the link's own mode,
101
+ # since the trust check uses lstat semantics but the later conf read follows
102
+ # the link — a mismatched check/read object. A fake HOME isolates these
103
+ # checks from any real ~/.flow-agents on the machine running the suite.
104
+ echo ""
105
+ echo "--- Config File Resolution ---"
106
+ CONFIG_SH="$ROOT_DIR/scripts/telemetry/lib/config.sh"
107
+ CONFIG_TEST_ROOT=$(mktemp -d /tmp/eval-telemetry-config-test.XXXXXX)
108
+ mkdir -p "$CONFIG_TEST_ROOT/scripts/telemetry/lib"
109
+ cp "$CONFIG_SH" "$CONFIG_TEST_ROOT/scripts/telemetry/lib/config.sh"
110
+ : > "$CONFIG_TEST_ROOT/scripts/telemetry/telemetry.conf"
111
+ FAKE_HOME="$CONFIG_TEST_ROOT/home"
112
+ mkdir -p "$FAKE_HOME"
113
+
114
+ _resolve_config_file() {
115
+ env -i PATH="$PATH" HOME="$FAKE_HOME" "$@" \
116
+ bash -c "source '$CONFIG_TEST_ROOT/scripts/telemetry/lib/config.sh' 2>/dev/null; echo \"\$TELEMETRY_CONFIG_FILE\""
117
+ }
118
+
119
+ # Same as _resolve_config_file but captures config.sh's stderr into
120
+ # $1 instead of discarding it, so the new "conf exists but is not trusted"
121
+ # warning (AC2, install-flow-foundations Thread B) can be asserted on
122
+ # directly. Call as: _resolve_config_file_with_stderr <stderr-file> [env=val ...]
123
+ _resolve_config_file_with_stderr() {
124
+ local stderr_file="$1"
125
+ shift
126
+ env -i PATH="$PATH" HOME="$FAKE_HOME" "$@" \
127
+ bash -c "source '$CONFIG_TEST_ROOT/scripts/telemetry/lib/config.sh' 2>'$stderr_file'; echo \"\$TELEMETRY_CONFIG_FILE\""
128
+ }
129
+
130
+ STDERR_CAPTURE="$CONFIG_TEST_ROOT/config-stderr.txt"
131
+
132
+ default_resolved=$(_resolve_config_file)
133
+ if [[ "$default_resolved" == "$CONFIG_TEST_ROOT/scripts/telemetry/telemetry.conf" ]]; then
134
+ _pass "config.sh: falls back to shipped telemetry.conf default"
135
+ else
136
+ _fail "config.sh: default expected '$CONFIG_TEST_ROOT/scripts/telemetry/telemetry.conf', got '$default_resolved'"
137
+ fi
138
+
139
+ : > "$STDERR_CAPTURE"
140
+ _resolve_config_file_with_stderr "$STDERR_CAPTURE" >/dev/null
141
+ if [[ ! -s "$STDERR_CAPTURE" ]]; then
142
+ _pass "config.sh: no conf on disk at all produces no untrusted-conf warning"
143
+ else
144
+ _fail "config.sh: baseline (no conf) should produce no warning, got: $(cat "$STDERR_CAPTURE")"
145
+ fi
146
+
147
+ mkdir -p "$FAKE_HOME/.flow-agents"
148
+ GLOBAL_CONF="$FAKE_HOME/.flow-agents/telemetry-console.conf"
149
+ : > "$GLOBAL_CONF"
150
+ chmod 644 "$GLOBAL_CONF"
151
+ untrusted_global_resolved=$(_resolve_config_file)
152
+ if [[ "$untrusted_global_resolved" == "$CONFIG_TEST_ROOT/scripts/telemetry/telemetry.conf" ]]; then
153
+ _pass "config.sh: untrusted (644) user-global conf falls through to shipped default"
154
+ else
155
+ _fail "config.sh: untrusted global conf should fall through, got '$untrusted_global_resolved'"
156
+ fi
157
+
158
+ : > "$STDERR_CAPTURE"
159
+ _resolve_config_file_with_stderr "$STDERR_CAPTURE" >/dev/null
160
+ if grep -qF "$GLOBAL_CONF" "$STDERR_CAPTURE" 2>/dev/null; then
161
+ _pass "config.sh: untrusted (644) user-global conf prints a visible warning naming its path"
162
+ else
163
+ _fail "config.sh: expected a warning naming '$GLOBAL_CONF', got: $(cat "$STDERR_CAPTURE" 2>/dev/null)"
164
+ fi
165
+
166
+ chmod 600 "$GLOBAL_CONF"
167
+ global_resolved=$(_resolve_config_file)
168
+ if [[ "$global_resolved" == "$GLOBAL_CONF" ]]; then
169
+ _pass "config.sh: trusted (600) user-global telemetry-console.conf wins over shipped default"
170
+ else
171
+ _fail "config.sh: global conf expected '$GLOBAL_CONF', got '$global_resolved'"
172
+ fi
173
+
174
+ : > "$STDERR_CAPTURE"
175
+ _resolve_config_file_with_stderr "$STDERR_CAPTURE" >/dev/null
176
+ if [[ ! -s "$STDERR_CAPTURE" ]]; then
177
+ _pass "config.sh: trusted (600) user-global conf produces no warning"
178
+ else
179
+ _fail "config.sh: trusted global conf should produce no warning, got: $(cat "$STDERR_CAPTURE")"
180
+ fi
181
+
182
+ mkdir -p "$CONFIG_TEST_ROOT/.kontourai"
183
+ LOCAL_CONF="$CONFIG_TEST_ROOT/.kontourai/telemetry-console.conf"
184
+ : > "$LOCAL_CONF"
185
+ chmod 644 "$LOCAL_CONF"
186
+ untrusted_local_resolved=$(_resolve_config_file)
187
+ if [[ "$untrusted_local_resolved" == "$GLOBAL_CONF" ]]; then
188
+ _pass "config.sh: untrusted (644) workspace-local conf falls through to trusted global conf"
189
+ else
190
+ _fail "config.sh: untrusted local conf should fall through to global, got '$untrusted_local_resolved'"
191
+ fi
192
+
193
+ # Local is untrusted here while global is trusted (600) and wins the
194
+ # resolution -- the local warning must still fire even though local isn't
195
+ # the file config.sh ultimately picks, since the local conf is still being
196
+ # silently ignored on its own terms.
197
+ : > "$STDERR_CAPTURE"
198
+ _resolve_config_file_with_stderr "$STDERR_CAPTURE" >/dev/null
199
+ if grep -qF "$LOCAL_CONF" "$STDERR_CAPTURE" 2>/dev/null; then
200
+ _pass "config.sh: untrusted (644) workspace-local conf prints a visible warning naming its path even though global wins"
201
+ else
202
+ _fail "config.sh: expected a warning naming '$LOCAL_CONF', got: $(cat "$STDERR_CAPTURE" 2>/dev/null)"
203
+ fi
204
+
205
+ chmod 600 "$LOCAL_CONF"
206
+ local_resolved=$(_resolve_config_file)
207
+ if [[ "$local_resolved" == "$LOCAL_CONF" ]]; then
208
+ _pass "config.sh: trusted (600) workspace-local telemetry-console.conf wins over user-global conf"
209
+ else
210
+ _fail "config.sh: local conf expected '$LOCAL_CONF', got '$local_resolved'"
211
+ fi
212
+
213
+ : > "$STDERR_CAPTURE"
214
+ _resolve_config_file_with_stderr "$STDERR_CAPTURE" >/dev/null
215
+ if [[ ! -s "$STDERR_CAPTURE" ]]; then
216
+ _pass "config.sh: trusted (600) workspace-local conf produces no warning"
217
+ else
218
+ _fail "config.sh: trusted local conf should produce no warning, got: $(cat "$STDERR_CAPTURE")"
219
+ fi
220
+
221
+ # A symlink at the workspace conf path must be ignored outright, even if the
222
+ # link's own mode is 600 (macOS `chmod -h` sets the link's own bits without
223
+ # following it) and it points at a genuinely trusted 600 regular file, since
224
+ # the later conf read follows the link to a possibly different target.
225
+ TARGET_CONF="$CONFIG_TEST_ROOT/.kontourai/telemetry-console-target.conf"
226
+ : > "$TARGET_CONF"
227
+ chmod 600 "$TARGET_CONF"
228
+ rm -f "$LOCAL_CONF"
229
+ ln -s "$TARGET_CONF" "$LOCAL_CONF"
230
+ chmod -h 600 "$LOCAL_CONF" 2>/dev/null || true
231
+ symlink_resolved=$(_resolve_config_file)
232
+ if [[ "$symlink_resolved" == "$GLOBAL_CONF" ]]; then
233
+ _pass "config.sh: symlinked workspace-local conf is ignored, falls through to trusted global conf"
234
+ else
235
+ _fail "config.sh: symlinked local conf should fall through to global, got '$symlink_resolved'"
236
+ fi
237
+
238
+ : > "$STDERR_CAPTURE"
239
+ _resolve_config_file_with_stderr "$STDERR_CAPTURE" >/dev/null
240
+ if grep -qF "$LOCAL_CONF" "$STDERR_CAPTURE" 2>/dev/null; then
241
+ _pass "config.sh: symlinked workspace-local conf also prints a visible untrusted-conf warning ('-f' follows the link to a real file)"
242
+ else
243
+ _fail "config.sh: expected a warning naming '$LOCAL_CONF' for the symlink case, got: $(cat "$STDERR_CAPTURE" 2>/dev/null)"
244
+ fi
245
+
246
+ # A DANGLING symlink (target does not exist) at the workspace conf path is
247
+ # rejected by the trust gate just like the symlink-to-a-real-file case above,
248
+ # but `-f` (which follows the link) returns false for a dangling target --
249
+ # so the warning gate must additionally check `-L` to still fire here
250
+ # (LOW fix: telemetry_conf_warn_untrusted broadened from `-f` alone to
251
+ # `-e || -L`).
252
+ rm -f "$LOCAL_CONF"
253
+ ln -s "$CONFIG_TEST_ROOT/.kontourai/telemetry-console-target-missing.conf" "$LOCAL_CONF"
254
+ dangling_symlink_resolved=$(_resolve_config_file)
255
+ if [[ "$dangling_symlink_resolved" == "$GLOBAL_CONF" ]]; then
256
+ _pass "config.sh: dangling-symlink workspace-local conf is ignored, falls through to trusted global conf"
257
+ else
258
+ _fail "config.sh: dangling-symlink local conf should fall through to global, got '$dangling_symlink_resolved'"
259
+ fi
260
+
261
+ : > "$STDERR_CAPTURE"
262
+ _resolve_config_file_with_stderr "$STDERR_CAPTURE" >/dev/null
263
+ if grep -qF "$LOCAL_CONF" "$STDERR_CAPTURE" 2>/dev/null; then
264
+ _pass "config.sh: dangling-symlink workspace-local conf prints a visible untrusted-conf warning ('-f' alone would miss this; '-L' catches it)"
265
+ else
266
+ _fail "config.sh: expected a warning naming '$LOCAL_CONF' for the dangling-symlink case, got: $(cat "$STDERR_CAPTURE" 2>/dev/null)"
267
+ fi
268
+
269
+ # A symlink pointing at a DIRECTORY (not a regular file) hits the same `-f`
270
+ # gap: `-f` is false for a directory target, so the pre-fix warning gate
271
+ # silently skipped it too.
272
+ rm -f "$LOCAL_CONF"
273
+ mkdir -p "$CONFIG_TEST_ROOT/.kontourai/telemetry-console-target-dir"
274
+ ln -s "$CONFIG_TEST_ROOT/.kontourai/telemetry-console-target-dir" "$LOCAL_CONF"
275
+ symlink_to_dir_resolved=$(_resolve_config_file)
276
+ if [[ "$symlink_to_dir_resolved" == "$GLOBAL_CONF" ]]; then
277
+ _pass "config.sh: symlink-to-directory workspace-local conf is ignored, falls through to trusted global conf"
278
+ else
279
+ _fail "config.sh: symlink-to-directory local conf should fall through to global, got '$symlink_to_dir_resolved'"
280
+ fi
281
+
282
+ : > "$STDERR_CAPTURE"
283
+ _resolve_config_file_with_stderr "$STDERR_CAPTURE" >/dev/null
284
+ if grep -qF "$LOCAL_CONF" "$STDERR_CAPTURE" 2>/dev/null; then
285
+ _pass "config.sh: symlink-to-directory workspace-local conf prints a visible untrusted-conf warning ('-f' alone would miss this; '-L' catches it)"
286
+ else
287
+ _fail "config.sh: expected a warning naming '$LOCAL_CONF' for the symlink-to-directory case, got: $(cat "$STDERR_CAPTURE" 2>/dev/null)"
288
+ fi
289
+
290
+ rm -f "$LOCAL_CONF"
291
+
292
+ explicit_resolved=$(_resolve_config_file TELEMETRY_CONFIG_FILE="/tmp/explicit-telemetry.conf")
293
+ if [[ "$explicit_resolved" == "/tmp/explicit-telemetry.conf" ]]; then
294
+ _pass "config.sh: explicit TELEMETRY_CONFIG_FILE env wins over workspace-local conf"
295
+ else
296
+ _fail "config.sh: explicit env expected '/tmp/explicit-telemetry.conf', got '$explicit_resolved'"
297
+ fi
298
+
299
+ _resolve_pricing_url() {
300
+ env -i PATH="$PATH" HOME="$FAKE_HOME" "$@" \
301
+ bash -c "source '$CONFIG_TEST_ROOT/scripts/telemetry/lib/config.sh' 2>/dev/null; printf '%s\n' \"\${TELEMETRY_PRICING_URL:-}\""
302
+ }
303
+
304
+ PRICING_EMPTY_CONF="$CONFIG_TEST_ROOT/pricing-empty.conf"
305
+ : > "$PRICING_EMPTY_CONF"
306
+ pricing_from_console=$(_resolve_pricing_url TELEMETRY_CONFIG_FILE="$PRICING_EMPTY_CONF" CONSOLE_TELEMETRY_URL="https://console.example.test")
307
+ if [[ -z "$pricing_from_console" ]]; then
308
+ _pass "config.sh: console telemetry URL alone does not derive TELEMETRY_PRICING_URL (bundled default)"
309
+ else
310
+ _fail "config.sh: expected no derived pricing URL from console URL alone, got '$pricing_from_console'"
311
+ fi
312
+
313
+ pricing_from_env=$(_resolve_pricing_url TELEMETRY_CONFIG_FILE="$PRICING_EMPTY_CONF" CONSOLE_TELEMETRY_URL="https://console.example.test" FLOW_AGENTS_PRICING_URL="https://pricing.example.test/registry.json")
314
+ if [[ "$pricing_from_env" == "https://pricing.example.test/registry.json" ]]; then
315
+ _pass "config.sh: explicit FLOW_AGENTS_PRICING_URL remains honored"
316
+ else
317
+ _fail "config.sh: expected explicit FLOW_AGENTS_PRICING_URL to win, got '$pricing_from_env'"
318
+ fi
319
+
320
+ PRICING_CONF="$CONFIG_TEST_ROOT/pricing-explicit.conf"
321
+ printf '%s\n' 'console_pricing_url=https://pricing.example.test/from-config.json' > "$PRICING_CONF"
322
+ pricing_from_config=$(_resolve_pricing_url TELEMETRY_CONFIG_FILE="$PRICING_CONF" CONSOLE_TELEMETRY_URL="https://console.example.test")
323
+ if [[ "$pricing_from_config" == "https://pricing.example.test/from-config.json" ]]; then
324
+ _pass "config.sh: explicit console_pricing_url config remains honored"
325
+ else
326
+ _fail "config.sh: expected console_pricing_url config to win, got '$pricing_from_config'"
327
+ fi
328
+
329
+ rm -rf "$CONFIG_TEST_ROOT"
330
+
331
+ # --- 1c. Endpoint allowlist drop warning ---
332
+ # transport.sh's console_post_json silently dropped an endpoint rejected by
333
+ # console_telemetry_endpoint_allowed before this change (AC2, Thread B). It
334
+ # now prints a one-time (per shell process) "warning: transport.sh: ..." line
335
+ # to stderr instead, guarded by a plain global var so a second call in the
336
+ # same sourced shell does not repeat it. Sourcing transport.sh directly
337
+ # (mirroring the Config File Resolution block's own cp-then-source pattern)
338
+ # with a disallowed http (non-local) endpoint means console_post_json must
339
+ # return before ever invoking curl, so PATH is left untouched deliberately.
340
+ echo ""
341
+ echo "--- Endpoint Allowlist Drop Warning ---"
342
+ TRANSPORT_TEST_ROOT=$(mktemp -d /tmp/eval-telemetry-transport-test.XXXXXX)
343
+ mkdir -p "$TRANSPORT_TEST_ROOT/scripts/telemetry/lib"
344
+ cp "$ROOT_DIR/scripts/telemetry/lib/transport.sh" "$TRANSPORT_TEST_ROOT/scripts/telemetry/lib/transport.sh"
345
+ cp "$ROOT_DIR/scripts/telemetry/lib/redact.sh" "$TRANSPORT_TEST_ROOT/scripts/telemetry/lib/redact.sh"
346
+ TRANSPORT_STDERR="$TRANSPORT_TEST_ROOT/transport-stderr.txt"
347
+ TRANSPORT_CURL_MARKER="$TRANSPORT_TEST_ROOT/curl-invoked"
348
+ NO_CURL_BIN="$TRANSPORT_TEST_ROOT/no-curl-bin"
349
+ mkdir -p "$NO_CURL_BIN"
350
+ cat > "$NO_CURL_BIN/curl" <<SH
351
+ #!/usr/bin/env bash
352
+ touch "$TRANSPORT_CURL_MARKER"
353
+ SH
354
+ chmod +x "$NO_CURL_BIN/curl"
355
+ TELEMETRY_DIR="$TRANSPORT_TEST_ROOT/scripts/telemetry" PATH="$NO_CURL_BIN:$PATH" bash -c "
356
+ source '$TRANSPORT_TEST_ROOT/scripts/telemetry/lib/transport.sh'
357
+ console_post_json 'http://example.test' '{}'
358
+ console_post_json 'http://example.test' '{}'
359
+ " 2>"$TRANSPORT_STDERR"
360
+
361
+ warning_count=$(grep -c "warning: transport.sh:" "$TRANSPORT_STDERR" 2>/dev/null || echo 0)
362
+ if [[ "$warning_count" -eq 1 ]] && grep -qF "http://example.test" "$TRANSPORT_STDERR" 2>/dev/null; then
363
+ _pass "transport.sh: disallowed endpoint prints exactly one drop warning across two calls in the same shell"
364
+ else
365
+ _fail "transport.sh: expected exactly 1 warning naming the endpoint, got $warning_count: $(cat "$TRANSPORT_STDERR" 2>/dev/null)"
366
+ fi
367
+
368
+ if [[ ! -f "$TRANSPORT_CURL_MARKER" ]]; then
369
+ _pass "transport.sh: disallowed endpoint never invokes curl"
370
+ else
371
+ _fail "transport.sh: disallowed endpoint should never invoke curl, but it did"
372
+ fi
373
+
374
+ rm -rf "$TRANSPORT_TEST_ROOT"
375
+
376
+ # --- Console relay project attribution (canonical precedence, path-free) ---
377
+ echo ""
378
+ echo "--- Console Relay Project Attribution ---"
379
+ PROJ_TEST_ROOT=$(mktemp -d /tmp/eval-telemetry-project.XXXXXX)
380
+ mkdir -p "$PROJ_TEST_ROOT/work/pkg" "$PROJ_TEST_ROOT/plain-dir-name"
381
+ printf '{"name":"@kontourai/inner-pkg"}\n' > "$PROJ_TEST_ROOT/work/pkg/package.json"
382
+ # git fixtures (no package.json ancestor) for the git-remote precedence tier and host-strip hardening
383
+ git init -q "$PROJ_TEST_ROOT/gitrepo" && git -C "$PROJ_TEST_ROOT/gitrepo" remote add origin 'https://github.com/test-org/test-repo.git'
384
+ git init -q "$PROJ_TEST_ROOT/gitslash" && git -C "$PROJ_TEST_ROOT/gitslash" remote add origin 'https://github.com/test-org/test-repo/'
385
+ git init -q "$PROJ_TEST_ROOT/gitsingle" && git -C "$PROJ_TEST_ROOT/gitsingle" remote add origin 'https://github.com/loneseg.git'
386
+ git init -q "$PROJ_TEST_ROOT/githostless" && git -C "$PROJ_TEST_ROOT/githostless" remote add origin 'https://gitserver/reponame.git'
387
+ git init -q "$PROJ_TEST_ROOT/gitselfhost" && git -C "$PROJ_TEST_ROOT/gitselfhost" remote add origin 'https://gitea.example.com/team/proj.git'
388
+ git init -q "$PROJ_TEST_ROOT/gitscphost" && git -C "$PROJ_TEST_ROOT/gitscphost" remote add origin 'git@gitserver:reponame.git'
389
+ git init -q "$PROJ_TEST_ROOT/gitfile" && git -C "$PROJ_TEST_ROOT/gitfile" remote add origin 'file:///srv/git/secretuser/reponame.git'
390
+ git init -q "$PROJ_TEST_ROOT/gitlocal" && git -C "$PROJ_TEST_ROOT/gitlocal" remote add origin '/srv/git/secretuser/reponame'
391
+
392
+ # Unit-test the precedence engine console_project_label directly. It is byte-identical between the
393
+ # source copy and the context/ mirror (their emit wrappers differ: source delegates to
394
+ # console_post_json, the mirror inlines curl), so calling the function directly is the way to catch
395
+ # a future edit landing in one copy but not the other. Echoes the derived label.
396
+ # args: <full path to a transport.sh> <cwd> [bash prefix run before the call]. TELEMETRY_DIR is
397
+ # derived from the transport path, and only the two full copy paths below are written as literals
398
+ # (both resolve), so the source-tree path validator has nothing unresolvable to flag.
399
+ _label_of() {
400
+ local transport="$1" cwd="$2" pre="${3:-}"
401
+ local teldir; teldir=$(dirname "$(dirname "$transport")")
402
+ bash -c "
403
+ $pre
404
+ export TELEMETRY_DIR='$teldir'
405
+ source \"$transport\"
406
+ console_project_label '$cwd'
407
+ "
408
+ }
409
+ _proj_of() { printf '%s' "$1" | jq -r '.context.project // "MISSING"' 2>/dev/null; }
410
+
411
+ # Precedence matrix against BOTH the source copy and the context/ mirror.
412
+ for transport in \
413
+ "$ROOT_DIR/scripts/telemetry/lib/transport.sh" \
414
+ "$ROOT_DIR/context/scripts/telemetry/lib/transport.sh"; do
415
+ [[ -f "$transport" ]] || continue
416
+ case "$transport" in *"/context/"*) tag="context-mirror" ;; *) tag="source" ;; esac
417
+
418
+ # step 2: nearest package.json name wins
419
+ lbl=$(_label_of "$transport" "$PROJ_TEST_ROOT/work/pkg")
420
+ [[ "$lbl" == "@kontourai/inner-pkg" ]] \
421
+ && _pass "console_project_label ($tag): nearest package.json name wins" \
422
+ || _fail "console_project_label ($tag): expected @kontourai/inner-pkg, got '$lbl'"
423
+
424
+ # step 1: FLOW_AGENTS_PROJECT override beats a present package.json
425
+ lbl=$(_label_of "$transport" "$PROJ_TEST_ROOT/work/pkg" "export FLOW_AGENTS_PROJECT=my-explicit-proj")
426
+ [[ "$lbl" == "my-explicit-proj" ]] \
427
+ && _pass "console_project_label ($tag): FLOW_AGENTS_PROJECT overrides all other tiers" \
428
+ || _fail "console_project_label ($tag): expected my-explicit-proj, got '$lbl'"
429
+
430
+ # step 3: git remote org/repo when no manifest is found on the walk-up
431
+ lbl=$(_label_of "$transport" "$PROJ_TEST_ROOT/gitrepo")
432
+ [[ "$lbl" == "test-org/test-repo" ]] \
433
+ && _pass "console_project_label ($tag): git remote resolves to org/repo" \
434
+ || _fail "console_project_label ($tag): expected test-org/test-repo, got '$lbl'"
435
+
436
+ # step 5: no manifest/git remote -> cwd basename fallback
437
+ lbl=$(_label_of "$transport" "$PROJ_TEST_ROOT/plain-dir-name")
438
+ [[ "$lbl" == "plain-dir-name" ]] \
439
+ && _pass "console_project_label ($tag): falls back to cwd basename when no manifest/git remote" \
440
+ || _fail "console_project_label ($tag): expected plain-dir-name, got '$lbl'"
441
+
442
+ # sed hardening: trailing-slash remote still reduces to a clean org/repo (not the raw URL)
443
+ lbl=$(_label_of "$transport" "$PROJ_TEST_ROOT/gitslash")
444
+ [[ "$lbl" == "test-org/test-repo" ]] \
445
+ && _pass "console_project_label ($tag): trailing-slash git remote reduces to org/repo" \
446
+ || _fail "console_project_label ($tag): expected test-org/test-repo (trailing slash), got '$lbl'"
447
+
448
+ # host-strip: single-segment remote (dotted host, no org tier) must NOT leak host; falls through
449
+ lbl=$(_label_of "$transport" "$PROJ_TEST_ROOT/gitsingle")
450
+ [[ "$lbl" == "gitsingle" ]] \
451
+ && _pass "console_project_label ($tag): single-segment git remote does not leak host" \
452
+ || _fail "console_project_label ($tag): expected gitsingle fallthrough, got '$lbl'"
453
+
454
+ # host-strip: DOT-LESS self-hosted host (URL), single path segment -> no host leak, falls through
455
+ lbl=$(_label_of "$transport" "$PROJ_TEST_ROOT/githostless")
456
+ [[ "$lbl" == "githostless" ]] \
457
+ && _pass "console_project_label ($tag): dot-less self-hosted host does not leak; falls through" \
458
+ || _fail "console_project_label ($tag): expected githostless fallthrough, got '$lbl'"
459
+
460
+ # host-strip: dotted self-hosted FQDN WITH an org tier reduces to org/repo (host stripped)
461
+ lbl=$(_label_of "$transport" "$PROJ_TEST_ROOT/gitselfhost")
462
+ [[ "$lbl" == "team/proj" ]] \
463
+ && _pass "console_project_label ($tag): self-hosted FQDN with org tier reduces to org/repo" \
464
+ || _fail "console_project_label ($tag): expected team/proj, got '$lbl'"
465
+
466
+ # host-strip: scp-form dot-less host, single path segment -> no user@host leak, falls through
467
+ lbl=$(_label_of "$transport" "$PROJ_TEST_ROOT/gitscphost")
468
+ [[ "$lbl" == "gitscphost" ]] \
469
+ && _pass "console_project_label ($tag): scp-form dot-less host does not leak; falls through" \
470
+ || _fail "console_project_label ($tag): expected gitscphost fallthrough, got '$lbl'"
471
+
472
+ # path-free: a file:// remote must not leak local path components; falls through to basename
473
+ lbl=$(_label_of "$transport" "$PROJ_TEST_ROOT/gitfile")
474
+ [[ "$lbl" == "gitfile" ]] \
475
+ && _pass "console_project_label ($tag): file:// remote does not leak local path; falls through" \
476
+ || _fail "console_project_label ($tag): expected gitfile fallthrough, got '$lbl'"
477
+
478
+ # path-free: a bare absolute-local-path remote must not leak path components; falls through
479
+ lbl=$(_label_of "$transport" "$PROJ_TEST_ROOT/gitlocal")
480
+ [[ "$lbl" == "gitlocal" ]] \
481
+ && _pass "console_project_label ($tag): absolute-local-path remote does not leak; falls through" \
482
+ || _fail "console_project_label ($tag): expected gitlocal fallthrough, got '$lbl'"
483
+ done
484
+
485
+ # Integration: exercise the real shipping emit wrapper (source copy) end-to-end — proves the label
486
+ # is stamped AND the full cwd is still redacted to null in the same relayed event.
487
+ _relay_source() {
488
+ local cwd="$1" preset="${2:-}"
489
+ local ev="{\"type\":\"tool\",\"context\":{\"cwd\":\"$cwd\"}}"
490
+ [[ -n "$preset" ]] && ev="{\"type\":\"tool\",\"context\":{\"cwd\":\"$cwd\",\"project\":\"$preset\"}}"
491
+ bash -c "
492
+ export TELEMETRY_DIR='$ROOT_DIR/scripts/telemetry'
493
+ source '$ROOT_DIR/scripts/telemetry/lib/transport.sh'
494
+ console_telemetry_endpoint_url() { echo 'https://console.example.test/records'; }
495
+ console_post_json() { printf '%s' \"\$2\"; }
496
+ CONSOLE_TELEMETRY_REDACT='context.cwd' TELEMETRY_SESSION_DIR='' \
497
+ console_telemetry_emit '$ev'
498
+ "
499
+ }
500
+ cap=$(_relay_source "$PROJ_TEST_ROOT/work/pkg")
501
+ if [[ "$(_proj_of "$cap")" == "@kontourai/inner-pkg" && "$(printf '%s' "$cap" | jq -r '.context.cwd // "null"')" == "null" ]]; then
502
+ _pass "console_telemetry_emit: stamps context.project while the full cwd stays redacted to null"
503
+ else
504
+ _fail "console_telemetry_emit: expected project=@kontourai/inner-pkg cwd=null, got '$cap'"
505
+ fi
506
+ # no-clobber: an adapter-supplied context.project is never overwritten
507
+ cap=$(_relay_source "$PROJ_TEST_ROOT/work/pkg" "adapter-supplied")
508
+ [[ "$(_proj_of "$cap")" == "adapter-supplied" ]] \
509
+ && _pass "console_telemetry_emit: pre-set context.project is left untouched" \
510
+ || _fail "console_telemetry_emit: expected project=adapter-supplied, got '$(_proj_of "$cap")'"
511
+
512
+ # set -e safety: a missing/stale cwd must relay the event unchanged, never abort the caller
513
+ sete_out=$(bash -c "
514
+ set -euo pipefail
515
+ export TELEMETRY_DIR='$ROOT_DIR/scripts/telemetry'
516
+ source '$ROOT_DIR/scripts/telemetry/lib/transport.sh'
517
+ console_telemetry_endpoint_url() { echo 'https://console.example.test/records'; }
518
+ console_post_json() { printf '%s' \"\$2\"; }
519
+ CONSOLE_TELEMETRY_REDACT='context.cwd' TELEMETRY_SESSION_DIR='' \
520
+ console_telemetry_emit '{\"type\":\"tool\",\"context\":{\"cwd\":\"/no/such/dir/xyz\"}}'
521
+ echo REACHED_END
522
+ ")
523
+ sete_rc=$?
524
+ if [[ $sete_rc -eq 0 && "$sete_out" == *REACHED_END* ]]; then
525
+ _pass "console_telemetry_emit: under set -e survives a missing cwd (relays, no abort)"
526
+ else
527
+ _fail "console_telemetry_emit: set -e aborted on missing cwd (rc=$sete_rc, out='$sete_out')"
528
+ fi
529
+ rm -rf "$PROJ_TEST_ROOT"
530
+
86
531
  # --- 2. Event type mapping ---
87
532
  echo ""
88
533
  echo "--- Event Type Mapping ---"
@@ -2,6 +2,9 @@
2
2
  set -euo pipefail
3
3
 
4
4
  ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
5
+ # shellcheck source=/dev/null
6
+ source "$ROOT_DIR/scripts/telemetry/console-presets.sh"
7
+ KONTOUR_HOSTED_CONSOLE_URL="$(flow_agents_kontour_hosted_console_url)"
5
8
  FLOW_AGENTS_CLI="${FLOW_AGENTS_CLI:-node}"
6
9
  FLOW_AGENTS_CLI_ARGS=()
7
10
  if [[ "$FLOW_AGENTS_CLI" == "node" ]]; then
@@ -186,6 +189,69 @@ else
186
189
  fi
187
190
  fi
188
191
 
192
+ # --- Hosted Console reachability by default (AC3, install-flow-foundations Thread C) ---
193
+ # console.kontourai.io (or FLOW_AGENTS_KONTOUR_CLOUD_CONSOLE_URL when
194
+ # overridden) is the ONE known hosted Console hostname that is reachability-
195
+ # checkable without --allow-network. Real network reachability is not
196
+ # guaranteed in a CI sandbox, so only endpointAllowed/reachability.checked
197
+ # are asserted, not .ok.
198
+ cat > "$DEST/scripts/telemetry/telemetry.conf" <<CONF
199
+ enabled=true
200
+ channels=full
201
+ console_telemetry_url=$KONTOUR_HOSTED_CONSOLE_URL
202
+ CONF
203
+
204
+ $FLOW_AGENTS_CLI "${FLOW_AGENTS_CLI_ARGS[@]}" telemetry-doctor --dest "$DEST" --json --headless --timeout-ms 250 >"$TMPDIR_EVAL/hosted.json" 2>&1 || true
205
+ if jq -e '
206
+ .console.endpointAllowed == true and
207
+ .console.reachability.checked == true
208
+ ' "$TMPDIR_EVAL/hosted.json" >/dev/null 2>&1; then
209
+ _pass "doctor checks known hosted Console reachability by default (no --allow-network)"
210
+ else
211
+ _fail "doctor hosted-Console-by-default JSON did not match expected shape: $(cat "$TMPDIR_EVAL/hosted.json" 2>/dev/null)"
212
+ fi
213
+
214
+ # Regression guard: a generic non-local https endpoint (not the known hosted
215
+ # hostname) must still require --allow-network exactly as before.
216
+ cat > "$DEST/scripts/telemetry/telemetry.conf" <<'CONF'
217
+ enabled=true
218
+ channels=full
219
+ console_telemetry_url=https://console.example.test
220
+ CONF
221
+
222
+ if $FLOW_AGENTS_CLI "${FLOW_AGENTS_CLI_ARGS[@]}" telemetry-doctor --dest "$DEST" --json --headless --timeout-ms 250 >"$TMPDIR_EVAL/generic-nonlocal.json"; then
223
+ _fail "doctor returned success for generic non-local Console without --allow-network"
224
+ else
225
+ if jq -e '
226
+ .console.endpointAllowed == false and
227
+ .console.reachability.checked == false
228
+ ' "$TMPDIR_EVAL/generic-nonlocal.json" >/dev/null; then
229
+ _pass "doctor still blocks a generic (non-hosted) non-local Console without --allow-network"
230
+ else
231
+ _fail "doctor generic non-local block JSON did not match expected shape"
232
+ fi
233
+ fi
234
+
235
+ # --- Untrusted telemetry-console.conf surfaces a doctor warning (AC2, Thread B/C wiring) ---
236
+ mkdir -p "$DEST/.kontourai"
237
+ UNTRUSTED_CONF="$DEST/.kontourai/telemetry-console.conf"
238
+ : > "$UNTRUSTED_CONF"
239
+ chmod 644 "$UNTRUSTED_CONF"
240
+ cp "$ROOT_DIR/scripts/telemetry/telemetry.conf" "$DEST/scripts/telemetry/telemetry.conf"
241
+
242
+ if $FLOW_AGENTS_CLI "${FLOW_AGENTS_CLI_ARGS[@]}" telemetry-doctor --dest "$DEST" --json --headless >"$TMPDIR_EVAL/untrusted-conf.json"; then
243
+ if jq -e --arg path "$UNTRUSTED_CONF" '
244
+ (.warnings | any(contains($path)))
245
+ ' "$TMPDIR_EVAL/untrusted-conf.json" >/dev/null; then
246
+ _pass "doctor warns about an untrusted-but-present local telemetry-console.conf"
247
+ else
248
+ _fail "doctor did not surface a warning for the untrusted local conf: $(cat "$TMPDIR_EVAL/untrusted-conf.json")"
249
+ fi
250
+ else
251
+ _fail "doctor failed for untrusted-conf-present case"
252
+ fi
253
+ rm -f "$UNTRUSTED_CONF"
254
+
189
255
  echo ""
190
256
  echo "Passed: $pass"
191
257
  echo "Failed: $fail"