@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
package/scripts/install-merge.js
CHANGED
|
@@ -23,10 +23,13 @@
|
|
|
23
23
|
* flow-agents marker (the COLLISION_MARKER strings from init.ts).
|
|
24
24
|
* (c) APPEND the current managed hook groups from the bundle.
|
|
25
25
|
* (d) Preserve ALL other keys (permissions, statusLine, user hooks, auth).
|
|
26
|
+
* Managed non-hook values are added only when absent or when replacing
|
|
27
|
+
* an existing Flow Agents-owned value. Conflicting user-owned values win
|
|
28
|
+
* and are reported to stderr.
|
|
26
29
|
* (e) Atomic write (write tmp + rename).
|
|
27
30
|
* (f) Write/update .flow-agents/install.json version stamp.
|
|
28
31
|
*
|
|
29
|
-
* Export: mergeSettings(existing, managed) — pure, testable.
|
|
32
|
+
* Export: mergeSettings(existing, managed, options) — pure, testable.
|
|
30
33
|
* The managed ownership region is identified purely by statusMessage markers
|
|
31
34
|
* (cross-runtime, no top-level key needed in settings.json).
|
|
32
35
|
*/
|
|
@@ -47,6 +50,33 @@ const FA_MARKERS = [
|
|
|
47
50
|
"Capturing Flow Agents command evidence",
|
|
48
51
|
];
|
|
49
52
|
|
|
53
|
+
const FA_OWNERSHIP_MARKERS = [
|
|
54
|
+
...FA_MARKERS,
|
|
55
|
+
"flow-agents",
|
|
56
|
+
"Flow Agents",
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
function stableJson(value) {
|
|
60
|
+
if (value === undefined) return "undefined";
|
|
61
|
+
return JSON.stringify(value);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function valuesEqual(a, b) {
|
|
65
|
+
return stableJson(a) === stableJson(b);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function valueContainsManagedMarker(value) {
|
|
69
|
+
return FA_OWNERSHIP_MARKERS.some((marker) => stableJson(value).includes(marker));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function conflict(path, existingValue, managedValue) {
|
|
73
|
+
return { path, existingValue, managedValue };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function emitConflict(onConflict, item) {
|
|
77
|
+
if (typeof onConflict === "function") onConflict(item);
|
|
78
|
+
}
|
|
79
|
+
|
|
50
80
|
/**
|
|
51
81
|
* Returns true if a hook-group entry is owned by flow-agents.
|
|
52
82
|
* A hook-group in Claude Code settings looks like:
|
|
@@ -95,16 +125,28 @@ function mergeArrayUnion(a, b) {
|
|
|
95
125
|
* @param {unknown} existingPerms @param {unknown} managedPerms
|
|
96
126
|
* @returns {Record<string, unknown>}
|
|
97
127
|
*/
|
|
98
|
-
function mergePermissions(existingPerms, managedPerms) {
|
|
128
|
+
function mergePermissions(existingPerms, managedPerms, options = {}) {
|
|
99
129
|
const e = existingPerms && typeof existingPerms === "object" ? existingPerms : {};
|
|
100
130
|
const m = managedPerms && typeof managedPerms === "object" ? managedPerms : {};
|
|
101
|
-
const out = Object.assign({}, e
|
|
131
|
+
const out = Object.assign({}, e);
|
|
102
132
|
for (const listKey of ["allow", "deny", "ask"]) {
|
|
103
133
|
if (Array.isArray(e[listKey]) || Array.isArray(m[listKey])) {
|
|
104
134
|
out[listKey] = mergeArrayUnion(e[listKey], m[listKey]);
|
|
105
135
|
}
|
|
106
136
|
}
|
|
107
|
-
|
|
137
|
+
for (const [key, value] of Object.entries(m)) {
|
|
138
|
+
if (["allow", "deny", "ask"].includes(key)) continue;
|
|
139
|
+
if (!(key in e)) {
|
|
140
|
+
out[key] = value;
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (valuesEqual(e[key], value) || valueContainsManagedMarker(e[key])) {
|
|
144
|
+
out[key] = value;
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
out[key] = e[key];
|
|
148
|
+
emitConflict(options.onConflict, conflict(`permissions.${key}`, e[key], value));
|
|
149
|
+
}
|
|
108
150
|
return out;
|
|
109
151
|
}
|
|
110
152
|
|
|
@@ -117,14 +159,17 @@ function mergePermissions(existingPerms, managedPerms) {
|
|
|
117
159
|
*
|
|
118
160
|
* Returns a new object with:
|
|
119
161
|
* - All keys from `existing` preserved (permissions, statusLine, auth, etc.)
|
|
120
|
-
* -
|
|
162
|
+
* - Managed keys added when absent, or updated only when the existing value
|
|
163
|
+
* is already Flow Agents-owned.
|
|
164
|
+
* - User-owned managed-key conflicts preserved from `existing` and surfaced
|
|
165
|
+
* through `options.onConflict`.
|
|
121
166
|
* - For the `hooks` key: user-owned hook groups (non-FA) survive; FA groups are
|
|
122
167
|
* replaced with the current managed set from `managed`.
|
|
123
168
|
*
|
|
124
169
|
* Strategy:
|
|
125
170
|
* 1. Start with a shallow copy of `existing` (preserves all user keys).
|
|
126
|
-
* 2.
|
|
127
|
-
*
|
|
171
|
+
* 2. Add or update Flow Agents-owned non-hooks keys from `managed`; preserve
|
|
172
|
+
* user-owned conflicting values and report conflicts.
|
|
128
173
|
* 3. For `hooks`: iterate each event key from both existing and managed;
|
|
129
174
|
* keep user (non-FA) groups from existing, append the current FA groups
|
|
130
175
|
* from managed.
|
|
@@ -133,19 +178,26 @@ function mergePermissions(existingPerms, managedPerms) {
|
|
|
133
178
|
* @param {Record<string, unknown>} managed
|
|
134
179
|
* @returns {Record<string, unknown>}
|
|
135
180
|
*/
|
|
136
|
-
function mergeSettings(existing, managed) {
|
|
181
|
+
function mergeSettings(existing, managed, options = {}) {
|
|
137
182
|
// 1. Start with all existing keys (preserves user-owned data).
|
|
138
183
|
const result = Object.assign({}, existing);
|
|
139
184
|
|
|
140
|
-
// 2.
|
|
141
|
-
//
|
|
142
|
-
//
|
|
185
|
+
// 2. Add non-hooks keys from managed without overwriting user-owned values.
|
|
186
|
+
// If a key is absent, add the managed default. If the existing value is
|
|
187
|
+
// already Flow Agents-owned (for example the generated statusLine command),
|
|
188
|
+
// replace it with the current managed value. Otherwise preserve the user's
|
|
189
|
+
// value and surface a conflict so the user can decide.
|
|
143
190
|
for (const [key, value] of Object.entries(managed)) {
|
|
144
191
|
if (key === "hooks") continue;
|
|
145
192
|
if (key === "permissions") {
|
|
146
|
-
result.permissions = mergePermissions(existing.permissions, value);
|
|
147
|
-
} else {
|
|
193
|
+
result.permissions = mergePermissions(existing.permissions, value, options);
|
|
194
|
+
} else if (!(key in existing)) {
|
|
148
195
|
result[key] = value;
|
|
196
|
+
} else if (valuesEqual(existing[key], value) || valueContainsManagedMarker(existing[key])) {
|
|
197
|
+
result[key] = value;
|
|
198
|
+
} else {
|
|
199
|
+
result[key] = existing[key];
|
|
200
|
+
emitConflict(options.onConflict, conflict(key, existing[key], value));
|
|
149
201
|
}
|
|
150
202
|
}
|
|
151
203
|
|
|
@@ -267,7 +319,13 @@ function runMerge({ configPath, managedHooksPath, version, installRecordPath, ru
|
|
|
267
319
|
}
|
|
268
320
|
|
|
269
321
|
// (b) + (c) + (d) Merge.
|
|
270
|
-
const
|
|
322
|
+
const conflicts = [];
|
|
323
|
+
const merged = mergeSettings(existing, managed, { onConflict: (item) => conflicts.push(item) });
|
|
324
|
+
for (const item of conflicts) {
|
|
325
|
+
process.stderr.write(
|
|
326
|
+
`install-merge: conflict: preserving existing setting '${item.path}' and not applying Flow Agents managed value\n`
|
|
327
|
+
);
|
|
328
|
+
}
|
|
271
329
|
|
|
272
330
|
// (e) Atomic write.
|
|
273
331
|
atomicWriteJson(configPath, merged);
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Install a prepared Flow Agents overlay without claiming or deleting unrelated files.
|
|
3
|
+
const crypto = require("node:crypto");
|
|
4
|
+
const fs = require("node:fs");
|
|
5
|
+
const path = require("node:path");
|
|
6
|
+
|
|
7
|
+
function fail(message) {
|
|
8
|
+
process.stderr.write(`install-owned-files: ${message}\n`);
|
|
9
|
+
process.exit(1);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const [sourceArg, destArg, manifestArg] = process.argv.slice(2);
|
|
13
|
+
if (!sourceArg || !destArg || !manifestArg) fail("usage: install-owned-files.js <overlay> <destination> <manifest-relative-path>");
|
|
14
|
+
const source = fs.realpathSync(sourceArg);
|
|
15
|
+
const dest = fs.realpathSync(destArg);
|
|
16
|
+
const manifestPath = path.join(dest, manifestArg);
|
|
17
|
+
|
|
18
|
+
function hashFile(file) {
|
|
19
|
+
return crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function relativeFiles(root, current = root) {
|
|
23
|
+
const out = [];
|
|
24
|
+
for (const name of fs.readdirSync(current).sort()) {
|
|
25
|
+
const file = path.join(current, name);
|
|
26
|
+
const stat = fs.lstatSync(file);
|
|
27
|
+
if (stat.isSymbolicLink()) fail(`refusing symlink in install overlay: ${file}`);
|
|
28
|
+
if (stat.isDirectory()) out.push(...relativeFiles(root, file));
|
|
29
|
+
else if (stat.isFile()) out.push(path.relative(root, file).split(path.sep).join("/"));
|
|
30
|
+
else fail(`unsupported install overlay entry: ${file}`);
|
|
31
|
+
}
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function targetFor(rel) {
|
|
36
|
+
if (!rel || rel.split("/").some((part) => !part || part === "." || part === "..")) fail(`unsafe relative path: ${rel}`);
|
|
37
|
+
const target = path.resolve(dest, ...rel.split("/"));
|
|
38
|
+
const relative = path.relative(dest, target);
|
|
39
|
+
if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) fail(`path escapes destination: ${rel}`);
|
|
40
|
+
return target;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function ensureSafeParent(target, create) {
|
|
44
|
+
const relative = path.relative(dest, path.dirname(target));
|
|
45
|
+
let current = dest;
|
|
46
|
+
for (const part of relative.split(path.sep).filter(Boolean)) {
|
|
47
|
+
current = path.join(current, part);
|
|
48
|
+
if (fs.existsSync(current)) {
|
|
49
|
+
const stat = fs.lstatSync(current);
|
|
50
|
+
if (stat.isSymbolicLink()) fail(`refusing to write through symlink: ${current}`);
|
|
51
|
+
if (!stat.isDirectory()) fail(`destination component is not a directory: ${current}`);
|
|
52
|
+
} else if (create) {
|
|
53
|
+
fs.mkdirSync(current);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function readManifest() {
|
|
59
|
+
if (!fs.existsSync(manifestPath)) return new Map();
|
|
60
|
+
ensureSafeParent(manifestPath, false);
|
|
61
|
+
const stat = fs.lstatSync(manifestPath);
|
|
62
|
+
if (stat.isSymbolicLink() || !stat.isFile()) fail(`ownership manifest is not a regular file: ${manifestPath}`);
|
|
63
|
+
let parsed;
|
|
64
|
+
try { parsed = JSON.parse(fs.readFileSync(manifestPath, "utf8")); } catch (error) { fail(`invalid ownership manifest: ${error.message}`); }
|
|
65
|
+
if (parsed.schema_version !== "1.0" || !Array.isArray(parsed.files)) fail("unsupported ownership manifest; remove it only after manually auditing installed files");
|
|
66
|
+
return new Map(parsed.files.map((entry) => [String(entry.path), String(entry.sha256)]));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function legacyInstallCutoff() {
|
|
70
|
+
if (fs.existsSync(manifestPath)) return null;
|
|
71
|
+
const recordPath = path.join(dest, ".flow-agents", "install.json");
|
|
72
|
+
const hooksPath = path.join(dest, "hooks.json");
|
|
73
|
+
for (const file of [recordPath, hooksPath]) {
|
|
74
|
+
if (!fs.existsSync(file)) return null;
|
|
75
|
+
ensureSafeParent(file, false);
|
|
76
|
+
const stat = fs.lstatSync(file);
|
|
77
|
+
if (stat.isSymbolicLink() || !stat.isFile()) return null;
|
|
78
|
+
}
|
|
79
|
+
let record;
|
|
80
|
+
try { record = JSON.parse(fs.readFileSync(recordPath, "utf8")); } catch { return null; }
|
|
81
|
+
const installedAt = Date.parse(String(record.installedAt ?? ""));
|
|
82
|
+
if (record.runtime !== "codex" || !/^\d+\.\d+\.\d+(?:[-+].*)?$/.test(String(record.version ?? "")) || !Number.isFinite(installedAt)) return null;
|
|
83
|
+
const hooks = fs.readFileSync(hooksPath, "utf8");
|
|
84
|
+
if (!hooks.includes("Recording Flow Agents telemetry") && !hooks.includes("stop-goal-fit.js")) return null;
|
|
85
|
+
return installedAt + 2000;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Exact path classes managed by the pre-manifest installer. These are an
|
|
89
|
+
// auditable historical contract, intentionally narrower than "all incoming
|
|
90
|
+
// files": root config/auth/hooks, kits/local, user-extensible skills/agents,
|
|
91
|
+
// and arbitrary destination files are never bootstrapped from timestamps.
|
|
92
|
+
function wasLegacyManagedPath(rel) {
|
|
93
|
+
const [top] = rel.split("/");
|
|
94
|
+
const managedTrees = new Set([
|
|
95
|
+
".flow-agents", "agent-cards", "build", "context", "docs", "evals",
|
|
96
|
+
"integrations", "packaging", "powers", "prompts", "schemas", "scripts",
|
|
97
|
+
]);
|
|
98
|
+
if (managedTrees.has(top)) return rel !== ".flow-agents/install.json";
|
|
99
|
+
if (top === "kits") return !rel.startsWith("kits/local/");
|
|
100
|
+
return new Set(["README.md", "console.telemetry.json", "install.sh"]).has(rel);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const previous = readManifest();
|
|
104
|
+
const legacyCutoff = legacyInstallCutoff();
|
|
105
|
+
const incoming = relativeFiles(source).map((rel) => ({ rel, source: path.join(source, ...rel.split("/")), hash: hashFile(path.join(source, ...rel.split("/"))) }));
|
|
106
|
+
|
|
107
|
+
// Complete preflight before the first destination mutation.
|
|
108
|
+
for (const entry of incoming) {
|
|
109
|
+
const target = targetFor(entry.rel);
|
|
110
|
+
ensureSafeParent(target, false);
|
|
111
|
+
if (!fs.existsSync(target)) continue;
|
|
112
|
+
const stat = fs.lstatSync(target);
|
|
113
|
+
if (stat.isSymbolicLink()) fail(`refusing to replace symlink: ${target}`);
|
|
114
|
+
if (!stat.isFile()) fail(`destination collision is not a regular file: ${target}`);
|
|
115
|
+
const oldHash = previous.get(entry.rel);
|
|
116
|
+
const currentHash = hashFile(target);
|
|
117
|
+
// Migration from the pre-manifest installer: byte-identical bundle files can
|
|
118
|
+
// be adopted without overwriting them. Any differing unowned file is a hard
|
|
119
|
+
// collision and remains untouched.
|
|
120
|
+
if (!oldHash) {
|
|
121
|
+
if (currentHash !== entry.hash) {
|
|
122
|
+
const recognizableLegacy = legacyCutoff !== null && wasLegacyManagedPath(entry.rel) && stat.mtimeMs <= legacyCutoff;
|
|
123
|
+
if (!recognizableLegacy) fail(`refusing to overwrite unowned or ambiguous file: ${target}`);
|
|
124
|
+
}
|
|
125
|
+
} else if (currentHash !== oldHash && currentHash !== entry.hash) {
|
|
126
|
+
fail(`refusing to overwrite modified Flow Agents file: ${target}`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
for (const [rel] of previous) {
|
|
130
|
+
const target = targetFor(rel);
|
|
131
|
+
ensureSafeParent(target, false);
|
|
132
|
+
if (fs.existsSync(target) && fs.lstatSync(target).isSymbolicLink()) fail(`refusing to remove symlink replacing owned file: ${target}`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
for (const entry of incoming) {
|
|
136
|
+
const target = targetFor(entry.rel);
|
|
137
|
+
ensureSafeParent(target, true);
|
|
138
|
+
const temp = path.join(path.dirname(target), `.${path.basename(target)}.flow-agents-${process.pid}-${crypto.randomBytes(6).toString("hex")}.tmp`);
|
|
139
|
+
try {
|
|
140
|
+
fs.copyFileSync(entry.source, temp, fs.constants.COPYFILE_EXCL);
|
|
141
|
+
fs.chmodSync(temp, fs.statSync(entry.source).mode & 0o777);
|
|
142
|
+
fs.renameSync(temp, target);
|
|
143
|
+
} finally {
|
|
144
|
+
fs.rmSync(temp, { force: true });
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const incomingPaths = new Set(incoming.map((entry) => entry.rel));
|
|
149
|
+
const staleParents = new Set();
|
|
150
|
+
for (const [rel, oldHash] of previous) {
|
|
151
|
+
if (incomingPaths.has(rel)) continue;
|
|
152
|
+
const target = targetFor(rel);
|
|
153
|
+
if (!fs.existsSync(target)) continue;
|
|
154
|
+
const stat = fs.lstatSync(target);
|
|
155
|
+
if (!stat.isFile() || hashFile(target) !== oldHash) {
|
|
156
|
+
process.stderr.write(`install-owned-files: preserving modified former Flow Agents file: ${target}\n`);
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
fs.rmSync(target);
|
|
160
|
+
staleParents.add(path.dirname(target));
|
|
161
|
+
}
|
|
162
|
+
for (const start of [...staleParents].sort((a, b) => b.length - a.length)) {
|
|
163
|
+
let current = start;
|
|
164
|
+
while (current !== dest) {
|
|
165
|
+
try { fs.rmdirSync(current); } catch { break; }
|
|
166
|
+
current = path.dirname(current);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
ensureSafeParent(manifestPath, true);
|
|
171
|
+
if (fs.existsSync(manifestPath) && fs.lstatSync(manifestPath).isSymbolicLink()) fail(`refusing to replace symlink: ${manifestPath}`);
|
|
172
|
+
const manifestTemp = `${manifestPath}.tmp.${process.pid}.${crypto.randomBytes(6).toString("hex")}`;
|
|
173
|
+
try {
|
|
174
|
+
fs.writeFileSync(manifestTemp, `${JSON.stringify({ schema_version: "1.0", files: incoming.map((entry) => ({ path: entry.rel, sha256: entry.hash })) }, null, 2)}\n`, { flag: "wx" });
|
|
175
|
+
fs.renameSync(manifestTemp, manifestPath);
|
|
176
|
+
} finally {
|
|
177
|
+
fs.rmSync(manifestTemp, { force: true });
|
|
178
|
+
}
|