@kontourai/flow-agents 3.9.0 → 3.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +8 -0
- package/CHANGELOG.md +17 -0
- package/README.md +1 -1
- package/build/src/cli/assignment-provider.js +4 -18
- package/build/src/cli/continuation-adapter.d.ts +10 -8
- package/build/src/cli/continuation-adapter.js +22 -4
- package/build/src/cli/validate-workflow-artifacts.js +21 -1
- package/build/src/cli/workflow-sidecar.js +15 -20
- package/build/src/cli/workflow.js +58 -11
- package/build/src/continuation-driver.d.ts +27 -3
- package/build/src/continuation-driver.js +97 -14
- package/build/src/index.d.ts +2 -2
- package/build/src/index.js +1 -1
- package/build/src/tools/build-universal-bundles.js +52 -25
- package/build/src/tools/validate-source-tree.js +5 -2
- package/context/scripts/hooks/lib/continuation-turn-authority.js +386 -0
- package/context/scripts/hooks/stop-goal-fit.js +244 -73
- package/context/scripts/hooks/workflow-steering.js +24 -15
- package/context/scripts/telemetry/lib/pricing.sh +18 -6
- package/context/scripts/telemetry/lib/transport.sh +139 -37
- package/context/scripts/telemetry/lib/usage.sh +282 -32
- package/context/scripts/telemetry/lib/usage_model_guard.sh +42 -0
- package/context/scripts/telemetry/telemetry.sh +76 -1
- package/docs/coordination-guide.md +21 -5
- package/docs/fixture-ownership.md +2 -1
- package/docs/public-workflow-cli.md +44 -0
- package/docs/spec/runtime-hook-surface.md +8 -4
- package/evals/acceptance/prove-capture-teeth-declared.sh +13 -0
- package/evals/acceptance/prove-capture-teeth.sh +13 -0
- package/evals/ci/run-baseline.sh +2 -0
- package/evals/fixtures/codex-legacy-agents/5273878130bdafc8a024a650bb5b66c9b003f1f859b5dc6e5b588cbf4ab23228.md +25 -0
- package/evals/fixtures/telemetry/usage-transcript-adversarial.jsonl +2 -0
- package/evals/fixtures/telemetry/usage-transcript-malformed-sandwich.jsonl +3 -0
- package/evals/fixtures/telemetry/usage-transcript-negative-tokens.jsonl +1 -0
- package/evals/fixtures/telemetry/usage-transcript-oversized-prefix.jsonl +201 -0
- package/evals/fixtures/telemetry/usage-transcript-oversized-tokens.jsonl +1 -0
- package/evals/fixtures/telemetry/usage-transcript-prefixed-secret-lower.jsonl +1 -0
- package/evals/fixtures/telemetry/usage-transcript-prefixed-secret-upper.jsonl +1 -0
- package/evals/fixtures/telemetry/usage-transcript-secret-model-jwt.jsonl +1 -0
- package/evals/fixtures/telemetry/usage-transcript-secret-model.jsonl +1 -0
- package/evals/fixtures/telemetry/usage-transcript-torn-trailing.jsonl +2 -0
- package/evals/integration/test_actor_identity.sh +63 -0
- package/evals/integration/test_assignment_provider_local_file.sh +34 -0
- package/evals/integration/test_builder_entry_enforcement.sh +5 -1
- package/evals/integration/test_bundle_install.sh +106 -17
- package/evals/integration/test_bundle_lifecycle.sh +17 -3
- package/evals/integration/test_captured_fail_reconciliation.sh +11 -0
- package/evals/integration/test_ci_actor_identity.sh +2 -2
- package/evals/integration/test_command_log_concurrency.sh +11 -0
- package/evals/integration/test_command_log_fork_classification.sh +11 -0
- package/evals/integration/test_command_log_integrity.sh +11 -0
- package/evals/integration/test_current_json_per_actor.sh +416 -21
- package/evals/integration/test_enforcer_expects_driven.sh +11 -0
- package/evals/integration/test_ensure_session_ownership_guard.sh +39 -0
- package/evals/integration/test_evidence_capture_hook.sh +25 -6
- package/evals/integration/test_fixture_retirement_audit.sh +2 -2
- package/evals/integration/test_flowdef_union_floor_regression.sh +11 -0
- package/evals/integration/test_gate_bypass_chain.sh +11 -0
- package/evals/integration/test_gate_lockdown.sh +10 -0
- package/evals/integration/test_goal_fit_ghost_session.sh +11 -0
- package/evals/integration/test_goal_fit_hook.sh +257 -9
- package/evals/integration/test_goal_fit_rederive.sh +11 -0
- package/evals/integration/test_install_merge.sh +141 -3
- package/evals/integration/test_liveness_conflict_injection.sh +39 -3
- package/evals/integration/test_liveness_heartbeat.sh +46 -9
- package/evals/integration/test_phase_map_and_gate_claim.sh +10 -1
- package/evals/integration/test_public_workflow_cli.sh +6 -6
- package/evals/integration/test_session_resume_roundtrip.sh +30 -0
- package/evals/integration/test_stop_hook_release.sh +111 -11
- package/evals/integration/test_telemetry_tool_usage.sh +394 -0
- package/evals/integration/test_verify_hold.sh +64 -1
- package/evals/integration/test_workflow_sidecar_writer.sh +10 -1
- package/evals/integration/test_workflow_steering_hook.sh +20 -4
- package/evals/run.sh +2 -0
- package/evals/static/test_universal_bundles.sh +123 -0
- package/package.json +1 -1
- package/packaging/README.md +21 -0
- package/packaging/codex-legacy-agents-fingerprints.json +33 -0
- package/scripts/README.md +2 -1
- package/scripts/audit-codex-legacy-agents.js +43 -0
- package/scripts/classify-codex-legacy-agents.js +71 -0
- package/scripts/hooks/evidence-capture.js +18 -6
- package/scripts/hooks/lib/actor-identity.js +62 -29
- package/scripts/hooks/lib/continuation-turn-authority.js +386 -0
- package/scripts/hooks/lib/current-pointer.js +114 -12
- package/scripts/hooks/lib/liveness-heartbeat.js +15 -12
- package/scripts/hooks/stop-goal-fit.js +244 -73
- package/scripts/hooks/workflow-steering.js +24 -15
- package/scripts/install-codex-home.sh +8 -7
- package/scripts/telemetry/lib/pricing.sh +18 -6
- package/scripts/telemetry/lib/transport.sh +71 -0
- package/scripts/telemetry/lib/usage.sh +282 -32
- package/scripts/telemetry/lib/usage_model_guard.sh +42 -0
- package/scripts/telemetry/telemetry.sh +76 -1
- package/src/cli/assignment-provider.ts +5 -26
- package/src/cli/builder-flow-runtime.test.mjs +93 -6
- package/src/cli/continuation-adapter.ts +33 -7
- package/src/cli/continuation-driver.test.mjs +741 -2
- package/src/cli/public-api.test.mjs +1 -0
- package/src/cli/validate-workflow-artifacts.ts +19 -1
- package/src/cli/workflow-sidecar.ts +18 -23
- package/src/cli/workflow.ts +71 -11
- package/src/continuation-driver.ts +125 -19
- package/src/index.ts +4 -0
- package/src/tools/build-universal-bundles.ts +61 -25
- package/src/tools/validate-source-tree.ts +5 -2
|
@@ -36,6 +36,7 @@ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
|
36
36
|
HOOK="$ROOT/scripts/hooks/stop-goal-fit.js"
|
|
37
37
|
BUILT_SIDECAR="$ROOT/build/src/cli/workflow-sidecar.js"
|
|
38
38
|
BUILT_PROVIDER="$ROOT/build/src/cli/assignment-provider.js"
|
|
39
|
+
CURRENT_POINTER_HELPER="$ROOT/scripts/hooks/lib/current-pointer.js"
|
|
39
40
|
|
|
40
41
|
for m in "$HOOK" "$BUILT_SIDECAR" "$BUILT_PROVIDER"; do
|
|
41
42
|
if [[ ! -f "$m" ]]; then
|
|
@@ -129,6 +130,41 @@ write_session_state() {
|
|
|
129
130
|
JSON
|
|
130
131
|
}
|
|
131
132
|
|
|
133
|
+
# seed_current_pointer <repo> <slug> [actor_key] — #440: writes BOTH the legacy
|
|
134
|
+
# current.json AND (when actor_key is given) the per-actor current/<actor>.json pointer, with
|
|
135
|
+
# the SAME payload — mirroring workflow-sidecar.ts's real writeCurrent() dual-write exactly, via
|
|
136
|
+
# the shared current-pointer.js helper's own writePerActorCurrent (not a hand-rolled
|
|
137
|
+
# reimplementation of its sanitize/path rule), so #440's readOwnCurrentPointer finds this
|
|
138
|
+
# fixture's session exactly like a real ensure-session'd one would. Call AFTER seed_session (same
|
|
139
|
+
# slug). A resolved actor with no per-actor pointer is intentionally out of scope for the
|
|
140
|
+
# Stop-hook's blocking/release scoping per #440 — every scenario below that exercises "self, owns
|
|
141
|
+
# this session" behavior (release, handoff refresh, fail-open reachability) now needs this seed;
|
|
142
|
+
# scenarios that are actually about an unresolved/foreign actor do not call this.
|
|
143
|
+
seed_current_pointer() {
|
|
144
|
+
local repo="$1" slug="$2" actor_key="${3:-}"
|
|
145
|
+
CP_HELPER_ARG="$CURRENT_POINTER_HELPER" FLOW_AGENTS_DIR_ARG="$repo/.kontourai/flow-agents" \
|
|
146
|
+
SLUG_ARG="$slug" ACTOR_KEY_ARG="$actor_key" node - <<'NODE'
|
|
147
|
+
const fs = require('fs');
|
|
148
|
+
const path = require('path');
|
|
149
|
+
const { writePerActorCurrent } = require(process.env.CP_HELPER_ARG);
|
|
150
|
+
const flowAgentsDir = process.env.FLOW_AGENTS_DIR_ARG;
|
|
151
|
+
const slug = process.env.SLUG_ARG;
|
|
152
|
+
const actorKey = process.env.ACTOR_KEY_ARG;
|
|
153
|
+
const payload = {
|
|
154
|
+
schema_version: '1.0',
|
|
155
|
+
active_slug: slug,
|
|
156
|
+
artifact_dir: slug,
|
|
157
|
+
updated_at: '2026-06-25T00:00:00Z',
|
|
158
|
+
owner: 'eval',
|
|
159
|
+
source: 'test-fixture',
|
|
160
|
+
active_agents: [],
|
|
161
|
+
};
|
|
162
|
+
fs.mkdirSync(flowAgentsDir, { recursive: true });
|
|
163
|
+
fs.writeFileSync(path.join(flowAgentsDir, 'current.json'), JSON.stringify(payload, null, 2) + '\n');
|
|
164
|
+
if (actorKey) writePerActorCurrent(flowAgentsDir, actorKey, payload);
|
|
165
|
+
NODE
|
|
166
|
+
}
|
|
167
|
+
|
|
132
168
|
# assignment_provider_settings_local_file <repo> — writes a project settings file
|
|
133
169
|
# naming the local-file provider (schema shape per schemas/assignment-provider-settings.schema.json).
|
|
134
170
|
assignment_provider_settings_local_file() {
|
|
@@ -271,6 +307,10 @@ A_DIR="$(seed_session "$A_REPO" "task-a" "in_progress")"
|
|
|
271
307
|
# (never a hardcoded placeholder, which the real process could never match).
|
|
272
308
|
A_SESSION_ID="eval-a-session-$$"
|
|
273
309
|
A_ACTOR_TRIPLE="claude-code:${A_SESSION_ID}:${REAL_HOSTNAME}"
|
|
310
|
+
# #440: A owns task-a — seed its own per-actor current pointer so the Stop hook's
|
|
311
|
+
# ownership-scoped analyze() finds it (FIXTURE-GAP: this scenario asserts real "self, owns this
|
|
312
|
+
# session" release behavior, not stale ownership semantics).
|
|
313
|
+
seed_current_pointer "$A_REPO" "task-a" "$A_ACTOR_TRIPLE"
|
|
274
314
|
seed_liveness_claim "$A_REPO" "task-a" "$A_ACTOR_TRIPLE" "2026-06-25T09:00:00.000Z"
|
|
275
315
|
seed_assignment_claim "$A_REPO" "task-a" "claude-code" "$A_SESSION_ID" "$REAL_HOSTNAME"
|
|
276
316
|
|
|
@@ -307,8 +347,16 @@ B_REPO="$(new_repo repo-b)"
|
|
|
307
347
|
assignment_provider_settings_github "$B_REPO"
|
|
308
348
|
B_DIR="$(seed_session "$B_REPO" "task-b" "in_progress")"
|
|
309
349
|
seed_liveness_claim "$B_REPO" "task-b" "claude-code:sess-b:host-b" "2026-06-25T09:00:00.000Z"
|
|
310
|
-
|
|
311
|
-
|
|
350
|
+
# #440: B owns task-b — a stable "self, derived form" actor (matching A's convention) plus its
|
|
351
|
+
# own per-actor current pointer, so the Stop hook's ownership-scoped analyze() finds it
|
|
352
|
+
# (FIXTURE-GAP: this scenario asserts real handoff/provider-release behavior for B's own session,
|
|
353
|
+
# not stale ownership semantics). Previously this scenario ran under whatever ambient/ancestry
|
|
354
|
+
# actor happened to resolve — never asserted on, but now load-bearing for scoping.
|
|
355
|
+
B_SESSION_ID="eval-b-session-$$"
|
|
356
|
+
B_ACTOR_TRIPLE="claude-code:${B_SESSION_ID}:${REAL_HOSTNAME}"
|
|
357
|
+
seed_current_pointer "$B_REPO" "task-b" "$B_ACTOR_TRIPLE"
|
|
358
|
+
|
|
359
|
+
B_OUT="$(call_stop_hook "$B_REPO" "{\"CLAUDE_CODE_SESSION_ID\":\"$B_SESSION_ID\"}" 2>"$TMPDIR_EVAL/b.err")"
|
|
312
360
|
B_STATUS=$?
|
|
313
361
|
|
|
314
362
|
if ! grep -Eq '(^|[^a-zA-Z])gh (issue|api|pr) ' "$TMPDIR_EVAL/b.err" && ! grep -Eq '(^|[^a-zA-Z])gh (issue|api|pr) ' <<<"$B_OUT"; then
|
|
@@ -349,7 +397,13 @@ cat > "$C_DIR/handoff.json" <<'JSON'
|
|
|
349
397
|
}
|
|
350
398
|
JSON
|
|
351
399
|
|
|
352
|
-
|
|
400
|
+
# #440: C owns task-c — stable "self, derived form" actor + own per-actor current pointer
|
|
401
|
+
# (FIXTURE-GAP: this scenario asserts real handoff-refresh behavior for C's own session).
|
|
402
|
+
C_SESSION_ID="eval-c-session-$$"
|
|
403
|
+
C_ACTOR_TRIPLE="claude-code:${C_SESSION_ID}:${REAL_HOSTNAME}"
|
|
404
|
+
seed_current_pointer "$C_REPO" "task-c" "$C_ACTOR_TRIPLE"
|
|
405
|
+
|
|
406
|
+
call_stop_hook "$C_REPO" "{\"CLAUDE_CODE_SESSION_ID\":\"$C_SESSION_ID\"}" > /dev/null 2>"$TMPDIR_EVAL/c.err"
|
|
353
407
|
|
|
354
408
|
C_HANDOFF="$C_DIR/handoff.json"
|
|
355
409
|
C_SUMMARY="$(json_query "$C_HANDOFF" "summary")"
|
|
@@ -365,11 +419,18 @@ echo "--- D. Terminal (delivered) status: no release, no mutation ---"
|
|
|
365
419
|
D_REPO="$(new_repo repo-d)"
|
|
366
420
|
assignment_provider_settings_local_file "$D_REPO"
|
|
367
421
|
D_DIR="$(seed_session "$D_REPO" "task-d" "delivered")"
|
|
368
|
-
|
|
369
|
-
|
|
422
|
+
# #440 FIX 2 (de-vacuate, independent review): D owns task-d — a stable "self, derived form"
|
|
423
|
+
# actor + own per-actor current pointer, so releaseOnNonTerminalStop is REACHED and its OWN
|
|
424
|
+
# terminal-status (LIVENESS_TERMINAL) early-return is what's actually exercised here, not the
|
|
425
|
+
# outer "no artifactDir at all" early-return (which would pass vacuously for the same reason).
|
|
426
|
+
D_SESSION_ID="eval-d-session-$$"
|
|
427
|
+
D_ACTOR_TRIPLE="claude-code:${D_SESSION_ID}:${REAL_HOSTNAME}"
|
|
428
|
+
seed_current_pointer "$D_REPO" "task-d" "$D_ACTOR_TRIPLE"
|
|
429
|
+
seed_liveness_claim "$D_REPO" "task-d" "$D_ACTOR_TRIPLE" "2026-06-25T09:00:00.000Z"
|
|
430
|
+
seed_assignment_claim "$D_REPO" "task-d" "claude-code" "$D_SESSION_ID" "$REAL_HOSTNAME"
|
|
370
431
|
|
|
371
432
|
D_LINES_BEFORE="$(liveness_line_count "$D_REPO")"
|
|
372
|
-
call_stop_hook "$D_REPO"
|
|
433
|
+
call_stop_hook "$D_REPO" "{\"CLAUDE_CODE_SESSION_ID\":\"$D_SESSION_ID\"}" > /dev/null 2>"$TMPDIR_EVAL/d.err"
|
|
373
434
|
D_LINES_AFTER="$(liveness_line_count "$D_REPO")"
|
|
374
435
|
|
|
375
436
|
if [[ "$D_LINES_AFTER" -eq "$D_LINES_BEFORE" ]]; then
|
|
@@ -391,9 +452,18 @@ echo "--- E. Foreign-actor holder is never released (derived form) ---"
|
|
|
391
452
|
E_REPO="$(new_repo repo-e)"
|
|
392
453
|
assignment_provider_settings_local_file "$E_REPO"
|
|
393
454
|
E_DIR="$(seed_session "$E_REPO" "task-e" "in_progress")"
|
|
455
|
+
# #440 FIX 2 (de-vacuate, independent review): the ACTING actor (the one running the Stop hook
|
|
456
|
+
# below) owns task-e — a stable "self, derived form" actor + own per-actor pointer, so
|
|
457
|
+
# releaseOnNonTerminalStop is REACHED and its actual holder-comparison logic is what rejects the
|
|
458
|
+
# release, not the outer "no artifactDir at all" early-return. The ASSIGNMENT claim itself stays
|
|
459
|
+
# held by a genuinely DIFFERENT actor ("actor-other-session"/"actor-other-host") — the real
|
|
460
|
+
# foreign-holder differential this scenario is about.
|
|
461
|
+
E_SESSION_ID="eval-e-session-$$"
|
|
462
|
+
E_ACTOR_TRIPLE="claude-code:${E_SESSION_ID}:${REAL_HOSTNAME}"
|
|
463
|
+
seed_current_pointer "$E_REPO" "task-e" "$E_ACTOR_TRIPLE"
|
|
394
464
|
seed_assignment_claim "$E_REPO" "task-e" "claude-code" "actor-other-session" "actor-other-host"
|
|
395
465
|
|
|
396
|
-
E_OUT="$(call_stop_hook "$E_REPO"
|
|
466
|
+
E_OUT="$(call_stop_hook "$E_REPO" "{\"CLAUDE_CODE_SESSION_ID\":\"$E_SESSION_ID\"}" 2>"$TMPDIR_EVAL/e.err")"
|
|
397
467
|
E_STATUS=$?
|
|
398
468
|
|
|
399
469
|
E_RECORD="$(assignment_record_file "$E_REPO" "task-e")"
|
|
@@ -421,8 +491,16 @@ F1_REPO="$(new_repo repo-f1)"
|
|
|
421
491
|
assignment_provider_settings_local_file "$F1_REPO"
|
|
422
492
|
seed_session "$F1_REPO" "task-f1" "in_progress" > /dev/null
|
|
423
493
|
seed_liveness_claim "$F1_REPO" "task-f1" "claude-code:sess-f1:host-f1" "2026-06-25T09:00:00.000Z"
|
|
424
|
-
|
|
425
|
-
|
|
494
|
+
# #440: F1 owns task-f1 — stable "self, derived form" actor + own per-actor current pointer, so
|
|
495
|
+
# analyze() actually resolves an artifactDir and releaseOnNonTerminalStop is REACHED far enough to
|
|
496
|
+
# emit its "missing build/" diagnostic (FIXTURE-GAP: this scenario asserts real fail-open
|
|
497
|
+
# reachability for F1's own session, not stale ownership semantics — without this seed the Stop
|
|
498
|
+
# hook never gets far enough to exercise the missing-build/ code path at all).
|
|
499
|
+
F1_SESSION_ID="eval-f1-session-$$"
|
|
500
|
+
F1_ACTOR_TRIPLE="claude-code:${F1_SESSION_ID}:${REAL_HOSTNAME}"
|
|
501
|
+
seed_current_pointer "$F1_REPO" "task-f1" "$F1_ACTOR_TRIPLE"
|
|
502
|
+
|
|
503
|
+
F1_OUT="$(STOP_HOOK_OVERRIDE="$F1_PKG_ROOT/scripts/hooks/stop-goal-fit.js" call_stop_hook "$F1_REPO" "{\"CLAUDE_CODE_SESSION_ID\":\"$F1_SESSION_ID\"}" 2>"$TMPDIR_EVAL/f1.err")"
|
|
426
504
|
F1_STATUS=$?
|
|
427
505
|
if [[ "$F1_STATUS" -eq 0 || "$F1_STATUS" -eq 2 ]] && ! grep -q 'Node\.js v' "$TMPDIR_EVAL/f1.err" && ! grep -q ' at ' "$TMPDIR_EVAL/f1.err"; then
|
|
428
506
|
pass "F(i): missing build/ fails open — hook does not crash, exit code unaffected by release logic (AC7)"
|
|
@@ -437,11 +515,18 @@ grep -q 'build/src/cli/workflow-sidecar.js not available' "$TMPDIR_EVAL/f1.err"
|
|
|
437
515
|
F2_REPO="$(new_repo repo-f2)"
|
|
438
516
|
assignment_provider_settings_local_file "$F2_REPO"
|
|
439
517
|
seed_session "$F2_REPO" "task-f2" "in_progress" > /dev/null
|
|
440
|
-
|
|
518
|
+
# #440 FIX 2 (de-vacuate, independent review): F2 owns task-f2 — stable "self, derived form"
|
|
519
|
+
# actor + own per-actor pointer, so releaseOnNonTerminalStop is REACHED far enough to attempt
|
|
520
|
+
# reading the corrupt assignment record below (not short-circuited by the outer "no artifactDir
|
|
521
|
+
# at all" early-return, which would fail open for an unrelated reason).
|
|
522
|
+
F2_SESSION_ID="eval-f2-session-$$"
|
|
523
|
+
F2_ACTOR_TRIPLE="claude-code:${F2_SESSION_ID}:${REAL_HOSTNAME}"
|
|
524
|
+
seed_current_pointer "$F2_REPO" "task-f2" "$F2_ACTOR_TRIPLE"
|
|
525
|
+
seed_liveness_claim "$F2_REPO" "task-f2" "$F2_ACTOR_TRIPLE" "2026-06-25T09:00:00.000Z"
|
|
441
526
|
mkdir -p "$F2_REPO/.kontourai/flow-agents/assignment"
|
|
442
527
|
printf '{not valid json at all' > "$(assignment_record_file "$F2_REPO" "task-f2")"
|
|
443
528
|
|
|
444
|
-
F2_OUT="$(call_stop_hook "$F2_REPO"
|
|
529
|
+
F2_OUT="$(call_stop_hook "$F2_REPO" "{\"CLAUDE_CODE_SESSION_ID\":\"$F2_SESSION_ID\"}" 2>"$TMPDIR_EVAL/f2.err")"
|
|
445
530
|
F2_STATUS=$?
|
|
446
531
|
if [[ "$F2_STATUS" -eq 0 || "$F2_STATUS" -eq 2 ]] && ! grep -q 'Node\.js v' "$TMPDIR_EVAL/f2.err" && ! grep -q ' at ' "$TMPDIR_EVAL/f2.err"; then
|
|
447
532
|
pass "F(ii): corrupt assignment record fails open — no uncaught throw, exit unaffected (AC7)"
|
|
@@ -478,6 +563,10 @@ assignment_provider_settings_local_file "$G_REPO"
|
|
|
478
563
|
seed_session "$G_REPO" "task-g" "in_progress" > /dev/null
|
|
479
564
|
G_SESSION_ID="eval-g-session-$$"
|
|
480
565
|
G_ENV_JSON="{\"CLAUDE_CODE_SESSION_ID\":\"$G_SESSION_ID\"}"
|
|
566
|
+
# #440: G owns task-g — own per-actor current pointer, keyed to the SAME stable actor triple G
|
|
567
|
+
# already uses for its liveness/assignment claims (FIXTURE-GAP: this scenario asserts real
|
|
568
|
+
# idempotent-release behavior for G's own session).
|
|
569
|
+
seed_current_pointer "$G_REPO" "task-g" "claude-code:${G_SESSION_ID}:${REAL_HOSTNAME}"
|
|
481
570
|
seed_liveness_claim "$G_REPO" "task-g" "claude-code:${G_SESSION_ID}:${REAL_HOSTNAME}" "2026-06-25T09:00:00.000Z"
|
|
482
571
|
seed_assignment_claim "$G_REPO" "task-g" "claude-code" "$G_SESSION_ID" "$REAL_HOSTNAME"
|
|
483
572
|
|
|
@@ -510,6 +599,11 @@ echo "--- H. Override-actor canonical-key release (Task A proof) ---"
|
|
|
510
599
|
H1_REPO="$(new_repo repo-h1)"
|
|
511
600
|
assignment_provider_settings_local_file "$H1_REPO"
|
|
512
601
|
seed_session "$H1_REPO" "task-h1" "in_progress" > /dev/null
|
|
602
|
+
# #440: H1 owns task-h1 under the SAME override actor ("canonical-x") the Stop hook will run as
|
|
603
|
+
# below — own per-actor current pointer keyed to that exact override value (FIXTURE-GAP: this
|
|
604
|
+
# scenario asserts real actor_key-keyed release for the override-actor's own session, the exact
|
|
605
|
+
# Task A regression lock; not stale ownership semantics).
|
|
606
|
+
seed_current_pointer "$H1_REPO" "task-h1" "canonical-x"
|
|
513
607
|
seed_liveness_claim "$H1_REPO" "task-h1" "canonical-x" "2026-06-25T09:00:00.000Z"
|
|
514
608
|
# runtime/host mirror what the Stop hook itself would derive (detectRuntime + os.hostname())
|
|
515
609
|
# under a plain env with no runtime markers set — "unknown" runtime, real hostname — but the
|
|
@@ -532,6 +626,12 @@ fi
|
|
|
532
626
|
H2_REPO="$(new_repo repo-h2)"
|
|
533
627
|
assignment_provider_settings_local_file "$H2_REPO"
|
|
534
628
|
seed_session "$H2_REPO" "task-h2" "in_progress" > /dev/null
|
|
629
|
+
# #440 FIX 2 (de-vacuate, independent review): the ACTING override actor ("different-y") owns
|
|
630
|
+
# task-h2 — its own per-actor pointer, keyed to that exact override value, so
|
|
631
|
+
# releaseOnNonTerminalStop is REACHED and its actual actor_key comparison against the assignment
|
|
632
|
+
# record's "canonical-x" holder is what rejects the release, not the outer "no artifactDir at
|
|
633
|
+
# all" early-return.
|
|
634
|
+
seed_current_pointer "$H2_REPO" "task-h2" "different-y"
|
|
535
635
|
seed_assignment_claim "$H2_REPO" "task-h2" "unknown" "some-other-session-id" "some-other-host" "canonical-x"
|
|
536
636
|
|
|
537
637
|
H2_OUT="$(call_stop_hook "$H2_REPO" '{"FLOW_AGENTS_ACTOR":"different-y"}' 2>"$TMPDIR_EVAL/h2.err")"
|
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# test_telemetry_tool_usage.sh — Layer 2: per-tool-call usage enrichment
|
|
3
|
+
# (kontourai/flow-agents#568 slice 1)
|
|
4
|
+
#
|
|
5
|
+
# Proves that tool.invoke (preToolUse) and tool.result (postToolUse) telemetry
|
|
6
|
+
# events carry a .usage object sourced from the transcript's LAST assistant
|
|
7
|
+
# turn — not the session-cumulative aggregate — with the same fallback tiers
|
|
8
|
+
# usage_parse_transcript uses at `stop`: transcript join -> full usage;
|
|
9
|
+
# model-only when hook.model is present but the transcript join fails; fully
|
|
10
|
+
# null when neither is available. Also proves the bounded tail-read still
|
|
11
|
+
# finds the last usage entry behind an oversized filler prefix, and that
|
|
12
|
+
# permissionRequest is explicitly excluded from .usage enrichment.
|
|
13
|
+
#
|
|
14
|
+
# Uses the same TELEMETRY_DIR resolution convention as test_telemetry.sh /
|
|
15
|
+
# test_telemetry_usage_pipeline.sh (prefers context/scripts/telemetry when
|
|
16
|
+
# present, so this exercises the same copy CI actually runs), the same
|
|
17
|
+
# hermetic env variables, and the same FLOW_AGENTS_TELEMETRY_FOREGROUND=true
|
|
18
|
+
# convention so assertions don't race a backgrounded subshell.
|
|
19
|
+
set -uo pipefail
|
|
20
|
+
|
|
21
|
+
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
22
|
+
if [[ -d "$ROOT_DIR/context/scripts/telemetry" ]]; then
|
|
23
|
+
TELEMETRY_DIR="$ROOT_DIR/context/scripts/telemetry"
|
|
24
|
+
else
|
|
25
|
+
TELEMETRY_DIR="$HOME/.flow-agents/context/scripts/telemetry"
|
|
26
|
+
fi
|
|
27
|
+
TELEMETRY_SH="${TELEMETRY_DIR}/telemetry.sh"
|
|
28
|
+
PRICING_FILE="$ROOT_DIR/scripts/telemetry/pricing.json"
|
|
29
|
+
FIXTURE_TRANSCRIPT="$ROOT_DIR/evals/fixtures/telemetry/usage-transcript-sample.jsonl"
|
|
30
|
+
OVERSIZED_TRANSCRIPT="$ROOT_DIR/evals/fixtures/telemetry/usage-transcript-oversized-prefix.jsonl"
|
|
31
|
+
ADVERSARIAL_TRANSCRIPT="$ROOT_DIR/evals/fixtures/telemetry/usage-transcript-adversarial.jsonl"
|
|
32
|
+
MALFORMED_SANDWICH_TRANSCRIPT="$ROOT_DIR/evals/fixtures/telemetry/usage-transcript-malformed-sandwich.jsonl"
|
|
33
|
+
TORN_TRAILING_TRANSCRIPT="$ROOT_DIR/evals/fixtures/telemetry/usage-transcript-torn-trailing.jsonl"
|
|
34
|
+
SECRET_MODEL_TRANSCRIPT="$ROOT_DIR/evals/fixtures/telemetry/usage-transcript-secret-model.jsonl"
|
|
35
|
+
SECRET_MODEL_JWT_TRANSCRIPT="$ROOT_DIR/evals/fixtures/telemetry/usage-transcript-secret-model-jwt.jsonl"
|
|
36
|
+
PREFIXED_SECRET_UPPER_TRANSCRIPT="$ROOT_DIR/evals/fixtures/telemetry/usage-transcript-prefixed-secret-upper.jsonl"
|
|
37
|
+
PREFIXED_SECRET_LOWER_TRANSCRIPT="$ROOT_DIR/evals/fixtures/telemetry/usage-transcript-prefixed-secret-lower.jsonl"
|
|
38
|
+
NEGATIVE_TOKENS_TRANSCRIPT="$ROOT_DIR/evals/fixtures/telemetry/usage-transcript-negative-tokens.jsonl"
|
|
39
|
+
OVERSIZED_TOKENS_TRANSCRIPT="$ROOT_DIR/evals/fixtures/telemetry/usage-transcript-oversized-tokens.jsonl"
|
|
40
|
+
|
|
41
|
+
TMPDIR_EVAL=$(mktemp -d /tmp/eval-telemetry-tool-usage.XXXXXX)
|
|
42
|
+
TMPLOG="${TMPDIR_EVAL}/test-output.jsonl"
|
|
43
|
+
|
|
44
|
+
pass=0; fail=0
|
|
45
|
+
_pass() { echo " ✓ $1"; pass=$((pass + 1)); }
|
|
46
|
+
_fail() { echo " ✗ $1"; fail=$((fail + 1)); }
|
|
47
|
+
|
|
48
|
+
echo "=== Layer 2: Telemetry Tool-Event Usage Enrichment (#568 slice 1) ==="
|
|
49
|
+
echo ""
|
|
50
|
+
|
|
51
|
+
if [[ ! -f "$TELEMETRY_SH" ]]; then
|
|
52
|
+
_fail "telemetry.sh not found at $TELEMETRY_SH"
|
|
53
|
+
echo "Cannot continue without telemetry script"
|
|
54
|
+
rm -rf "$TMPDIR_EVAL"
|
|
55
|
+
exit 1
|
|
56
|
+
fi
|
|
57
|
+
if [[ ! -f "$FIXTURE_TRANSCRIPT" ]]; then
|
|
58
|
+
_fail "fixture transcript not found at $FIXTURE_TRANSCRIPT"
|
|
59
|
+
rm -rf "$TMPDIR_EVAL"
|
|
60
|
+
exit 1
|
|
61
|
+
fi
|
|
62
|
+
if [[ ! -f "$OVERSIZED_TRANSCRIPT" ]]; then
|
|
63
|
+
_fail "oversized-prefix fixture not found at $OVERSIZED_TRANSCRIPT"
|
|
64
|
+
rm -rf "$TMPDIR_EVAL"
|
|
65
|
+
exit 1
|
|
66
|
+
fi
|
|
67
|
+
if [[ ! -f "$ADVERSARIAL_TRANSCRIPT" ]]; then
|
|
68
|
+
_fail "adversarial fixture not found at $ADVERSARIAL_TRANSCRIPT"
|
|
69
|
+
rm -rf "$TMPDIR_EVAL"
|
|
70
|
+
exit 1
|
|
71
|
+
fi
|
|
72
|
+
if [[ ! -f "$MALFORMED_SANDWICH_TRANSCRIPT" ]]; then
|
|
73
|
+
_fail "malformed-sandwich fixture not found at $MALFORMED_SANDWICH_TRANSCRIPT"
|
|
74
|
+
rm -rf "$TMPDIR_EVAL"
|
|
75
|
+
exit 1
|
|
76
|
+
fi
|
|
77
|
+
if [[ ! -f "$TORN_TRAILING_TRANSCRIPT" ]]; then
|
|
78
|
+
_fail "torn-trailing fixture not found at $TORN_TRAILING_TRANSCRIPT"
|
|
79
|
+
rm -rf "$TMPDIR_EVAL"
|
|
80
|
+
exit 1
|
|
81
|
+
fi
|
|
82
|
+
if [[ ! -f "$SECRET_MODEL_TRANSCRIPT" ]]; then
|
|
83
|
+
_fail "secret-model fixture not found at $SECRET_MODEL_TRANSCRIPT"
|
|
84
|
+
rm -rf "$TMPDIR_EVAL"
|
|
85
|
+
exit 1
|
|
86
|
+
fi
|
|
87
|
+
if [[ ! -f "$SECRET_MODEL_JWT_TRANSCRIPT" ]]; then
|
|
88
|
+
_fail "secret-model-jwt fixture not found at $SECRET_MODEL_JWT_TRANSCRIPT"
|
|
89
|
+
rm -rf "$TMPDIR_EVAL"
|
|
90
|
+
exit 1
|
|
91
|
+
fi
|
|
92
|
+
if [[ ! -f "$NEGATIVE_TOKENS_TRANSCRIPT" ]]; then
|
|
93
|
+
_fail "negative-tokens fixture not found at $NEGATIVE_TOKENS_TRANSCRIPT"
|
|
94
|
+
rm -rf "$TMPDIR_EVAL"
|
|
95
|
+
exit 1
|
|
96
|
+
fi
|
|
97
|
+
if [[ ! -f "$OVERSIZED_TOKENS_TRANSCRIPT" ]]; then
|
|
98
|
+
_fail "oversized-tokens fixture not found at $OVERSIZED_TOKENS_TRANSCRIPT"
|
|
99
|
+
rm -rf "$TMPDIR_EVAL"
|
|
100
|
+
exit 1
|
|
101
|
+
fi
|
|
102
|
+
|
|
103
|
+
# Run a single hook event (preToolUse/postToolUse/permissionRequest) against
|
|
104
|
+
# telemetry.sh in foreground mode and return the resulting event (jq-compact,
|
|
105
|
+
# one line) from the full-channel log.
|
|
106
|
+
_run_tool_event() {
|
|
107
|
+
local hook_type="$1" input="$2"; shift 2
|
|
108
|
+
local extra_env=("$@")
|
|
109
|
+
local common_env=(
|
|
110
|
+
HOME="${TMPDIR_EVAL}/home"
|
|
111
|
+
TELEMETRY_ENABLED=true
|
|
112
|
+
TELEMETRY_CHANNELS=full
|
|
113
|
+
TELEMETRY_CHANNEL_FULL_LOG_FILE="$TMPLOG"
|
|
114
|
+
FLOW_AGENTS_TELEMETRY_FOREGROUND=true
|
|
115
|
+
TELEMETRY_CONFIG_FILE="$TMPDIR_EVAL/telemetry.conf"
|
|
116
|
+
TELEMETRY_DATA_DIR="$TMPDIR_EVAL"
|
|
117
|
+
TELEMETRY_SESSION_DIR="$TMPDIR_EVAL/sessions"
|
|
118
|
+
TELEMETRY_USAGE_TRACKING=true
|
|
119
|
+
)
|
|
120
|
+
mkdir -p "${TMPDIR_EVAL}/home" "${TMPDIR_EVAL}/sessions"
|
|
121
|
+
|
|
122
|
+
local before_lines
|
|
123
|
+
touch "$TMPLOG"
|
|
124
|
+
before_lines=$(wc -l < "$TMPLOG" | tr -d ' ')
|
|
125
|
+
|
|
126
|
+
echo "$input" | env "${common_env[@]}" "${extra_env[@]}" bash "$TELEMETRY_SH" "$hook_type" dev 2>/dev/null
|
|
127
|
+
|
|
128
|
+
tail -n +"$((before_lines + 1))" "$TMPLOG" 2>/dev/null | tail -1
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
# --- 1. Per-turn, not aggregate: last-line model/tokens differ from the
|
|
132
|
+
# session-aggregate dominant model (claude-opus-4-8) ------------------
|
|
133
|
+
echo "--- preToolUse/postToolUse with fixture transcript: per-turn, not aggregate (AC1) ---"
|
|
134
|
+
input1=$(jq -nc --arg tp "$FIXTURE_TRANSCRIPT" '{session_id:"tool-usage-1",transcript_path:$tp,hook_event_name:"PreToolUse",tool_name:"Bash",tool_input:{command:"echo hi"}}')
|
|
135
|
+
out1=$(_run_tool_event "preToolUse" "$input1" TELEMETRY_PRICING_FILE="$PRICING_FILE")
|
|
136
|
+
|
|
137
|
+
if [[ -n "$out1" ]]; then
|
|
138
|
+
event_type1=$(echo "$out1" | jq -r '.event_type // empty')
|
|
139
|
+
model1=$(echo "$out1" | jq -r '.usage.model // empty')
|
|
140
|
+
it1=$(echo "$out1" | jq -r '.usage.input_tokens // empty')
|
|
141
|
+
ot1=$(echo "$out1" | jq -r '.usage.output_tokens // empty')
|
|
142
|
+
cost1=$(echo "$out1" | jq -r '.usage.estimated_cost_usd // empty')
|
|
143
|
+
pv1=$(echo "$out1" | jq -r '.usage.pricing_version // empty')
|
|
144
|
+
|
|
145
|
+
[[ "$event_type1" == "tool.invoke" ]] && _pass "preToolUse maps to tool.invoke" || _fail "expected tool.invoke, got $event_type1"
|
|
146
|
+
[[ "$model1" == "claude-fable-5" ]] && _pass "usage.model is the LAST turn's model (claude-fable-5), not the aggregate-dominant model" || _fail "expected usage.model=claude-fable-5 (last turn), got $model1"
|
|
147
|
+
[[ "$it1" == "50" ]] && _pass "usage.input_tokens is the last turn's value (50)" || _fail "expected usage.input_tokens=50, got $it1"
|
|
148
|
+
[[ "$ot1" == "200" ]] && _pass "usage.output_tokens is the last turn's value (200)" || _fail "expected usage.output_tokens=200, got $ot1"
|
|
149
|
+
[[ -n "$cost1" && "$cost1" != "null" ]] && _pass "usage.estimated_cost_usd is non-null (got: $cost1)" || _fail "expected non-null estimated_cost_usd, got $cost1"
|
|
150
|
+
[[ "$pv1" != "null" && -n "$pv1" ]] && _pass "usage.pricing_version is populated" || _fail "expected non-null pricing_version, got $pv1"
|
|
151
|
+
else
|
|
152
|
+
_fail "no tool.invoke event emitted for fixture transcript"
|
|
153
|
+
fi
|
|
154
|
+
|
|
155
|
+
echo ""
|
|
156
|
+
echo "--- postToolUse with fixture transcript: same per-turn usage as preToolUse ---"
|
|
157
|
+
input1b=$(jq -nc --arg tp "$FIXTURE_TRANSCRIPT" '{session_id:"tool-usage-1b",transcript_path:$tp,hook_event_name:"PostToolUse",tool_name:"Bash",tool_response:{output:"hi"}}')
|
|
158
|
+
out1b=$(_run_tool_event "postToolUse" "$input1b" TELEMETRY_PRICING_FILE="$PRICING_FILE")
|
|
159
|
+
|
|
160
|
+
if [[ -n "$out1b" ]]; then
|
|
161
|
+
event_type1b=$(echo "$out1b" | jq -r '.event_type // empty')
|
|
162
|
+
model1b=$(echo "$out1b" | jq -r '.usage.model // empty')
|
|
163
|
+
[[ "$event_type1b" == "tool.result" ]] && _pass "postToolUse maps to tool.result" || _fail "expected tool.result, got $event_type1b"
|
|
164
|
+
[[ "$model1b" == "claude-fable-5" ]] && _pass "postToolUse usage.model is also the last turn's model (claude-fable-5)" || _fail "expected usage.model=claude-fable-5, got $model1b"
|
|
165
|
+
else
|
|
166
|
+
_fail "no tool.result event emitted for fixture transcript"
|
|
167
|
+
fi
|
|
168
|
+
|
|
169
|
+
# --- 2. Bounded tail-read: oversized filler prefix, still finds the last entry (AC2)
|
|
170
|
+
echo ""
|
|
171
|
+
echo "--- preToolUse with oversized-prefix transcript: bounded tail-read still finds it (AC2) ---"
|
|
172
|
+
input2=$(jq -nc --arg tp "$OVERSIZED_TRANSCRIPT" '{session_id:"tool-usage-2",transcript_path:$tp,hook_event_name:"PreToolUse",tool_name:"Bash",tool_input:{command:"echo hi"}}')
|
|
173
|
+
out2=$(_run_tool_event "preToolUse" "$input2" TELEMETRY_PRICING_FILE="$PRICING_FILE")
|
|
174
|
+
|
|
175
|
+
if [[ -n "$out2" ]]; then
|
|
176
|
+
model2=$(echo "$out2" | jq -r '.usage.model // empty')
|
|
177
|
+
it2=$(echo "$out2" | jq -r '.usage.input_tokens // empty')
|
|
178
|
+
ot2=$(echo "$out2" | jq -r '.usage.output_tokens // empty')
|
|
179
|
+
|
|
180
|
+
[[ "$model2" == "claude-oversized-prefix-model" ]] && _pass "bounded tail-read finds the last usage entry behind an oversized filler prefix" || _fail "expected model=claude-oversized-prefix-model, got $model2"
|
|
181
|
+
[[ "$it2" == "777" ]] && _pass "bounded tail-read recovers correct input_tokens (777)" || _fail "expected input_tokens=777, got $it2"
|
|
182
|
+
[[ "$ot2" == "333" ]] && _pass "bounded tail-read recovers correct output_tokens (333)" || _fail "expected output_tokens=333, got $ot2"
|
|
183
|
+
else
|
|
184
|
+
_fail "no tool.invoke event emitted for oversized-prefix transcript"
|
|
185
|
+
fi
|
|
186
|
+
|
|
187
|
+
# --- 3. Model-only fallback: no transcript_path, but hook.model present (AC3)
|
|
188
|
+
echo ""
|
|
189
|
+
echo "--- preToolUse with no transcript_path but hook.model present: model-only fallback (AC3) ---"
|
|
190
|
+
input3=$(jq -nc '{session_id:"tool-usage-3",hook_event_name:"PreToolUse","model":"test-model",tool_name:"Bash",tool_input:{command:"echo hi"}}')
|
|
191
|
+
out3=$(_run_tool_event "preToolUse" "$input3")
|
|
192
|
+
|
|
193
|
+
if [[ -n "$out3" ]]; then
|
|
194
|
+
model3=$(echo "$out3" | jq -r '.usage.model // empty')
|
|
195
|
+
it3=$(echo "$out3" | jq -r '.usage.input_tokens')
|
|
196
|
+
ot3=$(echo "$out3" | jq -r '.usage.output_tokens')
|
|
197
|
+
cct3=$(echo "$out3" | jq -r '.usage.cache_creation_input_tokens')
|
|
198
|
+
crt3=$(echo "$out3" | jq -r '.usage.cache_read_input_tokens')
|
|
199
|
+
cost3=$(echo "$out3" | jq -r '.usage.estimated_cost_usd')
|
|
200
|
+
pv3=$(echo "$out3" | jq -r '.usage.pricing_version')
|
|
201
|
+
|
|
202
|
+
[[ "$model3" == "test-model" ]] && _pass "usage.model falls back to hook.model (test-model)" || _fail "expected usage.model=test-model, got $model3"
|
|
203
|
+
[[ "$it3" == "null" && "$ot3" == "null" && "$cct3" == "null" && "$crt3" == "null" && "$cost3" == "null" && "$pv3" == "null" ]] && _pass "all token/cost fields are explicitly null (never guessed) in model-only fallback" || _fail "expected all token/cost fields null, got input_tokens=$it3 output_tokens=$ot3 cache_creation=$cct3 cache_read=$crt3 cost=$cost3 pricing_version=$pv3"
|
|
204
|
+
else
|
|
205
|
+
_fail "no tool.invoke event emitted for model-only fallback case"
|
|
206
|
+
fi
|
|
207
|
+
|
|
208
|
+
# --- 4. Full-null degradation: neither transcript nor hook.model (AC4) -------
|
|
209
|
+
echo ""
|
|
210
|
+
echo "--- preToolUse with neither transcript_path nor hook.model: full-null degradation (AC4) ---"
|
|
211
|
+
input4='{"session_id":"tool-usage-4","hook_event_name":"PreToolUse","tool_name":"Bash","tool_input":{"command":"echo hi"}}'
|
|
212
|
+
out4=$(_run_tool_event "preToolUse" "$input4")
|
|
213
|
+
|
|
214
|
+
if [[ -n "$out4" ]]; then
|
|
215
|
+
model4=$(echo "$out4" | jq -r '.usage.model')
|
|
216
|
+
it4=$(echo "$out4" | jq -r '.usage.input_tokens')
|
|
217
|
+
[[ "$model4" == "null" ]] && _pass "usage.model is null when neither transcript nor hook.model is available" || _fail "expected usage.model=null, got $model4"
|
|
218
|
+
[[ "$it4" == "null" ]] && _pass "usage.input_tokens is null when neither source is available" || _fail "expected usage.input_tokens=null, got $it4"
|
|
219
|
+
else
|
|
220
|
+
_fail "no tool.invoke event emitted for full-null case (should still emit, just with null usage)"
|
|
221
|
+
fi
|
|
222
|
+
|
|
223
|
+
# --- 6. Adversarial line: crafted model/token strings never leak verbatim ---
|
|
224
|
+
echo ""
|
|
225
|
+
echo "--- preToolUse with adversarial transcript: sanitized, never leaks raw strings (security review) ---"
|
|
226
|
+
input6=$(jq -nc --arg tp "$ADVERSARIAL_TRANSCRIPT" '{session_id:"tool-usage-6",transcript_path:$tp,hook_event_name:"PreToolUse",tool_name:"Bash",tool_input:{command:"echo hi"}}')
|
|
227
|
+
out6=$(_run_tool_event "preToolUse" "$input6" TELEMETRY_PRICING_FILE="$PRICING_FILE")
|
|
228
|
+
|
|
229
|
+
if [[ -n "$out6" ]]; then
|
|
230
|
+
model6=$(echo "$out6" | jq -r '.usage.model // empty')
|
|
231
|
+
it6=$(echo "$out6" | jq -r '.usage.input_tokens')
|
|
232
|
+
ot6=$(echo "$out6" | jq -r '.usage.output_tokens')
|
|
233
|
+
cct6=$(echo "$out6" | jq -r '.usage.cache_creation_input_tokens')
|
|
234
|
+
crt6=$(echo "$out6" | jq -r '.usage.cache_read_input_tokens')
|
|
235
|
+
cost6=$(echo "$out6" | jq -r '.usage.estimated_cost_usd')
|
|
236
|
+
|
|
237
|
+
[[ "$model6" == "unknown" ]] && _pass "adversarial model string degrades to 'unknown' (never the leak string)" || _fail "expected usage.model=unknown, got '$model6' (POSSIBLE LEAK)"
|
|
238
|
+
[[ "$it6" == "null" ]] && _pass "adversarial (string-typed) input_tokens degrades to null, not fabricated" || _fail "expected null input_tokens, got $it6"
|
|
239
|
+
[[ "$ot6" == "null" ]] && _pass "adversarial (string-typed) output_tokens degrades to null" || _fail "expected null output_tokens, got $ot6"
|
|
240
|
+
[[ "$cct6" == "null" ]] && _pass "adversarial (object-typed) cache_creation_input_tokens degrades to null" || _fail "expected null cache_creation_input_tokens, got $cct6"
|
|
241
|
+
[[ "$crt6" == "null" ]] && _pass "adversarial (array-typed) cache_read_input_tokens degrades to null" || _fail "expected null cache_read_input_tokens, got $crt6"
|
|
242
|
+
[[ "$cost6" == "null" ]] && _pass "estimated_cost_usd degrades to null when any token field is invalid (no partial/fabricated cost)" || _fail "expected null estimated_cost_usd, got $cost6"
|
|
243
|
+
else
|
|
244
|
+
_fail "no tool.invoke event emitted for adversarial transcript"
|
|
245
|
+
fi
|
|
246
|
+
|
|
247
|
+
# --- 7. Malformed lines BEFORE and AFTER the real usage line are skipped, not fatal (HIGH fix) ---
|
|
248
|
+
echo ""
|
|
249
|
+
echo "--- preToolUse with malformed-sandwich transcript: real line still found despite noise on both sides ---"
|
|
250
|
+
input7=$(jq -nc --arg tp "$MALFORMED_SANDWICH_TRANSCRIPT" '{session_id:"tool-usage-7",transcript_path:$tp,hook_event_name:"PreToolUse",tool_name:"Bash",tool_input:{command:"echo hi"}}')
|
|
251
|
+
out7=$(_run_tool_event "preToolUse" "$input7" TELEMETRY_PRICING_FILE="$PRICING_FILE")
|
|
252
|
+
|
|
253
|
+
if [[ -n "$out7" ]]; then
|
|
254
|
+
model7=$(echo "$out7" | jq -r '.usage.model // empty')
|
|
255
|
+
it7=$(echo "$out7" | jq -r '.usage.input_tokens // empty')
|
|
256
|
+
ot7=$(echo "$out7" | jq -r '.usage.output_tokens // empty')
|
|
257
|
+
|
|
258
|
+
[[ "$model7" == "claude-malformed-sandwich-safe" ]] && _pass "malformed lines before/after the real line do not abort the parse; real model recovered" || _fail "expected model=claude-malformed-sandwich-safe, got $model7"
|
|
259
|
+
[[ "$it7" == "10" ]] && _pass "real input_tokens recovered despite surrounding malformed noise (10)" || _fail "expected input_tokens=10, got $it7"
|
|
260
|
+
[[ "$ot7" == "20" ]] && _pass "real output_tokens recovered despite surrounding malformed noise (20)" || _fail "expected output_tokens=20, got $ot7"
|
|
261
|
+
else
|
|
262
|
+
_fail "no tool.invoke event emitted for malformed-sandwich transcript (a single bad line should never abort the whole parse)"
|
|
263
|
+
fi
|
|
264
|
+
|
|
265
|
+
# --- 8. Torn trailing line (concurrent-append style) after a valid line: prior valid line found ---
|
|
266
|
+
echo ""
|
|
267
|
+
echo "--- preToolUse with torn-trailing transcript: prior valid line found, torn line skipped ---"
|
|
268
|
+
input8=$(jq -nc --arg tp "$TORN_TRAILING_TRANSCRIPT" '{session_id:"tool-usage-8",transcript_path:$tp,hook_event_name:"PreToolUse",tool_name:"Bash",tool_input:{command:"echo hi"}}')
|
|
269
|
+
out8=$(_run_tool_event "preToolUse" "$input8" TELEMETRY_PRICING_FILE="$PRICING_FILE")
|
|
270
|
+
|
|
271
|
+
if [[ -n "$out8" ]]; then
|
|
272
|
+
model8=$(echo "$out8" | jq -r '.usage.model // empty')
|
|
273
|
+
it8=$(echo "$out8" | jq -r '.usage.input_tokens // empty')
|
|
274
|
+
|
|
275
|
+
[[ "$model8" == "claude-torn-trailing-safe" ]] && _pass "torn trailing line (no closing brace) is skipped; prior valid line's model found" || _fail "expected model=claude-torn-trailing-safe, got $model8"
|
|
276
|
+
[[ "$it8" == "15" ]] && _pass "torn trailing line does not leak its own tokens (999) -- prior valid line's tokens found (15)" || _fail "expected input_tokens=15, got $it8"
|
|
277
|
+
else
|
|
278
|
+
_fail "no tool.invoke event emitted for torn-trailing transcript"
|
|
279
|
+
fi
|
|
280
|
+
|
|
281
|
+
# --- 9. Secret-shaped model (sk-ant-...) never leaks; valid tokens still resolve (HIGH fix) ---
|
|
282
|
+
echo ""
|
|
283
|
+
echo "--- preToolUse with sk-ant-api03-... as model: sanitized to 'unknown', tokens still resolve ---"
|
|
284
|
+
input9=$(jq -nc --arg tp "$SECRET_MODEL_TRANSCRIPT" '{session_id:"tool-usage-9",transcript_path:$tp,hook_event_name:"PreToolUse",tool_name:"Bash",tool_input:{command:"echo hi"}}')
|
|
285
|
+
out9=$(_run_tool_event "preToolUse" "$input9" TELEMETRY_PRICING_FILE="$PRICING_FILE")
|
|
286
|
+
|
|
287
|
+
if [[ -n "$out9" ]]; then
|
|
288
|
+
model9=$(echo "$out9" | jq -r '.usage.model // empty')
|
|
289
|
+
it9=$(echo "$out9" | jq -r '.usage.input_tokens // empty')
|
|
290
|
+
ot9=$(echo "$out9" | jq -r '.usage.output_tokens // empty')
|
|
291
|
+
|
|
292
|
+
[[ "$model9" == "unknown" ]] && _pass "sk-ant-api03-... model string is NEVER relayed; degrades to 'unknown'" || _fail "SECRET LEAK: expected usage.model=unknown, got '$model9'"
|
|
293
|
+
[[ "$it9" == "42" ]] && _pass "valid numeric input_tokens still resolves despite invalid model (42)" || _fail "expected input_tokens=42, got $it9"
|
|
294
|
+
[[ "$ot9" == "17" ]] && _pass "valid numeric output_tokens still resolves despite invalid model (17)" || _fail "expected output_tokens=17, got $ot9"
|
|
295
|
+
else
|
|
296
|
+
_fail "no tool.invoke event emitted for secret-model transcript"
|
|
297
|
+
fi
|
|
298
|
+
|
|
299
|
+
# --- 10. Secret-shaped model (JWT eyJ...) never leaks --------------------------
|
|
300
|
+
echo ""
|
|
301
|
+
echo "--- preToolUse with a JWT (eyJ...) as model: sanitized to 'unknown' ---"
|
|
302
|
+
input10=$(jq -nc --arg tp "$SECRET_MODEL_JWT_TRANSCRIPT" '{session_id:"tool-usage-10",transcript_path:$tp,hook_event_name:"PreToolUse",tool_name:"Bash",tool_input:{command:"echo hi"}}')
|
|
303
|
+
out10=$(_run_tool_event "preToolUse" "$input10" TELEMETRY_PRICING_FILE="$PRICING_FILE")
|
|
304
|
+
|
|
305
|
+
if [[ -n "$out10" ]]; then
|
|
306
|
+
model10=$(echo "$out10" | jq -r '.usage.model // empty')
|
|
307
|
+
it10=$(echo "$out10" | jq -r '.usage.input_tokens // empty')
|
|
308
|
+
|
|
309
|
+
[[ "$model10" == "unknown" ]] && _pass "JWT (eyJ...) model string, with an embedded email, is NEVER relayed; degrades to 'unknown'" || _fail "SECRET/PII LEAK: expected usage.model=unknown, got '$model10'"
|
|
310
|
+
[[ "$it10" == "30" ]] && _pass "valid numeric input_tokens still resolves despite invalid model (30)" || _fail "expected input_tokens=30, got $it10"
|
|
311
|
+
else
|
|
312
|
+
_fail "no tool.invoke event emitted for JWT secret-model transcript"
|
|
313
|
+
fi
|
|
314
|
+
|
|
315
|
+
# --- 10c. Vendor-PREFIXED secret (uppercase suffix) never leaks (r4: suffix gap) ---
|
|
316
|
+
echo ""
|
|
317
|
+
echo "--- preToolUse with 'claude-AKIA...' (vendor-prefixed secret, uppercase): sanitized to 'unknown' ---"
|
|
318
|
+
input10c=$(jq -nc --arg tp "$PREFIXED_SECRET_UPPER_TRANSCRIPT" '{session_id:"tool-usage-10c",transcript_path:$tp,hook_event_name:"PreToolUse",tool_name:"Bash",tool_input:{command:"echo hi"}}')
|
|
319
|
+
out10c=$(_run_tool_event "preToolUse" "$input10c" TELEMETRY_PRICING_FILE="$PRICING_FILE")
|
|
320
|
+
if [[ -n "$out10c" ]]; then
|
|
321
|
+
model10c=$(echo "$out10c" | jq -r '.usage.model // empty')
|
|
322
|
+
it10c=$(echo "$out10c" | jq -r '.usage.input_tokens // empty')
|
|
323
|
+
[[ "$model10c" == "unknown" ]] && _pass "vendor-prefixed uppercase secret (claude-AKIA...) is NEVER relayed; degrades to 'unknown'" || _fail "SECRET LEAK: expected usage.model=unknown, got '$model10c'"
|
|
324
|
+
[[ "$it10c" == "11" ]] && _pass "valid input_tokens still resolves despite prefixed-secret model (11)" || _fail "expected input_tokens=11, got $it10c"
|
|
325
|
+
else
|
|
326
|
+
_fail "no tool.invoke event emitted for prefixed-secret-upper transcript"
|
|
327
|
+
fi
|
|
328
|
+
|
|
329
|
+
# --- 10d. Vendor-PREFIXED secret (long lowercase suffix > 16 chars) never leaks ---
|
|
330
|
+
echo ""
|
|
331
|
+
echo "--- preToolUse with 'claude-<27-char lowercase>' (prefixed, long token): sanitized to 'unknown' ---"
|
|
332
|
+
input10d=$(jq -nc --arg tp "$PREFIXED_SECRET_LOWER_TRANSCRIPT" '{session_id:"tool-usage-10d",transcript_path:$tp,hook_event_name:"PreToolUse",tool_name:"Bash",tool_input:{command:"echo hi"}}')
|
|
333
|
+
out10d=$(_run_tool_event "preToolUse" "$input10d" TELEMETRY_PRICING_FILE="$PRICING_FILE")
|
|
334
|
+
if [[ -n "$out10d" ]]; then
|
|
335
|
+
model10d=$(echo "$out10d" | jq -r '.usage.model // empty')
|
|
336
|
+
it10d=$(echo "$out10d" | jq -r '.usage.input_tokens // empty')
|
|
337
|
+
[[ "$model10d" == "unknown" ]] && _pass "vendor-prefixed long-lowercase secret is NEVER relayed; degrades to 'unknown' (>16-char token cap)" || _fail "SECRET LEAK: expected usage.model=unknown, got '$model10d'"
|
|
338
|
+
[[ "$it10d" == "13" ]] && _pass "valid input_tokens still resolves despite prefixed-secret model (13)" || _fail "expected input_tokens=13, got $it10d"
|
|
339
|
+
else
|
|
340
|
+
_fail "no tool.invoke event emitted for prefixed-secret-lower transcript"
|
|
341
|
+
fi
|
|
342
|
+
|
|
343
|
+
# --- 11. Negative token value degrades to null (LOW fix: sign/magnitude bound) ---
|
|
344
|
+
echo ""
|
|
345
|
+
echo "--- preToolUse with a negative input_tokens value: degrades to null ---"
|
|
346
|
+
input11=$(jq -nc --arg tp "$NEGATIVE_TOKENS_TRANSCRIPT" '{session_id:"tool-usage-11",transcript_path:$tp,hook_event_name:"PreToolUse",tool_name:"Bash",tool_input:{command:"echo hi"}}')
|
|
347
|
+
out11=$(_run_tool_event "preToolUse" "$input11" TELEMETRY_PRICING_FILE="$PRICING_FILE")
|
|
348
|
+
|
|
349
|
+
if [[ -n "$out11" ]]; then
|
|
350
|
+
it11=$(echo "$out11" | jq -r '.usage.input_tokens')
|
|
351
|
+
cost11=$(echo "$out11" | jq -r '.usage.estimated_cost_usd')
|
|
352
|
+
|
|
353
|
+
[[ "$it11" == "null" ]] && _pass "negative input_tokens (-500) degrades to null, never a negative number" || _fail "expected null input_tokens, got $it11"
|
|
354
|
+
[[ "$cost11" == "null" ]] && _pass "estimated_cost_usd degrades to null when a token field is out of bounds" || _fail "expected null estimated_cost_usd, got $cost11"
|
|
355
|
+
else
|
|
356
|
+
_fail "no tool.invoke event emitted for negative-tokens transcript"
|
|
357
|
+
fi
|
|
358
|
+
|
|
359
|
+
# --- 12. Oversized (1e308) token value degrades to null (LOW fix) -----------
|
|
360
|
+
echo ""
|
|
361
|
+
echo "--- preToolUse with an absurd-magnitude (1e308) input_tokens value: degrades to null ---"
|
|
362
|
+
input12=$(jq -nc --arg tp "$OVERSIZED_TOKENS_TRANSCRIPT" '{session_id:"tool-usage-12",transcript_path:$tp,hook_event_name:"PreToolUse",tool_name:"Bash",tool_input:{command:"echo hi"}}')
|
|
363
|
+
out12=$(_run_tool_event "preToolUse" "$input12" TELEMETRY_PRICING_FILE="$PRICING_FILE")
|
|
364
|
+
|
|
365
|
+
if [[ -n "$out12" ]]; then
|
|
366
|
+
it12=$(echo "$out12" | jq -r '.usage.input_tokens')
|
|
367
|
+
cost12=$(echo "$out12" | jq -r '.usage.estimated_cost_usd')
|
|
368
|
+
|
|
369
|
+
[[ "$it12" == "null" ]] && _pass "absurd-magnitude input_tokens (1e308) degrades to null, never a fabricated huge number" || _fail "expected null input_tokens, got $it12"
|
|
370
|
+
[[ "$cost12" == "null" ]] && _pass "estimated_cost_usd degrades to null when a token field is out of bounds (oversized case)" || _fail "expected null estimated_cost_usd, got $cost12"
|
|
371
|
+
else
|
|
372
|
+
_fail "no tool.invoke event emitted for oversized-tokens transcript"
|
|
373
|
+
fi
|
|
374
|
+
|
|
375
|
+
# --- 5. permissionRequest exclusion: no .usage key at all (AC7) --------------
|
|
376
|
+
echo ""
|
|
377
|
+
echo "--- permissionRequest with transcript_path present: no .usage enrichment (AC7) ---"
|
|
378
|
+
input5=$(jq -nc --arg tp "$FIXTURE_TRANSCRIPT" '{session_id:"tool-usage-5",transcript_path:$tp,hook_event_name:"PermissionRequest",tool_name:"Bash",tool_input:{command:"echo hi", description:"run a command"}}')
|
|
379
|
+
out5=$(_run_tool_event "permissionRequest" "$input5" TELEMETRY_PRICING_FILE="$PRICING_FILE")
|
|
380
|
+
|
|
381
|
+
if [[ -n "$out5" ]]; then
|
|
382
|
+
event_type5=$(echo "$out5" | jq -r '.event_type // empty')
|
|
383
|
+
has_usage5=$(echo "$out5" | jq -r 'has("usage")')
|
|
384
|
+
[[ "$event_type5" == "tool.permission_request" ]] && _pass "permissionRequest maps to tool.permission_request" || _fail "expected tool.permission_request, got $event_type5"
|
|
385
|
+
[[ "$has_usage5" == "false" ]] && _pass "permissionRequest event carries no .usage key (explicit scope boundary preserved)" || _fail "expected permissionRequest to have no .usage key, but has(\"usage\")=$has_usage5"
|
|
386
|
+
else
|
|
387
|
+
_fail "no tool.permission_request event emitted"
|
|
388
|
+
fi
|
|
389
|
+
|
|
390
|
+
rm -rf "$TMPDIR_EVAL"
|
|
391
|
+
|
|
392
|
+
echo ""
|
|
393
|
+
echo "Telemetry tool-event usage: $pass passed, $fail failed"
|
|
394
|
+
[ "$fail" -eq 0 ]
|