@kontourai/flow-agents 3.8.0 → 3.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/.github/workflows/ci.yml +8 -0
  2. package/CHANGELOG.md +24 -0
  3. package/README.md +1 -1
  4. package/build/src/builder-flow-runtime.js +16 -10
  5. package/build/src/cli/assignment-provider.js +4 -18
  6. package/build/src/cli/continuation-adapter.d.ts +26 -0
  7. package/build/src/cli/continuation-adapter.js +243 -0
  8. package/build/src/cli/validate-workflow-artifacts.js +21 -1
  9. package/build/src/cli/workflow-sidecar.js +15 -20
  10. package/build/src/cli/workflow.js +99 -6
  11. package/build/src/continuation-driver.d.ts +116 -0
  12. package/build/src/continuation-driver.js +527 -0
  13. package/build/src/index.d.ts +2 -0
  14. package/build/src/index.js +1 -0
  15. package/build/src/tools/build-universal-bundles.js +52 -25
  16. package/build/src/tools/validate-source-tree.js +5 -2
  17. package/context/scripts/hooks/lib/continuation-turn-authority.js +386 -0
  18. package/context/scripts/hooks/stop-goal-fit.js +244 -73
  19. package/context/scripts/hooks/workflow-steering.js +24 -15
  20. package/context/scripts/telemetry/lib/pricing.sh +18 -6
  21. package/context/scripts/telemetry/lib/transport.sh +139 -37
  22. package/context/scripts/telemetry/lib/usage.sh +282 -32
  23. package/context/scripts/telemetry/lib/usage_model_guard.sh +42 -0
  24. package/context/scripts/telemetry/telemetry.sh +76 -1
  25. package/docs/coordination-guide.md +21 -5
  26. package/docs/fixture-ownership.md +2 -1
  27. package/docs/public-workflow-cli.md +98 -0
  28. package/docs/spec/runtime-hook-surface.md +8 -4
  29. package/docs/workflow-usage-guide.md +7 -0
  30. package/evals/acceptance/prove-capture-teeth-declared.sh +13 -0
  31. package/evals/acceptance/prove-capture-teeth.sh +13 -0
  32. package/evals/ci/run-baseline.sh +2 -0
  33. package/evals/fixtures/codex-legacy-agents/5273878130bdafc8a024a650bb5b66c9b003f1f859b5dc6e5b588cbf4ab23228.md +25 -0
  34. package/evals/fixtures/telemetry/usage-transcript-adversarial.jsonl +2 -0
  35. package/evals/fixtures/telemetry/usage-transcript-malformed-sandwich.jsonl +3 -0
  36. package/evals/fixtures/telemetry/usage-transcript-negative-tokens.jsonl +1 -0
  37. package/evals/fixtures/telemetry/usage-transcript-oversized-prefix.jsonl +201 -0
  38. package/evals/fixtures/telemetry/usage-transcript-oversized-tokens.jsonl +1 -0
  39. package/evals/fixtures/telemetry/usage-transcript-prefixed-secret-lower.jsonl +1 -0
  40. package/evals/fixtures/telemetry/usage-transcript-prefixed-secret-upper.jsonl +1 -0
  41. package/evals/fixtures/telemetry/usage-transcript-secret-model-jwt.jsonl +1 -0
  42. package/evals/fixtures/telemetry/usage-transcript-secret-model.jsonl +1 -0
  43. package/evals/fixtures/telemetry/usage-transcript-torn-trailing.jsonl +2 -0
  44. package/evals/integration/test_actor_identity.sh +63 -0
  45. package/evals/integration/test_assignment_provider_local_file.sh +34 -0
  46. package/evals/integration/test_builder_entry_enforcement.sh +5 -1
  47. package/evals/integration/test_bundle_install.sh +106 -17
  48. package/evals/integration/test_bundle_lifecycle.sh +17 -3
  49. package/evals/integration/test_captured_fail_reconciliation.sh +11 -0
  50. package/evals/integration/test_ci_actor_identity.sh +2 -2
  51. package/evals/integration/test_command_log_concurrency.sh +11 -0
  52. package/evals/integration/test_command_log_fork_classification.sh +11 -0
  53. package/evals/integration/test_command_log_integrity.sh +11 -0
  54. package/evals/integration/test_current_json_per_actor.sh +416 -21
  55. package/evals/integration/test_enforcer_expects_driven.sh +11 -0
  56. package/evals/integration/test_ensure_session_ownership_guard.sh +39 -0
  57. package/evals/integration/test_evidence_capture_hook.sh +25 -6
  58. package/evals/integration/test_fixture_retirement_audit.sh +2 -2
  59. package/evals/integration/test_flowdef_union_floor_regression.sh +11 -0
  60. package/evals/integration/test_gate_bypass_chain.sh +11 -0
  61. package/evals/integration/test_gate_lockdown.sh +10 -0
  62. package/evals/integration/test_goal_fit_ghost_session.sh +11 -0
  63. package/evals/integration/test_goal_fit_hook.sh +257 -9
  64. package/evals/integration/test_goal_fit_rederive.sh +11 -0
  65. package/evals/integration/test_install_merge.sh +141 -3
  66. package/evals/integration/test_liveness_conflict_injection.sh +39 -3
  67. package/evals/integration/test_liveness_heartbeat.sh +46 -9
  68. package/evals/integration/test_phase_map_and_gate_claim.sh +10 -1
  69. package/evals/integration/test_public_workflow_cli.sh +6 -6
  70. package/evals/integration/test_session_resume_roundtrip.sh +30 -0
  71. package/evals/integration/test_stop_hook_release.sh +111 -11
  72. package/evals/integration/test_telemetry_tool_usage.sh +394 -0
  73. package/evals/integration/test_verify_hold.sh +64 -1
  74. package/evals/integration/test_workflow_sidecar_writer.sh +10 -1
  75. package/evals/integration/test_workflow_steering_hook.sh +20 -4
  76. package/evals/run.sh +2 -0
  77. package/evals/static/test_universal_bundles.sh +123 -0
  78. package/package.json +4 -4
  79. package/packaging/README.md +21 -0
  80. package/packaging/codex-legacy-agents-fingerprints.json +33 -0
  81. package/scripts/README.md +2 -1
  82. package/scripts/audit-codex-legacy-agents.js +43 -0
  83. package/scripts/classify-codex-legacy-agents.js +71 -0
  84. package/scripts/hooks/evidence-capture.js +18 -6
  85. package/scripts/hooks/lib/actor-identity.js +62 -29
  86. package/scripts/hooks/lib/continuation-turn-authority.js +386 -0
  87. package/scripts/hooks/lib/current-pointer.js +114 -12
  88. package/scripts/hooks/lib/liveness-heartbeat.js +15 -12
  89. package/scripts/hooks/stop-goal-fit.js +244 -73
  90. package/scripts/hooks/workflow-steering.js +24 -15
  91. package/scripts/install-codex-home.sh +8 -7
  92. package/scripts/telemetry/lib/pricing.sh +18 -6
  93. package/scripts/telemetry/lib/transport.sh +71 -0
  94. package/scripts/telemetry/lib/usage.sh +282 -32
  95. package/scripts/telemetry/lib/usage_model_guard.sh +42 -0
  96. package/scripts/telemetry/telemetry.sh +76 -1
  97. package/src/builder-flow-runtime.ts +9 -3
  98. package/src/cli/assignment-provider.ts +5 -26
  99. package/src/cli/builder-flow-runtime.test.mjs +215 -2
  100. package/src/cli/continuation-adapter.ts +265 -0
  101. package/src/cli/continuation-driver.test.mjs +1303 -0
  102. package/src/cli/public-api.test.mjs +1 -0
  103. package/src/cli/validate-workflow-artifacts.ts +19 -1
  104. package/src/cli/workflow-sidecar.ts +18 -23
  105. package/src/cli/workflow.ts +113 -6
  106. package/src/continuation-driver.ts +638 -0
  107. package/src/index.ts +24 -0
  108. package/src/tools/build-universal-bundles.ts +61 -25
  109. package/src/tools/validate-source-tree.ts +5 -2
@@ -2,7 +2,7 @@
2
2
  /**
3
3
  * current-pointer.js — shared pure-CJS per-actor "current" pointer reader/writer (#291)
4
4
  *
5
- * Zero external dependencies (only Node core: fs, path). Consumed by:
5
+ * Zero external dependencies (only Node core: fs, path, crypto). Consumed by:
6
6
  * - build/src/cli/workflow-sidecar.js (ESM compiled, via createRequire — Wave 2 Task 2.1)
7
7
  * - build/src/lib/flow-resolver.js (ESM compiled, via createRequire — Wave 2 Task 2.2)
8
8
  * - scripts/hooks/stop-goal-fit.js, evidence-capture.js, lib/liveness-heartbeat.js,
@@ -18,26 +18,72 @@
18
18
  * calls `readCurrentPointer()` instead of hand-rolling its own fallback, so the compat-shim rule
19
19
  * (per-actor first, legacy-global fallback) can never drift between call sites.
20
20
  *
21
+ * Per-actor filename scheme (#440 fix-wave 2, collision fix): the per-actor filename is
22
+ * `<sanitizeSegment(actorKey) capped at 40 chars>-<first 16 hex chars of sha256(actorKey)>.json`.
21
23
  * `sanitizeSegment` is required from `./actor-identity.js` (already reviewed: restricts to
22
- * `[A-Za-z0-9_.-]`, caps 64 chars) this file deliberately does NOT re-implement a second
23
- * sanitizer; it is the same charset restriction `assignment-provider.ts`'s `assignmentFilePath()`
24
- * already applies to its own per-subject filenames, so the naming convention is consistent across
25
- * both per-key-file stores in this repo (`assignment/<subject>.json` and now
26
- * `current/<actor>.json`).
24
+ * `[A-Za-z0-9_.-]`, caps 64 chars) and is reused, not re-implemented, for the readable prefix —
25
+ * but the sanitized-and-capped value ALONE is not collision-resistant: two distinct actor keys
26
+ * that share the first 64 sanitized characters, or differ only in characters `sanitizeSegment`
27
+ * strips (e.g. "a:bc" vs "ab:c", both sanitizing to "abc"), previously mapped onto the SAME file —
28
+ * actor B's dual-write would silently overwrite actor A's pointer, and A's own hook reads would
29
+ * then ground onto B's session. The appended hash of the FULL (untruncated, unsanitized) actor
30
+ * key makes two distinct actor keys collide only on a 64-bit truncated-digest collision (the
31
+ * retained 16 hex chars of sha256, ~2^32-operation birthday bound) — negligible at any real fleet
32
+ * scale (nowhere near the number of distinct actor identities any deployment will ever generate),
33
+ * but NOT cryptographic full-strength collision resistance (that would need the full 256-bit
34
+ * digest); the sanitized prefix keeps the filename human-legible for debugging.
35
+ *
36
+ * Compatibility / transition (#440 fix-wave 2): `writePerActorCurrent` writes ONLY the new
37
+ * collision-resistant filename from here on. `readCurrentPointer`'s per-actor branch and
38
+ * `readOwnCurrentPointer` both try the NEW filename first, then fall back to the LEGACY filename
39
+ * (`legacyPerActorCurrentFile` — the pre-fix-wave-2 `sanitizeSegment(actorKey).slice(0, 64)+".json"`
40
+ * scheme, exported for callers/evals that need to construct it, e.g. to prove the fallback) ONLY
41
+ * when the new file doesn't exist/parse — so a pointer already written by a pre-fix-wave-2 sidecar
42
+ * (e.g. a still-running published-3.9.0-era session) keeps resolving during the rollout window.
43
+ * This legacy-name fallback intentionally retains the OLD (status-quo, pre-fix) collision exposure
44
+ * — but ONLY for that fallback read of a pre-existing file, never for a new write, which always
45
+ * uses the collision-resistant name. The fallback is TRANSITION-WINDOW ONLY, not a permanent
46
+ * feature of this module: every write (including a read-triggered migration via
47
+ * `updateCurrentAgent` in workflow-sidecar.ts, #440 fix-wave 3) upgrades a pointer to the new
48
+ * name, so live pointers self-migrate through ordinary use. Note the migration WRITES the
49
+ * new-name file but never deletes the legacy file — stale legacy files may linger harmlessly
50
+ * (the new name always wins on read). Removal criterion: once no supported/live actor still
51
+ * depends EXCLUSIVELY on a legacy-name pointer (i.e. every live session has written at least
52
+ * once past this fix's rollout) — practically, at the next major version as a deliberate
53
+ * cutover point — `legacyPerActorCurrentFile` and its two call sites below can be deleted
54
+ * outright.
27
55
  *
28
56
  * Exports:
29
- * perActorCurrentFile(flowAgentsDir, actorKey) → string (path, unsanitized existence not
30
- * implied caller must still fs.existsSync)
57
+ * perActorCurrentFile(flowAgentsDir, actorKey) → string (NEW collision-resistant path;
58
+ * existence not implied)
59
+ * legacyPerActorCurrentFile(flowAgentsDir, actorKey) → string (pre-fix-wave-2 path, read-side
60
+ * fallback only; existence not implied)
31
61
  * readCurrentPointer(flowAgentsDir, actorKey) → { payload: object|null,
32
62
  * source: "per-actor"|"legacy"|"none",
33
63
  * file: string|null }
64
+ * readOwnCurrentPointer(flowAgentsDir, actorKey) → { payload: object|null,
65
+ * source: "per-actor"|"legacy"|"none",
66
+ * file: string|null } (#440 — ownership-bearing
67
+ * read; never falls back to the shared legacy
68
+ * current.json for a RESOLVED actor)
34
69
  * writePerActorCurrent(flowAgentsDir, actorKey, payload) → void
35
70
  */
36
71
 
37
72
  const fs = require('fs');
38
73
  const path = require('path');
74
+ const crypto = require('crypto');
39
75
  const { sanitizeSegment, isUnresolvedActor } = require('./actor-identity.js');
40
76
 
77
+ // #440 fix-wave 2: readable-prefix cap for the NEW collision-resistant filename. Generous (not
78
+ // the collision boundary — the appended hash below is), just keeps filenames legible for
79
+ // debugging. First 16 hex chars of sha256(actorKey) is a 64-bit truncated digest (~2^32-operation
80
+ // birthday bound) — negligible at any real fleet scale, though NOT cryptographic full-strength
81
+ // collision resistance (that needs the full 256-bit digest) — appended so two distinct actor keys
82
+ // collide only on a genuine 64-bit truncated-digest collision, not merely a shared 64-char
83
+ // sanitized prefix.
84
+ const PER_ACTOR_PREFIX_LEN = 40;
85
+ const PER_ACTOR_HASH_LEN = 16;
86
+
41
87
  /**
42
88
  * Best-effort tolerant JSON read: missing file or corrupt/unparseable content are BOTH treated
43
89
  * as "absent" (returns null), never thrown — this is an advisory read used to decide which
@@ -57,15 +103,37 @@ function readJsonFileTolerant(file) {
57
103
  }
58
104
 
59
105
  /**
60
- * Path to the per-actor current pointer file for a given actor key, sanitized via the shared
61
- * `sanitizeSegment` (reused, not re-implemented) so the filename is path-traversal-safe exactly
62
- * like `assignment-provider.ts`'s `assignmentFilePath()`.
106
+ * Path to the per-actor current pointer file for a given actor key (#440 fix-wave 2:
107
+ * collision-resistant scheme see module header). The sanitized, 40-char-capped prefix keeps
108
+ * the filename path-traversal-safe (via the shared `sanitizeSegment`, reused not re-implemented,
109
+ * same convention as `assignment-provider.ts`'s `assignmentFilePath()`) and human-legible; the
110
+ * appended hash of the FULL, untruncated, unsanitized actorKey is what actually makes two
111
+ * distinct actor keys map to distinct files.
63
112
  *
64
113
  * @param {string} flowAgentsDir
65
114
  * @param {string} actorKey
66
115
  * @returns {string}
67
116
  */
68
117
  function perActorCurrentFile(flowAgentsDir, actorKey) {
118
+ const prefix = sanitizeSegment(actorKey).slice(0, PER_ACTOR_PREFIX_LEN);
119
+ const hash = crypto.createHash('sha256').update(String(actorKey)).digest('hex').slice(0, PER_ACTOR_HASH_LEN);
120
+ return path.join(flowAgentsDir, 'current', `${prefix}-${hash}.json`);
121
+ }
122
+
123
+ /**
124
+ * #440 fix-wave 2 (read-side compat only): the PRE-FIX per-actor filename scheme —
125
+ * `sanitizeSegment(actorKey)` alone, capped at 64 chars, no hash suffix. This is the exact
126
+ * scheme that collided (see module header) and is NEVER written by `writePerActorCurrent`
127
+ * anymore; it exists solely so `readCurrentPointer`/`readOwnCurrentPointer` can fall back to a
128
+ * pointer file already written under this name by a pre-fix-wave-2 sidecar, and so callers/evals
129
+ * that need to construct that legacy path (e.g. to prove the fallback) have one canonical
130
+ * function to call rather than re-deriving the old rule by hand.
131
+ *
132
+ * @param {string} flowAgentsDir
133
+ * @param {string} actorKey
134
+ * @returns {string}
135
+ */
136
+ function legacyPerActorCurrentFile(flowAgentsDir, actorKey) {
69
137
  return path.join(flowAgentsDir, 'current', `${sanitizeSegment(actorKey)}.json`);
70
138
  }
71
139
 
@@ -92,6 +160,11 @@ function readCurrentPointer(flowAgentsDir, actorKey) {
92
160
  const perActorFile = perActorCurrentFile(flowAgentsDir, key);
93
161
  const perActorPayload = readJsonFileTolerant(perActorFile);
94
162
  if (perActorPayload !== null) return { payload: perActorPayload, source: 'per-actor', file: perActorFile };
163
+ // #440 fix-wave 2: the new collision-resistant file doesn't exist/parse — fall back to the
164
+ // pre-fix-wave-2 legacy per-actor filename (transition window only; see module header).
165
+ const legacyPerActorFile = legacyPerActorCurrentFile(flowAgentsDir, key);
166
+ const legacyPerActorPayload = readJsonFileTolerant(legacyPerActorFile);
167
+ if (legacyPerActorPayload !== null) return { payload: legacyPerActorPayload, source: 'per-actor', file: legacyPerActorFile };
95
168
  }
96
169
 
97
170
  const legacyFile = path.join(flowAgentsDir, 'current.json');
@@ -101,6 +174,35 @@ function readCurrentPointer(flowAgentsDir, actorKey) {
101
174
  return { payload: null, source: 'none', file: null };
102
175
  }
103
176
 
177
+ /**
178
+ * #440: the ownership-bearing read. Identical inputs/shape to readCurrentPointer, but for a
179
+ * RESOLVED actor NEVER falls back to the shared legacy current.json or (by construction, since
180
+ * this function makes no repo-wide scan) a global mtime scan — only this actor's own per-actor
181
+ * projection counts (D1). An unresolved/empty actorKey delegates unchanged to readCurrentPointer
182
+ * (D3 compat — today's legacy-fallback behavior is preserved exactly for that case).
183
+ *
184
+ * @param {string} flowAgentsDir
185
+ * @param {string} [actorKey]
186
+ * @returns {{ payload: object|null, source: "per-actor"|"none"|"legacy", file: string|null }}
187
+ */
188
+ function readOwnCurrentPointer(flowAgentsDir, actorKey) {
189
+ const key = actorKey == null ? '' : String(actorKey);
190
+ if (!key || isUnresolvedActor(key)) {
191
+ return readCurrentPointer(flowAgentsDir, actorKey);
192
+ }
193
+ const perActorFile = perActorCurrentFile(flowAgentsDir, key);
194
+ const perActorPayload = readJsonFileTolerant(perActorFile);
195
+ if (perActorPayload !== null) return { payload: perActorPayload, source: 'per-actor', file: perActorFile };
196
+ // #440 fix-wave 2: the new collision-resistant file doesn't exist/parse — fall back to the
197
+ // pre-fix-wave-2 legacy per-actor filename (transition window only; see module header). This
198
+ // is still THIS actor's own read (never the shared legacy current.json, never a global scan) —
199
+ // D1 is unaffected, only the per-actor filename resolution gained a second name to try.
200
+ const legacyPerActorFile = legacyPerActorCurrentFile(flowAgentsDir, key);
201
+ const legacyPerActorPayload = readJsonFileTolerant(legacyPerActorFile);
202
+ if (legacyPerActorPayload !== null) return { payload: legacyPerActorPayload, source: 'per-actor', file: legacyPerActorFile };
203
+ return { payload: null, source: 'none', file: null };
204
+ }
205
+
104
206
  /**
105
207
  * Durable write-side counterpart to `readCurrentPointer`'s per-actor branch — used only by
106
208
  * `workflow-sidecar.ts`'s `writeCurrent()` (Wave 2 Task 2.1), which calls this ALONGSIDE
@@ -120,4 +222,4 @@ function writePerActorCurrent(flowAgentsDir, actorKey, payload) {
120
222
  fs.writeFileSync(file, `${JSON.stringify(payload, null, 2)}\n`);
121
223
  }
122
224
 
123
- module.exports = { perActorCurrentFile, readCurrentPointer, writePerActorCurrent };
225
+ module.exports = { perActorCurrentFile, legacyPerActorCurrentFile, readCurrentPointer, readOwnCurrentPointer, writePerActorCurrent };
@@ -93,7 +93,7 @@ const { livenessStreamFile, appendLivenessEvent } = require('./liveness-write');
93
93
  const { readLivenessEvents, readLivenessEventsTail, freshHolders } = require('./liveness-read');
94
94
  const { resolveActor, isUnresolvedActor, sanitizeSegment } = require('./actor-identity');
95
95
  const { flowAgentsArtifactRoot } = require('./local-artifact-paths');
96
- const { readCurrentPointer } = require('./current-pointer');
96
+ const { readOwnCurrentPointer } = require('./current-pointer');
97
97
 
98
98
  /**
99
99
  * Resolve a caller-supplied `now` (Date, ISO string, or omitted) to epoch ms.
@@ -115,16 +115,19 @@ function resolveNowMs(now) {
115
115
  }
116
116
 
117
117
  /**
118
- * Read the active_slug from the actor-scoped "current" pointer (#291: per-actor
119
- * `current/<actor>.json` preferred over the legacy global `current.json`, via
120
- * `readCurrentPointer` the single choke point for that preference rule), sanitized through the
121
- * same charset+cap restriction as actor-identity.js's `sanitizeSegment` (F5, #288 fix iteration 1,
122
- * sec-LOW: this is a local file that could be hand-edited or otherwise hostile `active_slug`
123
- * must never be trusted verbatim before it is used as a JSONL grouping key / emitted `subjectId`
124
- * or compared against event data). Tolerates a missing file or malformed JSON (returns "") exactly
125
- * like the plain-`fs.readFileSync` read this replaces; when `actorKey` is empty/unresolved,
126
- * `readCurrentPointer` falls straight to the legacy global file, so behavior for that case is
127
- * unchanged from before #291.
118
+ * Read the active_slug from the actor-scoped "current" pointer, via `readOwnCurrentPointer` — the
119
+ * single choke point for that preference rule. #440: for a RESOLVED actorKey this reads ONLY
120
+ * that actor's own per-actor `current/<actor>.json` projection it never falls back to the
121
+ * shared legacy global `current.json`, so a session co-located with another actor's work never
122
+ * emits a heartbeat naming that other actor's subject (D1). When `actorKey` is empty/unresolved,
123
+ * `readOwnCurrentPointer` delegates unchanged to `readCurrentPointer`, which DOES fall straight
124
+ * to the legacy global file behavior for that case is unchanged from before #291/#440 (D3
125
+ * compat). The returned `active_slug` is sanitized through the same charset+cap restriction as
126
+ * actor-identity.js's `sanitizeSegment` (F5, #288 fix iteration 1, sec-LOW: this is a local file
127
+ * that could be hand-edited or otherwise hostile — `active_slug` must never be trusted verbatim
128
+ * before it is used as a JSONL grouping key / emitted `subjectId` or compared against event
129
+ * data). Tolerates a missing file or malformed JSON (returns "") exactly like the plain-
130
+ * `fs.readFileSync` read this replaces.
128
131
  *
129
132
  * @param {string} root
130
133
  * @param {string} [actorKey]
@@ -150,7 +153,7 @@ function mightHaveActiveSession(root) {
150
153
  }
151
154
 
152
155
  function readActiveSlug(root, actorKey) {
153
- const { payload: current } = readCurrentPointer(root, actorKey);
156
+ const { payload: current } = readOwnCurrentPointer(root, actorKey);
154
157
  if (!current) return '';
155
158
  const activeSlug = (current && current.active_slug) || '';
156
159
  if (!activeSlug) return '';