@basein/runner 0.3.25 → 0.3.27
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/README.md +27 -26
- package/dist/auth/client.d.ts +4 -1
- package/dist/auth/client.js +83 -14
- package/dist/bin/bir-hooks.js +4 -2
- package/dist/bin/bir.js +153 -37
- package/dist/bin/investigate.d.ts +13 -0
- package/dist/bin/investigate.js +72 -2
- package/dist/bin/scenario-edit.d.ts +5 -0
- package/dist/bin/scenario-edit.js +17 -1
- package/dist/config/adapters/claude-code.d.ts +51 -7
- package/dist/config/adapters/claude-code.js +105 -18
- package/dist/config/generate.d.ts +61 -13
- package/dist/config/generate.js +108 -20
- package/dist/config/resolve.d.ts +5 -0
- package/dist/config/resolve.js +7 -0
- package/dist/control/correlation.js +0 -0
- package/dist/control/daemon.d.ts +28 -0
- package/dist/control/daemon.js +62 -0
- package/dist/control/paths.d.ts +19 -1
- package/dist/control/paths.js +20 -2
- package/dist/control/pin-guard.js +8 -2
- package/dist/control/recorder-main.js +17 -5
- package/dist/control/server.d.ts +75 -11
- package/dist/control/server.js +440 -77
- package/dist/control/transcript.d.ts +64 -3
- package/dist/control/transcript.js +318 -37
- package/dist/jsonrpc/framing.d.ts +18 -3
- package/dist/jsonrpc/framing.js +57 -4
- package/dist/proxy/intercept.d.ts +4 -0
- package/dist/proxy/intercept.js +4 -0
- package/dist/proxy/relay.d.ts +13 -0
- package/dist/proxy/relay.js +12 -1
- package/dist/proxy/session.d.ts +8 -1
- package/dist/proxy/session.js +67 -10
- package/dist/record/recorder.d.ts +6 -1
- package/dist/record/redact.d.ts +18 -4
- package/dist/record/redact.js +538 -79
- package/dist/record/remote-recorder.d.ts +26 -5
- package/dist/record/remote-recorder.js +174 -18
- package/dist/record/truncate.js +3 -1
- package/dist/replay/bash-channel.d.ts +7 -1
- package/dist/replay/bash-channel.js +9 -3
- package/dist/replay/builtins.d.ts +14 -0
- package/dist/replay/builtins.js +460 -95
- package/dist/replay/bundle.d.ts +15 -11
- package/dist/replay/bundle.js +167 -33
- package/dist/replay/controller.d.ts +92 -1
- package/dist/replay/controller.js +246 -47
- package/dist/replay/derive.js +46 -4
- package/dist/replay/handover.d.ts +13 -0
- package/dist/replay/handover.js +18 -0
- package/dist/replay/plan.d.ts +41 -0
- package/dist/replay/plan.js +171 -3
- package/dist/replay/pricing.d.ts +61 -11
- package/dist/replay/pricing.js +135 -42
- package/dist/replay/reads-only.d.ts +48 -0
- package/dist/replay/reads-only.js +161 -0
- package/dist/replay/safety.js +43 -5
- package/dist/upstream/http-client.d.ts +6 -3
- package/dist/upstream/http-client.js +54 -13
- package/dist/upstream/stdio-client.d.ts +7 -0
- package/dist/upstream/stdio-client.js +1 -1
- package/dist/util/atomic-write.d.ts +9 -1
- package/dist/util/atomic-write.js +9 -6
- package/dist/util/service-health.d.ts +7 -0
- package/dist/util/service-health.js +10 -3
- package/package.json +1 -1
- package/production.md +2 -2
- package/scripts/install.ps1 +14 -2
package/dist/jsonrpc/framing.js
CHANGED
|
@@ -35,6 +35,56 @@ export const DEFAULT_MAX_LINE_BYTES = 128 * 1024 * 1024;
|
|
|
35
35
|
export const OVERSIZE_SAMPLE_BYTES = 256;
|
|
36
36
|
const NEWLINE = 0x0a;
|
|
37
37
|
const EMPTY = Buffer.alloc(0);
|
|
38
|
+
/**
|
|
39
|
+
* The text each received message was parsed from, by the message object itself.
|
|
40
|
+
*
|
|
41
|
+
* WHY IT IS KEPT. A relay that parses a line and serialises the result again
|
|
42
|
+
* does not forward the message, it forwards JavaScript's reading of it:
|
|
43
|
+
* `{"big":9007199254740993,"snowflake":1234567890123456789,"f":1.0}` went out
|
|
44
|
+
* as `{"big":9007199254740992,"snowflake":1234567890123456800,"f":1}`. A
|
|
45
|
+
* JavaScript sender had lost those digits already; a Python or Rust one had
|
|
46
|
+
* not, and neither had the server it was talking to. So a message that is
|
|
47
|
+
* written as it was read goes out as the text that came in ({@link writeFrame}),
|
|
48
|
+
* which also saves serialising every message a second time.
|
|
49
|
+
*
|
|
50
|
+
* WHY BY IDENTITY. The text belongs to one object: the one the reader made.
|
|
51
|
+
* Whoever wants a different message on the wire builds a different object (the
|
|
52
|
+
* interceptor does, for the two messages it rewrites), and a different object
|
|
53
|
+
* has no entry here, so it is serialised as it always was. Nothing between a
|
|
54
|
+
* reader and a writer changes a message in place, and nothing may: the change
|
|
55
|
+
* would not be sent. Code that may — an embedder's interceptor — gets the
|
|
56
|
+
* message after {@link forgetText} (proxy/relay.ts), and so does whoever calls
|
|
57
|
+
* readFrames and writeFrame directly and changes what it read: call it first.
|
|
58
|
+
*
|
|
59
|
+
* Weak, so an entry lives exactly as long as its message does.
|
|
60
|
+
*/
|
|
61
|
+
const received = new WeakMap();
|
|
62
|
+
/** Note that `msg` was parsed from `text`, so it can be written again as that text. */
|
|
63
|
+
export function rememberText(msg, text) {
|
|
64
|
+
received.set(msg, text);
|
|
65
|
+
}
|
|
66
|
+
/** Forget the text `msg` was parsed from: it is serialised again when written, with any change made to it. */
|
|
67
|
+
export function forgetText(msg) {
|
|
68
|
+
if (msg && typeof msg === "object")
|
|
69
|
+
received.delete(msg);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* The text `msg` was parsed from, when it can be sent again as one line;
|
|
73
|
+
* undefined for a message nobody read off the wire, and for one whose text
|
|
74
|
+
* holds a raw line break (a pretty-printed HTTP body: inside a JSON string a
|
|
75
|
+
* line break is always escaped, so a raw one is layout, and layout cannot
|
|
76
|
+
* cross a line-delimited stream). One trailing CR is a CRLF sender's line
|
|
77
|
+
* ending, not part of the message.
|
|
78
|
+
*/
|
|
79
|
+
export function receivedText(msg) {
|
|
80
|
+
if (!msg || typeof msg !== "object")
|
|
81
|
+
return undefined;
|
|
82
|
+
const text = received.get(msg);
|
|
83
|
+
if (text === undefined)
|
|
84
|
+
return undefined;
|
|
85
|
+
const line = text.endsWith("\r") ? text.slice(0, -1) : text;
|
|
86
|
+
return line.includes("\n") || line.includes("\r") ? undefined : line;
|
|
87
|
+
}
|
|
38
88
|
/** The last `n` bytes of `a` followed by `b`, as a fresh buffer. */
|
|
39
89
|
function lastBytes(a, b, n) {
|
|
40
90
|
if (b.length >= n)
|
|
@@ -143,6 +193,7 @@ export function readFrames(input, handlers, opts = {}) {
|
|
|
143
193
|
});
|
|
144
194
|
return;
|
|
145
195
|
}
|
|
196
|
+
rememberText(msg, text);
|
|
146
197
|
handlers.onMessage(msg);
|
|
147
198
|
};
|
|
148
199
|
const onData = (chunk) => {
|
|
@@ -207,14 +258,16 @@ export function readFrames(input, handlers, opts = {}) {
|
|
|
207
258
|
};
|
|
208
259
|
}
|
|
209
260
|
/**
|
|
210
|
-
* Write one message as a single line
|
|
211
|
-
*
|
|
212
|
-
*
|
|
261
|
+
* Write one message as a single line: the line it was read from when `msg` is
|
|
262
|
+
* the very object a reader produced ({@link receivedText}), its serialisation
|
|
263
|
+
* otherwise. Serialization failure is swallowed after being reported: a proxy
|
|
264
|
+
* that throws while answering the host takes the host's session down with it,
|
|
265
|
+
* which rule 4 of §1 forbids.
|
|
213
266
|
*/
|
|
214
267
|
export function writeFrame(output, msg, onError) {
|
|
215
268
|
let line;
|
|
216
269
|
try {
|
|
217
|
-
line = JSON.stringify(msg);
|
|
270
|
+
line = receivedText(msg) ?? JSON.stringify(msg);
|
|
218
271
|
}
|
|
219
272
|
catch (err) {
|
|
220
273
|
onError?.(err instanceof Error ? err : new Error(String(err)));
|
|
@@ -56,6 +56,8 @@ export declare function tolerateCallId(schema: unknown): unknown;
|
|
|
56
56
|
/** Apply {@link tolerateCallId} to every tool of a `tools/list` result. */
|
|
57
57
|
export declare function tolerateCallIdInToolsList(result: unknown): unknown;
|
|
58
58
|
export declare class RecordingInterceptor implements Interceptor {
|
|
59
|
+
/** It builds a new message for each one it changes (relay.ts). */
|
|
60
|
+
readonly keepsMessagesWhole = true;
|
|
59
61
|
private readonly serverName;
|
|
60
62
|
private readonly session;
|
|
61
63
|
/** Host request id → the call it opened. Only `tools/call` ids appear here. */
|
|
@@ -92,6 +94,8 @@ export declare class RecordingInterceptor implements Interceptor {
|
|
|
92
94
|
* tool error the runner caused. No tracking, no reporting, no schema change.
|
|
93
95
|
*/
|
|
94
96
|
export declare class StripCallIdInterceptor implements Interceptor {
|
|
97
|
+
/** It builds a new message for each one it changes (relay.ts). */
|
|
98
|
+
readonly keepsMessagesWhole = true;
|
|
95
99
|
onHostMessage(msg: JsonRpcMessage): ReturnType<NonNullable<Interceptor["onHostMessage"]>>;
|
|
96
100
|
}
|
|
97
101
|
/** Re-exported so callers wiring a proxy need one import for the correlation key. */
|
package/dist/proxy/intercept.js
CHANGED
|
@@ -99,6 +99,8 @@ function capForReport(value) {
|
|
|
99
99
|
}
|
|
100
100
|
}
|
|
101
101
|
export class RecordingInterceptor {
|
|
102
|
+
/** It builds a new message for each one it changes (relay.ts). */
|
|
103
|
+
keepsMessagesWhole = true;
|
|
102
104
|
serverName;
|
|
103
105
|
session;
|
|
104
106
|
/** Host request id → the call it opened. Only `tools/call` ids appear here. */
|
|
@@ -268,6 +270,8 @@ export class RecordingInterceptor {
|
|
|
268
270
|
* tool error the runner caused. No tracking, no reporting, no schema change.
|
|
269
271
|
*/
|
|
270
272
|
export class StripCallIdInterceptor {
|
|
273
|
+
/** It builds a new message for each one it changes (relay.ts). */
|
|
274
|
+
keepsMessagesWhole = true;
|
|
271
275
|
onHostMessage(msg) {
|
|
272
276
|
if (msg.method !== "tools/call")
|
|
273
277
|
return { action: "forward" };
|
package/dist/proxy/relay.d.ts
CHANGED
|
@@ -48,8 +48,21 @@ export type HostDecision =
|
|
|
48
48
|
* The intercept table's interface. Every method is optional and **synchronous**:
|
|
49
49
|
* recording must never sit between the upstream's answer and the host (§3,
|
|
50
50
|
* Phase 3), so an interceptor queues work rather than awaiting it.
|
|
51
|
+
*
|
|
52
|
+
* AN INTERCEPTOR NEVER CHANGES THE MESSAGE IT IS HANDED. To put something else
|
|
53
|
+
* on the wire it returns another object. A message that comes back as the same
|
|
54
|
+
* object is forwarded as the text it arrived in (jsonrpc/framing.ts), not
|
|
55
|
+
* serialised again — that is what keeps an integer past 2^53 whole across the
|
|
56
|
+
* proxy — so a change made in place would never be sent.
|
|
57
|
+
*
|
|
58
|
+
* Only for an interceptor that says so ({@link Interceptor.keepsMessagesWhole}):
|
|
59
|
+
* the runner's own do. One that does not — an embedder's, written when a
|
|
60
|
+
* forwarded message was always serialised again — gets each message as it
|
|
61
|
+
* always did, and a change it makes in place is sent.
|
|
51
62
|
*/
|
|
52
63
|
export interface Interceptor {
|
|
64
|
+
/** It never changes a message it is handed: what it forwards may go out as the text that came in. */
|
|
65
|
+
readonly keepsMessagesWhole?: boolean;
|
|
53
66
|
/** Called for every host→upstream message. Default: forward unchanged. */
|
|
54
67
|
onHostMessage?(msg: JsonRpcMessage): HostDecision | void;
|
|
55
68
|
/**
|
package/dist/proxy/relay.js
CHANGED
|
@@ -22,9 +22,17 @@
|
|
|
22
22
|
* request the proxy originates itself lives in the disjoint `bir:` id space
|
|
23
23
|
* (see `upstream/client.ts`), so a collision cannot happen.
|
|
24
24
|
*/
|
|
25
|
-
import { readFrames, writeFrame } from "../jsonrpc/framing.js";
|
|
25
|
+
import { forgetText, readFrames, writeFrame } from "../jsonrpc/framing.js";
|
|
26
26
|
import { JsonRpcErrorCode, errorResponse, isRequest, isResponse, paramsObject, } from "../jsonrpc/types.js";
|
|
27
27
|
import { logDetail, logLine, errText } from "../util/log.js";
|
|
28
|
+
/**
|
|
29
|
+
* Before `interceptor` sees `msg`: one that may change it in place gets it to
|
|
30
|
+
* be serialised again when written, so the change is what goes out.
|
|
31
|
+
*/
|
|
32
|
+
function handTo(interceptor, msg) {
|
|
33
|
+
if (interceptor && !interceptor.keepsMessagesWhole)
|
|
34
|
+
forgetText(msg);
|
|
35
|
+
}
|
|
28
36
|
export class Relay {
|
|
29
37
|
pending = new Map();
|
|
30
38
|
opts;
|
|
@@ -46,6 +54,7 @@ export class Relay {
|
|
|
46
54
|
this.pending.delete(id);
|
|
47
55
|
let out = msg;
|
|
48
56
|
try {
|
|
57
|
+
handTo(interceptor, msg);
|
|
49
58
|
const replaced = interceptor?.onUpstreamResponse?.(p, msg);
|
|
50
59
|
if (replaced)
|
|
51
60
|
out = replaced;
|
|
@@ -58,6 +67,7 @@ export class Relay {
|
|
|
58
67
|
return;
|
|
59
68
|
}
|
|
60
69
|
try {
|
|
70
|
+
handTo(interceptor, msg);
|
|
61
71
|
interceptor?.onUpstreamMessage?.(msg);
|
|
62
72
|
}
|
|
63
73
|
catch (err) {
|
|
@@ -94,6 +104,7 @@ export class Relay {
|
|
|
94
104
|
const { upstream, hostOut, interceptor, label } = this.opts;
|
|
95
105
|
let decision = { action: "forward" };
|
|
96
106
|
try {
|
|
107
|
+
handTo(interceptor, msg);
|
|
97
108
|
decision = interceptor?.onHostMessage?.(msg) ?? { action: "forward" };
|
|
98
109
|
}
|
|
99
110
|
catch (err) {
|
package/dist/proxy/session.d.ts
CHANGED
|
@@ -261,7 +261,13 @@ export declare class ProxySession {
|
|
|
261
261
|
private reregister;
|
|
262
262
|
/** Sleep `ms`, or less if {@link wakeLoop} is called first. True when woken early. */
|
|
263
263
|
private pause;
|
|
264
|
-
/**
|
|
264
|
+
/**
|
|
265
|
+
* No control server: record on our own if this project opted in, else stay out of the way.
|
|
266
|
+
*
|
|
267
|
+
* `atClose`: for {@link close}, with steps still held. No jitter, and no
|
|
268
|
+
* stopping because the proxy is closing: both are for a proxy with a session
|
|
269
|
+
* ahead of it, and at close they are what left the held steps without an owner.
|
|
270
|
+
*/
|
|
265
271
|
private settleUnbound;
|
|
266
272
|
/** While not bound, look for a control server again now and then. */
|
|
267
273
|
private scheduleRebind;
|
|
@@ -352,5 +358,6 @@ export declare class ProxySession {
|
|
|
352
358
|
* long as the process lives.
|
|
353
359
|
*/
|
|
354
360
|
close(): Promise<void>;
|
|
361
|
+
private closeWithinBudget;
|
|
355
362
|
}
|
|
356
363
|
//# sourceMappingURL=session.d.ts.map
|
package/dist/proxy/session.js
CHANGED
|
@@ -546,17 +546,31 @@ export class ProxySession {
|
|
|
546
546
|
this.wakeLoop = wake;
|
|
547
547
|
});
|
|
548
548
|
}
|
|
549
|
-
/**
|
|
550
|
-
|
|
549
|
+
/**
|
|
550
|
+
* No control server: record on our own if this project opted in, else stay out of the way.
|
|
551
|
+
*
|
|
552
|
+
* `atClose`: for {@link close}, with steps still held. No jitter, and no
|
|
553
|
+
* stopping because the proxy is closing: both are for a proxy with a session
|
|
554
|
+
* ahead of it, and at close they are what left the held steps without an owner.
|
|
555
|
+
*/
|
|
556
|
+
async settleUnbound(why, atClose = false) {
|
|
551
557
|
if (!(await this.optedIn())) {
|
|
552
558
|
this.becomePassthrough("this directory is not set up for recording, and no recorder is running for it");
|
|
553
559
|
return;
|
|
554
560
|
}
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
561
|
+
if (!atClose) {
|
|
562
|
+
// Only the real recorder authenticates; an injected one (tests) has
|
|
563
|
+
// nothing to spread. Waited in short turns, so a proxy that closes
|
|
564
|
+
// meanwhile hands over at once: close() has its whole budget for the
|
|
565
|
+
// steps it holds, not what a 2 s wait left of it.
|
|
566
|
+
if (!this.opts.recorderFactory) {
|
|
567
|
+
const until = Date.now() + Math.floor(Math.random() * STANDALONE_JITTER_MS);
|
|
568
|
+
while (!this.closing && Date.now() < until)
|
|
569
|
+
await sleep(Math.min(100, until - Date.now()));
|
|
570
|
+
}
|
|
571
|
+
if (this.closing)
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
560
574
|
await this.becomeStandalone(why);
|
|
561
575
|
}
|
|
562
576
|
/** While not bound, look for a control server again now and then. */
|
|
@@ -782,7 +796,10 @@ export class ProxySession {
|
|
|
782
796
|
// instead read as "issued by another service" (auth.url_mismatch), and
|
|
783
797
|
// the proxy recorded nothing for its whole life.
|
|
784
798
|
const baseUrl = await normalizedServiceUrl(configured);
|
|
785
|
-
|
|
799
|
+
// Closing with nothing held: not worth a sign-in round trip, nor an empty
|
|
800
|
+
// run on the service. With steps held it is: close() is waiting, inside
|
|
801
|
+
// its budget, for exactly this recorder.
|
|
802
|
+
if (this.closing && this.buffer.length === 0)
|
|
786
803
|
return new NullRecorder();
|
|
787
804
|
// A proxy runs inside the host's process tree with its stdio bound to the
|
|
788
805
|
// JSON-RPC stream: there is nowhere to prompt, and blocking on one would
|
|
@@ -984,6 +1001,21 @@ export class ProxySession {
|
|
|
984
1001
|
* long as the process lives.
|
|
985
1002
|
*/
|
|
986
1003
|
async close() {
|
|
1004
|
+
// Every wait in here is on a timer that does not hold the process up, and
|
|
1005
|
+
// at shutdown nothing else may: the host has closed stdin, and a stdio
|
|
1006
|
+
// server exits within milliseconds of that. bir-proxy then ended on its
|
|
1007
|
+
// own, exit 0, some 20 ms into this function — before a held step had an
|
|
1008
|
+
// owner, and before `finishRun`. One timer that does hold it, for no
|
|
1009
|
+
// longer than the budget (and the final flush's minimum past it).
|
|
1010
|
+
const hold = setTimeout(() => undefined, CLOSE_BUDGET_MS + MIN_FINAL_FLUSH_MS);
|
|
1011
|
+
try {
|
|
1012
|
+
await this.closeWithinBudget();
|
|
1013
|
+
}
|
|
1014
|
+
finally {
|
|
1015
|
+
clearTimeout(hold);
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
async closeWithinBudget() {
|
|
987
1019
|
const deadline = Date.now() + CLOSE_BUDGET_MS;
|
|
988
1020
|
const left = () => Math.max(0, deadline - Date.now());
|
|
989
1021
|
// Stops the work loop at its next turn. It is not awaited: it may be parked
|
|
@@ -1004,8 +1036,33 @@ export class ProxySession {
|
|
|
1004
1036
|
await new Promise((resolve) => setImmediate(resolve));
|
|
1005
1037
|
// Negotiation stops at its next turn once `closing` is set, but a turn can
|
|
1006
1038
|
// be a sign-in refresh against a slow service: bounded, not awaited whole.
|
|
1007
|
-
|
|
1008
|
-
|
|
1039
|
+
let negotiated = !this.negotiation;
|
|
1040
|
+
if (this.negotiation) {
|
|
1041
|
+
await settledWithin(this.negotiation.then(() => {
|
|
1042
|
+
negotiated = true;
|
|
1043
|
+
}), left());
|
|
1044
|
+
}
|
|
1045
|
+
if (this.tierValue === "pending" && this.buffer.length > 0) {
|
|
1046
|
+
// Stopped before it had settled a tier, and the steps taken meanwhile
|
|
1047
|
+
// are still held: a host with no control server whose session was
|
|
1048
|
+
// shorter than the discovery window (a scripted agent run of a few
|
|
1049
|
+
// seconds). Flushed as they were, they found no recorder, were held
|
|
1050
|
+
// again, and went with the process — a whole run, and not a line about
|
|
1051
|
+
// it. So they get their owner now, inside what is left of the budget.
|
|
1052
|
+
// Not while negotiation is still out (it overran the budget): two
|
|
1053
|
+
// recorders would be built, and there is no time left for one.
|
|
1054
|
+
if (negotiated) {
|
|
1055
|
+
await settledWithin(this.settleUnbound("the proxy closed before a control server answered", true), left());
|
|
1056
|
+
}
|
|
1057
|
+
if (this.tierValue === "pending") {
|
|
1058
|
+
this.lossyValue = true;
|
|
1059
|
+
logLine("run.lossy", {
|
|
1060
|
+
why: "closed before a recorder was ready",
|
|
1061
|
+
server: this.serverName,
|
|
1062
|
+
dropped: this.buffer.length,
|
|
1063
|
+
});
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1009
1066
|
this.flushBuffer();
|
|
1010
1067
|
// Steps waiting behind a slow service: each is handed to the recorder at
|
|
1011
1068
|
// once now (dispatch skips its per-step wait while closing), so this is
|
|
@@ -13,12 +13,16 @@ export interface StartRunOptions {
|
|
|
13
13
|
* `handoverId`: the hand-over's id, which its continuation request carried
|
|
14
14
|
* too, so the service counts the hand-over once. `counted`: the same for a
|
|
15
15
|
* service from before ids — it counted the hand-over when asked for its
|
|
16
|
-
* continuation.
|
|
16
|
+
* continuation. `stop`: where the stopped step stood — `not_run`, `failed`,
|
|
17
|
+
* `ran`, `uncertain`, … — sent for a `step_failed`, so the service never
|
|
18
|
+
* continues a hand-over after a step that ran, or may have, with work that
|
|
19
|
+
* starts with that step (the service's fallbk.md, "never continued").
|
|
17
20
|
*/
|
|
18
21
|
fallbackOf?: {
|
|
19
22
|
scenarioId: string;
|
|
20
23
|
stepIndex: number;
|
|
21
24
|
kind?: string;
|
|
25
|
+
stop?: string;
|
|
22
26
|
handoverId?: string;
|
|
23
27
|
counted?: boolean;
|
|
24
28
|
};
|
|
@@ -327,6 +331,7 @@ export interface IntentMatchRequest {
|
|
|
327
331
|
scenarioId: string;
|
|
328
332
|
stepIndex: number;
|
|
329
333
|
kind?: string;
|
|
334
|
+
stop?: string;
|
|
330
335
|
id?: string;
|
|
331
336
|
};
|
|
332
337
|
}
|
package/dist/record/redact.d.ts
CHANGED
|
@@ -45,13 +45,23 @@
|
|
|
45
45
|
* `token: newToken`). A separator never spans a line break: an empty
|
|
46
46
|
* `DB_PASSWORD=` must not take the next line with it.
|
|
47
47
|
*
|
|
48
|
+
* A VALUE IN QUOTES IS READ TO ITS CLOSING QUOTE, spaces, `&`, `;` and all: a
|
|
49
|
+
* passphrase has words in it. Nor is it code for what it starts with or holds:
|
|
50
|
+
* a password may start with `$`, `!` or `(` and have a bracket in it
|
|
51
|
+
* (`"p@ss(w0rd)1"`), and went out as it was while that made a value look like
|
|
52
|
+
* code. Only a reference — `${VAR}`, `$(cmd)`, `<placeholder>`, `{{ template }}`
|
|
53
|
+
* — exempts it. Unquoted, an expression still shows by its shape.
|
|
54
|
+
*
|
|
48
55
|
* JSON TEXT IS REDACTED AS JSON. A serialized tool payload goes through
|
|
49
56
|
* {@link redactJsonText}, which parses it and runs the rules on each string
|
|
50
57
|
* leaf — the same pass its producer's {@link redact} made — so the result is
|
|
51
58
|
* always valid JSON and, when nothing new was found, the producer's bytes
|
|
52
|
-
* exactly (live replay serializes a result the same way and compares).
|
|
53
|
-
*
|
|
54
|
-
*
|
|
59
|
+
* exactly (live replay serializes a result the same way and compares). A
|
|
60
|
+
* string leaf is text, whatever it holds: a JSON document in one — a file's
|
|
61
|
+
* content, what an MCP tool returns in `content[].text` — gets the text rules
|
|
62
|
+
* where it stands, and is never parsed and written back. The text rules read
|
|
63
|
+
* JSON text, plain and JSON-escaped (a document held in a string, a preview of
|
|
64
|
+
* one cut off before it parses): they never start a match inside an escape
|
|
55
65
|
* sequence and never replace a quote or a backslash, so an escaped document
|
|
56
66
|
* stays parseable.
|
|
57
67
|
*
|
|
@@ -117,7 +127,11 @@ export declare function redactText(text: string): string;
|
|
|
117
127
|
* producers redact first — the original bytes come back untouched: live replay
|
|
118
128
|
* serializes a result the same way and compares against what was recorded.
|
|
119
129
|
* Text that is not JSON (a truncated preview, a producer's plain error) gets
|
|
120
|
-
* {@link redactText}: there is no structure to keep.
|
|
130
|
+
* {@link redactText}: there is no structure to keep. Unless the rules gave it
|
|
131
|
+
* one: a private key written over several lines can be all that kept a
|
|
132
|
+
* document from parsing, and with the key gone it is redacted as the JSON it
|
|
133
|
+
* now is — or the next pass over it (the recorder's, after its producer's)
|
|
134
|
+
* would be the one to do that, and move its bytes.
|
|
121
135
|
*/
|
|
122
136
|
export declare function redactJsonText(text: string): string;
|
|
123
137
|
/**
|