@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
|
@@ -46,6 +46,7 @@
|
|
|
46
46
|
set -uo pipefail
|
|
47
47
|
|
|
48
48
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
49
|
+
CURRENT_POINTER_HELPER="$ROOT/scripts/hooks/lib/current-pointer.js"
|
|
49
50
|
source "$ROOT/evals/lib/node.sh"
|
|
50
51
|
|
|
51
52
|
CLI="$ROOT/build/src/cli.js"
|
|
@@ -196,6 +197,31 @@ write_steering_fixture_repo() {
|
|
|
196
197
|
mv "$repoRoot/.kontourai/flow-agents/$slug/state.dot.json.tmp" "$repoRoot/.kontourai/flow-agents/$slug/state.json"
|
|
197
198
|
}
|
|
198
199
|
|
|
200
|
+
# seed_current_pointer <repo_root> <slug> <actor> — #440 FIXTURE-GAP: this suite's
|
|
201
|
+
# write_steering_fixture_repo fixtures were written before #440's per-actor ownership scoping and
|
|
202
|
+
# never establish a per-actor current pointer for the invoking actor -- under a RESOLVED
|
|
203
|
+
# FLOW_AGENTS_ACTOR override, workflow-steering.js's actorScopedWorkflowState now scopes to that
|
|
204
|
+
# actor's own (nonexistent) pointer and never reaches the fixture-under-test. Seeds BOTH the
|
|
205
|
+
# legacy current.json AND the per-actor current/<actor>.json pointer with the SAME payload,
|
|
206
|
+
# mirroring workflow-sidecar.ts's real writeCurrent() dual-write via current-pointer.js's own
|
|
207
|
+
# writePerActorCurrent.
|
|
208
|
+
seed_current_pointer() {
|
|
209
|
+
local repoRoot="$1" slug="$2" actor="$3"
|
|
210
|
+
local flowAgentsDir="$repoRoot/.kontourai/flow-agents"
|
|
211
|
+
CP_HELPER_ARG="$CURRENT_POINTER_HELPER" DIR_ARG="$flowAgentsDir" SLUG_ARG="$slug" ACTOR_ARG="$actor" node - <<'NODE'
|
|
212
|
+
const fs = require('fs');
|
|
213
|
+
const path = require('path');
|
|
214
|
+
const { writePerActorCurrent } = require(process.env.CP_HELPER_ARG);
|
|
215
|
+
const dir = process.env.DIR_ARG;
|
|
216
|
+
const slug = process.env.SLUG_ARG;
|
|
217
|
+
const actor = process.env.ACTOR_ARG;
|
|
218
|
+
const payload = { schema_version: '1.0', active_slug: slug, artifact_dir: slug };
|
|
219
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
220
|
+
fs.writeFileSync(path.join(dir, 'current.json'), JSON.stringify(payload, null, 2) + '\n');
|
|
221
|
+
writePerActorCurrent(dir, actor, payload);
|
|
222
|
+
NODE
|
|
223
|
+
}
|
|
224
|
+
|
|
199
225
|
ROOT_SCRATCH_STATE_WRITER="$TMPDIR_EVAL/write-state-json.js"
|
|
200
226
|
cat > "$ROOT_SCRATCH_STATE_WRITER" << 'JSEOF'
|
|
201
227
|
const fs = require("fs");
|
|
@@ -388,7 +414,7 @@ mkdir -p "$UNSTABLE_DIR"
|
|
|
388
414
|
# (a) Unstable current actor: unset the explicit override, the native runtime session id, AND every
|
|
389
415
|
# CI-provider marker (#398) so resolveActor() genuinely falls through to process-ancestry/unresolved
|
|
390
416
|
# regardless of whether this suite is running inside a CI job.
|
|
391
|
-
if (unset FLOW_AGENTS_ACTOR CLAUDE_CODE_SESSION_ID CODEX_SESSION_ID OPENCODE_SESSION_ID PI_SESSION_ID CLAUDECODE \
|
|
417
|
+
if (unset FLOW_AGENTS_ACTOR CLAUDE_CODE_SESSION_ID CODEX_THREAD_ID CODEX_SESSION_ID OPENCODE_SESSION_ID PI_SESSION_ID CLAUDECODE \
|
|
392
418
|
GITHUB_ACTIONS GITLAB_CI CIRCLECI JENKINS_URL TF_BUILD BUILDKITE; \
|
|
393
419
|
flow_agents_node "$WRITER" verify-hold "$UNSTABLE_DIR") >"$TMPDIR_EVAL/vh-unstable.out" 2>"$TMPDIR_EVAL/vh-unstable.err"; then
|
|
394
420
|
pass "verify-hold with an unstable (ancestry/unresolved) current actor does NOT hard-block a subject held by a different actor (advisory-only, AC1 SECOND fix)"
|
|
@@ -713,6 +739,12 @@ grep -qF "hostile-holder-actor" "$TMPDIR_EVAL/vh-hostile.out" && pass "the sanit
|
|
|
713
739
|
STEERING_REPO="$TMPDIR_EVAL/steering-hostile-repo"
|
|
714
740
|
STEERING_HOSTILE_NOW="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
715
741
|
write_steering_fixture_repo "$STEERING_REPO" "$HOSTILE_SLUG" "$STEERING_HOSTILE_NOW"
|
|
742
|
+
# #440 FIX 2 (de-vacuate, delta review): the invoking actor (eval-actor-hostile-reader) owns
|
|
743
|
+
# HOSTILE_SLUG -- seed its own per-actor pointer (real dual-write helper) so
|
|
744
|
+
# actorScopedWorkflowState finds it and supersessionSteering is REACHED at all; without this the
|
|
745
|
+
# absence assertion below passed only because no steering output existed in the first place, not
|
|
746
|
+
# because the sanitization itself was exercised.
|
|
747
|
+
seed_current_pointer "$STEERING_REPO" "$HOSTILE_SLUG" "eval-actor-hostile-reader"
|
|
716
748
|
append_liveness_event "$STEERING_REPO/.kontourai/flow-agents" "$HOSTILE_SLUG" "$HOSTILE_HOLDER_ACTOR" "$STEERING_HOSTILE_NOW" 1800
|
|
717
749
|
|
|
718
750
|
if FLOW_AGENTS_ACTOR="eval-actor-hostile-reader" node "$ROOT/scripts/hooks/workflow-steering.js" \
|
|
@@ -724,6 +756,14 @@ then
|
|
|
724
756
|
else
|
|
725
757
|
fail "steering hook should not fail for the hostile-holder fixture"
|
|
726
758
|
fi
|
|
759
|
+
# #440 FIX 2 reachability control (delta review): prove the supersession notice is ACTUALLY
|
|
760
|
+
# present and names the sanitized holder -- the ANSI/control-byte absence checks below would
|
|
761
|
+
# otherwise pass vacuously if no notice were emitted at all (exactly the pre-fix defect).
|
|
762
|
+
if grep -qF "[SUPERSEDED:" "$TMPDIR_EVAL/steering-hostile.out" && grep -qF "hostile-holder-actor" "$TMPDIR_EVAL/steering-hostile.out"; then
|
|
763
|
+
pass "#440 reachability control: the steering supersession notice is present and names the sanitized (non-control-byte) hostile holder actor (AC7)"
|
|
764
|
+
else
|
|
765
|
+
fail "#440 reachability control: no supersession notice was emitted, or it did not name the sanitized hostile holder actor: $(cat "$TMPDIR_EVAL/steering-hostile.out")"
|
|
766
|
+
fi
|
|
727
767
|
if grep -qF $'\x1b[31;1m' "$TMPDIR_EVAL/steering-hostile.out" || grep -qF $'\x07' "$TMPDIR_EVAL/steering-hostile.out"; then
|
|
728
768
|
fail "hostile holder actor's raw ANSI/control bytes leaked into the steering supersession notice (AC7)"
|
|
729
769
|
else
|
|
@@ -835,6 +875,7 @@ STEERING_SUP_REPO="$TMPDIR_EVAL/steering-supersede-repo"
|
|
|
835
875
|
SUP_SLUG="steering-superseded-demo"
|
|
836
876
|
SUP_NOW="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
837
877
|
write_steering_fixture_repo "$STEERING_SUP_REPO" "$SUP_SLUG" "$SUP_NOW"
|
|
878
|
+
seed_current_pointer "$STEERING_SUP_REPO" "$SUP_SLUG" "eval-actor-steering-self-a"
|
|
838
879
|
append_liveness_event "$STEERING_SUP_REPO/.kontourai/flow-agents" "$SUP_SLUG" "eval-actor-steering-holder-b" "$SUP_NOW" 1800
|
|
839
880
|
|
|
840
881
|
# 7a. UserPromptSubmit on a SECOND, unrelated turn (not the takeover turn itself) — the
|
|
@@ -869,6 +910,13 @@ NO_SUP_REPO="$TMPDIR_EVAL/steering-no-supersede-repo"
|
|
|
869
910
|
NO_SUP_SLUG="steering-not-superseded-demo"
|
|
870
911
|
NO_SUP_NOW="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
871
912
|
write_steering_fixture_repo "$NO_SUP_REPO" "$NO_SUP_SLUG" "$NO_SUP_NOW"
|
|
913
|
+
# #440 FIX 2 (de-vacuate, delta review): the invoking actor (eval-actor-steering-self-a) owns
|
|
914
|
+
# NO_SUP_SLUG -- seed its own per-actor pointer (real dual-write helper) so
|
|
915
|
+
# actorScopedWorkflowState finds it. Without this, the "no notice" assertions below would pass
|
|
916
|
+
# only because NO scoped session existed at all (analyze()/actorScopedWorkflowState finds
|
|
917
|
+
# nothing), not because a genuinely scoped session correctly found no other-actor holder -- the
|
|
918
|
+
# exact "no scoped session" vs "scoped session, no other holder" distinction this case must prove.
|
|
919
|
+
seed_current_pointer "$NO_SUP_REPO" "$NO_SUP_SLUG" "eval-actor-steering-self-a"
|
|
872
920
|
# No liveness stream at all for this repo — no other-actor holder can exist.
|
|
873
921
|
|
|
874
922
|
if FLOW_AGENTS_ACTOR="eval-actor-steering-self-a" node "$ROOT/scripts/hooks/workflow-steering.js" \
|
|
@@ -880,6 +928,14 @@ then
|
|
|
880
928
|
else
|
|
881
929
|
fail "steering hook should not fail for UserPromptSubmit on the non-superseded fixture"
|
|
882
930
|
fi
|
|
931
|
+
# #440 FIX 2 reachability control (delta review): prove the session WAS scoped/found (the STATE
|
|
932
|
+
# banner is present) so the "no SUPERSEDED notice" checks below prove "scoped session, no other
|
|
933
|
+
# holder" rather than vacuously "no scoped session at all".
|
|
934
|
+
if grep -qF "STATE: $NO_SUP_SLUG is status:in_progress phase:execution" "$TMPDIR_EVAL/steering-no-sup-prompt.out"; then
|
|
935
|
+
pass "#440 reachability control: actor A's own scoped session is found at UserPromptSubmit (STATE banner present) (AC5)"
|
|
936
|
+
else
|
|
937
|
+
fail "#440 reachability control: actor A's own scoped session was NOT found at UserPromptSubmit (no STATE banner): $(cat "$TMPDIR_EVAL/steering-no-sup-prompt.out")"
|
|
938
|
+
fi
|
|
883
939
|
if grep -qF "[SUPERSEDED:" "$TMPDIR_EVAL/steering-no-sup-prompt.out"; then
|
|
884
940
|
fail "supersession notice incorrectly appeared at UserPromptSubmit with no other-actor fresh holder (false positive) (AC5)"
|
|
885
941
|
else
|
|
@@ -895,6 +951,13 @@ then
|
|
|
895
951
|
else
|
|
896
952
|
fail "steering hook should not fail for SessionStart on the non-superseded fixture"
|
|
897
953
|
fi
|
|
954
|
+
# #440 FIX 2 reachability control (delta review): same rationale as the UserPromptSubmit check
|
|
955
|
+
# above, for SessionStart's RESUME block.
|
|
956
|
+
if grep -qF "RESUME: $NO_SUP_SLUG" "$TMPDIR_EVAL/steering-no-sup-start.out"; then
|
|
957
|
+
pass "#440 reachability control: actor A's own scoped session is found at SessionStart (RESUME banner present) (AC5)"
|
|
958
|
+
else
|
|
959
|
+
fail "#440 reachability control: actor A's own scoped session was NOT found at SessionStart (no RESUME banner): $(cat "$TMPDIR_EVAL/steering-no-sup-start.out")"
|
|
960
|
+
fi
|
|
898
961
|
if grep -qF "[SUPERSEDED:" "$TMPDIR_EVAL/steering-no-sup-start.out"; then
|
|
899
962
|
fail "supersession notice incorrectly appeared at SessionStart with no other-actor fresh holder (false positive) (AC5)"
|
|
900
963
|
else
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
set -uo pipefail
|
|
4
4
|
|
|
5
5
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
6
|
+
CURRENT_POINTER_HELPER="$ROOT/scripts/hooks/lib/current-pointer.js"
|
|
6
7
|
source "$ROOT/evals/lib/node.sh"
|
|
7
8
|
|
|
8
9
|
TMPDIR_EVAL="$(mktemp -d)"
|
|
@@ -4467,9 +4468,17 @@ fi
|
|
|
4467
4468
|
|
|
4468
4469
|
# Poison BOTH navigation pointers. Exact-session writers must continue to use the persisted
|
|
4469
4470
|
# canonical flow_run from the session instead of treating ambient pointers as authority.
|
|
4471
|
+
# #440 fix-wave 2: the per-actor filename is now collision-resistant (sanitized prefix + hash of
|
|
4472
|
+
# the FULL actor key, see current-pointer.js's perActorCurrentFile) -- resolve the real per-actor
|
|
4473
|
+
# path via the shared helper instead of hand-deriving the old (now-stale) "<actor>.json" name.
|
|
4474
|
+
UNRESOLVABLE_PER_ACTOR_FILE="$(CP_HELPER_ARG="$CURRENT_POINTER_HELPER" ROOT_ARG="$UNRESOLVABLE_ROOT" ACTOR_ARG="unresolvable-actor" node - <<'NODE'
|
|
4475
|
+
const { perActorCurrentFile } = require(process.env.CP_HELPER_ARG);
|
|
4476
|
+
process.stdout.write(perActorCurrentFile(process.env.ROOT_ARG, process.env.ACTOR_ARG));
|
|
4477
|
+
NODE
|
|
4478
|
+
)"
|
|
4470
4479
|
node --input-type=module <<NODEOF 2>"$TMPDIR_EVAL/unresolvable-mutate.err"
|
|
4471
4480
|
import { readFileSync, writeFileSync } from 'node:fs';
|
|
4472
|
-
for (const f of ['${UNRESOLVABLE_ROOT}/current.json', '${
|
|
4481
|
+
for (const f of ['${UNRESOLVABLE_ROOT}/current.json', '${UNRESOLVABLE_PER_ACTOR_FILE}']) {
|
|
4473
4482
|
const d = JSON.parse(readFileSync(f, 'utf8'));
|
|
4474
4483
|
d.active_flow_id = 'builder.bogus-nonexistent-flow-270';
|
|
4475
4484
|
writeFileSync(f, JSON.stringify(d, null, 2) + '\n');
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
set -uo pipefail
|
|
4
4
|
|
|
5
5
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
6
|
+
CURRENT_POINTER_HELPER="$ROOT/scripts/hooks/lib/current-pointer.js"
|
|
6
7
|
|
|
7
8
|
TMPDIR_EVAL="$(mktemp -d)"
|
|
8
9
|
errors=0
|
|
@@ -43,7 +44,22 @@ cat > "$REPO/.kontourai/flow-agents/steering-demo/trust.bundle" <<'JSON'
|
|
|
43
44
|
{"schema_version":"1.0","claims":[]}
|
|
44
45
|
JSON
|
|
45
46
|
|
|
46
|
-
|
|
47
|
+
# #440 FIXTURE-GAP: this file never set FLOW_AGENTS_ACTOR (every invocation ran under whichever
|
|
48
|
+
# ambient/ancestry actor happened to resolve, never asserted on). The 4 assertions below that
|
|
49
|
+
# expect steering-demo's STATE/RESUME banner need a RESOLVED actor that legitimately owns
|
|
50
|
+
# steering-demo -- a stable, explicit override plus its own per-actor current pointer (mirroring
|
|
51
|
+
# workflow-sidecar.ts's real writeCurrent() dual-write via current-pointer.js's own
|
|
52
|
+
# writePerActorCurrent). Only the specific invocations that need this pass FLOW_AGENTS_ACTOR below
|
|
53
|
+
# -- every other invocation in this file is untouched and keeps resolving via whatever ambient
|
|
54
|
+
# actor it always has (harmless: none of the untouched assertions depend on `current`).
|
|
55
|
+
STEERING_ACTOR="eval-workflow-steering-actor"
|
|
56
|
+
CP_HELPER_ARG="$CURRENT_POINTER_HELPER" FLOW_AGENTS_DIR_ARG="$REPO/.kontourai/flow-agents" \
|
|
57
|
+
SLUG_ARG="steering-demo" ACTOR_ARG="$STEERING_ACTOR" node - <<'NODE'
|
|
58
|
+
const { writePerActorCurrent } = require(process.env.CP_HELPER_ARG);
|
|
59
|
+
writePerActorCurrent(process.env.FLOW_AGENTS_DIR_ARG, process.env.ACTOR_ARG, { active_slug: process.env.SLUG_ARG });
|
|
60
|
+
NODE
|
|
61
|
+
|
|
62
|
+
if FLOW_AGENTS_ACTOR="$STEERING_ACTOR" node "$ROOT/scripts/hooks/workflow-steering.js" >"$TMPDIR_EVAL/steering.out" 2>"$TMPDIR_EVAL/steering.err" <<JSON
|
|
47
63
|
{"cwd":"$REPO","tool_input":{"command":"InvokeSubagents","content":{"subagents":[{"agent_name":"tool-verifier"}]}},"tool_response":"verification finished"}
|
|
48
64
|
JSON
|
|
49
65
|
then
|
|
@@ -137,7 +153,7 @@ else
|
|
|
137
153
|
_fail "workflow steering hook should not fail for ordinary non-subagent tools"
|
|
138
154
|
fi
|
|
139
155
|
|
|
140
|
-
if node "$ROOT/scripts/hooks/workflow-steering.js" >"$TMPDIR_EVAL/prompt.out" 2>"$TMPDIR_EVAL/prompt.err" <<JSON
|
|
156
|
+
if FLOW_AGENTS_ACTOR="$STEERING_ACTOR" node "$ROOT/scripts/hooks/workflow-steering.js" >"$TMPDIR_EVAL/prompt.out" 2>"$TMPDIR_EVAL/prompt.err" <<JSON
|
|
141
157
|
{"hook_event_name":"UserPromptSubmit","cwd":"$REPO","prompt":"continue"}
|
|
142
158
|
JSON
|
|
143
159
|
then
|
|
@@ -174,7 +190,7 @@ else
|
|
|
174
190
|
_fail "Claude hook adapter should not fail for workflow steering"
|
|
175
191
|
fi
|
|
176
192
|
|
|
177
|
-
if node "$ROOT/scripts/hooks/claude-hook-adapter.js" UserPromptSubmit prompt:workflow-steering workflow-steering.js standard,strict >"$TMPDIR_EVAL/claude-prompt-adapter.out" 2>"$TMPDIR_EVAL/claude-prompt-adapter.err" <<JSON
|
|
193
|
+
if FLOW_AGENTS_ACTOR="$STEERING_ACTOR" node "$ROOT/scripts/hooks/claude-hook-adapter.js" UserPromptSubmit prompt:workflow-steering workflow-steering.js standard,strict >"$TMPDIR_EVAL/claude-prompt-adapter.out" 2>"$TMPDIR_EVAL/claude-prompt-adapter.err" <<JSON
|
|
178
194
|
{"hook_event_name":"UserPromptSubmit","cwd":"$REPO","prompt":"continue"}
|
|
179
195
|
JSON
|
|
180
196
|
then
|
|
@@ -551,7 +567,7 @@ else
|
|
|
551
567
|
_fail "Codex hook adapter should not fail after tool-worker execution"
|
|
552
568
|
fi
|
|
553
569
|
|
|
554
|
-
if node "$ROOT/scripts/hooks/codex-hook-adapter.js" prompt:workflow-steering workflow-steering.js standard,strict >"$TMPDIR_EVAL/codex-prompt-adapter.out" 2>"$TMPDIR_EVAL/codex-prompt-adapter.err" <<JSON
|
|
570
|
+
if FLOW_AGENTS_ACTOR="$STEERING_ACTOR" node "$ROOT/scripts/hooks/codex-hook-adapter.js" prompt:workflow-steering workflow-steering.js standard,strict >"$TMPDIR_EVAL/codex-prompt-adapter.out" 2>"$TMPDIR_EVAL/codex-prompt-adapter.err" <<JSON
|
|
555
571
|
{"hook_event_name":"UserPromptSubmit","cwd":"$REPO","prompt":"continue"}
|
|
556
572
|
JSON
|
|
557
573
|
then
|
package/evals/run.sh
CHANGED
|
@@ -175,6 +175,8 @@ run_integration() {
|
|
|
175
175
|
echo ""
|
|
176
176
|
bash "$EVAL_DIR/integration/test_telemetry_usage_pipeline.sh" || result=1
|
|
177
177
|
echo ""
|
|
178
|
+
bash "$EVAL_DIR/integration/test_telemetry_tool_usage.sh" || result=1
|
|
179
|
+
echo ""
|
|
178
180
|
bash "$EVAL_DIR/integration/test_telemetry_doctor.sh" || result=1
|
|
179
181
|
echo ""
|
|
180
182
|
bash "$EVAL_DIR/integration/test_usage_feedback_outcomes.sh" || result=1
|
|
@@ -65,12 +65,135 @@ for dir in "$DIST_DIR/kiro" "$DIST_DIR/claude-code" "$DIST_DIR/codex" "$DIST_DIR
|
|
|
65
65
|
fi
|
|
66
66
|
done
|
|
67
67
|
|
|
68
|
+
if node - "$ROOT_DIR/package.json" "$DIST_DIR" <<'NODE'
|
|
69
|
+
const fs = require("node:fs");
|
|
70
|
+
const path = require("node:path");
|
|
71
|
+
const [sourcePackageFile, dist] = process.argv.slice(2);
|
|
72
|
+
const source = JSON.parse(fs.readFileSync(sourcePackageFile, "utf8"));
|
|
73
|
+
for (const runtime of ["base", "kiro", "claude-code", "codex", "opencode", "pi"]) {
|
|
74
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(dist, runtime, "build", "package.json"), "utf8"));
|
|
75
|
+
if (manifest.name !== "@kontourai/flow-agents" || manifest.version !== source.version || manifest.type !== "module") {
|
|
76
|
+
throw new Error(`${runtime} build package identity does not match Flow Agents ${source.version}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
NODE
|
|
80
|
+
then
|
|
81
|
+
_pass "all bundles stamp their compiled runtime with Flow Agents package identity"
|
|
82
|
+
else
|
|
83
|
+
_fail "bundle compiled-runtime package identity is missing or stale"
|
|
84
|
+
fi
|
|
85
|
+
|
|
68
86
|
if [[ -d "$DIST_DIR/codex/.agents/skills/plan-work" && ! -e "$DIST_DIR/codex/.codex/skills" ]]; then
|
|
69
87
|
_pass "Codex exports portable skills only in the universal .agents catalog"
|
|
70
88
|
else
|
|
71
89
|
_fail "Codex portable skills are not rooted exclusively at .agents/skills"
|
|
72
90
|
fi
|
|
73
91
|
|
|
92
|
+
if [[ ! -e "$DIST_DIR/codex/AGENTS.md" ]] \
|
|
93
|
+
&& grep -q 'Generated from the canonical source' "$DIST_DIR/codex/README.md" \
|
|
94
|
+
&& grep -q '## Exported agents' "$DIST_DIR/codex/README.md" \
|
|
95
|
+
&& grep -q '`tool-worker`' "$DIST_DIR/codex/README.md" \
|
|
96
|
+
&& ! grep -q 'Universal Agent Bundle (Codex)' "$DIST_DIR/codex/README.md"; then
|
|
97
|
+
_pass "Codex provenance and agent inventory are documentation, not global instructions"
|
|
98
|
+
else
|
|
99
|
+
_fail "Codex bundle still exposes generated global instructions or lost README discovery metadata"
|
|
100
|
+
fi
|
|
101
|
+
|
|
102
|
+
echo ""
|
|
103
|
+
echo "--- Documentation and Instruction Discovery Contract ---"
|
|
104
|
+
# Keep this table aligned with the supported-runtime capability policy. A dash
|
|
105
|
+
# means the harness has no bundle-owned repository instruction file.
|
|
106
|
+
while IFS='|' read -r runtime bundle_dir instruction_rel; do
|
|
107
|
+
readme="$DIST_DIR/$bundle_dir/README.md"
|
|
108
|
+
if [[ -f "$readme" ]] \
|
|
109
|
+
&& grep -q 'Generated from the canonical source' "$readme" \
|
|
110
|
+
&& grep -q '## Exported agents' "$readme" \
|
|
111
|
+
&& grep -q '`tool-worker`' "$readme"; then
|
|
112
|
+
_pass "$runtime keeps provenance and the exported-agent inventory in README.md"
|
|
113
|
+
else
|
|
114
|
+
_fail "$runtime README.md is missing canonical provenance or exported-agent inventory"
|
|
115
|
+
fi
|
|
116
|
+
|
|
117
|
+
if [[ "$instruction_rel" == "-" ]]; then
|
|
118
|
+
if [[ ! -e "$DIST_DIR/$bundle_dir/AGENTS.md" && ! -e "$DIST_DIR/$bundle_dir/CLAUDE.md" ]]; then
|
|
119
|
+
_pass "$runtime does not publish a repository instruction surface"
|
|
120
|
+
else
|
|
121
|
+
_fail "$runtime publishes an instruction file despite declaring no discovery surface"
|
|
122
|
+
fi
|
|
123
|
+
continue
|
|
124
|
+
fi
|
|
125
|
+
|
|
126
|
+
instruction="$DIST_DIR/$bundle_dir/$instruction_rel"
|
|
127
|
+
if [[ -f "$instruction" ]]; then
|
|
128
|
+
_pass "$runtime publishes concise guidance on $instruction_rel"
|
|
129
|
+
else
|
|
130
|
+
_fail "$runtime is missing its $instruction_rel discovery surface"
|
|
131
|
+
continue
|
|
132
|
+
fi
|
|
133
|
+
|
|
134
|
+
if ! grep -Eq 'Treat the repo root as the source of truth|regenerate the bundle|were copied from the canonical source|Kiro-only hook wiring was stripped|## Exported Agents|## Exported agents' "$instruction" \
|
|
135
|
+
&& [[ "$(grep -Ec '^- `tool-' "$instruction" || true)" -eq 0 ]]; then
|
|
136
|
+
_pass "$runtime instructions exclude maintainer prose and the exported-agent catalog"
|
|
137
|
+
else
|
|
138
|
+
_fail "$runtime instructions contain maintainer provenance, copied-directory notes, Kiro stripping notes, or a full agent catalog"
|
|
139
|
+
fi
|
|
140
|
+
done <<'MATRIX'
|
|
141
|
+
Base|base|AGENTS.md
|
|
142
|
+
Kiro|kiro|AGENTS.md
|
|
143
|
+
Claude Code|claude-code|CLAUDE.md
|
|
144
|
+
Codex|codex|-
|
|
145
|
+
OpenCode|opencode|AGENTS.md
|
|
146
|
+
pi|pi|AGENTS.md
|
|
147
|
+
MATRIX
|
|
148
|
+
|
|
149
|
+
if node - "$ROOT_DIR/src/tools/build-universal-bundles.ts" <<'NODE'
|
|
150
|
+
const fs = require("node:fs");
|
|
151
|
+
const source = fs.readFileSync(process.argv[2], "utf8");
|
|
152
|
+
for (const runtime of ["base", "kiro", "claude-code", "codex", "opencode", "pi"]) {
|
|
153
|
+
if (!source.includes(`\"${runtime}\"`)) throw new Error(`shared capability policy missing ${runtime}`);
|
|
154
|
+
}
|
|
155
|
+
for (const required of ["BUNDLE_CAPABILITIES", "renderBundleReadme", "renderOperationalInstructions", "installScript"]) {
|
|
156
|
+
if (!source.includes(required)) throw new Error(`shared bundle policy missing ${required}`);
|
|
157
|
+
}
|
|
158
|
+
if (source.includes("exportRootAgentsMd")) throw new Error("legacy combined provenance/instruction renderer remains");
|
|
159
|
+
|
|
160
|
+
const builders = source.match(/function build(?:Base|Kiro|ClaudeCode|Codex|Opencode|Pi)\([^]*?(?=\nfunction |\nexport |\nmain\()/g) || [];
|
|
161
|
+
if (builders.length !== 6) throw new Error(`expected six runtime builders, found ${builders.length}`);
|
|
162
|
+
for (const builder of builders) {
|
|
163
|
+
if (!builder.includes("BUNDLE_CAPABILITIES")) throw new Error("runtime builder does not select shared capability data");
|
|
164
|
+
if (!builder.includes("installScript(")) throw new Error("runtime builder bypasses the shared installer generator");
|
|
165
|
+
if (/AGENTS\.md|CLAUDE\.md/.test(builder)) throw new Error("runtime builder contains a local instruction-file branch");
|
|
166
|
+
}
|
|
167
|
+
NODE
|
|
168
|
+
then
|
|
169
|
+
_pass "all runtime builders select shared capabilities, renderers, and installer policy without local instruction branches"
|
|
170
|
+
else
|
|
171
|
+
_fail "runtime bundle policy is duplicated or bypasses shared capabilities/renderers/installer preservation"
|
|
172
|
+
fi
|
|
173
|
+
|
|
174
|
+
if (cd "$ROOT_DIR" && node scripts/audit-codex-legacy-agents.js packaging/codex-legacy-agents-fingerprints.json >/tmp/codex-legacy-agents-audit.txt 2>&1) \
|
|
175
|
+
&& grep -q 'Audited 14 seed-capable releases; 1 unique payload(s); gaps=0' /tmp/codex-legacy-agents-audit.txt; then
|
|
176
|
+
_pass "Codex legacy fingerprint catalog independently covers all seed-capable release tags"
|
|
177
|
+
else
|
|
178
|
+
_fail "Codex legacy fingerprint catalog is incomplete, stale, or not byte-reproducible"
|
|
179
|
+
fi
|
|
180
|
+
|
|
181
|
+
if node - "$ROOT_DIR/scripts/classify-codex-legacy-agents.js" <<'NODE'
|
|
182
|
+
const fs = require("node:fs");
|
|
183
|
+
const source = fs.readFileSync(process.argv[2], "utf8");
|
|
184
|
+
for (const forbidden of ["renameSync", "unlinkSync", "writeFileSync", "copyFileSync", "mkdirSync", "chmodSync", "MIGRATION_TEST", "--apply", "quarantine", "mv --", "rm --", "cp --"]) {
|
|
185
|
+
if (source.includes(forbidden)) throw new Error(`production classifier contains destructive/test path: ${forbidden}`);
|
|
186
|
+
}
|
|
187
|
+
for (const required of ["lstatSync", "openSync", "O_RDONLY", "O_NOFOLLOW", "fstatSync", "readFileSync(fd)", "createHash", "manual remediation checklist", "shellQuote"]) {
|
|
188
|
+
if (!source.includes(required)) throw new Error(`production classifier missing read-only behavior: ${required}`);
|
|
189
|
+
}
|
|
190
|
+
NODE
|
|
191
|
+
then
|
|
192
|
+
_pass "Codex legacy classifier is read-only and exposes no production apply or test hooks"
|
|
193
|
+
else
|
|
194
|
+
_fail "Codex legacy classifier contains mutation/apply/test-hook behavior"
|
|
195
|
+
fi
|
|
196
|
+
|
|
74
197
|
if node - "$DIST_DIR/codex/.agents/skills" <<'NODE'
|
|
75
198
|
const fs = require("node:fs");
|
|
76
199
|
const path = require("node:path");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kontourai/flow-agents",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.10.0",
|
|
4
4
|
"description": "Flow Agents — a Kontour product that applies Flow and Veritas discipline as a portable process layer inside the agent tools you already use: Claude Code, Codex, Kiro, opencode, pi, and GitHub Actions — with framework adapters (AWS Strands preview) on the same policy-engine contract.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agents",
|
package/packaging/README.md
CHANGED
|
@@ -25,6 +25,27 @@ All targets also receive shared canonical directories where supported: `context/
|
|
|
25
25
|
|
|
26
26
|
`docs/` and `evals/` are intentionally included in generated bundles today. `docs/` gives installed agents durable local reference material, and `evals/` provides install-time and runtime smoke tests for the exported bundle. If bundle size becomes a product constraint, prune these through `packaging/manifest.json` and update install tests rather than deleting generated output by hand.
|
|
27
27
|
|
|
28
|
+
## Documentation And Instruction Discovery
|
|
29
|
+
|
|
30
|
+
Every runtime bundle puts generated-source provenance and the complete exported-agent inventory in `README.md`. That content is maintainer documentation, not agent instructions. A shared runtime capability policy separately decides whether a bundle publishes a concise repository instruction file:
|
|
31
|
+
|
|
32
|
+
| Runtime | Bundle documentation | Repository instruction surface |
|
|
33
|
+
| --- | --- | --- |
|
|
34
|
+
| Base | `README.md` | `AGENTS.md` |
|
|
35
|
+
| Kiro | `README.md` | `AGENTS.md` |
|
|
36
|
+
| Claude Code | `README.md` | `CLAUDE.md` |
|
|
37
|
+
| Codex | `README.md` | None |
|
|
38
|
+
| OpenCode | `README.md` | `AGENTS.md` |
|
|
39
|
+
| pi | `README.md` | `AGENTS.md` |
|
|
40
|
+
|
|
41
|
+
Instruction files contain only concise operational guidance discoverable by the target harness. They do not contain regeneration advice, packaging provenance, or the exported-agent catalog.
|
|
42
|
+
|
|
43
|
+
## Repository Instruction Preservation
|
|
44
|
+
|
|
45
|
+
All generated installers use one preservation policy for repository instructions. An existing `AGENTS.md` or `CLAUDE.md` is repository-owned and is never overwritten or deleted. The installer excludes both paths from synchronization, including Kiro's `rsync --delete` path, and creates a runtime's declared instruction file only when that destination path is absent. This applies equally to direct bundle installation and `flow-agents init`.
|
|
46
|
+
|
|
47
|
+
The CLI does not own a second instruction policy: `flow-agents init` selects a generated runtime bundle and invokes that bundle's `install.sh`. Changes to discovery or preservation therefore belong in the shared bundle capability and installer generation code, with direct and CLI-mediated fixtures covering the same behavior.
|
|
48
|
+
|
|
28
49
|
## Generated And Runtime Boundaries
|
|
29
50
|
|
|
30
51
|
`dist/` is a generated export surface, not the source of truth. Installed runtime directories such as `.codex/` and `.claude/` are also not source. They are created from the generated target bundle and installer scripts. If generated or installed hook config is wrong, fix the canonical source, rebuild `dist/`, and reinstall the runtime config.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema_version": 2,
|
|
3
|
+
"audit": {
|
|
4
|
+
"first_tag": "v2.4.0",
|
|
5
|
+
"last_tag": "v3.9.0",
|
|
6
|
+
"selection": "Every reachable release tag whose scripts/install-codex-home.sh contains the exact root seed loop 'for seed_file in AGENTS.md'.",
|
|
7
|
+
"reproduction_command": "node scripts/audit-codex-legacy-agents.js packaging/codex-legacy-agents-fingerprints.json",
|
|
8
|
+
"gaps": []
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
{
|
|
12
|
+
"sha256": "5273878130bdafc8a024a650bb5b66c9b003f1f859b5dc6e5b588cbf4ab23228",
|
|
13
|
+
"bytes": 3315,
|
|
14
|
+
"fixture": "evals/fixtures/codex-legacy-agents/5273878130bdafc8a024a650bb5b66c9b003f1f859b5dc6e5b588cbf4ab23228.md",
|
|
15
|
+
"covered_releases": [
|
|
16
|
+
{ "tag": "v2.4.0", "commit": "bee760272b8bb770df02400ccc5881bd3dbc8806" },
|
|
17
|
+
{ "tag": "v3.0.0", "commit": "99abf1f6f44a333867b03702548927a12ad01275" },
|
|
18
|
+
{ "tag": "v3.1.0", "commit": "7e6a9594212ef7813c428069b9f7fc6f39def8f1" },
|
|
19
|
+
{ "tag": "v3.2.0", "commit": "15d5e9ae2bb9a80c3b3a89bf7887b5eff596f5d8" },
|
|
20
|
+
{ "tag": "v3.3.0", "commit": "21c98807a259dfc4449bae2f359e975893dc4f4f" },
|
|
21
|
+
{ "tag": "v3.4.0", "commit": "130a94696dac17cf1a65e6525e9168a835db79ff" },
|
|
22
|
+
{ "tag": "v3.4.1", "commit": "0b41434f6422de62a8e06e48998a0699e3f623d2" },
|
|
23
|
+
{ "tag": "v3.4.2", "commit": "c0d1724f0a5212942b0f457f4a7ae99c5f71da6b" },
|
|
24
|
+
{ "tag": "v3.4.3", "commit": "07f98df4b7f737b543e3469ce3792fee2e5e6b81" },
|
|
25
|
+
{ "tag": "v3.5.0", "commit": "4ffc9f9d7a5eeea006acddcdb259299fcc56354d" },
|
|
26
|
+
{ "tag": "v3.6.0", "commit": "9f59f16143d7c6cc877778e4ad6620e2fc0cd829" },
|
|
27
|
+
{ "tag": "v3.7.0", "commit": "4901adc14036c25d4ed98a7eac8c8e7b15b6a048" },
|
|
28
|
+
{ "tag": "v3.8.0", "commit": "697795c87b0296d037ae76ee1984ce0eaa6684fb" },
|
|
29
|
+
{ "tag": "v3.9.0", "commit": "a5c272bd19b563bd4e3da5db017e996eb0f854d0" }
|
|
30
|
+
]
|
|
31
|
+
}
|
|
32
|
+
]
|
|
33
|
+
}
|
package/scripts/README.md
CHANGED
|
@@ -67,10 +67,11 @@ renamed, or changes category, update the table and the validator together.
|
|
|
67
67
|
| `workflow-steering.js` | policy hook | `evals/integration/test_workflow_steering_hook.sh` | Provides workflow guidance from current artifact state. |
|
|
68
68
|
| `pre-commit-quality.js` | repo guardrail hook | `evals/integration/test_hook_category_behaviors.sh` | Supports repository Git hook checks, not installed runtime hooks. |
|
|
69
69
|
| `desktop-notify.sh` | local notification helper | `evals/integration/test_hook_category_behaviors.sh` | Optional local desktop notification helper. |
|
|
70
|
-
| `lib/actor-identity.js` | shared hook library | `evals/integration/test_actor_identity.sh` |
|
|
70
|
+
| `lib/actor-identity.js` | shared hook library | `evals/integration/test_actor_identity.sh` | Canonical runtime-agnostic actor resolver (`resolveActor`), including privacy-safe Codex thread identity and explicitly unstable process-ancestry fallback. TypeScript consumers load this file directly; there is no maintained mirror. |
|
|
71
71
|
| `lib/audit-transport.sh` | shared hook library | `evals/integration/test_hook_category_behaviors.sh`, `evals/integration/test_telemetry.sh` | Shared audit event transport functions. |
|
|
72
72
|
| `lib/codex-exit-code.js` | shared hook library | `evals/acceptance/prove-capture-teeth.sh` | Shared codex-only host-banner exit-code extraction (`extractExitCodeFromBanner`, `readExitCodeFromRollout`) — parses the `Process exited with code N` prose the codex CLI writes into a `function_call_output` payload or session rollout so `codex-hook-adapter.js` can inject a structured exit code before `evidence-capture.js` observes (issue #470). |
|
|
73
73
|
| `lib/config-protection-remedies.js` | shared hook library | `evals/integration/test_hook_category_behaviors.sh` | Sanctioned-remedy table for `config-protection.js` (WS8/ADR 0020 extraction). |
|
|
74
|
+
| `lib/continuation-turn-authority.js` | shared hook library | `src/cli/continuation-driver.test.mjs`, `evals/integration/test_goal_fit_hook.sh` | Signed transient authority for one public continuation adapter turn, independently anchored by the signer digest in driver mission state. Assignment-gated evidence mutation can fall back to its live signed assignment identity only after ordinary resolution mismatches; lifecycle controls never use it. Stop keeps a base-valid signed run selected across canonical dispositions, validates canonical state against its definition, and treats only ordinary active-gate warnings as advisory. Same-UID filesystem/process attacks remain outside the cooperative boundary. |
|
|
74
75
|
| `lib/current-pointer.js` | shared hook library | `evals/integration/test_current_json_per_actor.sh` | Shared per-actor "current" pointer reader/writer (`readCurrentPointer`, `writePerActorCurrent`) — the compat-shim fallback (per-actor `current/<actor>.json` first, legacy global `current.json` fallback) every actor-aware consumer routes through (issue #291). |
|
|
75
76
|
| `lib/hook-flags.js` | shared hook library | `evals/integration/test_hook_category_behaviors.sh` | Shared profile/disable flag parsing. |
|
|
76
77
|
| `lib/kit-catalog.js` | shared hook library | `evals/integration/test_workflow_steering_hook.sh` | Build-free kit catalog reader used by `workflow-steering.js` to load kit-declared workflow triggers while hooks remain fail-open and independent of compiled TypeScript output. |
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
const cp = require("node:child_process");
|
|
5
|
+
const crypto = require("node:crypto");
|
|
6
|
+
const fs = require("node:fs");
|
|
7
|
+
|
|
8
|
+
function git(...args) { return cp.execFileSync("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }); }
|
|
9
|
+
function show(tag, file) { return git("show", `${tag}:${file}`); }
|
|
10
|
+
function payloadAt(tag) {
|
|
11
|
+
const manifest = JSON.parse(show(tag, "packaging/manifest.json"));
|
|
12
|
+
const excluded = new Set(manifest.codex?.excluded_agents ?? []);
|
|
13
|
+
const names = git("ls-tree", "--name-only", `${tag}:agents`).trim().split(/\n/).filter((name) => name.endsWith(".json"));
|
|
14
|
+
const agents = names.map((name) => JSON.parse(show(tag, `agents/${name}`)))
|
|
15
|
+
.filter((agent) => !excluded.has(agent.name)).sort((a, b) => a.name.localeCompare(b.name));
|
|
16
|
+
const summary = agents.map((agent) => `- \`${agent.name}\` — ${String(agent.description ?? "").trim()}`).join("\n");
|
|
17
|
+
return Buffer.from(`# Universal Agent Bundle (Codex)\n\nThis bundle was generated from the canonical source in this repo. Treat the repo root as the source of truth and regenerate the bundle instead of editing exported agent files by hand.\n\n## Shared Conventions\n\n- \`skills/\`, \`context/\`, \`powers/\`, \`prompts/\`, \`scripts/\`, and \`evals/\` were copied from the canonical source.\n- Cross-session task artifacts should live under \`${manifest.codex.task_dir}\`.\n- Kiro-only hook wiring was stripped from exported non-Kiro agents to keep the package portable.\n\n## Exported Agents\n\n${summary}\n`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const catalogPath = process.argv[2];
|
|
21
|
+
if (!catalogPath) throw new Error("usage: audit-codex-legacy-agents.js CATALOG");
|
|
22
|
+
const catalog = JSON.parse(fs.readFileSync(catalogPath, "utf8"));
|
|
23
|
+
const tags = git("tag", "--sort=version:refname").trim().split(/\n/).filter(Boolean);
|
|
24
|
+
const discovered = [];
|
|
25
|
+
for (const tag of tags) {
|
|
26
|
+
let installer;
|
|
27
|
+
try { installer = show(tag, "scripts/install-codex-home.sh"); } catch { continue; }
|
|
28
|
+
if (!/for seed_file in AGENTS\.md/.test(installer)) continue;
|
|
29
|
+
const bytes = payloadAt(tag);
|
|
30
|
+
discovered.push({ tag, commit: git("rev-list", "-n1", tag).trim(), bytes: bytes.length, sha256: crypto.createHash("sha256").update(bytes).digest("hex"), payload: bytes });
|
|
31
|
+
}
|
|
32
|
+
const covered = new Map();
|
|
33
|
+
for (const file of catalog.files ?? []) for (const release of file.covered_releases ?? []) covered.set(release.tag, { ...release, file });
|
|
34
|
+
for (const release of discovered) {
|
|
35
|
+
const entry = covered.get(release.tag);
|
|
36
|
+
if (!entry) throw new Error(`catalog omits seed-capable release ${release.tag}`);
|
|
37
|
+
if (entry.commit !== release.commit || entry.file.sha256 !== release.sha256 || entry.file.bytes !== release.bytes) throw new Error(`catalog mismatch for ${release.tag}`);
|
|
38
|
+
if (!entry.file.fixture || !fs.existsSync(entry.file.fixture) || !release.payload.equals(fs.readFileSync(entry.file.fixture))) throw new Error(`fixture mismatch for ${release.tag}`);
|
|
39
|
+
}
|
|
40
|
+
for (const tag of covered.keys()) if (!discovered.some((release) => release.tag === tag)) throw new Error(`catalog includes non-seed-capable or unreachable release ${tag}`);
|
|
41
|
+
if (catalog.audit?.first_tag !== discovered[0]?.tag || catalog.audit?.last_tag !== discovered.at(-1)?.tag) throw new Error("catalog audit interval does not match independently discovered releases");
|
|
42
|
+
if (!Array.isArray(catalog.audit?.gaps)) throw new Error("catalog must explicitly record reproduction gaps");
|
|
43
|
+
console.log(`Audited ${discovered.length} seed-capable releases; ${new Set(discovered.map((item) => item.sha256)).size} unique payload(s); gaps=${catalog.audit.gaps.length}`);
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
const crypto = require("node:crypto");
|
|
5
|
+
const fs = require("node:fs");
|
|
6
|
+
const path = require("node:path");
|
|
7
|
+
|
|
8
|
+
function shellQuote(value) { return `'${String(value).replace(/'/g, `'"'"'`)}'`; }
|
|
9
|
+
function advisory(message) { console.error(`classify-codex-legacy-agents: ${message}`); }
|
|
10
|
+
|
|
11
|
+
const [destinationArg, catalogArg] = process.argv.slice(2);
|
|
12
|
+
if (!destinationArg || !catalogArg) {
|
|
13
|
+
advisory("usage: classify-codex-legacy-agents.js DEST CATALOG");
|
|
14
|
+
process.exit(2);
|
|
15
|
+
}
|
|
16
|
+
const destination = path.resolve(destinationArg);
|
|
17
|
+
const source = path.join(destination, "AGENTS.md");
|
|
18
|
+
let stat;
|
|
19
|
+
try { stat = fs.lstatSync(source); }
|
|
20
|
+
catch (error) {
|
|
21
|
+
if (error?.code === "ENOENT") process.exit(0);
|
|
22
|
+
advisory(`cannot inspect ${source}; preserved without changes: ${error.message}`);
|
|
23
|
+
process.exit(2);
|
|
24
|
+
}
|
|
25
|
+
if (stat.isSymbolicLink() || !stat.isFile()) {
|
|
26
|
+
advisory(`preserved ambiguous non-regular or symlink object without changes: ${source}`);
|
|
27
|
+
process.exit(0);
|
|
28
|
+
}
|
|
29
|
+
if (fs.constants.O_NOFOLLOW === undefined) {
|
|
30
|
+
advisory(`platform lacks O_NOFOLLOW; preserved without classification: ${source}`);
|
|
31
|
+
process.exit(2);
|
|
32
|
+
}
|
|
33
|
+
let fd;
|
|
34
|
+
let bytes;
|
|
35
|
+
try {
|
|
36
|
+
fd = fs.openSync(source, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
|
|
37
|
+
const opened = fs.fstatSync(fd);
|
|
38
|
+
if (!opened.isFile()) {
|
|
39
|
+
advisory(`preserved non-regular descriptor without changes: ${source}`);
|
|
40
|
+
process.exit(0);
|
|
41
|
+
}
|
|
42
|
+
bytes = fs.readFileSync(fd);
|
|
43
|
+
} catch (error) {
|
|
44
|
+
advisory(`cannot open/read ${source} without following links; preserved without changes: ${error.message}`);
|
|
45
|
+
process.exit(2);
|
|
46
|
+
} finally {
|
|
47
|
+
if (fd !== undefined) fs.closeSync(fd);
|
|
48
|
+
}
|
|
49
|
+
const digest = crypto.createHash("sha256").update(bytes).digest("hex");
|
|
50
|
+
const catalog = JSON.parse(fs.readFileSync(catalogArg, "utf8"));
|
|
51
|
+
const match = (catalog.files ?? []).find((entry) => entry.sha256 === digest && entry.bytes === bytes.length);
|
|
52
|
+
if (!match) {
|
|
53
|
+
advisory(`preserved unrecognized or user-owned ${source} (sha256=${digest}, bytes=${bytes.length})`);
|
|
54
|
+
process.exit(0);
|
|
55
|
+
}
|
|
56
|
+
const releases = (match.covered_releases ?? []).map((item) => `${item.tag}@${item.commit}`).join(", ");
|
|
57
|
+
advisory(`REFUSING INSTALL: exact historical Flow Agents-generated global instructions remain active`);
|
|
58
|
+
advisory(`path=${source}`);
|
|
59
|
+
advisory(`sha256=${digest} bytes=${bytes.length}`);
|
|
60
|
+
advisory(`matching_releases=${releases}`);
|
|
61
|
+
advisory("automatic mutation is disabled because portable Node cannot condition pathname removal on the inode that was classified");
|
|
62
|
+
advisory(`evidence command (hash): shasum -a 256 -- ${shellQuote(source)}`);
|
|
63
|
+
advisory(`evidence command (bytes): wc -c < ${shellQuote(source)}`);
|
|
64
|
+
advisory("manual remediation checklist:");
|
|
65
|
+
advisory("1. Stop all agent and writer processes that can modify the Codex home.");
|
|
66
|
+
advisory(`2. Revalidate the file is still exactly sha256=${digest} and bytes=${bytes.length} using the evidence commands above.`);
|
|
67
|
+
advisory("3. Choose a new, unused recovery path outside every agent instruction-discovery location.");
|
|
68
|
+
advisory("4. Move the file manually without overwrite using operator-trusted tooling; no mutation command is emitted here.");
|
|
69
|
+
advisory("5. Inspect the preserved recovery file and confirm its exact hash and byte count.");
|
|
70
|
+
advisory("6. Rerun the Flow Agents Codex installer.");
|
|
71
|
+
process.exit(3);
|
|
@@ -57,8 +57,8 @@
|
|
|
57
57
|
const fs = require('fs');
|
|
58
58
|
const path = require('path');
|
|
59
59
|
const { flowAgentsArtifactRootsForRead } = require('./lib/local-artifact-paths');
|
|
60
|
-
const { resolveActor } = require('./lib/actor-identity.js');
|
|
61
|
-
const {
|
|
60
|
+
const { resolveActor, isUnresolvedActor } = require('./lib/actor-identity.js');
|
|
61
|
+
const { readOwnCurrentPointer } = require('./lib/current-pointer.js');
|
|
62
62
|
const { isAmbiguousAbsenceCommand } = require('./lib/runnable-command.js');
|
|
63
63
|
const crypto = require('crypto');
|
|
64
64
|
|
|
@@ -204,14 +204,19 @@ function latestStateDir(flowAgentsDir) {
|
|
|
204
204
|
}
|
|
205
205
|
|
|
206
206
|
/**
|
|
207
|
-
* Resolve the active artifact directory the same way the other hooks do:
|
|
208
|
-
*
|
|
209
|
-
*
|
|
207
|
+
* Resolve the active artifact directory the same way the other #440-migrated hooks do: prefer
|
|
208
|
+
* the RESOLVED actor's own per-actor `current/<actor>.json` pointer (active_slug / artifact_dir)
|
|
209
|
+
* via `readOwnCurrentPointer` — never the shared legacy `current.json`, and never the repo-wide
|
|
210
|
+
* newest-mtime scan (D1: that would append this actor's OWN captured evidence into an unrelated
|
|
211
|
+
* actor's session directory). Only for an empty/unresolved actor does this fall back to the
|
|
212
|
+
* legacy global `current.json` and, failing that, the newest-mtime `state.json` directory scan —
|
|
213
|
+
* the pre-#440/#291 behavior, unchanged for that case (D3 compat). See the D1/D2/D3 comment
|
|
214
|
+
* inline below for the exact branching.
|
|
210
215
|
*/
|
|
211
216
|
function resolveArtifactDir(root) {
|
|
212
217
|
const actorKey = resolveActor(process.env).actor;
|
|
213
218
|
for (const flowAgentsDir of flowAgentsArtifactRootsForRead(root)) {
|
|
214
|
-
const { payload: current } =
|
|
219
|
+
const { payload: current } = readOwnCurrentPointer(flowAgentsDir, actorKey);
|
|
215
220
|
if (current) {
|
|
216
221
|
const slug = current.artifact_dir || current.active_slug;
|
|
217
222
|
if (typeof slug === 'string' && slug.trim()) {
|
|
@@ -222,6 +227,13 @@ function resolveArtifactDir(root) {
|
|
|
222
227
|
}
|
|
223
228
|
}
|
|
224
229
|
}
|
|
230
|
+
// #440 D1: a resolved actor with no own per-actor pointer never falls back to the repo-wide
|
|
231
|
+
// newest-mtime scan below — that would append this actor's OWN captured evidence into an
|
|
232
|
+
// unrelated actor's session directory (write-side ownership conflation). D2 accepted gap:
|
|
233
|
+
// captured evidence is simply dropped (existing `if (!artifactDir) return rawInput;` no-op in
|
|
234
|
+
// run()) until this actor's next sidecar command establishes its own per-actor pointer. D3: an
|
|
235
|
+
// unresolved actor keeps today's exact global-scan fallback, unchanged.
|
|
236
|
+
if (!isUnresolvedActor(actorKey)) return null;
|
|
225
237
|
for (const flowAgentsDir of flowAgentsArtifactRootsForRead(root)) {
|
|
226
238
|
const latest = latestStateDir(flowAgentsDir);
|
|
227
239
|
if (latest) return latest;
|