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