@kontourai/flow-agents 2.1.1 → 2.3.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/CODEOWNERS +2 -0
- package/CHANGELOG.md +29 -0
- package/build/src/cli/sidecar-claim-explain.d.ts +45 -0
- package/build/src/cli/sidecar-claim-explain.js +87 -0
- package/build/src/cli/telemetry-doctor.js +2 -4
- package/build/src/cli/usage-feedback.js +16 -8
- package/build/src/cli/workflow-artifact-cleanup-audit.js +4 -2
- package/build/src/cli/workflow-sidecar.d.ts +2 -44
- package/build/src/cli/workflow-sidecar.js +18 -87
- package/build/src/index.d.ts +1 -0
- package/build/src/index.js +1 -0
- package/build/src/lib/local-artifact-root.d.ts +11 -0
- package/build/src/lib/local-artifact-root.js +30 -0
- package/build/src/tools/validate-source-tree.js +1 -0
- package/context/scripts/telemetry/lib/config.sh +3 -4
- package/docs/adr/0018-freeze-local-shell-heuristics.md +95 -0
- package/docs/repository-structure.md +7 -3
- package/evals/integration/test_bundle_lifecycle.sh +9 -9
- package/evals/integration/test_gate_lockdown.sh +4 -0
- package/evals/integration/test_migrate_local_artifacts.sh +102 -0
- package/evals/integration/test_workflow_sidecar_writer.sh +34 -0
- package/evals/run.sh +2 -0
- package/evals/static/test_unit_helpers.sh +26 -0
- package/integrations/strands-ts/package.json +3 -0
- package/integrations/strands-ts/src/telemetry.ts +31 -50
- package/kits/knowledge/adapters/flow-runner/index.js +1 -1
- package/kits/knowledge/adapters/flow-runner/telemetry.js +2 -2
- package/package.json +2 -1
- package/scripts/README.md +1 -0
- package/scripts/hooks/config-protection.js +6 -0
- package/scripts/hooks/evidence-capture.js +16 -10
- package/scripts/hooks/lib/local-artifact-paths.js +32 -0
- package/scripts/hooks/stop-goal-fit.js +19 -13
- package/scripts/hooks/workflow-steering.js +5 -3
- package/scripts/lib/command-log-chain.js +5 -0
- package/scripts/migrate-local-artifacts.mjs +230 -0
- package/scripts/telemetry/lib/config.sh +3 -4
- package/src/cli/public-api.test.mjs +21 -0
- package/src/cli/sidecar-claim-explain.ts +130 -0
- package/src/cli/sidecar-pure-helpers.test.mjs +168 -0
- package/src/cli/telemetry-doctor.ts +2 -4
- package/src/cli/usage-feedback.ts +15 -8
- package/src/cli/workflow-artifact-cleanup-audit.ts +4 -2
- package/src/cli/workflow-sidecar.ts +17 -131
- package/src/index.ts +14 -0
- package/src/lib/local-artifact-root.ts +39 -0
- package/src/tools/validate-source-tree.ts +1 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "ADR 0018: Freeze the Local Shell-Parsing Heuristics; Route New Enforcement to the CI Anchor"
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# ADR 0018: Freeze the Local Shell-Parsing Heuristics; Route New Enforcement to the CI Anchor
|
|
6
|
+
|
|
7
|
+
Status: Accepted
|
|
8
|
+
|
|
9
|
+
## Context
|
|
10
|
+
|
|
11
|
+
ADR 0017 records the anti-gaming trust model: a **layered** defense whose *authoritative*
|
|
12
|
+
boundary is the external CI anchor (`trust-reconcile.js` + the required anti-gaming suite),
|
|
13
|
+
with the local Stop gate and `config-protection.js` as **Layer 1 bar-raisers** — explicitly
|
|
14
|
+
"a bar-raiser, not a wall." That model was earned by a 7-round adversarial loop (PRs
|
|
15
|
+
#213–#228), and ADR 0017 states the loop is "run to convergence."
|
|
16
|
+
|
|
17
|
+
The local shell-parsing heuristics have nonetheless kept accreting per-round string-matching
|
|
18
|
+
rules:
|
|
19
|
+
|
|
20
|
+
- `config-protection.js` — redirect detection (`>`, `>>`, `tee`), interpreter-write
|
|
21
|
+
detection (`node -e`, `py -c`, `sed -i`, `perl -e`), and `cp`/`mv`/`install` target
|
|
22
|
+
matching, grown across R5a / R6 / R7.
|
|
23
|
+
- `hasLaunderingOperator` (now in `scripts/lib/command-log-chain.js`) — the exit-code-mask
|
|
24
|
+
screen, extended in R6a / R7.
|
|
25
|
+
|
|
26
|
+
Their **own headers already admit the ceiling**: *"An agent with shell access can still
|
|
27
|
+
evade. The real anchor is external."* Each new round adds another regex for another evasion
|
|
28
|
+
shape (runtime-constructed paths, base64, exotic tools, here-docs, …) while the set remains,
|
|
29
|
+
by construction, non-exhaustive. This is **low-ROI churn** that also manufactures *false
|
|
30
|
+
confidence* in a control ADR 0017 already classifies as best-effort.
|
|
31
|
+
|
|
32
|
+
Worse, the pattern-chasing has a real cost beyond effort: the blanket `||` rule in
|
|
33
|
+
`hasLaunderingOperator` flags **any** `||` in a claimed verification command, which
|
|
34
|
+
**false-positives legitimate commands** that use `||` for control flow rather than exit-code
|
|
35
|
+
laundering — e.g. `test -d node_modules || npm ci`, `command -v shellcheck || npm i -g shellcheck`.
|
|
36
|
+
It errs toward blocking, which is acceptable for a bar-raiser but not something to keep
|
|
37
|
+
extending.
|
|
38
|
+
|
|
39
|
+
## Decision
|
|
40
|
+
|
|
41
|
+
**Freeze the local shell-parsing heuristics as feature-complete bar-raisers.** Specifically:
|
|
42
|
+
|
|
43
|
+
1. **No new evasion-pattern rules** are added to `config-protection.js`'s redirect/
|
|
44
|
+
interpreter/copy detection or to `hasLaunderingOperator`. The current rule sets are the
|
|
45
|
+
frozen surface. They stand as documented, intentionally-incomplete local bar-raisers.
|
|
46
|
+
|
|
47
|
+
2. **New enforcement concerns route to the external anchor**, not to new local regex:
|
|
48
|
+
- a new way to launder a verification result → strengthen the CI **`trust-reconcile`**
|
|
49
|
+
reconciliation and add a case to the required **anti-gaming suite**
|
|
50
|
+
(`evals/ci/antigaming-suite.sh`);
|
|
51
|
+
- a new self-tamper / kill-switch vector → it is already defended structurally by Layer 2
|
|
52
|
+
(CI does not trust the agent's files or environment) and Layer 4 (CODEOWNERS + required
|
|
53
|
+
checks). Encode the *regression test* there, not another local path matcher.
|
|
54
|
+
|
|
55
|
+
3. **Still allowed** under the freeze: correctness/bug fixes that do **not** expand the
|
|
56
|
+
pattern surface (e.g. removing a clear false-positive, consolidating duplicated copies as
|
|
57
|
+
in ops#20), and changes required to keep the existing rules working. The freeze targets
|
|
58
|
+
*scope growth*, not maintenance.
|
|
59
|
+
|
|
60
|
+
The decisive line — already the consistent finding of ADR 0017 — is that a local control
|
|
61
|
+
running inside the agent's environment cannot be made airtight by adding rules; effort spent
|
|
62
|
+
making the bar-raiser taller has sharply diminishing returns once the external anchor exists.
|
|
63
|
+
|
|
64
|
+
## Accepted limitations (documented, not closed)
|
|
65
|
+
|
|
66
|
+
These are now **accepted properties** of the frozen Layer 1, with the CI anchor as the
|
|
67
|
+
authoritative backstop:
|
|
68
|
+
|
|
69
|
+
- **`config-protection.js` — incomplete coverage.** Runtime-constructed protected paths
|
|
70
|
+
(`homedir() + '/.bashrc'`), base64/multi-step path assembly, interpreters outside the
|
|
71
|
+
listed set (ruby, php, …), `rsync`/`dd`/process-substitution writes, and here-docs are
|
|
72
|
+
**not** caught. *Backstop:* Layer 2 does not trust those files; Layer 4 + human review.
|
|
73
|
+
- **`hasLaunderingOperator` — blanket `||` over-blocks.** A legitimate verification command
|
|
74
|
+
that uses `||` for control flow is flagged as laundering. *Backstop / workaround:* split
|
|
75
|
+
the conditional out of the single claimed-verify command; CI reconciles the real result
|
|
76
|
+
regardless. We keep the blanket rule (fail-toward-blocking) rather than enumerate "safe"
|
|
77
|
+
`||` forms — enumeration is exactly the churn this ADR freezes.
|
|
78
|
+
|
|
79
|
+
## Consequences
|
|
80
|
+
|
|
81
|
+
- The adversarial-hardening loop is declared **converged at the local layer**: ongoing trust
|
|
82
|
+
investment goes to the external anchor (re-run freshness, reconciliation breadth, the
|
|
83
|
+
required suite, activation of `Trust Reconcile` as a no-bypass check — ADR 0017 §Activation,
|
|
84
|
+
issue #225), not to more local string parsing.
|
|
85
|
+
- Reviewers can decline new "Round N" local-heuristic PRs by pointing here: the bar-raiser is
|
|
86
|
+
frozen by decision, and the gap belongs in the CI anchor.
|
|
87
|
+
- No behavior change ships with this ADR. The heuristics keep doing exactly what they do; we
|
|
88
|
+
stop growing them and we write down why.
|
|
89
|
+
|
|
90
|
+
## Related
|
|
91
|
+
|
|
92
|
+
ADR 0017 (anti-gaming trust security model — the layered defense this freezes the local
|
|
93
|
+
layer of), ADR 0016 (three-hard-boundary model), ADR 0010 (workflow trust state as a hachure
|
|
94
|
+
bundle). Implements ops#21. Consolidation of the frozen helpers: ops#20
|
|
95
|
+
(`scripts/lib/command-log-chain.js`).
|
|
@@ -10,9 +10,10 @@ This is the canonical developer-facing map for the Flow Agents repository. Use i
|
|
|
10
10
|
|
|
11
11
|
- Edit canonical source in the repo root areas listed below, then regenerate derived output with the documented commands.
|
|
12
12
|
- Do not edit `dist/`, `build/`, or `_site/` by hand. They are generated from tracked source.
|
|
13
|
-
- Do not commit local runtime state from `.flow-agents/<slug>/`, `.codex/`, `.claude/`, `.omx/`, `.promptfoo/`, `.telemetry/`, `.surface/`, `.veritas
|
|
13
|
+
- Do not commit local runtime state from `.flow-agents/<slug>/`, `.kontourai/`, `.codex/`, `.claude/`, `.omx/`, `.promptfoo/`, `.telemetry/`, `.surface/`, generated `.veritas/` output, or tool caches; intentionally tracked `.veritas/` governance/config remains durable source.
|
|
14
14
|
- Runtime workflow artifacts stay local and ignored; promote reviewable or durable outcomes to docs, source, schemas, or provider records before merging to `main`.
|
|
15
15
|
- Treat generated exports and installed runtime config as products of `packaging/manifest.json`, `src/tools/build-universal-bundles.ts`, `scripts/install-*.sh`, and the source directories they copy.
|
|
16
|
+
- Use `.kontourai/` for non-durable local or generated Kontour workspace state. Keep durable tracked files in intuitive product-owned paths such as `.veritas/`, `.flow/`, `.agents/`, `.claude/commands`, and `docs/`; existing transition ignores remain during migration.
|
|
16
17
|
|
|
17
18
|
## Target Layout
|
|
18
19
|
|
|
@@ -32,7 +33,7 @@ This is the canonical developer-facing map for the Flow Agents repository. Use i
|
|
|
32
33
|
docs/ # durable docs and GitHub Pages source
|
|
33
34
|
integrations/ # optional external integration config
|
|
34
35
|
dist/ build/ _site/ # generated output; ignored
|
|
35
|
-
.flow-agents/ .codex/ .claude/ ... # local runtime state; ignored by default
|
|
36
|
+
.flow-agents/ .kontourai/ .codex/ .claude/ ... # local runtime state; ignored by default
|
|
36
37
|
```
|
|
37
38
|
|
|
38
39
|
## Top-Level Inventory
|
|
@@ -40,11 +41,12 @@ This is the canonical developer-facing map for the Flow Agents repository. Use i
|
|
|
40
41
|
| Path | Classification | Source of truth | Generated or runtime policy | Safe cleanup rule |
|
|
41
42
|
| --- | --- | --- | --- | --- |
|
|
42
43
|
| `.flow-agents/` | runtime state | Workflow tools write local session artifacts. | Ignored. | Do not commit task runtime roots; promote durable decisions to docs, source, schemas, or providers before merge. |
|
|
44
|
+
| `.kontourai/` | local/generated workspace state | Local Kontour tools and developer workflows. | Ignored. | Safe local cleanup when no active workflow needs it; durable source and decisions stay in product-owned tracked paths. |
|
|
43
45
|
| `.claude/` | installed runtime config | Generated bundle or local runtime install. | Ignored. | Reinstall from `dist/claude-code/` instead of editing as source. |
|
|
44
46
|
| `.codex/` | installed runtime config | Generated bundle or local runtime install. | Ignored. | Reinstall from `dist/codex/` or `scripts/install-codex-home.sh`; do not treat local hooks as canonical. |
|
|
45
47
|
| `.githooks/` | canonical repo tooling | Tracked repository hook scripts. | Source, not runtime agent hooks. | Keep compatible with `npm run setup:repo-hooks` and `npm run validate:repo-hooks --`. |
|
|
46
48
|
| `.github/` | canonical CI config | GitHub workflow files. | Source. | Preserve workflow command names and artifact expectations. |
|
|
47
|
-
| `.ai/`, `.omx/`, `.promptfoo/`, `.surface/`, `.telemetry/`, `.veritas/` | runtime, cache, or integration output | Local tools and
|
|
49
|
+
| `.ai/`, `.omx/`, `.promptfoo/`, `.surface/`, `.telemetry/`, `.veritas/` | runtime, cache, or integration output | Local tools, optional integrations, and generated Veritas output. | Ignored runtime state, except intentionally tracked Veritas governance/config. | Clean locally when not needed; promote only stable integration config under `integrations/` or durable docs. |
|
|
48
50
|
| `.venv/`, `node_modules/`, `test-results/`, `__pycache__/` | dependency/cache output | Package managers and test tools. | Ignored. | Safe local cleanup; recreate with normal install or test commands. |
|
|
49
51
|
| `_site/` | generated docs output | Built from `docs/`. | Ignored. | Recreate with docs preview/build tooling. |
|
|
50
52
|
| `agent-cards/` | canonical source | Discovery metadata for routable agents. | Exported into runtime bundles. | Do not delete without checking bundle manifests and evals. |
|
|
@@ -115,6 +117,8 @@ The package requires Node `>=22`, and GitHub Actions runs CI on Node 22. Keep `@
|
|
|
115
117
|
|
|
116
118
|
`.flow-agents/<slug>/` is workflow working memory. Keep plans, sidecars, evidence, and handoffs there while work is active. Promote stable outcomes into `docs/`, schemas, source, or provider records before final acceptance.
|
|
117
119
|
|
|
120
|
+
Across Kontour repos, `.kontourai/` is the simple ignored home for non-durable local/generated workspace state. It is not a durable source root. Keep durable governance, Flow config, agent source, command source, and documentation in the product-owned tracked paths where developers expect them, including `.veritas/`, `.flow/`, `.agents/`, `.claude/commands`, and `docs/`. Existing transition ignores such as `.flow-agents/`, `.kontour/`, `.surface/`, `.veritas/evidence/`, `.flow/runs/`, and `.telemetry/` remain in place during migration.
|
|
121
|
+
|
|
118
122
|
`evals/fixtures/` ownership is tracked in [Fixture Ownership](fixture-ownership.md).
|
|
119
123
|
Do not delete or add fixture directories without updating that inventory and the
|
|
120
124
|
owning eval evidence.
|
|
@@ -630,14 +630,14 @@ fi
|
|
|
630
630
|
echo ""
|
|
631
631
|
echo "--- opencode Plugin Hook Chain (end-to-end telemetry persistence) ---"
|
|
632
632
|
# Execute the REAL generated plugin module under node, invoke its handlers,
|
|
633
|
-
# and assert telemetry events persist inside the workspace .telemetry/ —
|
|
633
|
+
# and assert telemetry events persist inside the workspace .kontourai/telemetry/ —
|
|
634
634
|
# not the workspace PARENT. Pins three live-smoke findings (2026-06-11):
|
|
635
635
|
# 1. spawning process.execPath fails under non-node hosts (NODE_BIN guard)
|
|
636
636
|
# 2. empty stdin makes the telemetry pipeline silently skip the emit
|
|
637
637
|
# 3. TELEMETRY_DATA_DIR escaping to the workspace parent (../../.. depth bug)
|
|
638
638
|
CHAIN_WS="$TMPDIR_EVAL/plugin-chain-opencode"
|
|
639
639
|
(cd "$ROOT_DIR/dist/opencode" && bash install.sh "$CHAIN_WS" >/dev/null 2>&1) || true
|
|
640
|
-
rm -rf "$CHAIN_WS/.telemetry" "$TMPDIR_EVAL/.telemetry"
|
|
640
|
+
rm -rf "$CHAIN_WS/.kontourai/telemetry" "$CHAIN_WS/.telemetry" "$TMPDIR_EVAL/.kontourai" "$TMPDIR_EVAL/.telemetry"
|
|
641
641
|
|
|
642
642
|
if (cd "$CHAIN_WS" && node --input-type=module -e "
|
|
643
643
|
const mod = await import('./.opencode/plugins/flow-agents.js');
|
|
@@ -653,21 +653,21 @@ fi
|
|
|
653
653
|
# The telemetry emit is detached (disowned) and can take a few seconds to
|
|
654
654
|
# land; poll rather than fixed-sleep.
|
|
655
655
|
for _i in 1 2 3 4 5 6 7 8 9 10; do
|
|
656
|
-
[[ -s "$CHAIN_WS/.telemetry/full.jsonl" ]] && break
|
|
656
|
+
[[ -s "$CHAIN_WS/.kontourai/telemetry/full.jsonl" ]] && break
|
|
657
657
|
sleep 1
|
|
658
658
|
done
|
|
659
|
-
if [[ -s "$CHAIN_WS/.telemetry/full.jsonl" ]] && node -e "
|
|
660
|
-
require('fs').readFileSync('$CHAIN_WS/.telemetry/full.jsonl','utf8').trim().split('\n').map(JSON.parse);
|
|
659
|
+
if [[ -s "$CHAIN_WS/.kontourai/telemetry/full.jsonl" ]] && node -e "
|
|
660
|
+
require('fs').readFileSync('$CHAIN_WS/.kontourai/telemetry/full.jsonl','utf8').trim().split('\n').map(JSON.parse);
|
|
661
661
|
" 2>/dev/null; then
|
|
662
|
-
_pass "opencode plugin: handlers persisted telemetry events in workspace .telemetry/"
|
|
662
|
+
_pass "opencode plugin: handlers persisted telemetry events in workspace .kontourai/telemetry/"
|
|
663
663
|
else
|
|
664
|
-
_fail "opencode plugin: no telemetry events persisted in workspace .telemetry/"
|
|
664
|
+
_fail "opencode plugin: no telemetry events persisted in workspace .kontourai/telemetry/"
|
|
665
665
|
fi
|
|
666
666
|
|
|
667
|
-
if [[ ! -e "$TMPDIR_EVAL/.telemetry" ]]; then
|
|
667
|
+
if [[ ! -e "$TMPDIR_EVAL/.kontourai/telemetry" && ! -e "$TMPDIR_EVAL/.telemetry" ]]; then
|
|
668
668
|
_pass "opencode plugin: telemetry did not leak into the workspace parent directory"
|
|
669
669
|
else
|
|
670
|
-
_fail "opencode plugin: telemetry leaked into workspace parent
|
|
670
|
+
_fail "opencode plugin: telemetry leaked into workspace parent"
|
|
671
671
|
fi
|
|
672
672
|
|
|
673
673
|
echo ""
|
|
@@ -802,7 +802,9 @@ cp "$ROOT/scripts/lib/command-log-chain.js" "$ISO_LIBDIR/"
|
|
|
802
802
|
# Create isolated node context that can't find @kontourai/surface
|
|
803
803
|
ISO_DIR="$TMP/surface-iso"
|
|
804
804
|
mkdir -p "$ISO_DIR/repo/.flow-agents/surftest"
|
|
805
|
+
mkdir -p "$ISO_DIR/lib"
|
|
805
806
|
cp "$GATE" "$ISO_DIR/stop-goal-fit.js"
|
|
807
|
+
cp "$ROOT/scripts/hooks/lib/local-artifact-paths.js" "$ISO_DIR/lib/"
|
|
806
808
|
printf '# Repo\n' > "$ISO_DIR/repo/AGENTS.md"
|
|
807
809
|
# Non-terminal session (execution phase, in_progress status)
|
|
808
810
|
printf '%s' '{"schema_version":"1.0","task_slug":"surftest","status":"in_progress","phase":"execution","updated_at":"2026-06-27T00:00:00Z","next_action":{"status":"in_progress","summary":"running"}}' \
|
|
@@ -846,7 +848,9 @@ echo "--- AC3.1b: Low-impact-only bundle with unavailable surface → NOT blocke
|
|
|
846
848
|
|
|
847
849
|
ISO2_DIR="$TMP/surface-iso2"
|
|
848
850
|
mkdir -p "$ISO2_DIR/repo/.flow-agents/lowtest"
|
|
851
|
+
mkdir -p "$ISO2_DIR/lib"
|
|
849
852
|
cp "$GATE" "$ISO2_DIR/stop-goal-fit.js"
|
|
853
|
+
cp "$ROOT/scripts/hooks/lib/local-artifact-paths.js" "$ISO2_DIR/lib/"
|
|
850
854
|
printf '# Repo\n' > "$ISO2_DIR/repo/AGENTS.md"
|
|
851
855
|
printf '%s' '{"schema_version":"1.0","task_slug":"lowtest","status":"in_progress","phase":"execution","updated_at":"2026-06-27T00:00:00Z","next_action":{"status":"in_progress","summary":"running"}}' \
|
|
852
856
|
> "$ISO2_DIR/repo/.flow-agents/lowtest/state.json"
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -uo pipefail
|
|
3
|
+
|
|
4
|
+
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
5
|
+
TMPDIR_EVAL="$(mktemp -d)"
|
|
6
|
+
errors=0
|
|
7
|
+
|
|
8
|
+
cleanup() { rm -rf "$TMPDIR_EVAL"; }
|
|
9
|
+
trap cleanup EXIT
|
|
10
|
+
|
|
11
|
+
_pass() { echo " ✓ $1"; }
|
|
12
|
+
_fail() { echo " ✗ $1"; errors=$((errors + 1)); }
|
|
13
|
+
|
|
14
|
+
REPO="$TMPDIR_EVAL/repo"
|
|
15
|
+
mkdir -p \
|
|
16
|
+
"$REPO/.telemetry" \
|
|
17
|
+
"$REPO/.flow/runs/run-1" \
|
|
18
|
+
"$REPO/.surface/runs/run-2" \
|
|
19
|
+
"$REPO/.veritas/evidence" \
|
|
20
|
+
"$REPO/.veritas/repo-standards" \
|
|
21
|
+
"$REPO/.flow/definitions" \
|
|
22
|
+
"$REPO/.flow-agents/task"
|
|
23
|
+
|
|
24
|
+
printf '%s\n' '{"event":1}' > "$REPO/.telemetry/full.jsonl"
|
|
25
|
+
printf '%s\n' '{"run":1}' > "$REPO/.flow/runs/run-1/state.json"
|
|
26
|
+
printf '%s\n' '{"surface":1}' > "$REPO/.surface/runs/run-2/latest.json"
|
|
27
|
+
printf '%s\n' '{"evidence":1}' > "$REPO/.veritas/evidence/e.json"
|
|
28
|
+
printf '%s\n' '{"durable":1}' > "$REPO/.veritas/repo-map.json"
|
|
29
|
+
printf '%s\n' '{"standard":1}' > "$REPO/.veritas/repo-standards/std.json"
|
|
30
|
+
printf '%s\n' '{"definition":1}' > "$REPO/.flow/definitions/main.json"
|
|
31
|
+
printf '%s\n' '{"state":1}' > "$REPO/.flow-agents/task/state.json"
|
|
32
|
+
ln -s "$REPO/.telemetry/full.jsonl" "$REPO/.telemetry/link.jsonl"
|
|
33
|
+
|
|
34
|
+
if node "$ROOT/scripts/migrate-local-artifacts.mjs" --repo "$REPO" --json > "$TMPDIR_EVAL/dry.json"; then
|
|
35
|
+
if [[ ! -e "$REPO/.kontourai/telemetry/full.jsonl" ]] \
|
|
36
|
+
&& node - "$TMPDIR_EVAL/dry.json" <<'NODE'
|
|
37
|
+
const fs = require("node:fs");
|
|
38
|
+
const data = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
|
|
39
|
+
if (!data.summary.dry_run || data.summary.applied) throw new Error("expected dry-run");
|
|
40
|
+
const text = JSON.stringify(data);
|
|
41
|
+
for (const needle of [".telemetry/full.jsonl", ".flow/runs/run-1/state.json", ".surface/runs/run-2/latest.json", ".veritas/evidence/e.json"]) {
|
|
42
|
+
if (!text.includes(needle)) throw new Error(`missing copy plan ${needle}`);
|
|
43
|
+
}
|
|
44
|
+
for (const needle of [".veritas/repo-map.json", ".veritas/repo-standards", ".flow/definitions", ".flow-agents"]) {
|
|
45
|
+
if (!text.includes(needle)) throw new Error(`missing durable skip ${needle}`);
|
|
46
|
+
}
|
|
47
|
+
if (!text.includes("source-is-symlink")) throw new Error("missing symlink skip");
|
|
48
|
+
NODE
|
|
49
|
+
then
|
|
50
|
+
_pass "migration script defaults to dry-run and reports generated copies plus durable skips"
|
|
51
|
+
else
|
|
52
|
+
_fail "migration dry-run output was wrong"
|
|
53
|
+
fi
|
|
54
|
+
else
|
|
55
|
+
_fail "migration dry-run command failed"
|
|
56
|
+
fi
|
|
57
|
+
|
|
58
|
+
if node "$ROOT/scripts/migrate-local-artifacts.mjs" --repo "$REPO" --apply --include-flow-agents > "$TMPDIR_EVAL/apply.out"; then
|
|
59
|
+
if [[ -f "$REPO/.kontourai/telemetry/full.jsonl" ]] \
|
|
60
|
+
&& [[ -f "$REPO/.kontourai/flow/runs/run-1/state.json" ]] \
|
|
61
|
+
&& [[ -f "$REPO/.kontourai/surface/runs/run-2/latest.json" ]] \
|
|
62
|
+
&& [[ -f "$REPO/.kontourai/veritas/evidence/e.json" ]] \
|
|
63
|
+
&& [[ -f "$REPO/.kontourai/flow-agents/task/state.json" ]] \
|
|
64
|
+
&& [[ -f "$REPO/.telemetry/full.jsonl" ]] \
|
|
65
|
+
&& [[ -f "$REPO/.flow-agents/task/state.json" ]] \
|
|
66
|
+
&& [[ ! -e "$REPO/.kontourai/veritas/repo-map.json" ]] \
|
|
67
|
+
&& [[ ! -e "$REPO/.kontourai/flow/definitions/main.json" ]]; then
|
|
68
|
+
_pass "migration apply copies only generated allowlisted paths without deleting old files"
|
|
69
|
+
else
|
|
70
|
+
_fail "migration apply copied wrong files or deleted old files"
|
|
71
|
+
fi
|
|
72
|
+
else
|
|
73
|
+
_fail "migration apply command failed"
|
|
74
|
+
fi
|
|
75
|
+
|
|
76
|
+
TARGET_SYMLINK_WORKSPACE="$TMPDIR_EVAL/target-symlink-workspace"
|
|
77
|
+
TARGET_SYMLINK_REPO="$TARGET_SYMLINK_WORKSPACE/repo"
|
|
78
|
+
OUTSIDE="$TMPDIR_EVAL/outside.txt"
|
|
79
|
+
mkdir -p "$TARGET_SYMLINK_REPO/.telemetry" "$TARGET_SYMLINK_REPO/.kontourai/telemetry"
|
|
80
|
+
printf '%s\n' '{"safe":true}' > "$TARGET_SYMLINK_REPO/.telemetry/full.jsonl"
|
|
81
|
+
printf '%s\n' 'outside-original' > "$OUTSIDE"
|
|
82
|
+
ln -s "$OUTSIDE" "$TARGET_SYMLINK_REPO/.kontourai/telemetry/full.jsonl"
|
|
83
|
+
|
|
84
|
+
if node "$ROOT/scripts/migrate-local-artifacts.mjs" --repo "$TARGET_SYMLINK_REPO" --apply --force --json > "$TMPDIR_EVAL/target-symlink.json"; then
|
|
85
|
+
if [[ "$(cat "$OUTSIDE")" == "outside-original" ]] \
|
|
86
|
+
&& node - "$TMPDIR_EVAL/target-symlink.json" <<'NODE'
|
|
87
|
+
const fs = require("node:fs");
|
|
88
|
+
const data = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
|
|
89
|
+
const text = JSON.stringify(data);
|
|
90
|
+
if (!text.includes("target-is-symlink")) throw new Error("missing target symlink skip");
|
|
91
|
+
if (data.summary.copies !== 0) throw new Error(`expected no copies, got ${data.summary.copies}`);
|
|
92
|
+
NODE
|
|
93
|
+
then
|
|
94
|
+
_pass "migration apply --force refuses target symlinks"
|
|
95
|
+
else
|
|
96
|
+
_fail "migration apply --force followed or failed to report target symlink"
|
|
97
|
+
fi
|
|
98
|
+
else
|
|
99
|
+
_fail "migration apply --force target symlink command failed"
|
|
100
|
+
fi
|
|
101
|
+
|
|
102
|
+
exit "$errors"
|
|
@@ -37,6 +37,40 @@ VALIDATOR="validate-workflow-artifacts"
|
|
|
37
37
|
ARTIFACT_DIR="$TMPDIR_EVAL/repo/.flow-agents/auto-sidecars"
|
|
38
38
|
mkdir -p "$ARTIFACT_DIR"
|
|
39
39
|
|
|
40
|
+
DEFAULT_ROOT_REPO="$TMPDIR_EVAL/default-root-repo"
|
|
41
|
+
mkdir -p "$DEFAULT_ROOT_REPO"
|
|
42
|
+
if (cd "$DEFAULT_ROOT_REPO" && flow_agents_node "$WRITER" ensure-session \
|
|
43
|
+
--task-slug default-root \
|
|
44
|
+
--title "Default Root" \
|
|
45
|
+
--summary "Default root should use the Kontour runtime artifact home." \
|
|
46
|
+
--criterion "Default root exists" \
|
|
47
|
+
--timestamp "2026-05-09T00:00:00Z" >"$TMPDIR_EVAL/default-root.out" 2>"$TMPDIR_EVAL/default-root.err"); then
|
|
48
|
+
if [[ -f "$DEFAULT_ROOT_REPO/.kontourai/flow-agents/default-root/state.json" ]] \
|
|
49
|
+
&& [[ -f "$DEFAULT_ROOT_REPO/.kontourai/flow-agents/current.json" ]] \
|
|
50
|
+
&& [[ ! -e "$DEFAULT_ROOT_REPO/.flow-agents/default-root/state.json" ]]; then
|
|
51
|
+
_pass "sidecar writer defaults new sessions to .kontourai/flow-agents"
|
|
52
|
+
else
|
|
53
|
+
_fail "sidecar writer default root did not use .kontourai/flow-agents"
|
|
54
|
+
fi
|
|
55
|
+
else
|
|
56
|
+
_fail "sidecar writer default-root ensure-session failed: $(cat "$TMPDIR_EVAL/default-root.out" "$TMPDIR_EVAL/default-root.err")"
|
|
57
|
+
fi
|
|
58
|
+
|
|
59
|
+
OLD_ROOT_REPO="$TMPDIR_EVAL/old-root-repo"
|
|
60
|
+
mkdir -p "$OLD_ROOT_REPO/.flow-agents/old-session"
|
|
61
|
+
cat > "$OLD_ROOT_REPO/.flow-agents/current.json" <<'JSON'
|
|
62
|
+
{"schema_version":"1.0","active_slug":"old-session","artifact_dir":"old-session"}
|
|
63
|
+
JSON
|
|
64
|
+
cat > "$OLD_ROOT_REPO/.flow-agents/old-session/state.json" <<'JSON'
|
|
65
|
+
{"schema_version":"1.0","task_slug":"old-session","status":"planned","phase":"planning","next_action":{"status":"continue","summary":"continue"}}
|
|
66
|
+
JSON
|
|
67
|
+
if (cd "$OLD_ROOT_REPO" && flow_agents_node "$WRITER" current --format slug >"$TMPDIR_EVAL/old-current.out" 2>"$TMPDIR_EVAL/old-current.err") \
|
|
68
|
+
&& [[ "$(cat "$TMPDIR_EVAL/old-current.out")" == "old-session" ]]; then
|
|
69
|
+
_pass "sidecar writer reads legacy .flow-agents current session when no new root exists"
|
|
70
|
+
else
|
|
71
|
+
_fail "sidecar writer did not read legacy current session: $(cat "$TMPDIR_EVAL/old-current.out" "$TMPDIR_EVAL/old-current.err")"
|
|
72
|
+
fi
|
|
73
|
+
|
|
40
74
|
SESSION_ROOT="$TMPDIR_EVAL/repo/.flow-agents"
|
|
41
75
|
if flow_agents_node "$WRITER" ensure-session \
|
|
42
76
|
--artifact-root "$SESSION_ROOT" \
|
package/evals/run.sh
CHANGED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# test_unit_helpers.sh — Layer 1: TypeScript-layer unit tests for the PURE
|
|
3
|
+
# workflow-sidecar helpers (ops#22).
|
|
4
|
+
#
|
|
5
|
+
# Historically every assertion covering workflow-sidecar was black-box bash driving
|
|
6
|
+
# the CLI. These node:test units exercise the pure projection/validation helpers
|
|
7
|
+
# directly against the built JS — fast, deterministic, isolated — and complement
|
|
8
|
+
# (do not replace) the bash evals.
|
|
9
|
+
set -uo pipefail
|
|
10
|
+
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
11
|
+
cd "$ROOT_DIR"
|
|
12
|
+
|
|
13
|
+
echo "── TS pure-helper unit tests (node --test) ──"
|
|
14
|
+
|
|
15
|
+
# The units import the build output. CI builds before evals; build here too when run
|
|
16
|
+
# standalone so the test is self-sufficient.
|
|
17
|
+
if [[ ! -f build/src/cli/workflow-sidecar.js ]]; then
|
|
18
|
+
npm run build --silent
|
|
19
|
+
fi
|
|
20
|
+
|
|
21
|
+
if node --test src/cli/*.test.mjs; then
|
|
22
|
+
echo " PASS: workflow-sidecar pure-helper unit tests"
|
|
23
|
+
else
|
|
24
|
+
echo " FAIL: workflow-sidecar pure-helper unit tests"
|
|
25
|
+
exit 1
|
|
26
|
+
fi
|
|
@@ -41,6 +41,9 @@
|
|
|
41
41
|
"conformance:self": "node ../../packaging/conformance/run-conformance.js --self",
|
|
42
42
|
"conformance": "node ../../packaging/conformance/run-conformance.js --adapter-cmd \"node bin/conformance-shim.mjs\" --level L2"
|
|
43
43
|
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"@kontourai/console-telemetry": "^0.1.0"
|
|
46
|
+
},
|
|
44
47
|
"peerDependencies": {
|
|
45
48
|
"@strands-agents/sdk": ">=1.0.0"
|
|
46
49
|
},
|
|
@@ -14,6 +14,11 @@ import fs from "node:fs";
|
|
|
14
14
|
import path from "node:path";
|
|
15
15
|
import { fileURLToPath } from "node:url";
|
|
16
16
|
import { randomUUID } from "node:crypto";
|
|
17
|
+
import {
|
|
18
|
+
costForModel as pkgCostForModel,
|
|
19
|
+
currentPricingVersion as pkgPricingVersion,
|
|
20
|
+
setRegistry as setPkgRegistry
|
|
21
|
+
} from "@kontourai/console-telemetry";
|
|
17
22
|
|
|
18
23
|
// ESM has no __dirname; derive it (this package is "type":"module").
|
|
19
24
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -308,7 +313,12 @@ export class TelemetrySink {
|
|
|
308
313
|
}
|
|
309
314
|
|
|
310
315
|
// ---------------------------------------------------------------------------
|
|
311
|
-
// Usage / cost —
|
|
316
|
+
// Usage / cost — pricing math + registry shape come from the shared contract
|
|
317
|
+
// package @kontourai/console-telemetry (single source). This sink only loads
|
|
318
|
+
// the flow-agents authored registry (scripts/telemetry/pricing.json) and feeds
|
|
319
|
+
// it to the package so cost prices against pricing.json rather than the
|
|
320
|
+
// package's bundled fallback. Tokens are exact regardless; the console
|
|
321
|
+
// recomputes cost authoritatively.
|
|
312
322
|
// ---------------------------------------------------------------------------
|
|
313
323
|
|
|
314
324
|
export interface TokenCounts {
|
|
@@ -331,37 +341,15 @@ interface NormalizedTokens {
|
|
|
331
341
|
cacheRead: number;
|
|
332
342
|
}
|
|
333
343
|
|
|
334
|
-
//
|
|
335
|
-
//
|
|
336
|
-
//
|
|
337
|
-
//
|
|
338
|
-
//
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
zero_cost_models: string[];
|
|
344
|
-
}
|
|
345
|
-
interface PricingRegistry {
|
|
346
|
-
current_version: string;
|
|
347
|
-
versions: Record<string, PricingVersionBlock>;
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
const FALLBACK_REGISTRY: PricingRegistry = {
|
|
351
|
-
current_version: "fallback",
|
|
352
|
-
versions: {
|
|
353
|
-
fallback: {
|
|
354
|
-
cache_multipliers: { write_5m: 1.25, write_1h: 2.0, read: 0.1 },
|
|
355
|
-
models: {},
|
|
356
|
-
default: { input: 5.0, output: 25.0 },
|
|
357
|
-
zero_cost_models: ["<synthetic>", "synthetic", "unknown", ""]
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
};
|
|
361
|
-
|
|
362
|
-
let cachedRegistry: PricingRegistry | null = null;
|
|
363
|
-
function loadRegistry(): PricingRegistry {
|
|
364
|
-
if (cachedRegistry) return cachedRegistry;
|
|
344
|
+
// Feed the package the flow-agents authored registry when present (its single
|
|
345
|
+
// source of truth). Resolution: TELEMETRY_PRICING_FILE / FLOW_AGENTS_PRICING_FILE
|
|
346
|
+
// env path, else the repo-relative registry. Runs once; on failure the package
|
|
347
|
+
// keeps its bundled fallback. The console recomputes cost authoritatively, so a
|
|
348
|
+
// missing file only degrades the sink's stamped estimate.
|
|
349
|
+
let registryLoaded = false;
|
|
350
|
+
function ensureRegistry(): void {
|
|
351
|
+
if (registryLoaded) return;
|
|
352
|
+
registryLoaded = true;
|
|
365
353
|
const candidates = [
|
|
366
354
|
process.env.TELEMETRY_PRICING_FILE,
|
|
367
355
|
process.env.FLOW_AGENTS_PRICING_FILE,
|
|
@@ -372,19 +360,18 @@ function loadRegistry(): PricingRegistry {
|
|
|
372
360
|
try {
|
|
373
361
|
const parsed = JSON.parse(fs.readFileSync(candidate, "utf8"));
|
|
374
362
|
if (parsed && typeof parsed.current_version === "string" && parsed.versions) {
|
|
375
|
-
|
|
376
|
-
return
|
|
363
|
+
setPkgRegistry(parsed);
|
|
364
|
+
return;
|
|
377
365
|
}
|
|
378
366
|
} catch {
|
|
379
367
|
// try next candidate
|
|
380
368
|
}
|
|
381
369
|
}
|
|
382
|
-
cachedRegistry = FALLBACK_REGISTRY;
|
|
383
|
-
return cachedRegistry;
|
|
384
370
|
}
|
|
385
371
|
|
|
386
372
|
function pricingVersion(): string {
|
|
387
|
-
|
|
373
|
+
ensureRegistry();
|
|
374
|
+
return pkgPricingVersion();
|
|
388
375
|
}
|
|
389
376
|
|
|
390
377
|
function num(value: number | undefined): number {
|
|
@@ -405,17 +392,11 @@ function normalizeTokens(tokens: TokenCounts): NormalizedTokens {
|
|
|
405
392
|
}
|
|
406
393
|
|
|
407
394
|
function costForModel(model: string | undefined, tokens: NormalizedTokens): number {
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
(tokens.input * rate.input +
|
|
416
|
-
tokens.output * rate.output +
|
|
417
|
-
tokens.cacheCreation * rate.input * cm.write_5m +
|
|
418
|
-
tokens.cacheRead * rate.input * cm.read) /
|
|
419
|
-
1_000_000
|
|
420
|
-
);
|
|
395
|
+
ensureRegistry();
|
|
396
|
+
return pkgCostForModel(model, {
|
|
397
|
+
inputTokens: tokens.input,
|
|
398
|
+
outputTokens: tokens.output,
|
|
399
|
+
cacheCreationInputTokens: tokens.cacheCreation,
|
|
400
|
+
cacheReadInputTokens: tokens.cacheRead
|
|
401
|
+
});
|
|
421
402
|
}
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* - defaultSimilarityDetector — pluggable similarity interface default (R3)
|
|
18
18
|
*
|
|
19
19
|
* Telemetry:
|
|
20
|
-
* Gate events are emitted to <workspace>/.telemetry/full.jsonl using
|
|
20
|
+
* Gate events are emitted to <workspace>/.kontourai/telemetry/full.jsonl using
|
|
21
21
|
* canonical schema v0.3.0 events (preToolUse at gate entry, postToolUse at gate exit).
|
|
22
22
|
*
|
|
23
23
|
* Similarity Interface (R3):
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* Zero runtime dependencies beyond Node.js built-ins.
|
|
9
9
|
* Fails open: telemetry errors never block kit operations.
|
|
10
10
|
*
|
|
11
|
-
* Sink path: <workspace>/.telemetry/full.jsonl
|
|
11
|
+
* Sink path: <workspace>/.kontourai/telemetry/full.jsonl
|
|
12
12
|
* The workspace is resolved from FLOW_AGENTS_WORKSPACE env var, falling back
|
|
13
13
|
* to process.cwd().
|
|
14
14
|
*
|
|
@@ -47,7 +47,7 @@ function schemaEventType(canonical) {
|
|
|
47
47
|
|
|
48
48
|
function resolveSinkPath(workspace) {
|
|
49
49
|
const ws = workspace || process.env.FLOW_AGENTS_WORKSPACE || process.cwd();
|
|
50
|
-
return path.join(ws, ".telemetry", "full.jsonl");
|
|
50
|
+
return path.join(ws, ".kontourai", "telemetry", "full.jsonl");
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
// ---------------------------------------------------------------------------
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kontourai/flow-agents",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.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",
|
|
@@ -99,6 +99,7 @@
|
|
|
99
99
|
"scripts": {
|
|
100
100
|
"build": "tsc -p tsconfig.json",
|
|
101
101
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
102
|
+
"test:unit": "npm run build --silent && node --test src/cli/*.test.mjs",
|
|
102
103
|
"validate:source": "npm run build --silent && node build/src/cli.js validate-source",
|
|
103
104
|
"validate:artifacts": "npm run build --silent && node build/src/cli/validate-workflow-artifacts.js",
|
|
104
105
|
"context-map": "npm run build --silent && node build/src/cli.js context-map",
|
package/scripts/README.md
CHANGED
|
@@ -70,6 +70,7 @@ renamed, or changes category, update the table and the validator together.
|
|
|
70
70
|
| `lib/audit-transport.sh` | shared hook library | `evals/integration/test_hook_category_behaviors.sh`, `evals/integration/test_telemetry.sh` | Shared audit event transport functions. |
|
|
71
71
|
| `lib/hook-flags.js` | shared hook library | `evals/integration/test_hook_category_behaviors.sh` | Shared profile/disable flag parsing. |
|
|
72
72
|
| `lib/liveness-read.js` | shared hook library | `evals/integration/test_session_resume_roundtrip.sh` | Shared liveness event reader + freshness check (`readLivenessEvents`, `freshHolders`); consumed by the reground hook and `workflow-sidecar liveness status`. |
|
|
73
|
+
| `lib/local-artifact-paths.js` | shared hook library | `evals/integration/test_migrate_local_artifacts.sh`, `evals/integration/test_workflow_sidecar_writer.sh` | Shared `.kontourai/flow-agents` artifact-root helpers with legacy `.flow-agents` read compatibility for CJS hooks. |
|
|
73
74
|
| `lib/patterns.sh` | shared hook library | `evals/integration/test_hook_category_behaviors.sh`, `evals/integration/test_telemetry.sh` | Shared shell pattern constants. |
|
|
74
75
|
| `lib/resolve-formatter.js` | shared hook library | `evals/integration/test_hook_category_behaviors.sh` | Shared formatter resolution helper. |
|
|
75
76
|
|
|
@@ -40,6 +40,12 @@
|
|
|
40
40
|
* any interpreter not in the list (ruby, php, etc.), and multiline here-docs.
|
|
41
41
|
* The real anchor remains external (clean CI env + human review).
|
|
42
42
|
*
|
|
43
|
+
* FROZEN bar-raiser (ADR 0018). The redirect / interpreter-write / cp-target detection
|
|
44
|
+
* is intentionally non-exhaustive and is NOT to be grown with new evasion-pattern rules.
|
|
45
|
+
* A newly-discovered self-tamper vector belongs in the external CI anchor (trust-reconcile
|
|
46
|
+
* does not trust agent-written files) and the required anti-gaming suite — not another
|
|
47
|
+
* local string matcher. Bug fixes that don't expand the pattern surface are still welcome.
|
|
48
|
+
*
|
|
43
49
|
* Exit codes: 0 = allow, 2 = block
|
|
44
50
|
*/
|
|
45
51
|
|
|
@@ -48,6 +48,7 @@
|
|
|
48
48
|
|
|
49
49
|
const fs = require('fs');
|
|
50
50
|
const path = require('path');
|
|
51
|
+
const { flowAgentsArtifactRootsForRead } = require('./lib/local-artifact-paths');
|
|
51
52
|
const crypto = require('crypto');
|
|
52
53
|
|
|
53
54
|
const MAX_STDIN = 1024 * 1024;
|
|
@@ -197,18 +198,23 @@ function latestStateDir(flowAgentsDir) {
|
|
|
197
198
|
* to the newest-mtime state.json directory.
|
|
198
199
|
*/
|
|
199
200
|
function resolveArtifactDir(root) {
|
|
200
|
-
const flowAgentsDir
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
201
|
+
for (const flowAgentsDir of flowAgentsArtifactRootsForRead(root)) {
|
|
202
|
+
const current = readJsonFile(path.join(flowAgentsDir, 'current.json'));
|
|
203
|
+
if (current) {
|
|
204
|
+
const slug = current.artifact_dir || current.active_slug;
|
|
205
|
+
if (typeof slug === 'string' && slug.trim()) {
|
|
206
|
+
// Guard against path traversal in the slug.
|
|
207
|
+
const safe = slug.replace(/\.\.+/g, '').replace(/^[/\\]+/, '');
|
|
208
|
+
const dir = path.join(flowAgentsDir, safe);
|
|
209
|
+
if (dir.startsWith(flowAgentsDir + path.sep) && fs.existsSync(dir)) return dir;
|
|
210
|
+
}
|
|
209
211
|
}
|
|
210
212
|
}
|
|
211
|
-
|
|
213
|
+
for (const flowAgentsDir of flowAgentsArtifactRootsForRead(root)) {
|
|
214
|
+
const latest = latestStateDir(flowAgentsDir);
|
|
215
|
+
if (latest) return latest;
|
|
216
|
+
}
|
|
217
|
+
return null;
|
|
212
218
|
}
|
|
213
219
|
|
|
214
220
|
function isCommandTool(toolName, command) {
|