@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,261 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure, side-effect-free decision logic for the guided "Connect to Kontour
|
|
3
|
+
* Console?" wizard (install-flow-console-connect PR2). Mirrors the doc-comment
|
|
4
|
+
* convention of `console-telemetry-validate.ts` (this literally is "the future
|
|
5
|
+
* guided console-connect wizard (PR2/PR3)" that file's own docstring names).
|
|
6
|
+
*
|
|
7
|
+
* No `fs`/`child_process`/network imports here — every export is a pure
|
|
8
|
+
* function or a thin orchestration over injected `ask`/`askHidden` callbacks,
|
|
9
|
+
* so `interactiveOptions()` in `src/cli/init.ts` stays a thin caller and this
|
|
10
|
+
* module is fully unit-testable without a TTY.
|
|
11
|
+
*
|
|
12
|
+
* `TelemetrySink` is re-declared as a local type alias (not imported from
|
|
13
|
+
* `init.ts`) to avoid a lib -> cli import direction. Keep this union in sync
|
|
14
|
+
* with `src/cli/init.ts`'s `TelemetrySink` by comment cross-reference:
|
|
15
|
+
* "local-files" | "local-kontour-console" | "kontour-hosted-console" |
|
|
16
|
+
* "user-hosted-console" | "kontour-cloud" | "hosted-kontour-console"
|
|
17
|
+
*/
|
|
18
|
+
import { isValidConsoleUrl, isValidConsoleToken, isValidConsoleTenant } from "./console-telemetry-validate.js";
|
|
19
|
+
|
|
20
|
+
export type TelemetrySink =
|
|
21
|
+
| "local-files"
|
|
22
|
+
| "local-kontour-console"
|
|
23
|
+
| "kontour-hosted-console"
|
|
24
|
+
| "user-hosted-console"
|
|
25
|
+
| "kontour-cloud"
|
|
26
|
+
| "hosted-kontour-console";
|
|
27
|
+
|
|
28
|
+
export type ConsoleConnectChoice = "hosted" | "local" | "self-hosted" | "skip";
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Case-insensitive match on the four accepted choice spellings. Blank or
|
|
32
|
+
* unrecognized input falls back to `fallback` (the caller decides the
|
|
33
|
+
* default, typically "hosted").
|
|
34
|
+
*/
|
|
35
|
+
export function normalizeConsoleConnectChoice(answer: string, fallback: ConsoleConnectChoice): ConsoleConnectChoice {
|
|
36
|
+
const normalized = answer.trim().toLowerCase();
|
|
37
|
+
if (!normalized) return fallback;
|
|
38
|
+
if (normalized === "hosted" || normalized === "h") return "hosted";
|
|
39
|
+
if (normalized === "local" || normalized === "l") return "local";
|
|
40
|
+
if (normalized === "self-hosted" || normalized === "selfhosted" || normalized === "self" || normalized === "s") return "self-hosted";
|
|
41
|
+
if (normalized === "skip" || normalized === "none" || normalized === "n") return "skip";
|
|
42
|
+
return fallback;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Pure lookup table: choice -> the exact `telemetrySinks` array to install with. */
|
|
46
|
+
export function mapConsoleConnectChoiceToSinks(choice: ConsoleConnectChoice): TelemetrySink[] {
|
|
47
|
+
if (choice === "hosted") return ["kontour-hosted-console"];
|
|
48
|
+
if (choice === "local") return ["local-kontour-console"];
|
|
49
|
+
if (choice === "self-hosted") return ["user-hosted-console"];
|
|
50
|
+
return ["local-files"];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Only "self-hosted" requires an inline URL prompt. */
|
|
54
|
+
export function needsConsoleUrlPrompt(choice: ConsoleConnectChoice): boolean {
|
|
55
|
+
return choice === "self-hosted";
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Mirrors `needsConsoleCredentials`'s existing semantics one-for-one
|
|
60
|
+
* (choice-based instead of sinks-based): every choice except "skip" wants
|
|
61
|
+
* token/tenant prompts.
|
|
62
|
+
*/
|
|
63
|
+
export function needsConsoleCredentialPrompts(choice: ConsoleConnectChoice): boolean {
|
|
64
|
+
return choice !== "skip";
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The one hard-fail guard this PR adds: self-hosted Console with a blank URL
|
|
69
|
+
* is a downstream `die` in `install-console-config.sh` (the `user-hosted-console`
|
|
70
|
+
* branch requires `--console-url`/`--console-endpoint`). Falls back to
|
|
71
|
+
* `local-files` with a one-line warning instead of letting that crash happen.
|
|
72
|
+
*
|
|
73
|
+
* Only handles the blank case — a non-blank-but-malformed URL (e.g. a typo)
|
|
74
|
+
* is NOT this guard's job; callers separately run `isValidConsoleUrl` for
|
|
75
|
+
* that format-only warn-and-continue case.
|
|
76
|
+
*/
|
|
77
|
+
export function resolveSelfHostedUrlOrFallback(url: string): { sinks: TelemetrySink[]; url: string; fallbackWarning?: string } {
|
|
78
|
+
if (url.trim() === "") {
|
|
79
|
+
return {
|
|
80
|
+
sinks: ["local-files"],
|
|
81
|
+
url: "",
|
|
82
|
+
fallbackWarning: "Self-hosted Console requires a URL; no URL was given, so telemetry stays local-only. Re-run init or telemetry-doctor to add a Console URL later.",
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
return { sinks: ["user-hosted-console"], url };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export type ConsoleConnectIo = {
|
|
89
|
+
ask: (prompt: string) => Promise<string>;
|
|
90
|
+
askHidden: (prompt: string) => Promise<string>;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export type ConsoleConnectDefaults = {
|
|
94
|
+
hostedUrl: string;
|
|
95
|
+
localUrl: string;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export type ConsoleConnectResult = {
|
|
99
|
+
telemetrySinks: TelemetrySink[];
|
|
100
|
+
consoleUrl?: string;
|
|
101
|
+
consoleTokenValue?: string;
|
|
102
|
+
consoleTenant?: string;
|
|
103
|
+
warnings: string[];
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
// Token provisioning is console-side (CONSOLE_AUTH_TOKENS_JSON), not
|
|
107
|
+
// flow-agents-side. This hint is folded into the token prompt string itself
|
|
108
|
+
// (rather than printed directly via console.log) so the whole orchestration
|
|
109
|
+
// stays testable via the injected `askHidden` stub's captured prompt argument.
|
|
110
|
+
const TOKEN_PROVISIONING_HINT =
|
|
111
|
+
"Tokens are provisioned by your Console admin via CONSOLE_AUTH_TOKENS_JSON -- see docs/integrations/flow-agents-console.md.";
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Full interactive orchestration used by `interactiveOptions()`: prompts the
|
|
115
|
+
* four-way "Connect to Kontour Console?" choice (default Hosted, showing the
|
|
116
|
+
* real `console-presets.sh`-resolved hosted URL), applies the self-hosted
|
|
117
|
+
* blank-URL guard, prompts hidden token + tenant when a console connection is
|
|
118
|
+
* wanted, runs PR1's pure validators (validate-and-warn, not
|
|
119
|
+
* validate-and-block), and returns any format warnings in `warnings` for the
|
|
120
|
+
* caller to print -- rather than printing directly, so this stays testable
|
|
121
|
+
* via return-value assertions, not console-spy assertions.
|
|
122
|
+
*/
|
|
123
|
+
export async function runConsoleConnectWizard(
|
|
124
|
+
io: ConsoleConnectIo,
|
|
125
|
+
defaults: ConsoleConnectDefaults,
|
|
126
|
+
): Promise<ConsoleConnectResult> {
|
|
127
|
+
const warnings: string[] = [];
|
|
128
|
+
|
|
129
|
+
const choiceAnswer = await io.ask(
|
|
130
|
+
`Connect to Kontour Console? [Hosted (${defaults.hostedUrl})/Local (${defaults.localUrl})/Self-hosted/Skip] [Hosted]: `,
|
|
131
|
+
);
|
|
132
|
+
const choice = normalizeConsoleConnectChoice(choiceAnswer, "hosted");
|
|
133
|
+
|
|
134
|
+
let sinks = mapConsoleConnectChoiceToSinks(choice);
|
|
135
|
+
let consoleUrl: string | undefined;
|
|
136
|
+
|
|
137
|
+
if (needsConsoleUrlPrompt(choice)) {
|
|
138
|
+
const urlAnswer = await io.ask("Self-hosted Console URL: ");
|
|
139
|
+
const resolved = resolveSelfHostedUrlOrFallback(urlAnswer);
|
|
140
|
+
sinks = resolved.sinks;
|
|
141
|
+
consoleUrl = resolved.url ? resolved.url : undefined;
|
|
142
|
+
if (resolved.fallbackWarning) {
|
|
143
|
+
warnings.push(resolved.fallbackWarning);
|
|
144
|
+
} else if (consoleUrl && !isValidConsoleUrl(consoleUrl)) {
|
|
145
|
+
warnings.push(
|
|
146
|
+
`Console URL '${consoleUrl}' does not look valid (expected https://, or http:// to localhost/127.0.0.1); continuing anyway.`,
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
let consoleTokenValue: string | undefined;
|
|
152
|
+
let consoleTenant: string | undefined;
|
|
153
|
+
// Only prompt for credentials when a console sink actually survived (the
|
|
154
|
+
// self-hosted blank-URL fallback above may have already downgraded to
|
|
155
|
+
// local-files, in which case there is nothing to authenticate).
|
|
156
|
+
const wantsConsole = needsConsoleCredentialPrompts(choice) && sinks.some((sink) => sink !== "local-files");
|
|
157
|
+
if (wantsConsole) {
|
|
158
|
+
const tokenAnswer = await io.askHidden(`${TOKEN_PROVISIONING_HINT}\nConsole telemetry token (blank to skip): `);
|
|
159
|
+
consoleTokenValue = tokenAnswer.trim() || undefined;
|
|
160
|
+
if (consoleTokenValue && !isValidConsoleToken(consoleTokenValue)) {
|
|
161
|
+
warnings.push(
|
|
162
|
+
"Console token format looks unusual (expected <=4096 chars, charset [A-Za-z0-9._~+/=-]); continuing anyway.",
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
const tenantAnswer = await io.ask("Console tenant ID (blank to skip): ");
|
|
166
|
+
consoleTenant = tenantAnswer.trim() || undefined;
|
|
167
|
+
if (consoleTenant && !isValidConsoleTenant(consoleTenant)) {
|
|
168
|
+
warnings.push(
|
|
169
|
+
"Console tenant format looks unusual (expected charset [A-Za-z0-9._:-]); continuing anyway.",
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return { telemetrySinks: sinks, consoleUrl, consoleTokenValue, consoleTenant, warnings };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export type ConsoleStatus = "connected-verified" | "connected-unverified" | "local-only";
|
|
178
|
+
|
|
179
|
+
type DoctorConsoleShape = {
|
|
180
|
+
sink: "local-only" | "console";
|
|
181
|
+
reachability: { checked: boolean; ok: boolean | null; error?: string; statusCode?: number };
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Pure classifier over the exact structural shape `telemetry-doctor.ts`'s
|
|
186
|
+
* `DoctorReport.console` already has. Deliberately accepts a narrow
|
|
187
|
+
* structural type rather than importing `DoctorReport` itself, keeping this
|
|
188
|
+
* module dependency-free (no lib -> cli import).
|
|
189
|
+
*/
|
|
190
|
+
// Actionable detail shown when a Console sink's reachability was never
|
|
191
|
+
// attempted (checked:false). In practice this branch is reached ONLY via
|
|
192
|
+
// telemetry-doctor.ts's pre-attempt guard clauses (endpointAllowed() said no,
|
|
193
|
+
// or the endpoint URL was malformed) -- an actually-attempted check that
|
|
194
|
+
// fails always sets checked:true (see checkConsoleReachability's HEAD
|
|
195
|
+
// request path) and is handled by the branch above, which keeps surfacing
|
|
196
|
+
// error/statusCode unchanged. The most common guard-clause reason by far is
|
|
197
|
+
// a self-hosted/BYO HTTPS host that --allow-network was not passed for, so
|
|
198
|
+
// the hint below points at the one flag that unblocks it.
|
|
199
|
+
const NOT_CHECKED_DETAIL =
|
|
200
|
+
"not checked — self-hosted/BYO consoles need `flow-agents telemetry-doctor --allow-network` to verify reachability";
|
|
201
|
+
|
|
202
|
+
export function describeConsoleStatus(doctor: { console: DoctorConsoleShape }): { status: ConsoleStatus; detail?: string } {
|
|
203
|
+
const { sink, reachability } = doctor.console;
|
|
204
|
+
if (sink === "local-only") return { status: "local-only" };
|
|
205
|
+
if (reachability.checked && reachability.ok === true) return { status: "connected-verified" };
|
|
206
|
+
if (reachability.checked && reachability.ok === false) {
|
|
207
|
+
const detail = reachability.error ?? (reachability.statusCode !== undefined ? `HTTP ${reachability.statusCode}` : "reachability check failed");
|
|
208
|
+
return { status: "connected-unverified", detail };
|
|
209
|
+
}
|
|
210
|
+
// reachability.checked === false: never attempted (see NOT_CHECKED_DETAIL's
|
|
211
|
+
// doc comment for why this is always the not-allowed/skipped reason here,
|
|
212
|
+
// never an attempted-and-failed check).
|
|
213
|
+
return { status: "connected-unverified", detail: NOT_CHECKED_DETAIL };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export type PostInstallSummaryInput = {
|
|
217
|
+
runtime: string;
|
|
218
|
+
runtimeAutoDetected: boolean;
|
|
219
|
+
dest: string;
|
|
220
|
+
telemetrySinks: string[];
|
|
221
|
+
consoleStatus: { status: ConsoleStatus; detail?: string };
|
|
222
|
+
tokenConfigured: boolean;
|
|
223
|
+
tenantConfigured: boolean;
|
|
224
|
+
nextSteps: string[];
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
function consoleStatusLine(consoleStatus: { status: ConsoleStatus; detail?: string }): string {
|
|
228
|
+
if (consoleStatus.status === "connected-verified") return "✓ Console: connected + verified";
|
|
229
|
+
if (consoleStatus.status === "connected-unverified") {
|
|
230
|
+
return `✗ Console: connected, unverified${consoleStatus.detail ? `: ${consoleStatus.detail}` : ""}`;
|
|
231
|
+
}
|
|
232
|
+
return "- Console: local-only";
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Pure string[]-builder for the post-install summary block: one array entry
|
|
237
|
+
* per line, so callers `console.log` each entry and tests assert on array
|
|
238
|
+
* contents rather than scraping stdout. Never receives or emits the raw
|
|
239
|
+
* console token value -- only the `tokenConfigured`/`tenantConfigured`
|
|
240
|
+
* booleans are shown.
|
|
241
|
+
*/
|
|
242
|
+
export function buildPostInstallSummaryLines(input: PostInstallSummaryInput): string[] {
|
|
243
|
+
const lines: string[] = [];
|
|
244
|
+
lines.push("");
|
|
245
|
+
lines.push("Flow Agents install summary:");
|
|
246
|
+
const runtimeLine = input.runtimeAutoDetected ? `${input.runtime} (auto-detected)` : input.runtime;
|
|
247
|
+
lines.push(` ✓ Runtime: ${runtimeLine}`);
|
|
248
|
+
lines.push(` ✓ Destination: ${input.dest}`);
|
|
249
|
+
lines.push(` ✓ Telemetry sink: ${input.telemetrySinks.length ? input.telemetrySinks.join(", ") : "local-files"}`);
|
|
250
|
+
lines.push(` ${consoleStatusLine(input.consoleStatus)}`);
|
|
251
|
+
if (input.consoleStatus.status !== "local-only") {
|
|
252
|
+
lines.push(` Console token: ${input.tokenConfigured ? "configured" : "not configured"}`);
|
|
253
|
+
lines.push(` Console tenant: ${input.tenantConfigured ? "configured" : "not configured"}`);
|
|
254
|
+
}
|
|
255
|
+
if (input.nextSteps.length) {
|
|
256
|
+
lines.push("");
|
|
257
|
+
lines.push("Next steps:");
|
|
258
|
+
for (const step of input.nextSteps) lines.push(` - ${step}`);
|
|
259
|
+
}
|
|
260
|
+
return lines;
|
|
261
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure, side-effect-free validation predicates mirroring the bash Console
|
|
3
|
+
* telemetry validators exactly, for the future guided console-connect wizard
|
|
4
|
+
* (PR2/PR3) to reuse at prompt time. Deliberately NOT wired into `init.ts`'s
|
|
5
|
+
* interactive flow in this PR — see install-flow-foundations plan, Thread D.
|
|
6
|
+
*
|
|
7
|
+
* Every predicate returns a plain boolean (never throws), matching the
|
|
8
|
+
* "wizard re-prompts on false" use case, unlike the bash `die`-based
|
|
9
|
+
* validators these mirror (which exit non-zero on failure).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Mirrors `has_control_chars` (scripts/telemetry/install-console-config.sh:43-45).
|
|
14
|
+
* Rejects values containing a newline, carriage return, or tab.
|
|
15
|
+
*/
|
|
16
|
+
export function hasControlChars(value: string): boolean {
|
|
17
|
+
return value.includes("\n") || value.includes("\r") || value.includes("\t");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Shared https/localhost/127.0.0.1 scheme rule used by both URL validators below. */
|
|
21
|
+
function isHttpsOrLocalhostUrl(value: string): boolean {
|
|
22
|
+
if (value.startsWith("https://")) return true;
|
|
23
|
+
if (value === "http://127.0.0.1" || value.startsWith("http://127.0.0.1:") || value.startsWith("http://127.0.0.1/")) return true;
|
|
24
|
+
if (value === "http://localhost" || value.startsWith("http://localhost:") || value.startsWith("http://localhost/")) return true;
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Mirrors `validate_url` (scripts/telemetry/install-console-config.sh:47-62).
|
|
30
|
+
* Blank is valid (the field is optional at install time). Otherwise: no
|
|
31
|
+
* control characters, and must be `https://*`, or `http://` to localhost/127.0.0.1.
|
|
32
|
+
*/
|
|
33
|
+
export function isValidConsoleUrl(value: string): boolean {
|
|
34
|
+
if (value === "") return true;
|
|
35
|
+
if (hasControlChars(value)) return false;
|
|
36
|
+
return isHttpsOrLocalhostUrl(value);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Mirrors `console_telemetry_endpoint_allowed` (scripts/telemetry/lib/transport.sh:20-28).
|
|
41
|
+
* Unlike `isValidConsoleUrl`, blank is INVALID (a non-empty endpoint is
|
|
42
|
+
* required at runtime-post time). This check is deliberately narrower than
|
|
43
|
+
* `hasControlChars`: the bash function only rejects `\n`, `\r`, and a
|
|
44
|
+
* literal `"` (the value is later embedded in a curl config file as a
|
|
45
|
+
* quoted string) — it does NOT reject tab, so this must not call
|
|
46
|
+
* `hasControlChars` (which also rejects tab).
|
|
47
|
+
*/
|
|
48
|
+
export function isValidConsoleEndpointStrict(value: string): boolean {
|
|
49
|
+
if (value === "") return false;
|
|
50
|
+
if (value.includes("\n") || value.includes("\r") || value.includes('"')) return false;
|
|
51
|
+
return isHttpsOrLocalhostUrl(value);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const CONSOLE_TOKEN_CHARSET = /^[A-Za-z0-9._~+/=-]+$/;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Mirrors `validate_token` (scripts/telemetry/install-console-config.sh:64-69).
|
|
58
|
+
* Blank is valid (optional field). Otherwise: length <= 4096, no control
|
|
59
|
+
* characters, and charset restricted to `[A-Za-z0-9._~+/=-]`.
|
|
60
|
+
*/
|
|
61
|
+
export function isValidConsoleToken(value: string): boolean {
|
|
62
|
+
if (value === "") return true;
|
|
63
|
+
if (value.length > 4096) return false;
|
|
64
|
+
if (hasControlChars(value)) return false;
|
|
65
|
+
return CONSOLE_TOKEN_CHARSET.test(value);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const CONSOLE_TENANT_CHARSET = /^[A-Za-z0-9._:-]+$/;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Mirrors `validate_tenant` (scripts/telemetry/install-console-config.sh:82-86).
|
|
72
|
+
* Blank is valid (optional field). Otherwise: charset restricted to
|
|
73
|
+
* `[A-Za-z0-9._:-]` (no length bound at install time).
|
|
74
|
+
*/
|
|
75
|
+
export function isValidConsoleTenant(value: string): boolean {
|
|
76
|
+
if (value === "") return true;
|
|
77
|
+
return CONSOLE_TENANT_CHARSET.test(value);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Mirrors `console_telemetry_safe_tenant` (scripts/telemetry/lib/transport.sh:35-38).
|
|
82
|
+
* Unlike `isValidConsoleTenant`, non-empty is required and length is bounded
|
|
83
|
+
* to 1..128 (the runtime-safety gate, stricter than the install-time validator).
|
|
84
|
+
*/
|
|
85
|
+
export function isSafeConsoleTenantForRuntime(value: string): boolean {
|
|
86
|
+
if (value.length < 1 || value.length > 128) return false;
|
|
87
|
+
return CONSOLE_TENANT_CHARSET.test(value);
|
|
88
|
+
}
|
package/src/lib/flow-resolver.ts
CHANGED
|
@@ -73,11 +73,12 @@ export function resolveFlowFilePath(
|
|
|
73
73
|
flowName: string,
|
|
74
74
|
flowId: string,
|
|
75
75
|
repoRoot: string,
|
|
76
|
+
allowOverride = true,
|
|
76
77
|
): string | null {
|
|
77
78
|
// Primary defense: reject any slug containing traversal chars or non-identifier chars.
|
|
78
79
|
if (!SLUG_RE.test(kitId) || !SLUG_RE.test(flowName)) return null;
|
|
79
80
|
|
|
80
|
-
const override = process.env["FLOW_AGENTS_FLOW_DEFS_DIR"];
|
|
81
|
+
const override = allowOverride ? process.env["FLOW_AGENTS_FLOW_DEFS_DIR"] : undefined;
|
|
81
82
|
|
|
82
83
|
let expectedRoot: string;
|
|
83
84
|
let flowFilePath: string;
|
|
@@ -143,6 +144,7 @@ export type ActiveFlowStep = {
|
|
|
143
144
|
stepId: string;
|
|
144
145
|
gateId: string;
|
|
145
146
|
gateExpects: GateExpectation[];
|
|
147
|
+
routeBackReasons: string[];
|
|
146
148
|
/** When resolved through a parent step's uses_flow edge, names the child FlowDefinition that owns the gate. */
|
|
147
149
|
sourceFlowId?: string;
|
|
148
150
|
};
|
|
@@ -183,10 +185,10 @@ function flowIdParts(flowId: string): { kitId: string; flowName: string } | null
|
|
|
183
185
|
return { kitId, flowName };
|
|
184
186
|
}
|
|
185
187
|
|
|
186
|
-
function readFlowDefinition(flowId: string, repoRoot: string): FlowDefinition | null {
|
|
188
|
+
function readFlowDefinition(flowId: string, repoRoot: string, allowOverride = true): FlowDefinition | null {
|
|
187
189
|
const parts = flowIdParts(flowId);
|
|
188
190
|
if (!parts) return null;
|
|
189
|
-
const flowFilePath = resolveFlowFilePath(parts.kitId, parts.flowName, flowId, repoRoot);
|
|
191
|
+
const flowFilePath = resolveFlowFilePath(parts.kitId, parts.flowName, flowId, repoRoot, allowOverride);
|
|
190
192
|
if (!flowFilePath) return null;
|
|
191
193
|
|
|
192
194
|
try {
|
|
@@ -213,6 +215,116 @@ export function resolveFlowStep(flowId: string, stepId: string, repoRoot: string
|
|
|
213
215
|
return publicStep;
|
|
214
216
|
}
|
|
215
217
|
|
|
218
|
+
/**
|
|
219
|
+
* Compile Flow Agents' `uses_flow` kit extension into one effective definition
|
|
220
|
+
* that the Flow runtime can evaluate without understanding agent-layer composition.
|
|
221
|
+
*/
|
|
222
|
+
export function resolveEffectiveFlowDefinition(flowId: string, repoRoot: string, options: { allowOverride?: boolean } = {}): Record<string, unknown> | null {
|
|
223
|
+
return resolveEffectiveFlowDefinitionInternal(flowId, repoRoot, new Set<string>(), options.allowOverride !== false);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function resolveEffectiveFlowDefinitionInternal(flowId: string, repoRoot: string, seen: Set<string>, allowOverride: boolean): Record<string, unknown> | null {
|
|
227
|
+
if (seen.has(flowId)) return null;
|
|
228
|
+
const nextSeen = new Set(seen);
|
|
229
|
+
nextSeen.add(flowId);
|
|
230
|
+
const source = readFlowDefinition(flowId, repoRoot, allowOverride);
|
|
231
|
+
if (!source || !Array.isArray(source.steps)) return null;
|
|
232
|
+
const effective = JSON.parse(JSON.stringify(source)) as FlowDefinition;
|
|
233
|
+
effective.gates = { ...(effective.gates ?? {}) };
|
|
234
|
+
|
|
235
|
+
for (let index = 0; index < source.steps.length; index += 1) {
|
|
236
|
+
const sourceStep = source.steps[index]!;
|
|
237
|
+
if (typeof sourceStep.uses_flow !== "string" || !sourceStep.uses_flow.trim()) continue;
|
|
238
|
+
const child = resolveEffectiveFlowDefinitionInternal(sourceStep.uses_flow, repoRoot, nextSeen, allowOverride) as FlowDefinition | null;
|
|
239
|
+
if (!child || !child.gates) return null;
|
|
240
|
+
const childGateEntry = Object.entries(child.gates).find(([, gate]) => gate?.step === sourceStep.id);
|
|
241
|
+
if (!childGateEntry) return null;
|
|
242
|
+
if (Object.values(effective.gates).some((gate) => gate?.step === sourceStep.id)) return null;
|
|
243
|
+
const [childGateId, childGate] = childGateEntry;
|
|
244
|
+
const childExpects = Array.isArray(childGate.expects) ? childGate.expects : [];
|
|
245
|
+
const exported = exportedExpectations(childExpects, child.exports);
|
|
246
|
+
if (!exported) return null;
|
|
247
|
+
effective.gates[`${sourceStep.uses_flow}:${childGateId}`] = {
|
|
248
|
+
...childGate,
|
|
249
|
+
expects: exported,
|
|
250
|
+
};
|
|
251
|
+
const { uses_flow: _usesFlow, ...compiledStep } = effective.steps![index]!;
|
|
252
|
+
effective.steps![index] = compiledStep;
|
|
253
|
+
}
|
|
254
|
+
const effectiveSteps = effective.steps!;
|
|
255
|
+
const done = effectiveSteps.find((step) => step.id === "done" && step.next === null);
|
|
256
|
+
if (done && !Object.values(effective.gates).some((gate) => gate?.step === "done")) {
|
|
257
|
+
effective.steps = effectiveSteps
|
|
258
|
+
.filter((step) => step.id !== "done")
|
|
259
|
+
.map((step) => step.next === "done" ? { ...step, next: null } : step);
|
|
260
|
+
}
|
|
261
|
+
return effective as unknown as Record<string, unknown>;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* A single (stepId, gateId) → expects[] tuple, as part of the FULL enumeration of every gate in
|
|
266
|
+
* a FlowDefinition (across every step — see resolveAllFlowGateExpects below), not just the
|
|
267
|
+
* currently-active one.
|
|
268
|
+
*/
|
|
269
|
+
export type FlowGateExpectsEntry = {
|
|
270
|
+
stepId: string;
|
|
271
|
+
gateId: string;
|
|
272
|
+
gateExpects: GateExpectation[];
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Enumerate the gate expects[] for EVERY step in a FlowDefinition (#270 CRITICAL/HIGH fix) —
|
|
277
|
+
* not just the currently-active step. This is what lets a stamped gate-claim's
|
|
278
|
+
* (expectation_id, claim_type, subject_type, step_id) tuple be validated against the FULL,
|
|
279
|
+
* declared shape of the flow, instead of only against whatever step happens to be active at
|
|
280
|
+
* validation time (which is a DIFFERENT, and usually wrong, question: the stamp names the step
|
|
281
|
+
* it was ORIGINALLY recorded at, which by design may not be the currently-active one — see
|
|
282
|
+
* buildTrustBundle's #270(a)/(c) step_id-freezing comments in workflow-sidecar.ts).
|
|
283
|
+
*
|
|
284
|
+
* Walks flowDef.steps[] (in declaration order) and resolves each step's gate via the same
|
|
285
|
+
* resolveFlowStepInternal used by resolveFlowStep/resolveActiveFlowStep — including the
|
|
286
|
+
* uses_flow composed-step case, so a gate that lives in a child FlowDefinition (e.g.
|
|
287
|
+
* builder.publish-learn's pr-open-gate, composed into builder.build's "pr-open" step) is
|
|
288
|
+
* enumerated too, exports-filtered exactly as a live resolution would be.
|
|
289
|
+
*
|
|
290
|
+
* Pure and synchronous — no throws, fail-open (returns []) on any error, mirroring every other
|
|
291
|
+
* resolver in this module.
|
|
292
|
+
*
|
|
293
|
+
* Callers MUST distinguish the two null-ish outcomes (#270 MEDIUM fix, iteration 3):
|
|
294
|
+
* - `null` → the FlowDefinition could not be LOADED at all (missing file, unreadable, invalid
|
|
295
|
+
* JSON, or no `steps[]` array) — the caller has no basis to validate anything against this
|
|
296
|
+
* flow and must fail closed with a dedicated "cannot be loaded" message, never the "stamp
|
|
297
|
+
* does not match any expects[]" (forged/corrupt) message; those are different failure
|
|
298
|
+
* classes with different remedies (a load failure means "fix/restore the FlowDefinition
|
|
299
|
+
* file or the flowId", not "this stamp was forged").
|
|
300
|
+
* - `[]` → the FlowDefinition LOADED successfully but genuinely declares no steps with
|
|
301
|
+
* matching gates (or no gates at all) — a real, if unusual, flow shape, not an error.
|
|
302
|
+
*
|
|
303
|
+
* @param flowId e.g. "builder.build" — kitId is extracted as the prefix before the first ".".
|
|
304
|
+
* @param repoRoot Absolute path to the repository root (kits/ lives here).
|
|
305
|
+
* Honored only when FLOW_AGENTS_FLOW_DEFS_DIR is not set.
|
|
306
|
+
* @returns Every (stepId, gateId, gateExpects) tuple in the flow; `[]` when the flow loads but
|
|
307
|
+
* declares no matching gates; `null` when the FlowDefinition cannot be loaded/parsed at all.
|
|
308
|
+
*/
|
|
309
|
+
export function resolveAllFlowGateExpects(flowId: string, repoRoot: string): FlowGateExpectsEntry[] | null {
|
|
310
|
+
const flowDef = readFlowDefinition(flowId, repoRoot);
|
|
311
|
+
if (!flowDef || typeof flowDef !== "object" || !Array.isArray(flowDef.steps)) return null;
|
|
312
|
+
const out: FlowGateExpectsEntry[] = [];
|
|
313
|
+
const seenGateIds = new Set<string>();
|
|
314
|
+
for (const step of flowDef.steps) {
|
|
315
|
+
if (!step || typeof step.id !== "string" || !step.id) continue;
|
|
316
|
+
const resolved = resolveFlowStepInternal(flowId, step.id, repoRoot, new Set<string>());
|
|
317
|
+
if (!resolved) continue;
|
|
318
|
+
// A gate can be reached by more than one step declaration in degenerate/duplicate step
|
|
319
|
+
// lists; de-dupe by gateId so callers never see the same expects[] entries twice.
|
|
320
|
+
const dedupeKey = `${resolved.gateId}`;
|
|
321
|
+
if (seenGateIds.has(dedupeKey)) continue;
|
|
322
|
+
seenGateIds.add(dedupeKey);
|
|
323
|
+
out.push({ stepId: resolved.stepId, gateId: resolved.gateId, gateExpects: resolved.gateExpects });
|
|
324
|
+
}
|
|
325
|
+
return out;
|
|
326
|
+
}
|
|
327
|
+
|
|
216
328
|
function expectationExportKeys(expectation: GateExpectation): string[] {
|
|
217
329
|
const keys: string[] = [];
|
|
218
330
|
if (typeof expectation.id === "string" && expectation.id) keys.push(expectation.id);
|
|
@@ -249,7 +361,7 @@ function resolveFlowStepInternal(flowId: string, stepId: string, repoRoot: strin
|
|
|
249
361
|
for (const [gateId, gate] of Object.entries(flowDef.gates)) {
|
|
250
362
|
if (!gate || gate.step !== stepId) continue;
|
|
251
363
|
const expects = Array.isArray(gate.expects) ? gate.expects : [];
|
|
252
|
-
return { flowId, stepId, gateId, gateExpects: expects, flowExports: flowDef.exports };
|
|
364
|
+
return { flowId, stepId, gateId, gateExpects: expects, routeBackReasons: Object.keys(gate.on_route_back ?? {}), flowExports: flowDef.exports };
|
|
253
365
|
}
|
|
254
366
|
}
|
|
255
367
|
|
|
@@ -266,6 +378,7 @@ function resolveFlowStepInternal(flowId: string, stepId: string, repoRoot: strin
|
|
|
266
378
|
stepId,
|
|
267
379
|
gateId: `${child.flowId}:${child.gateId}`,
|
|
268
380
|
gateExpects: childGateExpects,
|
|
381
|
+
routeBackReasons: child.routeBackReasons,
|
|
269
382
|
sourceFlowId: child.flowId,
|
|
270
383
|
flowExports: flowDef.exports,
|
|
271
384
|
};
|