@kontourai/flow-agents 3.9.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.
- package/.github/workflows/ci.yml +8 -0
- package/CHANGELOG.md +17 -0
- package/README.md +1 -1
- package/build/src/cli/assignment-provider.js +4 -18
- package/build/src/cli/continuation-adapter.d.ts +10 -8
- package/build/src/cli/continuation-adapter.js +22 -4
- package/build/src/cli/validate-workflow-artifacts.js +21 -1
- package/build/src/cli/workflow-sidecar.js +15 -20
- package/build/src/cli/workflow.js +58 -11
- package/build/src/continuation-driver.d.ts +27 -3
- package/build/src/continuation-driver.js +97 -14
- package/build/src/index.d.ts +2 -2
- package/build/src/index.js +1 -1
- package/build/src/tools/build-universal-bundles.js +52 -25
- package/build/src/tools/validate-source-tree.js +5 -2
- package/context/scripts/hooks/lib/continuation-turn-authority.js +386 -0
- package/context/scripts/hooks/stop-goal-fit.js +244 -73
- package/context/scripts/hooks/workflow-steering.js +24 -15
- package/context/scripts/telemetry/lib/pricing.sh +18 -6
- package/context/scripts/telemetry/lib/transport.sh +139 -37
- package/context/scripts/telemetry/lib/usage.sh +282 -32
- package/context/scripts/telemetry/lib/usage_model_guard.sh +42 -0
- package/context/scripts/telemetry/telemetry.sh +76 -1
- package/docs/coordination-guide.md +21 -5
- package/docs/fixture-ownership.md +2 -1
- package/docs/public-workflow-cli.md +44 -0
- package/docs/spec/runtime-hook-surface.md +8 -4
- package/evals/acceptance/prove-capture-teeth-declared.sh +13 -0
- package/evals/acceptance/prove-capture-teeth.sh +13 -0
- package/evals/ci/run-baseline.sh +2 -0
- package/evals/fixtures/codex-legacy-agents/5273878130bdafc8a024a650bb5b66c9b003f1f859b5dc6e5b588cbf4ab23228.md +25 -0
- package/evals/fixtures/telemetry/usage-transcript-adversarial.jsonl +2 -0
- package/evals/fixtures/telemetry/usage-transcript-malformed-sandwich.jsonl +3 -0
- package/evals/fixtures/telemetry/usage-transcript-negative-tokens.jsonl +1 -0
- package/evals/fixtures/telemetry/usage-transcript-oversized-prefix.jsonl +201 -0
- package/evals/fixtures/telemetry/usage-transcript-oversized-tokens.jsonl +1 -0
- package/evals/fixtures/telemetry/usage-transcript-prefixed-secret-lower.jsonl +1 -0
- package/evals/fixtures/telemetry/usage-transcript-prefixed-secret-upper.jsonl +1 -0
- package/evals/fixtures/telemetry/usage-transcript-secret-model-jwt.jsonl +1 -0
- package/evals/fixtures/telemetry/usage-transcript-secret-model.jsonl +1 -0
- package/evals/fixtures/telemetry/usage-transcript-torn-trailing.jsonl +2 -0
- package/evals/integration/test_actor_identity.sh +63 -0
- package/evals/integration/test_assignment_provider_local_file.sh +34 -0
- package/evals/integration/test_builder_entry_enforcement.sh +5 -1
- package/evals/integration/test_bundle_install.sh +106 -17
- package/evals/integration/test_bundle_lifecycle.sh +17 -3
- package/evals/integration/test_captured_fail_reconciliation.sh +11 -0
- package/evals/integration/test_ci_actor_identity.sh +2 -2
- package/evals/integration/test_command_log_concurrency.sh +11 -0
- package/evals/integration/test_command_log_fork_classification.sh +11 -0
- package/evals/integration/test_command_log_integrity.sh +11 -0
- package/evals/integration/test_current_json_per_actor.sh +416 -21
- package/evals/integration/test_enforcer_expects_driven.sh +11 -0
- package/evals/integration/test_ensure_session_ownership_guard.sh +39 -0
- package/evals/integration/test_evidence_capture_hook.sh +25 -6
- package/evals/integration/test_fixture_retirement_audit.sh +2 -2
- package/evals/integration/test_flowdef_union_floor_regression.sh +11 -0
- package/evals/integration/test_gate_bypass_chain.sh +11 -0
- package/evals/integration/test_gate_lockdown.sh +10 -0
- package/evals/integration/test_goal_fit_ghost_session.sh +11 -0
- package/evals/integration/test_goal_fit_hook.sh +257 -9
- package/evals/integration/test_goal_fit_rederive.sh +11 -0
- package/evals/integration/test_install_merge.sh +141 -3
- package/evals/integration/test_liveness_conflict_injection.sh +39 -3
- package/evals/integration/test_liveness_heartbeat.sh +46 -9
- package/evals/integration/test_phase_map_and_gate_claim.sh +10 -1
- package/evals/integration/test_public_workflow_cli.sh +6 -6
- package/evals/integration/test_session_resume_roundtrip.sh +30 -0
- package/evals/integration/test_stop_hook_release.sh +111 -11
- package/evals/integration/test_telemetry_tool_usage.sh +394 -0
- package/evals/integration/test_verify_hold.sh +64 -1
- package/evals/integration/test_workflow_sidecar_writer.sh +10 -1
- package/evals/integration/test_workflow_steering_hook.sh +20 -4
- package/evals/run.sh +2 -0
- package/evals/static/test_universal_bundles.sh +123 -0
- package/package.json +1 -1
- package/packaging/README.md +21 -0
- package/packaging/codex-legacy-agents-fingerprints.json +33 -0
- package/scripts/README.md +2 -1
- package/scripts/audit-codex-legacy-agents.js +43 -0
- package/scripts/classify-codex-legacy-agents.js +71 -0
- package/scripts/hooks/evidence-capture.js +18 -6
- package/scripts/hooks/lib/actor-identity.js +62 -29
- package/scripts/hooks/lib/continuation-turn-authority.js +386 -0
- package/scripts/hooks/lib/current-pointer.js +114 -12
- package/scripts/hooks/lib/liveness-heartbeat.js +15 -12
- package/scripts/hooks/stop-goal-fit.js +244 -73
- package/scripts/hooks/workflow-steering.js +24 -15
- package/scripts/install-codex-home.sh +8 -7
- package/scripts/telemetry/lib/pricing.sh +18 -6
- package/scripts/telemetry/lib/transport.sh +71 -0
- package/scripts/telemetry/lib/usage.sh +282 -32
- package/scripts/telemetry/lib/usage_model_guard.sh +42 -0
- package/scripts/telemetry/telemetry.sh +76 -1
- package/src/cli/assignment-provider.ts +5 -26
- package/src/cli/builder-flow-runtime.test.mjs +93 -6
- package/src/cli/continuation-adapter.ts +33 -7
- package/src/cli/continuation-driver.test.mjs +741 -2
- package/src/cli/public-api.test.mjs +1 -0
- package/src/cli/validate-workflow-artifacts.ts +19 -1
- package/src/cli/workflow-sidecar.ts +18 -23
- package/src/cli/workflow.ts +71 -11
- package/src/continuation-driver.ts +125 -19
- package/src/index.ts +4 -0
- package/src/tools/build-universal-bundles.ts +61 -25
- 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)
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* both
|
|
26
|
-
*
|
|
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)
|
|
30
|
-
*
|
|
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
|
|
61
|
-
*
|
|
62
|
-
*
|
|
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 {
|
|
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
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
* `
|
|
127
|
-
*
|
|
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 } =
|
|
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 '';
|