@kontourai/flow-agents 3.2.0 → 3.4.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/add-to-project.yml +15 -0
- package/.github/workflows/ci.yml +173 -0
- package/CHANGELOG.md +54 -0
- package/CONTEXT.md +5 -1
- package/README.md +19 -8
- package/build/src/builder-flow-run-adapter.d.ts +80 -0
- package/build/src/builder-flow-run-adapter.js +241 -0
- package/build/src/builder-flow-runtime.d.ts +16 -0
- package/build/src/builder-flow-runtime.js +290 -0
- package/build/src/cli/assignment-provider.js +10 -1
- package/build/src/cli/builder-run.d.ts +1 -0
- package/build/src/cli/builder-run.js +27 -0
- package/build/src/cli/effective-backlog-settings.js +70 -2
- package/build/src/cli/init.d.ts +34 -0
- package/build/src/cli/init.js +341 -61
- package/build/src/cli/kit.js +55 -12
- package/build/src/cli/pull-work-provider.js +346 -5
- package/build/src/cli/skill-drift-check.d.ts +1 -0
- package/build/src/cli/skill-drift-check.js +165 -0
- package/build/src/cli/telemetry-doctor.d.ts +37 -0
- package/build/src/cli/telemetry-doctor.js +53 -6
- package/build/src/cli/validate-hook-influence.js +37 -7
- package/build/src/cli/workflow-artifact-cleanup-audit.js +418 -11
- package/build/src/cli/workflow-sidecar.d.ts +310 -1
- package/build/src/cli/workflow-sidecar.js +1914 -126
- package/build/src/cli.js +5 -0
- package/build/src/flow-kit/validate.d.ts +54 -34
- package/build/src/flow-kit/validate.js +237 -26
- package/build/src/index.d.ts +2 -0
- package/build/src/index.js +1 -0
- package/build/src/lib/console-connect-options.d.ts +97 -0
- package/build/src/lib/console-connect-options.js +199 -0
- package/build/src/lib/console-telemetry-validate.d.ts +49 -0
- package/build/src/lib/console-telemetry-validate.js +91 -0
- package/build/src/lib/flow-resolver.d.ts +54 -1
- package/build/src/lib/flow-resolver.js +112 -5
- package/build/src/lib/fs.d.ts +17 -0
- package/build/src/lib/fs.js +172 -0
- package/build/src/lib/local-artifact-root.d.ts +44 -1
- package/build/src/lib/local-artifact-root.js +131 -3
- package/build/src/runtime-adapters.d.ts +39 -3
- package/build/src/runtime-adapters.js +77 -31
- package/build/src/tools/build-universal-bundles.js +40 -2
- package/build/src/tools/codex-agent-routing.d.ts +2 -0
- package/build/src/tools/codex-agent-routing.js +49 -0
- package/build/src/tools/generate-context-map.js +1 -0
- package/build/src/tools/validate-source-tree.js +30 -3
- package/context/contracts/artifact-contract.md +16 -2
- package/context/scripts/hooks/lib/kit-catalog.js +235 -0
- package/context/scripts/hooks/lib/runnable-command.js +177 -0
- package/context/scripts/hooks/stop-goal-fit.js +278 -48
- package/context/scripts/hooks/workflow-steering.js +194 -22
- package/context/scripts/package.json +3 -0
- package/context/scripts/telemetry/install-console-config.sh +25 -4
- package/context/scripts/telemetry/lib/config.sh +102 -12
- package/context/scripts/telemetry/lib/pricing.sh +50 -0
- package/context/scripts/telemetry/lib/session.sh +3 -0
- package/context/scripts/telemetry/lib/transport.sh +87 -0
- package/context/scripts/telemetry/lib/usage.sh +205 -4
- package/context/scripts/telemetry/telemetry.conf +6 -0
- package/context/scripts/telemetry/telemetry.sh +48 -0
- package/context/settings/workspace-backlog-provider-settings.example.json +48 -0
- package/docs/agent-usage-feedback-loop.md +35 -0
- package/docs/architecture-engine-and-kits.md +110 -0
- package/docs/context-map.md +2 -0
- package/docs/coordination-guide.md +370 -0
- package/docs/decisions/agent-coordination.md +26 -9
- package/docs/decisions/embeddable-engine.md +152 -0
- package/docs/decisions/index.md +5 -3
- package/docs/decisions/trust-ledger-retention.md +88 -0
- package/docs/decisions/trust-reconcile.md +42 -9
- package/docs/decisions/workflow-enforcement.md +31 -9
- package/docs/fixture-ownership.md +6 -2
- package/docs/implementing-trust-reconciliation.md +129 -0
- package/docs/index.md +23 -9
- package/docs/integrations/flow-agents-console.md +275 -0
- package/docs/integrations/index.md +4 -0
- package/docs/kit-authoring-guide.md +52 -21
- package/docs/spec/builder-flow-runtime.md +80 -0
- package/docs/spec/runtime-hook-surface.md +45 -1
- package/docs/specs/economics-record-contract.md +270 -0
- package/docs/specs/harness-capability-matrix.md +74 -0
- package/docs/specs/learning-review-proposals-contract.md +340 -0
- package/docs/specs/routing-efficiency-review.md +59 -0
- package/docs/verifiable-trust.md +74 -25
- package/docs/workflow-artifact-lifecycle.md +38 -1
- package/docs/workflow-usage-guide.md +10 -0
- package/evals/acceptance/prove-capture-teeth.sh +132 -0
- package/evals/ci/antigaming-suite.sh +2 -0
- package/evals/ci/run-baseline.sh +78 -4
- package/evals/fixtures/economics/acceptance.json +12 -0
- package/evals/fixtures/economics/agents/tool-worker-1/events.jsonl +2 -0
- package/evals/fixtures/economics/agents/tool-worker-2/events.jsonl +2 -0
- package/evals/fixtures/economics/agents/tool-worker-3/events.jsonl +2 -0
- package/evals/fixtures/economics/agents/tool-worker-4/events.jsonl +1 -0
- package/evals/fixtures/economics/agents/tool-worker-5/events.jsonl +2 -0
- package/evals/fixtures/economics/critique.json +22 -0
- package/evals/fixtures/economics/expected-record.json +71 -0
- package/evals/fixtures/economics/session-usage-event.json +1 -0
- package/evals/fixtures/economics/state.json +11 -0
- package/evals/fixtures/economics/transcript.jsonl +3 -0
- package/evals/fixtures/hook-influence/cases.json +7 -7
- package/evals/fixtures/learning-review-proposals/balanced/economics.jsonl +6 -0
- package/evals/fixtures/learning-review-proposals/effect-follow-up/economics.jsonl +5 -0
- package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-1/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-2/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-3/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-4/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-5/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/economics.jsonl +6 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/expected-aggregates.json +30 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/expected-aggregates.md +66 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-1/gate-review.inquiries.json +26 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-1/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-2/gate-review.inquiries.json +26 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-2/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-3/gate-review.inquiries.json +26 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-3/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-4/gate-review.inquiries.json +26 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-4/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-5/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-6/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/repeat-window/economics.jsonl +6 -0
- package/evals/fixtures/learning-review-proposals/under-threshold/economics.jsonl +3 -0
- package/evals/fixtures/reconcile-preflight/disputed-critique-unsuperseded.json +48 -0
- package/evals/fixtures/reconcile-preflight/standalone-disputed-session-local.json +59 -0
- package/evals/fixtures/telemetry/usage-transcript-sample.jsonl +4 -0
- package/evals/fixtures/trust-reconcile-exploits/mcp-degrade.json +42 -0
- package/evals/integration/test_builder_entry_enforcement.sh +241 -0
- package/evals/integration/test_builder_step_producers.sh +18 -10
- package/evals/integration/test_bundle_install.sh +172 -0
- package/evals/integration/test_checkpoint_signing.sh +10 -2
- package/evals/integration/test_ci_actor_identity.sh +221 -0
- package/evals/integration/test_console_tenant_isolation.sh +167 -0
- package/evals/integration/test_critique_supersession_roundtrip.sh +4 -1
- package/evals/integration/test_dual_emit_flow_step.sh +10 -4
- package/evals/integration/test_economics_record.sh +674 -0
- package/evals/integration/test_effective_backlog_settings.sh +1 -1
- package/evals/integration/test_evidence_capture_hook.sh +17 -2
- package/evals/integration/test_exemption_usage_review.sh +198 -0
- package/evals/integration/test_fixture_retirement_audit.sh +2 -2
- package/evals/integration/test_flow_kit_install_git.sh +83 -0
- package/evals/integration/test_flowdef_session_activation.sh +0 -1
- package/evals/integration/test_flowdef_session_history_preservation.sh +13 -3
- package/evals/integration/test_gate_lockdown.sh +7 -0
- package/evals/integration/test_gate_review_inquiry_records.sh +9 -1
- package/evals/integration/test_goal_fit_hook.sh +2031 -0
- package/evals/integration/test_hook_category_behaviors.sh +8 -1
- package/evals/integration/test_hook_influence_cases.sh +25 -1
- package/evals/integration/test_install_merge.sh +227 -2
- package/evals/integration/test_kit_conformance_levels.sh +6 -6
- package/evals/integration/test_learning_review_proposals.sh +329 -0
- package/evals/integration/test_liveness_conflict_injection.sh +26 -22
- package/evals/integration/test_liveness_console_relay.sh +166 -0
- package/evals/integration/test_liveness_heartbeat.sh +17 -17
- package/evals/integration/test_liveness_worktree_root.sh +575 -0
- package/evals/integration/test_phase_map_and_gate_claim.sh +6 -1
- package/evals/integration/test_publish_delivery.sh +389 -2
- package/evals/integration/test_pull_work_board.sh +200 -0
- package/evals/integration/test_pull_work_provider.sh +1 -1
- package/evals/integration/test_reconcile_preflight.sh +304 -0
- package/evals/integration/test_record_check.sh +378 -0
- package/evals/integration/test_routing_efficiency.sh +71 -0
- package/evals/integration/test_runtime_adapter_activation.sh +28 -0
- package/evals/integration/test_session_resume_roundtrip.sh +16 -19
- package/evals/integration/test_skill_drift_check.sh +870 -0
- package/evals/integration/test_takeover_protocol.sh +340 -0
- package/evals/integration/test_telemetry.sh +445 -0
- package/evals/integration/test_telemetry_doctor.sh +66 -0
- package/evals/integration/test_telemetry_usage_pipeline.sh +228 -0
- package/evals/integration/test_trust_reconcile_negatives.sh +121 -13
- package/evals/integration/test_trust_reconcile_trailer_diagnostic.sh +247 -0
- package/evals/integration/test_usage_cost.sh +61 -0
- package/evals/integration/test_verify_hold.sh +910 -0
- package/evals/integration/test_veritas_governance_kit.sh +257 -0
- package/evals/integration/test_workflow_artifact_cleanup_audit.sh +575 -3
- package/evals/integration/test_workflow_sidecar_writer.sh +1395 -0
- package/evals/integration/test_workflow_steering_hook.sh +157 -16
- package/evals/integration/test_workspace_settings.sh +176 -0
- package/evals/lib/env.sh +26 -0
- package/evals/lib/node.sh +8 -0
- package/evals/run.sh +37 -0
- package/evals/static/test_ci_integration_coverage.sh +115 -0
- package/evals/static/test_declared_scope_forms_documented.sh +114 -0
- package/evals/static/test_universal_bundles.sh +34 -0
- package/evals/static/test_validate_source_kit_asset_scope.sh +259 -0
- package/evals/static/test_workflow_skills.sh +1 -1
- package/kits/builder/flows/build.flow.json +9 -18
- package/kits/builder/flows/publish-learn.flow.json +5 -1
- package/kits/builder/kit.json +120 -0
- package/kits/builder/skills/continue-work/SKILL.md +2 -0
- package/kits/builder/skills/deliver/SKILL.md +115 -0
- package/kits/builder/skills/evidence-gate/SKILL.md +12 -0
- package/kits/builder/skills/execute-plan/SKILL.md +9 -0
- package/kits/builder/skills/learning-review/SKILL.md +51 -0
- package/kits/builder/skills/plan-work/SKILL.md +17 -20
- package/kits/builder/skills/pull-work/SKILL.md +33 -2
- package/kits/builder/skills/release-readiness/SKILL.md +12 -0
- package/kits/knowledge/kit.json +9 -0
- package/kits/veritas-governance/docs/README.md +113 -7
- package/kits/veritas-governance/fixtures/exemption/approved.trust-bundle.json +74 -0
- package/kits/veritas-governance/fixtures/exemption/not-approved.trust-bundle.json +74 -0
- package/kits/veritas-governance/fixtures/exemption-review/mixed-fresh-stale.DECLARED.json +14 -0
- package/kits/veritas-governance/flows/exemption-issuance.flow.json +35 -0
- package/kits/veritas-governance/kit.json +19 -0
- package/kits/veritas-governance/skills/exemption-usage-review/SKILL.md +128 -0
- package/kits/veritas-governance/skills/exemption-usage-review/review-exemptions.mjs +231 -0
- package/package.json +2 -2
- package/packaging/manifest.json +29 -0
- package/schemas/backlog-provider-settings.schema.json +13 -0
- package/schemas/workflow-state.schema.json +44 -0
- package/scripts/README.md +4 -0
- package/scripts/check-content-boundary.cjs +8 -1
- package/scripts/ci/trust-reconcile.js +214 -253
- package/scripts/hooks/codex-hook-adapter.js +77 -2
- package/scripts/hooks/evidence-capture.js +38 -5
- package/scripts/hooks/lib/actor-identity.js +82 -0
- package/scripts/hooks/lib/codex-exit-code.js +316 -0
- package/scripts/hooks/lib/kit-catalog.js +235 -0
- package/scripts/hooks/lib/liveness-write.js +28 -1
- package/scripts/hooks/lib/local-artifact-paths.js +97 -1
- package/scripts/hooks/lib/runnable-command.js +177 -0
- package/scripts/hooks/lib/skill-drift.js +350 -0
- package/scripts/hooks/stop-goal-fit.js +278 -48
- package/scripts/hooks/workflow-steering.js +194 -22
- package/scripts/install-codex-home.sh +97 -47
- package/scripts/install-merge.js +72 -14
- package/scripts/install-owned-files.js +178 -0
- package/scripts/lib/reconcile-shape.js +381 -0
- package/scripts/liveness/relay.sh +84 -0
- package/scripts/telemetry/economics-record.schema.json +145 -0
- package/scripts/telemetry/economics-record.sh +331 -0
- package/scripts/telemetry/install-console-config.sh +25 -4
- package/scripts/telemetry/learning-review-decide.sh +124 -0
- package/scripts/telemetry/learning-review-proposals.schema.json +161 -0
- package/scripts/telemetry/learning-review-proposals.sh +484 -0
- package/scripts/telemetry/lib/config.sh +102 -12
- package/scripts/telemetry/lib/pricing.sh +14 -6
- package/scripts/telemetry/lib/session.sh +3 -0
- package/scripts/telemetry/lib/transport.sh +133 -15
- package/scripts/telemetry/lib/usage.sh +121 -28
- package/scripts/telemetry/routing-efficiency.sh +0 -0
- package/scripts/telemetry/telemetry.conf +6 -0
- package/scripts/telemetry/telemetry.sh +48 -0
- package/src/builder-flow-run-adapter.ts +357 -0
- package/src/builder-flow-runtime.ts +348 -0
- package/src/cli/assignment-provider.ts +12 -1
- package/src/cli/builder-flow-run-adapter.test.mjs +495 -0
- package/src/cli/builder-flow-runtime.test.mjs +213 -0
- package/src/cli/builder-run.ts +28 -0
- package/src/cli/codex-agent-routing.test.mjs +44 -0
- package/src/cli/codex-exit-code.test.mjs +207 -0
- package/src/cli/console-connect-options.test.mjs +329 -0
- package/src/cli/console-telemetry-validate.test.mjs +157 -0
- package/src/cli/effective-backlog-settings.ts +68 -2
- package/src/cli/flow-resolver-composition.test.mjs +72 -0
- package/src/cli/init.test.mjs +161 -0
- package/src/cli/init.ts +407 -62
- package/src/cli/kit-metadata-security.test.mjs +443 -0
- package/src/cli/kit.ts +50 -12
- package/src/cli/pull-work-provider.ts +377 -3
- package/src/cli/sidecar-pure-helpers.test.mjs +64 -0
- package/src/cli/skill-drift-check.ts +196 -0
- package/src/cli/telemetry-doctor.test.mjs +53 -0
- package/src/cli/telemetry-doctor.ts +50 -7
- package/src/cli/validate-hook-influence.ts +37 -6
- package/src/cli/workflow-artifact-cleanup-audit.ts +483 -10
- package/src/cli/workflow-sidecar.ts +1980 -119
- package/src/cli.ts +5 -0
- package/src/flow-kit/validate.ts +277 -38
- package/src/index.ts +19 -0
- package/src/lib/console-connect-options.ts +261 -0
- package/src/lib/console-telemetry-validate.ts +88 -0
- package/src/lib/flow-resolver.ts +117 -4
- package/src/lib/fs.ts +160 -0
- package/src/lib/local-artifact-root.ts +129 -3
- package/src/runtime-adapters.ts +113 -33
- package/src/tools/build-universal-bundles.ts +36 -2
- package/src/tools/codex-agent-routing.ts +48 -0
- package/src/tools/generate-context-map.ts +1 -0
- package/src/tools/validate-source-tree.ts +29 -3
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# test_takeover_protocol.sh — takeover protocol integration eval (issue #294, ADR 0021 §5).
|
|
3
|
+
#
|
|
4
|
+
# Exercises `runTakeoverPreflight()`/`takeover-preflight` CLI subcommand and its companion
|
|
5
|
+
# `ensure-session --supersede-stale` takeover-as-resumption path (src/cli/workflow-sidecar.ts),
|
|
6
|
+
# reusing the SAME assignment ⋈ liveness join #290/#291 already built (computeEffectiveState/
|
|
7
|
+
# readLocalAssignmentStatus/freshHolders) — no second computation is invented here.
|
|
8
|
+
#
|
|
9
|
+
# Follows evals/integration/test_verify_hold.sh's harness conventions VERBATIM (set -uo
|
|
10
|
+
# pipefail / ROOT / node.sh / WRITER / TMPDIR_EVAL / trap / ARTIFACT_ROOT / pass|fail|errors /
|
|
11
|
+
# json_query / flow_agents_node) and reuses its write_assignment_record / append_liveness_event
|
|
12
|
+
# fixture-writer helpers verbatim — no new plumbing invented where an existing helper already
|
|
13
|
+
# does the job.
|
|
14
|
+
#
|
|
15
|
+
# Sections (1-to-1 with the plan's acceptance criteria):
|
|
16
|
+
# 1. AC1 — takeover offered + continues the incumbent's branch (the headline): a STALE record
|
|
17
|
+
# with a specific branch resolves `grace-then-supersede`/`reclaimable`; `--supersede-stale`
|
|
18
|
+
# as a different successor prints SupersedeTakeover with resumed_branch == the incumbent's
|
|
19
|
+
# branch (NOT a new branch) and records a `supersede` audit entry ("resuming from trust
|
|
20
|
+
# bundle").
|
|
21
|
+
# 2. AC2a — revive during grace -> back off: a fresh incumbent liveness heartbeat flips the
|
|
22
|
+
# same fixture to `back-off`/`held`/ok:false; a follow-on `--supersede-stale` REFUSES.
|
|
23
|
+
# 3. AC2b — a superseded incumbent is blocked at publish (compose with verify-hold, #293): the
|
|
24
|
+
# woken zombie incumbent's verify-hold on the AC1 subject exits non-zero.
|
|
25
|
+
# 4. free / self / human quick cases: no record -> `claim`/free; self+fresh -> `proceed`.
|
|
26
|
+
# 5. Injection discipline: hostile control bytes + >64 chars in actor_key/branch never leak raw
|
|
27
|
+
# into holder.actor / resume_branch, and are length-capped (<=64). Modeled on
|
|
28
|
+
# test_verify_hold.sh's AC7 / section-6 injection check.
|
|
29
|
+
#
|
|
30
|
+
# Deterministic, no model spend, self-cleaning, no network, no `gh` process anywhere.
|
|
31
|
+
# Usage: bash evals/integration/test_takeover_protocol.sh
|
|
32
|
+
|
|
33
|
+
set -uo pipefail
|
|
34
|
+
|
|
35
|
+
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
36
|
+
source "$ROOT/evals/lib/node.sh"
|
|
37
|
+
|
|
38
|
+
CLI="$ROOT/build/src/cli.js"
|
|
39
|
+
WRITER="workflow-sidecar"
|
|
40
|
+
|
|
41
|
+
TMPDIR_EVAL="$(mktemp -d)"
|
|
42
|
+
trap 'rm -rf "$TMPDIR_EVAL"' EXIT
|
|
43
|
+
ARTIFACT_ROOT="$TMPDIR_EVAL/artifact-root"
|
|
44
|
+
|
|
45
|
+
errors=0
|
|
46
|
+
pass() { echo " [PASS] $1"; }
|
|
47
|
+
fail() { echo " [FAIL] $1"; errors=$((errors + 1)); }
|
|
48
|
+
|
|
49
|
+
json_query() {
|
|
50
|
+
node -e 'const fs=require("fs"); let cur=JSON.parse(fs.readFileSync(process.argv[1],"utf8")); for (const part of process.argv[2].split(".")) cur=part==="length" ? cur.length : (Array.isArray(cur) ? cur[Number(part)] : cur[part]); console.log(cur);' "$1" "$2"
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
# append_liveness_event <root> <subjectId> <actor> <at-iso> [ttlSeconds]
|
|
54
|
+
# Reused verbatim from test_verify_hold.sh's own helper.
|
|
55
|
+
append_liveness_event() {
|
|
56
|
+
local root="$1" subject="$2" actor="$3" at="$4" ttl="${5:-1800}"
|
|
57
|
+
mkdir -p "$root/liveness"
|
|
58
|
+
node -e '
|
|
59
|
+
const fs = require("fs");
|
|
60
|
+
const evt = { type: "claim", subjectId: process.argv[1], actor: process.argv[2], at: process.argv[3], ttlSeconds: Number(process.argv[4]) };
|
|
61
|
+
fs.appendFileSync(process.argv[5], JSON.stringify(evt) + "\n");
|
|
62
|
+
' "$subject" "$actor" "$at" "$ttl" "$root/liveness/events.jsonl"
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
# write_assignment_record <artifactRoot> <slug> <sessionId> <actorKey> [claimedAt]
|
|
66
|
+
# Reused verbatim from test_verify_hold.sh's own helper. Writes a minimal, well-formed,
|
|
67
|
+
# ALREADY-CLAIMED assignment record directly (branch:"main", claimed_at defaults to now). For a
|
|
68
|
+
# staleness fixture with NO liveness event, computeEffectiveState classifies it reclaimable.
|
|
69
|
+
write_assignment_record() {
|
|
70
|
+
local root="$1" slug="$2" sessionId="$3" actorKey="$4" claimedAt="${5:-}"
|
|
71
|
+
mkdir -p "$root/assignment"
|
|
72
|
+
node -e '
|
|
73
|
+
const fs = require("fs");
|
|
74
|
+
const [dest, slug, sessionId, actorKey, claimedAtOverride] = process.argv.slice(1);
|
|
75
|
+
const now = new Date().toISOString();
|
|
76
|
+
const claimedAt = claimedAtOverride || now;
|
|
77
|
+
const actor = { runtime: "claude-code", session_id: sessionId, host: "eval-host" };
|
|
78
|
+
const rec = {
|
|
79
|
+
schema_version: "1.0", role: "AssignmentClaimRecord", subject_id: slug,
|
|
80
|
+
actor, actor_key: actorKey, claimed_at: claimedAt, ttl_seconds: 1800, branch: "main",
|
|
81
|
+
artifact_dir: slug, status: "claimed",
|
|
82
|
+
audit_trail: [{ at: now, transition: "claim", from_actor: null, to_actor: actor, reason: "claim" }],
|
|
83
|
+
};
|
|
84
|
+
fs.writeFileSync(dest, JSON.stringify(rec, null, 2));
|
|
85
|
+
' "$root/assignment/$slug.json" "$slug" "$sessionId" "$actorKey" "$claimedAt"
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
# patch_record_branch <assignmentRecord.json> <branch> — read/modify/write the record's `branch`
|
|
89
|
+
# field so takeover-preflight/SupersedeTakeover surface a SPECIFIC resume branch (write_assignment_record
|
|
90
|
+
# always sets branch:"main"). This is the incumbent's branch the successor must CONTINUE (AC1).
|
|
91
|
+
patch_record_branch() {
|
|
92
|
+
node -e '
|
|
93
|
+
const fs = require("fs");
|
|
94
|
+
const [file, branch] = process.argv.slice(1);
|
|
95
|
+
const rec = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
96
|
+
rec.branch = branch;
|
|
97
|
+
fs.writeFileSync(file, JSON.stringify(rec, null, 2));
|
|
98
|
+
' "$1" "$2"
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if [[ ! -f "$CLI" ]]; then
|
|
102
|
+
echo "build/src/cli.js not found — run 'npm run build' first" >&2
|
|
103
|
+
exit 1
|
|
104
|
+
fi
|
|
105
|
+
flow_agents_build_ts || { echo "build failed" >&2; exit 1; }
|
|
106
|
+
|
|
107
|
+
echo "=== takeover protocol (#294) ==="
|
|
108
|
+
|
|
109
|
+
# ─── 1. AC1 — takeover offered + continues the incumbent's branch (the headline) ───────────
|
|
110
|
+
echo "--- 1. AC1: stale claim -> grace-then-supersede; --supersede-stale continues the incumbent's branch (not a new one) ---"
|
|
111
|
+
|
|
112
|
+
AC1_SLUG="takeover-ac1-resume-branch"
|
|
113
|
+
AC1_DIR="$ARTIFACT_ROOT/$AC1_SLUG"
|
|
114
|
+
INCUMBENT_ACTOR="eval-actor-incumbent-a"
|
|
115
|
+
SUCCESSOR_ACTOR="eval-actor-successor"
|
|
116
|
+
AC1_BRANCH="agent/a/$AC1_SLUG"
|
|
117
|
+
AC1_RECORD="$ARTIFACT_ROOT/assignment/$AC1_SLUG.json"
|
|
118
|
+
|
|
119
|
+
# Seed a STALE reclaimable record: write_assignment_record (branch:"main", claimed_at=now) then
|
|
120
|
+
# patch its branch to the incumbent's real branch. NO liveness event -> no fresh heartbeat, so
|
|
121
|
+
# the join classifies it reclaimable (assignment_present_liveness_stale_or_absent).
|
|
122
|
+
write_assignment_record "$ARTIFACT_ROOT" "$AC1_SLUG" "incumbent-a-session" "$INCUMBENT_ACTOR"
|
|
123
|
+
patch_record_branch "$AC1_RECORD" "$AC1_BRANCH"
|
|
124
|
+
mkdir -p "$AC1_DIR"
|
|
125
|
+
|
|
126
|
+
if flow_agents_node "$WRITER" takeover-preflight "$AC1_DIR" --actor "$SUCCESSOR_ACTOR" \
|
|
127
|
+
>"$TMPDIR_EVAL/tp-ac1.out" 2>"$TMPDIR_EVAL/tp-ac1.err"; then
|
|
128
|
+
pass "takeover-preflight on a reclaimable subject exits 0 (ok:true — a takeover is offered) (AC1)"
|
|
129
|
+
else
|
|
130
|
+
fail "takeover-preflight on a reclaimable subject should have exited 0: $(cat "$TMPDIR_EVAL/tp-ac1.out" "$TMPDIR_EVAL/tp-ac1.err")"
|
|
131
|
+
fi
|
|
132
|
+
[[ "$(json_query "$TMPDIR_EVAL/tp-ac1.out" "role")" == "TakeoverPreflight" ]] && pass "takeover-preflight JSON role is TakeoverPreflight (AC1)" || fail "takeover-preflight JSON role was not TakeoverPreflight: $(cat "$TMPDIR_EVAL/tp-ac1.out")"
|
|
133
|
+
[[ "$(json_query "$TMPDIR_EVAL/tp-ac1.out" "action")" == "grace-then-supersede" ]] && pass "takeover-preflight action is grace-then-supersede for a reclaimable subject (AC1)" || fail "takeover-preflight action was not grace-then-supersede: $(cat "$TMPDIR_EVAL/tp-ac1.out")"
|
|
134
|
+
[[ "$(json_query "$TMPDIR_EVAL/tp-ac1.out" "effective_state")" == "reclaimable" ]] && pass "takeover-preflight effective_state is reclaimable (AC1)" || fail "takeover-preflight effective_state was not reclaimable: $(cat "$TMPDIR_EVAL/tp-ac1.out")"
|
|
135
|
+
[[ "$(json_query "$TMPDIR_EVAL/tp-ac1.out" "resume_branch")" == "$AC1_BRANCH" ]] && pass "takeover-preflight resume_branch is the incumbent's branch ($AC1_BRANCH), never a new one (AC1 headline)" || fail "takeover-preflight resume_branch was not the incumbent's branch: $(cat "$TMPDIR_EVAL/tp-ac1.out")"
|
|
136
|
+
AC1_GRACE="$(json_query "$TMPDIR_EVAL/tp-ac1.out" "grace_seconds")"
|
|
137
|
+
if [[ "$AC1_GRACE" =~ ^[0-9]+$ && "$AC1_GRACE" -gt 0 ]]; then
|
|
138
|
+
pass "takeover-preflight grace_seconds is a positive number ($AC1_GRACE) (AC1)"
|
|
139
|
+
else
|
|
140
|
+
fail "takeover-preflight grace_seconds was not a positive number: '$AC1_GRACE'"
|
|
141
|
+
fi
|
|
142
|
+
[[ "$(json_query "$TMPDIR_EVAL/tp-ac1.out" "holder.actor")" == "$INCUMBENT_ACTOR" ]] && pass "takeover-preflight holder.actor names the incumbent ($INCUMBENT_ACTOR) (AC1)" || fail "takeover-preflight holder.actor did not name the incumbent: $(cat "$TMPDIR_EVAL/tp-ac1.out")"
|
|
143
|
+
|
|
144
|
+
# Now the successor takes over via ensure-session --supersede-stale — takeover-as-resumption:
|
|
145
|
+
# resumed_branch must be the incumbent's branch (NOT a fresh branch), and a `supersede` audit
|
|
146
|
+
# entry naming the incumbent + "resuming from trust bundle" must be recorded.
|
|
147
|
+
flow_agents_node "$WRITER" ensure-session \
|
|
148
|
+
--flow-id builder.build \
|
|
149
|
+
--task-slug "$AC1_SLUG" \
|
|
150
|
+
--work-item "kontourai/flow-agents#294" \
|
|
151
|
+
--title "Takeover AC1" \
|
|
152
|
+
--artifact-root "$ARTIFACT_ROOT" \
|
|
153
|
+
--supersede-stale \
|
|
154
|
+
--actor "$SUCCESSOR_ACTOR" \
|
|
155
|
+
>"$TMPDIR_EVAL/tp-ac1-supersede.out" 2>"$TMPDIR_EVAL/tp-ac1-supersede.err"
|
|
156
|
+
AC1_SUPERSEDE_EXIT=$?
|
|
157
|
+
[[ $AC1_SUPERSEDE_EXIT -eq 0 ]] && pass "ensure-session --supersede-stale as the successor exits 0 (AC1)" || fail "ensure-session --supersede-stale unexpectedly failed: $(cat "$TMPDIR_EVAL/tp-ac1-supersede.out" "$TMPDIR_EVAL/tp-ac1-supersede.err")"
|
|
158
|
+
# ensure-session's stdout carries the single-line SupersedeTakeover JSON AND a trailing
|
|
159
|
+
# `console.log(dir)` line — isolate the JSON object line (printJson emits it single-line) before
|
|
160
|
+
# json_query'ing it.
|
|
161
|
+
grep -F '"role": "SupersedeTakeover"' "$TMPDIR_EVAL/tp-ac1-supersede.out" > "$TMPDIR_EVAL/tp-ac1-supersede.json" || true
|
|
162
|
+
[[ "$(json_query "$TMPDIR_EVAL/tp-ac1-supersede.json" "role")" == "SupersedeTakeover" ]] && pass "supersede stdout JSON role is SupersedeTakeover (AC1)" || fail "supersede stdout JSON role was not SupersedeTakeover: $(cat "$TMPDIR_EVAL/tp-ac1-supersede.out")"
|
|
163
|
+
[[ "$(json_query "$TMPDIR_EVAL/tp-ac1-supersede.json" "resumed_branch")" == "$AC1_BRANCH" ]] && pass "supersede resumed_branch is the incumbent's branch ($AC1_BRANCH), NOT a new branch (AC1 headline)" || fail "supersede resumed_branch was not the incumbent's branch: $(cat "$TMPDIR_EVAL/tp-ac1-supersede.out")"
|
|
164
|
+
[[ "$(json_query "$TMPDIR_EVAL/tp-ac1-supersede.json" "superseded_actor")" == "$INCUMBENT_ACTOR" ]] && pass "supersede superseded_actor names the incumbent ($INCUMBENT_ACTOR) (AC1)" || fail "supersede superseded_actor did not name the incumbent: $(cat "$TMPDIR_EVAL/tp-ac1-supersede.out")"
|
|
165
|
+
|
|
166
|
+
# The record now carries a `supersede` audit_trail entry whose reason resumes from the trust
|
|
167
|
+
# bundle and names the superseded incumbent.
|
|
168
|
+
AC1_SUPERSEDE_REASON="$(node -e '
|
|
169
|
+
const fs = require("fs");
|
|
170
|
+
const rec = JSON.parse(fs.readFileSync(process.argv[1], "utf8"));
|
|
171
|
+
const trail = Array.isArray(rec.audit_trail) ? rec.audit_trail : [];
|
|
172
|
+
const entry = trail.find((e) => e && e.transition === "supersede");
|
|
173
|
+
process.stdout.write(entry && typeof entry.reason === "string" ? entry.reason : "");
|
|
174
|
+
' "$AC1_RECORD")"
|
|
175
|
+
[[ -n "$AC1_SUPERSEDE_REASON" ]] && pass "the assignment record now contains a supersede audit_trail entry (AC1)" || fail "no supersede audit_trail entry found on the record: $(cat "$AC1_RECORD")"
|
|
176
|
+
[[ "$AC1_SUPERSEDE_REASON" == *"resuming from trust bundle"* ]] && pass "the supersede audit reason contains 'resuming from trust bundle' (AC1)" || fail "supersede audit reason did not contain 'resuming from trust bundle': '$AC1_SUPERSEDE_REASON'"
|
|
177
|
+
[[ "$AC1_SUPERSEDE_REASON" == *"$INCUMBENT_ACTOR"* ]] && pass "the supersede audit reason names the superseded incumbent ($INCUMBENT_ACTOR) (AC1)" || fail "supersede audit reason did not name the superseded incumbent: '$AC1_SUPERSEDE_REASON'"
|
|
178
|
+
|
|
179
|
+
# ─── 2. AC2a — revive during grace -> back off; --supersede-stale then REFUSES ─────────────
|
|
180
|
+
echo "--- 2. AC2a: a fresh incumbent liveness heartbeat flips reclaimable -> back-off/held; --supersede-stale then refuses ---"
|
|
181
|
+
|
|
182
|
+
AC2A_SLUG="takeover-ac2a-revive"
|
|
183
|
+
AC2A_DIR="$ARTIFACT_ROOT/$AC2A_SLUG"
|
|
184
|
+
AC2A_INCUMBENT="eval-actor-incumbent-revived"
|
|
185
|
+
write_assignment_record "$ARTIFACT_ROOT" "$AC2A_SLUG" "incumbent-revived-session" "$AC2A_INCUMBENT"
|
|
186
|
+
mkdir -p "$AC2A_DIR"
|
|
187
|
+
|
|
188
|
+
# A FRESH liveness heartbeat for the INCUMBENT — the incumbent revived during the grace window,
|
|
189
|
+
# so the join now classifies the subject held(holder=incumbent), not reclaimable.
|
|
190
|
+
append_liveness_event "$ARTIFACT_ROOT" "$AC2A_SLUG" "$AC2A_INCUMBENT" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" 1800
|
|
191
|
+
|
|
192
|
+
if flow_agents_node "$WRITER" takeover-preflight "$AC2A_DIR" --actor "$SUCCESSOR_ACTOR" \
|
|
193
|
+
>"$TMPDIR_EVAL/tp-ac2a.out" 2>"$TMPDIR_EVAL/tp-ac2a.err"; then
|
|
194
|
+
fail "takeover-preflight against a revived incumbent should have exited non-zero (back off) (AC2a)"
|
|
195
|
+
else
|
|
196
|
+
pass "takeover-preflight against a revived incumbent exits non-zero — do NOT supersede a live holder (AC2a)"
|
|
197
|
+
fi
|
|
198
|
+
[[ "$(json_query "$TMPDIR_EVAL/tp-ac2a.out" "ok")" == "false" ]] && pass "takeover-preflight reports ok:false for the revived incumbent (AC2a)" || fail "takeover-preflight did not report ok:false: $(cat "$TMPDIR_EVAL/tp-ac2a.out")"
|
|
199
|
+
[[ "$(json_query "$TMPDIR_EVAL/tp-ac2a.out" "action")" == "back-off" ]] && pass "takeover-preflight action is back-off for the revived incumbent (AC2a)" || fail "takeover-preflight action was not back-off: $(cat "$TMPDIR_EVAL/tp-ac2a.out")"
|
|
200
|
+
[[ "$(json_query "$TMPDIR_EVAL/tp-ac2a.out" "effective_state")" == "held" ]] && pass "takeover-preflight effective_state is held (incumbent revived) (AC2a)" || fail "takeover-preflight effective_state was not held: $(cat "$TMPDIR_EVAL/tp-ac2a.out")"
|
|
201
|
+
|
|
202
|
+
# The successor's --supersede-stale must now REFUSE (the state is held, not reclaimable) and never
|
|
203
|
+
# write a supersede audit entry.
|
|
204
|
+
flow_agents_node "$WRITER" ensure-session \
|
|
205
|
+
--flow-id builder.build \
|
|
206
|
+
--task-slug "$AC2A_SLUG" \
|
|
207
|
+
--work-item "kontourai/flow-agents#294" \
|
|
208
|
+
--title "Takeover AC2a" \
|
|
209
|
+
--artifact-root "$ARTIFACT_ROOT" \
|
|
210
|
+
--supersede-stale \
|
|
211
|
+
--actor "$SUCCESSOR_ACTOR" \
|
|
212
|
+
>"$TMPDIR_EVAL/tp-ac2a-supersede.out" 2>"$TMPDIR_EVAL/tp-ac2a-supersede.err"
|
|
213
|
+
AC2A_SUPERSEDE_EXIT=$?
|
|
214
|
+
[[ $AC2A_SUPERSEDE_EXIT -ne 0 ]] && pass "ensure-session --supersede-stale as the successor REFUSES (exits non-zero) because the incumbent revived (AC2a)" || fail "ensure-session --supersede-stale should have refused against a revived incumbent: $(cat "$TMPDIR_EVAL/tp-ac2a-supersede.out" "$TMPDIR_EVAL/tp-ac2a-supersede.err")"
|
|
215
|
+
AC2A_HAS_SUPERSEDE="$(node -e '
|
|
216
|
+
const fs = require("fs");
|
|
217
|
+
const rec = JSON.parse(fs.readFileSync(process.argv[1], "utf8"));
|
|
218
|
+
const trail = Array.isArray(rec.audit_trail) ? rec.audit_trail : [];
|
|
219
|
+
process.stdout.write(trail.some((e) => e && e.transition === "supersede") ? "yes" : "no");
|
|
220
|
+
' "$ARTIFACT_ROOT/assignment/$AC2A_SLUG.json")"
|
|
221
|
+
[[ "$AC2A_HAS_SUPERSEDE" == "no" ]] && pass "no supersede audit entry was written for the refused takeover — the live incumbent's record is untouched (AC2a)" || fail "a supersede audit entry was written despite the refusal (AC2a)"
|
|
222
|
+
|
|
223
|
+
# ─── 3. AC2b — a superseded incumbent is blocked at publish (compose with verify-hold, #293) ─
|
|
224
|
+
echo "--- 3. AC2b: the woken zombie incumbent is blocked by verify-hold on the AC1 subject (takeover + #293 gate compose) ---"
|
|
225
|
+
|
|
226
|
+
# After AC1's takeover the AC1 record is held by the successor (successor's actor_key). The
|
|
227
|
+
# original incumbent, if it wakes, is no longer the holder — verify-hold (#293) must block it.
|
|
228
|
+
if flow_agents_node "$WRITER" verify-hold "$AC1_DIR" --actor "$INCUMBENT_ACTOR" \
|
|
229
|
+
>"$TMPDIR_EVAL/tp-ac2b.out" 2>"$TMPDIR_EVAL/tp-ac2b.err"; then
|
|
230
|
+
fail "verify-hold for the superseded (zombie) incumbent should have exited non-zero — it no longer holds the subject (AC2b)"
|
|
231
|
+
else
|
|
232
|
+
pass "verify-hold blocks the woken zombie incumbent (exit non-zero) — takeover + the #293 gate compose (AC2b)"
|
|
233
|
+
fi
|
|
234
|
+
[[ "$(json_query "$TMPDIR_EVAL/tp-ac2b.out" "ok")" == "false" ]] && pass "verify-hold reports ok:false for the superseded incumbent (AC2b)" || fail "verify-hold did not report ok:false for the superseded incumbent: $(cat "$TMPDIR_EVAL/tp-ac2b.out")"
|
|
235
|
+
|
|
236
|
+
# ─── 4. free / self quick cases ────────────────────────────────────────────────────────────
|
|
237
|
+
echo "--- 4. free (no record) -> claim; self+fresh -> proceed ---"
|
|
238
|
+
|
|
239
|
+
# free: no assignment record at all for this fresh slug.
|
|
240
|
+
FREE_SLUG="takeover-free-subject"
|
|
241
|
+
FREE_DIR="$ARTIFACT_ROOT/$FREE_SLUG"
|
|
242
|
+
mkdir -p "$FREE_DIR"
|
|
243
|
+
if flow_agents_node "$WRITER" takeover-preflight "$FREE_DIR" --actor "$SUCCESSOR_ACTOR" \
|
|
244
|
+
>"$TMPDIR_EVAL/tp-free.out" 2>"$TMPDIR_EVAL/tp-free.err"; then
|
|
245
|
+
pass "takeover-preflight on a free subject (no record) exits 0 (AC free)"
|
|
246
|
+
else
|
|
247
|
+
fail "takeover-preflight on a free subject unexpectedly refused: $(cat "$TMPDIR_EVAL/tp-free.out" "$TMPDIR_EVAL/tp-free.err")"
|
|
248
|
+
fi
|
|
249
|
+
[[ "$(json_query "$TMPDIR_EVAL/tp-free.out" "action")" == "claim" ]] && pass "takeover-preflight action is claim for a free subject (AC free)" || fail "takeover-preflight action was not claim: $(cat "$TMPDIR_EVAL/tp-free.out")"
|
|
250
|
+
[[ "$(json_query "$TMPDIR_EVAL/tp-free.out" "effective_state")" == "free" ]] && pass "takeover-preflight effective_state is free for a free subject (AC free)" || fail "takeover-preflight effective_state was not free: $(cat "$TMPDIR_EVAL/tp-free.out")"
|
|
251
|
+
|
|
252
|
+
# self_is_holder: a fresh record + fresh liveness for the SAME actor passed as --actor.
|
|
253
|
+
SELF_SLUG="takeover-self-holder"
|
|
254
|
+
SELF_DIR="$ARTIFACT_ROOT/$SELF_SLUG"
|
|
255
|
+
SELF_ACTOR="eval-actor-self-holder"
|
|
256
|
+
write_assignment_record "$ARTIFACT_ROOT" "$SELF_SLUG" "self-holder-session" "$SELF_ACTOR"
|
|
257
|
+
append_liveness_event "$ARTIFACT_ROOT" "$SELF_SLUG" "$SELF_ACTOR" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" 1800
|
|
258
|
+
mkdir -p "$SELF_DIR"
|
|
259
|
+
if flow_agents_node "$WRITER" takeover-preflight "$SELF_DIR" --actor "$SELF_ACTOR" \
|
|
260
|
+
>"$TMPDIR_EVAL/tp-self.out" 2>"$TMPDIR_EVAL/tp-self.err"; then
|
|
261
|
+
pass "takeover-preflight when the current actor IS the fresh holder exits 0 (AC self)"
|
|
262
|
+
else
|
|
263
|
+
fail "takeover-preflight for the self holder unexpectedly refused: $(cat "$TMPDIR_EVAL/tp-self.out" "$TMPDIR_EVAL/tp-self.err")"
|
|
264
|
+
fi
|
|
265
|
+
[[ "$(json_query "$TMPDIR_EVAL/tp-self.out" "action")" == "proceed" ]] && pass "takeover-preflight action is proceed when self is the holder (AC self)" || fail "takeover-preflight action was not proceed: $(cat "$TMPDIR_EVAL/tp-self.out")"
|
|
266
|
+
|
|
267
|
+
# ─── 5. Injection discipline — hostile control bytes + >64 chars are stripped AND capped ─────
|
|
268
|
+
echo "--- 5. injection: hostile actor_key/branch never leak raw control/ANSI bytes; holder.actor & resume_branch are length-capped (<=64) ---"
|
|
269
|
+
|
|
270
|
+
HOSTILE_SLUG="takeover-hostile-injection"
|
|
271
|
+
HOSTILE_DIR="$ARTIFACT_ROOT/$HOSTILE_SLUG"
|
|
272
|
+
HOSTILE_RECORD="$ARTIFACT_ROOT/assignment/$HOSTILE_SLUG.json"
|
|
273
|
+
# actor_key and branch: embed raw ESC (\x1b) + BEL (\x07) control bytes AND >64 chars each.
|
|
274
|
+
HOSTILE_ACTOR=$'hostile-actor\x1b[31;1mFAKE\x07-admin-'"$(printf 'A%.0s' {1..300})"
|
|
275
|
+
HOSTILE_BRANCH=$'agent/hostile\x1b[31;1mFAKE\x07-branch-'"$(printf 'B%.0s' {1..300})"
|
|
276
|
+
write_assignment_record "$ARTIFACT_ROOT" "$HOSTILE_SLUG" "hostile-session" "$HOSTILE_ACTOR"
|
|
277
|
+
patch_record_branch "$HOSTILE_RECORD" "$HOSTILE_BRANCH"
|
|
278
|
+
mkdir -p "$HOSTILE_DIR"
|
|
279
|
+
|
|
280
|
+
# No liveness event -> reclaimable, so both holder.actor (from the record's actor_key) and
|
|
281
|
+
# resume_branch (from the record's branch) are populated and pass through sanitize().
|
|
282
|
+
flow_agents_node "$WRITER" takeover-preflight "$HOSTILE_DIR" --actor eval-actor-hostile-reader \
|
|
283
|
+
>"$TMPDIR_EVAL/tp-hostile.out" 2>"$TMPDIR_EVAL/tp-hostile.err"
|
|
284
|
+
|
|
285
|
+
if grep -qF $'\x1b[31;1m' "$TMPDIR_EVAL/tp-hostile.out" "$TMPDIR_EVAL/tp-hostile.err" || grep -qF $'\x07' "$TMPDIR_EVAL/tp-hostile.out" "$TMPDIR_EVAL/tp-hostile.err"; then
|
|
286
|
+
fail "hostile actor_key/branch's raw ANSI/control bytes leaked into takeover-preflight's output (stdout+stderr) (injection AC)"
|
|
287
|
+
else
|
|
288
|
+
pass "hostile actor_key/branch's raw ANSI/control bytes never leak into takeover-preflight's output (injection AC)"
|
|
289
|
+
fi
|
|
290
|
+
|
|
291
|
+
HOSTILE_HOLDER_ACTOR="$(json_query "$TMPDIR_EVAL/tp-hostile.out" "holder.actor")"
|
|
292
|
+
if [[ "$HOSTILE_HOLDER_ACTOR" == *$'\x1b'* || "$HOSTILE_HOLDER_ACTOR" == *$'\x07'* ]]; then
|
|
293
|
+
fail "holder.actor leaked raw control/ANSI bytes (injection AC): $HOSTILE_HOLDER_ACTOR"
|
|
294
|
+
else
|
|
295
|
+
pass "holder.actor is control-char-stripped (injection AC)"
|
|
296
|
+
fi
|
|
297
|
+
HOSTILE_HOLDER_ACTOR_LEN="${#HOSTILE_HOLDER_ACTOR}"
|
|
298
|
+
[[ "$HOSTILE_HOLDER_ACTOR_LEN" -le 64 ]] && pass "holder.actor is length-capped at <=64 chars despite a 300+ char source (len=$HOSTILE_HOLDER_ACTOR_LEN) (injection AC)" || fail "holder.actor exceeded the 64-char cap (len=$HOSTILE_HOLDER_ACTOR_LEN): $HOSTILE_HOLDER_ACTOR"
|
|
299
|
+
|
|
300
|
+
HOSTILE_RESUME_BRANCH="$(json_query "$TMPDIR_EVAL/tp-hostile.out" "resume_branch")"
|
|
301
|
+
if [[ "$HOSTILE_RESUME_BRANCH" == *$'\x1b'* || "$HOSTILE_RESUME_BRANCH" == *$'\x07'* ]]; then
|
|
302
|
+
fail "resume_branch leaked raw control/ANSI bytes (injection AC): $HOSTILE_RESUME_BRANCH"
|
|
303
|
+
else
|
|
304
|
+
pass "resume_branch is control-char-stripped (injection AC)"
|
|
305
|
+
fi
|
|
306
|
+
HOSTILE_RESUME_BRANCH_LEN="${#HOSTILE_RESUME_BRANCH}"
|
|
307
|
+
# Branch uses the 240-char FREE-TEXT tier (not the 64-char id tier holder.actor uses): a realistic
|
|
308
|
+
# `agent/<actor>/<slug>` branch legitimately exceeds 64 chars, so capping the branch at 64 would
|
|
309
|
+
# truncate a VALID branch into a non-existent `git checkout` target (review finding #2). It is still
|
|
310
|
+
# capped (bounded at 240) and control-stripped (asserted above).
|
|
311
|
+
[[ "$HOSTILE_RESUME_BRANCH_LEN" -le 240 ]] && pass "resume_branch is length-capped at <=240 (free-text tier) despite a 300+ char source (len=$HOSTILE_RESUME_BRANCH_LEN) (injection AC)" || fail "resume_branch exceeded the 240-char free-text cap (len=$HOSTILE_RESUME_BRANCH_LEN): $HOSTILE_RESUME_BRANCH"
|
|
312
|
+
|
|
313
|
+
# ─── 6. A legitimate LONG branch (>64 chars, no control bytes) survives INTACT (review finding #2) ─
|
|
314
|
+
echo "--- 6. a realistic >64-char branch is NOT truncated (resume path would otherwise checkout a bad target) ---"
|
|
315
|
+
LONG_SLUG="takeover-long-branch"
|
|
316
|
+
LONG_DIR="$ARTIFACT_ROOT/$LONG_SLUG"
|
|
317
|
+
LONG_RECORD="$ARTIFACT_ROOT/assignment/$LONG_SLUG.json"
|
|
318
|
+
# 78 chars, well over 64, entirely valid git-branch charset — the exact case that would break AC1 if
|
|
319
|
+
# the branch were capped at 64.
|
|
320
|
+
LONG_BRANCH="agent/claude-code-sess-abcdef0123456789-hostname/flow-agents-294-takeover-xyz"
|
|
321
|
+
write_assignment_record "$ARTIFACT_ROOT" "$LONG_SLUG" "long-session" "long-actor-key"
|
|
322
|
+
patch_record_branch "$LONG_RECORD" "$LONG_BRANCH"
|
|
323
|
+
mkdir -p "$LONG_DIR"
|
|
324
|
+
flow_agents_node "$WRITER" takeover-preflight "$LONG_DIR" --actor eval-actor-long-reader \
|
|
325
|
+
>"$TMPDIR_EVAL/tp-long.out" 2>"$TMPDIR_EVAL/tp-long.err"
|
|
326
|
+
LONG_RESUME_BRANCH="$(json_query "$TMPDIR_EVAL/tp-long.out" "resume_branch")"
|
|
327
|
+
[[ "$LONG_RESUME_BRANCH" == "$LONG_BRANCH" ]] && pass "a legitimate ${#LONG_BRANCH}-char branch survives INTACT in resume_branch (never truncated into a bad checkout target — AC1 / review finding #2)" || fail "a legitimate long branch was altered/truncated in resume_branch: got '$LONG_RESUME_BRANCH' expected '$LONG_BRANCH'"
|
|
328
|
+
# And through the real supersede (resumed_branch) path.
|
|
329
|
+
flow_agents_node "$WRITER" ensure-session --flow-id builder.build --task-slug "$LONG_SLUG" --work-item 294 --title t --artifact-root "$ARTIFACT_ROOT" --supersede-stale --actor eval-actor-long-reader \
|
|
330
|
+
>"$TMPDIR_EVAL/tp-long-supersede.out" 2>"$TMPDIR_EVAL/tp-long-supersede.err"
|
|
331
|
+
grep -F '"role": "SupersedeTakeover"' "$TMPDIR_EVAL/tp-long-supersede.out" >"$TMPDIR_EVAL/tp-long-supersede.json"
|
|
332
|
+
[[ "$(json_query "$TMPDIR_EVAL/tp-long-supersede.json" "resumed_branch")" == "$LONG_BRANCH" ]] && pass "supersede resumed_branch preserves the full ${#LONG_BRANCH}-char incumbent branch (review finding #2)" || fail "supersede resumed_branch truncated the long branch: $(cat "$TMPDIR_EVAL/tp-long-supersede.json")"
|
|
333
|
+
|
|
334
|
+
echo ""
|
|
335
|
+
if [[ "$errors" -eq 0 ]]; then
|
|
336
|
+
echo "test_takeover_protocol: all checks passed."
|
|
337
|
+
else
|
|
338
|
+
echo "test_takeover_protocol: $errors check(s) failed."
|
|
339
|
+
fi
|
|
340
|
+
exit "$errors"
|