Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions bt-daemon/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions bt-daemon/Cargo.toml
Original file line numberDiff line numberDiff line change
Expand Up@@ -24,6 +24,8 @@ async-trait = "0.1"
chrono = "0.4"
clap = { version = "4", features = ["derive", "env"] }
regex = "1"
rquickjs = "0.12.2"
rquickjs-serde = "0.6.1"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
sha2 = "0.10"
Expand Down
95 changes: 95 additions & 0 deletions bt-daemon/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -59,6 +59,101 @@ the default `bt` profile. Credentials and backend URLs are never stored here;
production resolves and refreshes them through `bt`. `bt trace run` supplies a
process-local settings overlay and never changes any of these files.

### JavaScript span plugins

`--plugin PATH` registers a synchronous ES module that transforms each
sink-neutral span row after translation and immediately before delivery. Repeat
the flag to compose plugins from left to right. `enable` persists its ordered list
for ordinary agent sessions. Managed runs and imports are isolated from that
list and use only the `--plugin` flags passed to their command. Each path is
canonicalized to an absolute path before it is validated or stored.

Each module must default-export a synchronous function. It receives a span and
`{ operation, source, session_id, env }`, and must return a JSON-compatible span
object. Span, root, and parent identities cannot be changed:

```js
// redact.mjs
function redact(value) {
if (typeof value === "string") {
return value.replace(/sk-[A-Za-z0-9_-]+/g, "[REDACTED]");
}
if (Array.isArray(value)) return value.map(redact);
if (value && typeof value === "object") {
return Object.fromEntries(
Object.entries(value).map(([key, child]) => [key, redact(child)]),
);
}
return value;
}

export default function redactSpan(span) {
const next = { ...span };
for (const field of ["input", "output", "error"]) {
if (field in next) next[field] = redact(next[field]);
}
return next;
}
```

The context can drive a second transform without changing the first one:

```js
// tag-ci.mjs
export default function tagCi(span, context) {
if (!context.env.CI) return span;

return {
...span,
tags: [...new Set([...(span.tags ?? []), "ci"])],
metadata: {
...(span.metadata ?? {}),
deployment: context.env.DEPLOYMENT_ENV ?? "unknown",
trace_source: context.source,
},
};
}
```

Register both transforms persistently for ordinary Codex sessions. The
redactor runs first and its returned span becomes the tagger's input:

```bash
bt trace enable codex --plugin ./redact.mjs --plugin ./tag-ci.mjs
```

`run` and `import` plugins apply only to that command. They replace, rather than
merge with, plugins saved by `enable`:

```bash
# Only local.mjs runs; redact.mjs and tag-ci.mjs remain global enable behavior.
bt trace run --plugin ./local.mjs codex -- "summarize this change"

# Only sanitize-history.mjs transforms spans produced by this import.
bt trace import codex SESSION_ID --plugin ./sanitize-history.mjs
```

The journal stores raw input events, not transformed spans. After daemon
recovery, replayed events therefore pass through the resumed session's current
route: ordinary sessions use the current globally configured plugins, while a
managed session continues using only that run's isolated plugins.

`context.operation` is `"insert"` or `"merge"`; `context.source` and
`context.session_id` identify the translated event stream; and `context.env`
contains the daemon process environment. Environment variable names are
uppercased on Windows so common lookups such as `context.env.PATH` remain
portable.

The environment map is captured from the daemon process when each worker-local
span processor is constructed. Plugins execute in bounded, thread-local
QuickJS runtimes with no filesystem or network host APIs. Modules must be
self-contained and transforms must be stateless: module globals belong to a
worker thread, not a session. If a plugin fails, that worker reports and skips
only that plugin on subsequent spans; the remaining plugins continue to run.
Plugins are trusted local code: although they have no host APIs, they can copy
environment values into spans that are delivered to Braintrust. Read only the
specific variables needed by the transform; never attach `context.env` itself.

### Additional root metadata

`additional_metadata` is a JSON object merged into each traced session's root
Expand Down
5 changes: 4 additions & 1 deletion bt-daemon/config.json.example
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,6 +14,9 @@
"additional_metadata": {
"team": "platform",
"environment": "development"
}
},
"span_plugins": [
"/absolute/path/to/redact.mjs"
]
}
}
13 changes: 9 additions & 4 deletions bt-daemon/docs/protocol.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -205,7 +205,8 @@ Used for version handover and by tests.
"project_name": "codex"
},
"flush_mode": "fire_and_forget",
"additional_metadata": { "…": "…" }
"additional_metadata": { "…": "…" },
"span_plugins": ["/absolute/path/redact.mjs"]
}
}
```
Expand DownExpand Up@@ -270,7 +271,9 @@ Field notes:

Live credentials returned by the host provider are **never** written to the
journal, logs, status, or RPC response. Envelopes journal only their non-secret
`route`, allowing restart recovery to resolve a fresh lease.
`route`, allowing restart recovery to resolve a fresh lease. Span plugins read
an environment snapshot captured inside their daemon worker process; it is not
part of the envelope or journal schema.

## Daemon lifecycle

Expand DownExpand Up@@ -337,8 +340,10 @@ profiles, organizations, and destinations while sharing one daemon.
`$HOME/.braintrust/state/bt-daemon` on Unix, and
`%LOCALAPPDATA%\Braintrust\bt-daemon` on Windows. On restart the daemon
rebuilds each route's unfinished correlation state independently, replaying
only the journal entries whose `route` matches that pipeline into a fresh
translator. The resulting rows may be resubmitted to repair delivery
only the journal entries whose delivery route matches that pipeline into a
fresh translator. Span plugin paths are ignored for this comparison so raw
events can be replayed through the current plugin chain. The resulting rows
may be resubmitted to repair delivery
interrupted by a crash, but their deterministic ids target the same backend
rows and must never create duplicate spans, and a route never receives
another route's rows. Replay streams the journal and is bounded to the
Expand Down
33 changes: 31 additions & 2 deletions bt-daemon/src/dispatch.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -457,7 +457,36 @@ impl SessionActor {
&ops,
);
}
match sink.emit(&ops).await {
let plugin_paths = ctx
.config
.as_ref()
.map(|config| config.span_plugins.as_slice())
.unwrap_or_default();
let mut processed = Vec::with_capacity(ops.len());
for op in &ops {
match crate::span_processor::process(
plugin_paths,
op,
&self.source,
&self.session_id,
) {
Ok(result) => {
for failure in result.failures {
self.set_error(format!(
"span plugin {} failed; disabled on this worker: {}",
failure.path.display(),
failure.message
));
}
processed.push(result.op);
}
Err(error) => {
self.set_error(format!("span plugin processor failed: {error}"));
processed.push(op.clone());
}
}
}
match sink.emit(&processed).await {
Ok(n) => {
self.counters.spans_emitted.fetch_add(n, Ordering::Relaxed);
}
Expand DownExpand Up@@ -510,7 +539,7 @@ impl SessionActor {
if !entry
.route
.as_ref()
.is_some_and(|candidate| candidate.same_route(&plan.route))
.is_some_and(|candidate| candidate.same_replay_route(&plan.route))
{
continue;
}
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions bt-daemon/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions bt-daemon/Cargo.toml
Original file line numberDiff line numberDiff line change
Expand Up@@ -24,6 +24,8 @@ async-trait = "0.1"
chrono = "0.4"
clap = { version = "4", features = ["derive", "env"] }
regex = "1"
rquickjs = "0.12.2"
rquickjs-serde = "0.6.1"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
sha2 = "0.10"
Expand Down
95 changes: 95 additions & 0 deletions bt-daemon/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -59,6 +59,101 @@ the default `bt` profile. Credentials and backend URLs are never stored here;
production resolves and refreshes them through `bt`. `bt trace run` supplies a
process-local settings overlay and never changes any of these files.

### JavaScript span plugins

`--plugin PATH` registers a synchronous ES module that transforms each
sink-neutral span row after translation and immediately before delivery. Repeat
the flag to compose plugins from left to right. `enable` persists its ordered list
for ordinary agent sessions. Managed runs and imports are isolated from that
list and use only the `--plugin` flags passed to their command. Each path is
canonicalized to an absolute path before it is validated or stored.

Each module must default-export a synchronous function. It receives a span and
`{ operation, source, session_id, env }`, and must return a JSON-compatible span
object. Span, root, and parent identities cannot be changed:

```js
// redact.mjs
function redact(value) {
if (typeof value === "string") {
return value.replace(/sk-[A-Za-z0-9_-]+/g, "[REDACTED]");
}
if (Array.isArray(value)) return value.map(redact);
if (value && typeof value === "object") {
return Object.fromEntries(
Object.entries(value).map(([key, child]) => [key, redact(child)]),
);
}
return value;
}

export default function redactSpan(span) {
const next = { ...span };
for (const field of ["input", "output", "error"]) {
if (field in next) next[field] = redact(next[field]);
}
return next;
}
```

The context can drive a second transform without changing the first one:

```js
// tag-ci.mjs
export default function tagCi(span, context) {
if (!context.env.CI) return span;

return {
...span,
tags: [...new Set([...(span.tags ?? []), "ci"])],
metadata: {
...(span.metadata ?? {}),
deployment: context.env.DEPLOYMENT_ENV ?? "unknown",
trace_source: context.source,
},
};
}
```

Register both transforms persistently for ordinary Codex sessions. The
redactor runs first and its returned span becomes the tagger's input:

```bash
bt trace enable codex --plugin ./redact.mjs --plugin ./tag-ci.mjs
```

`run` and `import` plugins apply only to that command. They replace, rather than
merge with, plugins saved by `enable`:

```bash
# Only local.mjs runs; redact.mjs and tag-ci.mjs remain global enable behavior.
bt trace run --plugin ./local.mjs codex -- "summarize this change"

# Only sanitize-history.mjs transforms spans produced by this import.
bt trace import codex SESSION_ID --plugin ./sanitize-history.mjs
```

The journal stores raw input events, not transformed spans. After daemon
recovery, replayed events therefore pass through the resumed session's current
route: ordinary sessions use the current globally configured plugins, while a
managed session continues using only that run's isolated plugins.

`context.operation` is `"insert"` or `"merge"`; `context.source` and
`context.session_id` identify the translated event stream; and `context.env`
contains the daemon process environment. Environment variable names are
uppercased on Windows so common lookups such as `context.env.PATH` remain
portable.

The environment map is captured from the daemon process when each worker-local
span processor is constructed. Plugins execute in bounded, thread-local
QuickJS runtimes with no filesystem or network host APIs. Modules must be
self-contained and transforms must be stateless: module globals belong to a
worker thread, not a session. If a plugin fails, that worker reports and skips
only that plugin on subsequent spans; the remaining plugins continue to run.
Plugins are trusted local code: although they have no host APIs, they can copy
environment values into spans that are delivered to Braintrust. Read only the
specific variables needed by the transform; never attach `context.env` itself.

### Additional root metadata

`additional_metadata` is a JSON object merged into each traced session's root
Expand Down
5 changes: 4 additions & 1 deletion bt-daemon/config.json.example
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,6 +14,9 @@
"additional_metadata": {
"team": "platform",
"environment": "development"
}
},
"span_plugins": [
"/absolute/path/to/redact.mjs"
]
}
}
13 changes: 9 additions & 4 deletions bt-daemon/docs/protocol.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -205,7 +205,8 @@ Used for version handover and by tests.
"project_name": "codex"
},
"flush_mode": "fire_and_forget",
"additional_metadata": { "…": "…" }
"additional_metadata": { "…": "…" },
"span_plugins": ["/absolute/path/redact.mjs"]
}
}
```
Expand DownExpand Up@@ -270,7 +271,9 @@ Field notes:

Live credentials returned by the host provider are **never** written to the
journal, logs, status, or RPC response. Envelopes journal only their non-secret
`route`, allowing restart recovery to resolve a fresh lease.
`route`, allowing restart recovery to resolve a fresh lease. Span plugins read
an environment snapshot captured inside their daemon worker process; it is not
part of the envelope or journal schema.

## Daemon lifecycle

Expand DownExpand Up@@ -337,8 +340,10 @@ profiles, organizations, and destinations while sharing one daemon.
`$HOME/.braintrust/state/bt-daemon` on Unix, and
`%LOCALAPPDATA%\Braintrust\bt-daemon` on Windows. On restart the daemon
rebuilds each route's unfinished correlation state independently, replaying
only the journal entries whose `route` matches that pipeline into a fresh
translator. The resulting rows may be resubmitted to repair delivery
only the journal entries whose delivery route matches that pipeline into a
fresh translator. Span plugin paths are ignored for this comparison so raw
events can be replayed through the current plugin chain. The resulting rows
may be resubmitted to repair delivery
interrupted by a crash, but their deterministic ids target the same backend
rows and must never create duplicate spans, and a route never receives
another route's rows. Replay streams the journal and is bounded to the
Expand Down
33 changes: 31 additions & 2 deletions bt-daemon/src/dispatch.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -457,7 +457,36 @@ impl SessionActor {
&ops,
);
}
match sink.emit(&ops).await {
let plugin_paths = ctx
.config
.as_ref()
.map(|config| config.span_plugins.as_slice())
.unwrap_or_default();
let mut processed = Vec::with_capacity(ops.len());
for op in &ops {
match crate::span_processor::process(
plugin_paths,
op,
&self.source,
&self.session_id,
) {
Ok(result) => {
for failure in result.failures {
self.set_error(format!(
"span plugin {} failed; disabled on this worker: {}",
failure.path.display(),
failure.message
));
}
processed.push(result.op);
}
Err(error) => {
self.set_error(format!("span plugin processor failed: {error}"));
processed.push(op.clone());
}
}
}
match sink.emit(&processed).await {
Ok(n) => {
self.counters.spans_emitted.fetch_add(n, Ordering::Relaxed);
}
Expand DownExpand Up@@ -510,7 +539,7 @@ impl SessionActor {
if !entry
.route
.as_ref()
.is_some_and(|candidate| candidate.same_route(&plan.route))
.is_some_and(|candidate| candidate.same_replay_route(&plan.route))
{
continue;
}
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions bt-daemon/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions bt-daemon/Cargo.toml
Original file line numberDiff line numberDiff line change
Expand Up@@ -24,6 +24,8 @@ async-trait = "0.1"
chrono = "0.4"
clap = { version = "4", features = ["derive", "env"] }
regex = "1"
rquickjs = "0.12.2"
rquickjs-serde = "0.6.1"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
sha2 = "0.10"
Expand Down
95 changes: 95 additions & 0 deletions bt-daemon/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -59,6 +59,101 @@ the default `bt` profile. Credentials and backend URLs are never stored here;
production resolves and refreshes them through `bt`. `bt trace run` supplies a
process-local settings overlay and never changes any of these files.

### JavaScript span plugins

`--plugin PATH` registers a synchronous ES module that transforms each
sink-neutral span row after translation and immediately before delivery. Repeat
the flag to compose plugins from left to right. `enable` persists its ordered list
for ordinary agent sessions. Managed runs and imports are isolated from that
list and use only the `--plugin` flags passed to their command. Each path is
canonicalized to an absolute path before it is validated or stored.

Each module must default-export a synchronous function. It receives a span and
`{ operation, source, session_id, env }`, and must return a JSON-compatible span
object. Span, root, and parent identities cannot be changed:

```js
// redact.mjs
function redact(value) {
if (typeof value === "string") {
return value.replace(/sk-[A-Za-z0-9_-]+/g, "[REDACTED]");
}
if (Array.isArray(value)) return value.map(redact);
if (value && typeof value === "object") {
return Object.fromEntries(
Object.entries(value).map(([key, child]) => [key, redact(child)]),
);
}
return value;
}

export default function redactSpan(span) {
const next = { ...span };
for (const field of ["input", "output", "error"]) {
if (field in next) next[field] = redact(next[field]);
}
return next;
}
```

The context can drive a second transform without changing the first one:

```js
// tag-ci.mjs
export default function tagCi(span, context) {
if (!context.env.CI) return span;

return {
...span,
tags: [...new Set([...(span.tags ?? []), "ci"])],
metadata: {
...(span.metadata ?? {}),
deployment: context.env.DEPLOYMENT_ENV ?? "unknown",
trace_source: context.source,
},
};
}
```

Register both transforms persistently for ordinary Codex sessions. The
redactor runs first and its returned span becomes the tagger's input:

```bash
bt trace enable codex --plugin ./redact.mjs --plugin ./tag-ci.mjs
```

`run` and `import` plugins apply only to that command. They replace, rather than
merge with, plugins saved by `enable`:

```bash
# Only local.mjs runs; redact.mjs and tag-ci.mjs remain global enable behavior.
bt trace run --plugin ./local.mjs codex -- "summarize this change"

# Only sanitize-history.mjs transforms spans produced by this import.
bt trace import codex SESSION_ID --plugin ./sanitize-history.mjs
```

The journal stores raw input events, not transformed spans. After daemon
recovery, replayed events therefore pass through the resumed session's current
route: ordinary sessions use the current globally configured plugins, while a
managed session continues using only that run's isolated plugins.

`context.operation` is `"insert"` or `"merge"`; `context.source` and
`context.session_id` identify the translated event stream; and `context.env`
contains the daemon process environment. Environment variable names are
uppercased on Windows so common lookups such as `context.env.PATH` remain
portable.

The environment map is captured from the daemon process when each worker-local
span processor is constructed. Plugins execute in bounded, thread-local
QuickJS runtimes with no filesystem or network host APIs. Modules must be
self-contained and transforms must be stateless: module globals belong to a
worker thread, not a session. If a plugin fails, that worker reports and skips
only that plugin on subsequent spans; the remaining plugins continue to run.
Plugins are trusted local code: although they have no host APIs, they can copy
environment values into spans that are delivered to Braintrust. Read only the
specific variables needed by the transform; never attach `context.env` itself.

### Additional root metadata

`additional_metadata` is a JSON object merged into each traced session's root
Expand Down
5 changes: 4 additions & 1 deletion bt-daemon/config.json.example
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,6 +14,9 @@
"additional_metadata": {
"team": "platform",
"environment": "development"
}
},
"span_plugins": [
"/absolute/path/to/redact.mjs"
]
}
}
13 changes: 9 additions & 4 deletions bt-daemon/docs/protocol.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -205,7 +205,8 @@ Used for version handover and by tests.
"project_name": "codex"
},
"flush_mode": "fire_and_forget",
"additional_metadata": { "…": "…" }
"additional_metadata": { "…": "…" },
"span_plugins": ["/absolute/path/redact.mjs"]
}
}
```
Expand DownExpand Up@@ -270,7 +271,9 @@ Field notes:

Live credentials returned by the host provider are **never** written to the
journal, logs, status, or RPC response. Envelopes journal only their non-secret
`route`, allowing restart recovery to resolve a fresh lease.
`route`, allowing restart recovery to resolve a fresh lease. Span plugins read
an environment snapshot captured inside their daemon worker process; it is not
part of the envelope or journal schema.

## Daemon lifecycle

Expand DownExpand Up@@ -337,8 +340,10 @@ profiles, organizations, and destinations while sharing one daemon.
`$HOME/.braintrust/state/bt-daemon` on Unix, and
`%LOCALAPPDATA%\Braintrust\bt-daemon` on Windows. On restart the daemon
rebuilds each route's unfinished correlation state independently, replaying
only the journal entries whose `route` matches that pipeline into a fresh
translator. The resulting rows may be resubmitted to repair delivery
only the journal entries whose delivery route matches that pipeline into a
fresh translator. Span plugin paths are ignored for this comparison so raw
events can be replayed through the current plugin chain. The resulting rows
may be resubmitted to repair delivery
interrupted by a crash, but their deterministic ids target the same backend
rows and must never create duplicate spans, and a route never receives
another route's rows. Replay streams the journal and is bounded to the
Expand Down
33 changes: 31 additions & 2 deletions bt-daemon/src/dispatch.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -457,7 +457,36 @@ impl SessionActor {
&ops,
);
}
match sink.emit(&ops).await {
let plugin_paths = ctx
.config
.as_ref()
.map(|config| config.span_plugins.as_slice())
.unwrap_or_default();
let mut processed = Vec::with_capacity(ops.len());
for op in &ops {
match crate::span_processor::process(
plugin_paths,
op,
&self.source,
&self.session_id,
) {
Ok(result) => {
for failure in result.failures {
self.set_error(format!(
"span plugin {} failed; disabled on this worker: {}",
failure.path.display(),
failure.message
));
}
processed.push(result.op);
}
Err(error) => {
self.set_error(format!("span plugin processor failed: {error}"));
processed.push(op.clone());
}
}
}
match sink.emit(&processed).await {
Ok(n) => {
self.counters.spans_emitted.fetch_add(n, Ordering::Relaxed);
}
Expand DownExpand Up@@ -510,7 +539,7 @@ impl SessionActor {
if !entry
.route
.as_ref()
.is_some_and(|candidate| candidate.same_route(&plan.route))
.is_some_and(|candidate| candidate.same_replay_route(&plan.route))
{
continue;
}
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions bt-daemon/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions bt-daemon/Cargo.toml
Original file line numberDiff line numberDiff line change
Expand Up@@ -24,6 +24,8 @@ async-trait = "0.1"
chrono = "0.4"
clap = { version = "4", features = ["derive", "env"] }
regex = "1"
rquickjs = "0.12.2"
rquickjs-serde = "0.6.1"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
sha2 = "0.10"
Expand Down
95 changes: 95 additions & 0 deletions bt-daemon/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -59,6 +59,101 @@ the default `bt` profile. Credentials and backend URLs are never stored here;
production resolves and refreshes them through `bt`. `bt trace run` supplies a
process-local settings overlay and never changes any of these files.

### JavaScript span plugins

`--plugin PATH` registers a synchronous ES module that transforms each
sink-neutral span row after translation and immediately before delivery. Repeat
the flag to compose plugins from left to right. `enable` persists its ordered list
for ordinary agent sessions. Managed runs and imports are isolated from that
list and use only the `--plugin` flags passed to their command. Each path is
canonicalized to an absolute path before it is validated or stored.

Each module must default-export a synchronous function. It receives a span and
`{ operation, source, session_id, env }`, and must return a JSON-compatible span
object. Span, root, and parent identities cannot be changed:

```js
// redact.mjs
function redact(value) {
if (typeof value === "string") {
return value.replace(/sk-[A-Za-z0-9_-]+/g, "[REDACTED]");
}
if (Array.isArray(value)) return value.map(redact);
if (value && typeof value === "object") {
return Object.fromEntries(
Object.entries(value).map(([key, child]) => [key, redact(child)]),
);
}
return value;
}

export default function redactSpan(span) {
const next = { ...span };
for (const field of ["input", "output", "error"]) {
if (field in next) next[field] = redact(next[field]);
}
return next;
}
```

The context can drive a second transform without changing the first one:

```js
// tag-ci.mjs
export default function tagCi(span, context) {
if (!context.env.CI) return span;

return {
...span,
tags: [...new Set([...(span.tags ?? []), "ci"])],
metadata: {
...(span.metadata ?? {}),
deployment: context.env.DEPLOYMENT_ENV ?? "unknown",
trace_source: context.source,
},
};
}
```

Register both transforms persistently for ordinary Codex sessions. The
redactor runs first and its returned span becomes the tagger's input:

```bash
bt trace enable codex --plugin ./redact.mjs --plugin ./tag-ci.mjs
```

`run` and `import` plugins apply only to that command. They replace, rather than
merge with, plugins saved by `enable`:

```bash
# Only local.mjs runs; redact.mjs and tag-ci.mjs remain global enable behavior.
bt trace run --plugin ./local.mjs codex -- "summarize this change"

# Only sanitize-history.mjs transforms spans produced by this import.
bt trace import codex SESSION_ID --plugin ./sanitize-history.mjs
```

The journal stores raw input events, not transformed spans. After daemon
recovery, replayed events therefore pass through the resumed session's current
route: ordinary sessions use the current globally configured plugins, while a
managed session continues using only that run's isolated plugins.

`context.operation` is `"insert"` or `"merge"`; `context.source` and
`context.session_id` identify the translated event stream; and `context.env`
contains the daemon process environment. Environment variable names are
uppercased on Windows so common lookups such as `context.env.PATH` remain
portable.

The environment map is captured from the daemon process when each worker-local
span processor is constructed. Plugins execute in bounded, thread-local
QuickJS runtimes with no filesystem or network host APIs. Modules must be
self-contained and transforms must be stateless: module globals belong to a
worker thread, not a session. If a plugin fails, that worker reports and skips
only that plugin on subsequent spans; the remaining plugins continue to run.
Plugins are trusted local code: although they have no host APIs, they can copy
environment values into spans that are delivered to Braintrust. Read only the
specific variables needed by the transform; never attach `context.env` itself.

### Additional root metadata

`additional_metadata` is a JSON object merged into each traced session's root
Expand Down
5 changes: 4 additions & 1 deletion bt-daemon/config.json.example
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,6 +14,9 @@
"additional_metadata": {
"team": "platform",
"environment": "development"
}
},
"span_plugins": [
"/absolute/path/to/redact.mjs"
]
}
}
13 changes: 9 additions & 4 deletions bt-daemon/docs/protocol.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -205,7 +205,8 @@ Used for version handover and by tests.
"project_name": "codex"
},
"flush_mode": "fire_and_forget",
"additional_metadata": { "…": "…" }
"additional_metadata": { "…": "…" },
"span_plugins": ["/absolute/path/redact.mjs"]
}
}
```
Expand DownExpand Up@@ -270,7 +271,9 @@ Field notes:

Live credentials returned by the host provider are **never** written to the
journal, logs, status, or RPC response. Envelopes journal only their non-secret
`route`, allowing restart recovery to resolve a fresh lease.
`route`, allowing restart recovery to resolve a fresh lease. Span plugins read
an environment snapshot captured inside their daemon worker process; it is not
part of the envelope or journal schema.

## Daemon lifecycle

Expand DownExpand Up@@ -337,8 +340,10 @@ profiles, organizations, and destinations while sharing one daemon.
`$HOME/.braintrust/state/bt-daemon` on Unix, and
`%LOCALAPPDATA%\Braintrust\bt-daemon` on Windows. On restart the daemon
rebuilds each route's unfinished correlation state independently, replaying
only the journal entries whose `route` matches that pipeline into a fresh
translator. The resulting rows may be resubmitted to repair delivery
only the journal entries whose delivery route matches that pipeline into a
fresh translator. Span plugin paths are ignored for this comparison so raw
events can be replayed through the current plugin chain. The resulting rows
may be resubmitted to repair delivery
interrupted by a crash, but their deterministic ids target the same backend
rows and must never create duplicate spans, and a route never receives
another route's rows. Replay streams the journal and is bounded to the
Expand Down
33 changes: 31 additions & 2 deletions bt-daemon/src/dispatch.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -457,7 +457,36 @@ impl SessionActor {
&ops,
);
}
match sink.emit(&ops).await {
let plugin_paths = ctx
.config
.as_ref()
.map(|config| config.span_plugins.as_slice())
.unwrap_or_default();
let mut processed = Vec::with_capacity(ops.len());
for op in &ops {
match crate::span_processor::process(
plugin_paths,
op,
&self.source,
&self.session_id,
) {
Ok(result) => {
for failure in result.failures {
self.set_error(format!(
"span plugin {} failed; disabled on this worker: {}",
failure.path.display(),
failure.message
));
}
processed.push(result.op);
}
Err(error) => {
self.set_error(format!("span plugin processor failed: {error}"));
processed.push(op.clone());
}
}
}
match sink.emit(&processed).await {
Ok(n) => {
self.counters.spans_emitted.fetch_add(n, Ordering::Relaxed);
}
Expand DownExpand Up@@ -510,7 +539,7 @@ impl SessionActor {
if !entry
.route
.as_ref()
.is_some_and(|candidate| candidate.same_route(&plan.route))
.is_some_and(|candidate| candidate.same_replay_route(&plan.route))
{
continue;
}
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions bt-daemon/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions bt-daemon/Cargo.toml
Original file line numberDiff line numberDiff line change
Expand Up@@ -24,6 +24,8 @@ async-trait = "0.1"
chrono = "0.4"
clap = { version = "4", features = ["derive", "env"] }
regex = "1"
rquickjs = "0.12.2"
rquickjs-serde = "0.6.1"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
sha2 = "0.10"
Expand Down
95 changes: 95 additions & 0 deletions bt-daemon/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -59,6 +59,101 @@ the default `bt` profile. Credentials and backend URLs are never stored here;
production resolves and refreshes them through `bt`. `bt trace run` supplies a
process-local settings overlay and never changes any of these files.

### JavaScript span plugins

`--plugin PATH` registers a synchronous ES module that transforms each
sink-neutral span row after translation and immediately before delivery. Repeat
the flag to compose plugins from left to right. `enable` persists its ordered list
for ordinary agent sessions. Managed runs and imports are isolated from that
list and use only the `--plugin` flags passed to their command. Each path is
canonicalized to an absolute path before it is validated or stored.

Each module must default-export a synchronous function. It receives a span and
`{ operation, source, session_id, env }`, and must return a JSON-compatible span
object. Span, root, and parent identities cannot be changed:

```js
// redact.mjs
function redact(value) {
if (typeof value === "string") {
return value.replace(/sk-[A-Za-z0-9_-]+/g, "[REDACTED]");
}
if (Array.isArray(value)) return value.map(redact);
if (value && typeof value === "object") {
return Object.fromEntries(
Object.entries(value).map(([key, child]) => [key, redact(child)]),
);
}
return value;
}

export default function redactSpan(span) {
const next = { ...span };
for (const field of ["input", "output", "error"]) {
if (field in next) next[field] = redact(next[field]);
}
return next;
}
```

The context can drive a second transform without changing the first one:

```js
// tag-ci.mjs
export default function tagCi(span, context) {
if (!context.env.CI) return span;

return {
...span,
tags: [...new Set([...(span.tags ?? []), "ci"])],
metadata: {
...(span.metadata ?? {}),
deployment: context.env.DEPLOYMENT_ENV ?? "unknown",
trace_source: context.source,
},
};
}
```

Register both transforms persistently for ordinary Codex sessions. The
redactor runs first and its returned span becomes the tagger's input:

```bash
bt trace enable codex --plugin ./redact.mjs --plugin ./tag-ci.mjs
```

`run` and `import` plugins apply only to that command. They replace, rather than
merge with, plugins saved by `enable`:

```bash
# Only local.mjs runs; redact.mjs and tag-ci.mjs remain global enable behavior.
bt trace run --plugin ./local.mjs codex -- "summarize this change"

# Only sanitize-history.mjs transforms spans produced by this import.
bt trace import codex SESSION_ID --plugin ./sanitize-history.mjs
```

The journal stores raw input events, not transformed spans. After daemon
recovery, replayed events therefore pass through the resumed session's current
route: ordinary sessions use the current globally configured plugins, while a
managed session continues using only that run's isolated plugins.

`context.operation` is `"insert"` or `"merge"`; `context.source` and
`context.session_id` identify the translated event stream; and `context.env`
contains the daemon process environment. Environment variable names are
uppercased on Windows so common lookups such as `context.env.PATH` remain
portable.

The environment map is captured from the daemon process when each worker-local
span processor is constructed. Plugins execute in bounded, thread-local
QuickJS runtimes with no filesystem or network host APIs. Modules must be
self-contained and transforms must be stateless: module globals belong to a
worker thread, not a session. If a plugin fails, that worker reports and skips
only that plugin on subsequent spans; the remaining plugins continue to run.
Plugins are trusted local code: although they have no host APIs, they can copy
environment values into spans that are delivered to Braintrust. Read only the
specific variables needed by the transform; never attach `context.env` itself.

### Additional root metadata

`additional_metadata` is a JSON object merged into each traced session's root
Expand Down
5 changes: 4 additions & 1 deletion bt-daemon/config.json.example
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,6 +14,9 @@
"additional_metadata": {
"team": "platform",
"environment": "development"
}
},
"span_plugins": [
"/absolute/path/to/redact.mjs"
]
}
}
13 changes: 9 additions & 4 deletions bt-daemon/docs/protocol.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -205,7 +205,8 @@ Used for version handover and by tests.
"project_name": "codex"
},
"flush_mode": "fire_and_forget",
"additional_metadata": { "…": "…" }
"additional_metadata": { "…": "…" },
"span_plugins": ["/absolute/path/redact.mjs"]
}
}
```
Expand DownExpand Up@@ -270,7 +271,9 @@ Field notes:

Live credentials returned by the host provider are **never** written to the
journal, logs, status, or RPC response. Envelopes journal only their non-secret
`route`, allowing restart recovery to resolve a fresh lease.
`route`, allowing restart recovery to resolve a fresh lease. Span plugins read
an environment snapshot captured inside their daemon worker process; it is not
part of the envelope or journal schema.

## Daemon lifecycle

Expand DownExpand Up@@ -337,8 +340,10 @@ profiles, organizations, and destinations while sharing one daemon.
`$HOME/.braintrust/state/bt-daemon` on Unix, and
`%LOCALAPPDATA%\Braintrust\bt-daemon` on Windows. On restart the daemon
rebuilds each route's unfinished correlation state independently, replaying
only the journal entries whose `route` matches that pipeline into a fresh
translator. The resulting rows may be resubmitted to repair delivery
only the journal entries whose delivery route matches that pipeline into a
fresh translator. Span plugin paths are ignored for this comparison so raw
events can be replayed through the current plugin chain. The resulting rows
may be resubmitted to repair delivery
interrupted by a crash, but their deterministic ids target the same backend
rows and must never create duplicate spans, and a route never receives
another route's rows. Replay streams the journal and is bounded to the
Expand Down
33 changes: 31 additions & 2 deletions bt-daemon/src/dispatch.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -457,7 +457,36 @@ impl SessionActor {
&ops,
);
}
match sink.emit(&ops).await {
let plugin_paths = ctx
.config
.as_ref()
.map(|config| config.span_plugins.as_slice())
.unwrap_or_default();
let mut processed = Vec::with_capacity(ops.len());
for op in &ops {
match crate::span_processor::process(
plugin_paths,
op,
&self.source,
&self.session_id,
) {
Ok(result) => {
for failure in result.failures {
self.set_error(format!(
"span plugin {} failed; disabled on this worker: {}",
failure.path.display(),
failure.message
));
}
processed.push(result.op);
}
Err(error) => {
self.set_error(format!("span plugin processor failed: {error}"));
processed.push(op.clone());
}
}
}
match sink.emit(&processed).await {
Ok(n) => {
self.counters.spans_emitted.fetch_add(n, Ordering::Relaxed);
}
Expand DownExpand Up@@ -510,7 +539,7 @@ impl SessionActor {
if !entry
.route
.as_ref()
.is_some_and(|candidate| candidate.same_route(&plan.route))
.is_some_and(|candidate| candidate.same_replay_route(&plan.route))
{
continue;
}
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions bt-daemon/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions bt-daemon/Cargo.toml
Original file line numberDiff line numberDiff line change
Expand Up@@ -24,6 +24,8 @@ async-trait = "0.1"
chrono = "0.4"
clap = { version = "4", features = ["derive", "env"] }
regex = "1"
rquickjs = "0.12.2"
rquickjs-serde = "0.6.1"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
sha2 = "0.10"
Expand Down
95 changes: 95 additions & 0 deletions bt-daemon/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -59,6 +59,101 @@ the default `bt` profile. Credentials and backend URLs are never stored here;
production resolves and refreshes them through `bt`. `bt trace run` supplies a
process-local settings overlay and never changes any of these files.

### JavaScript span plugins

`--plugin PATH` registers a synchronous ES module that transforms each
sink-neutral span row after translation and immediately before delivery. Repeat
the flag to compose plugins from left to right. `enable` persists its ordered list
for ordinary agent sessions. Managed runs and imports are isolated from that
list and use only the `--plugin` flags passed to their command. Each path is
canonicalized to an absolute path before it is validated or stored.

Each module must default-export a synchronous function. It receives a span and
`{ operation, source, session_id, env }`, and must return a JSON-compatible span
object. Span, root, and parent identities cannot be changed:

```js
// redact.mjs
function redact(value) {
if (typeof value === "string") {
return value.replace(/sk-[A-Za-z0-9_-]+/g, "[REDACTED]");
}
if (Array.isArray(value)) return value.map(redact);
if (value && typeof value === "object") {
return Object.fromEntries(
Object.entries(value).map(([key, child]) => [key, redact(child)]),
);
}
return value;
}

export default function redactSpan(span) {
const next = { ...span };
for (const field of ["input", "output", "error"]) {
if (field in next) next[field] = redact(next[field]);
}
return next;
}
```

The context can drive a second transform without changing the first one:

```js
// tag-ci.mjs
export default function tagCi(span, context) {
if (!context.env.CI) return span;

return {
...span,
tags: [...new Set([...(span.tags ?? []), "ci"])],
metadata: {
...(span.metadata ?? {}),
deployment: context.env.DEPLOYMENT_ENV ?? "unknown",
trace_source: context.source,
},
};
}
```

Register both transforms persistently for ordinary Codex sessions. The
redactor runs first and its returned span becomes the tagger's input:

```bash
bt trace enable codex --plugin ./redact.mjs --plugin ./tag-ci.mjs
```

`run` and `import` plugins apply only to that command. They replace, rather than
merge with, plugins saved by `enable`:

```bash
# Only local.mjs runs; redact.mjs and tag-ci.mjs remain global enable behavior.
bt trace run --plugin ./local.mjs codex -- "summarize this change"

# Only sanitize-history.mjs transforms spans produced by this import.
bt trace import codex SESSION_ID --plugin ./sanitize-history.mjs
```

The journal stores raw input events, not transformed spans. After daemon
recovery, replayed events therefore pass through the resumed session's current
route: ordinary sessions use the current globally configured plugins, while a
managed session continues using only that run's isolated plugins.

`context.operation` is `"insert"` or `"merge"`; `context.source` and
`context.session_id` identify the translated event stream; and `context.env`
contains the daemon process environment. Environment variable names are
uppercased on Windows so common lookups such as `context.env.PATH` remain
portable.

The environment map is captured from the daemon process when each worker-local
span processor is constructed. Plugins execute in bounded, thread-local
QuickJS runtimes with no filesystem or network host APIs. Modules must be
self-contained and transforms must be stateless: module globals belong to a
worker thread, not a session. If a plugin fails, that worker reports and skips
only that plugin on subsequent spans; the remaining plugins continue to run.
Plugins are trusted local code: although they have no host APIs, they can copy
environment values into spans that are delivered to Braintrust. Read only the
specific variables needed by the transform; never attach `context.env` itself.

### Additional root metadata

`additional_metadata` is a JSON object merged into each traced session's root
Expand Down
5 changes: 4 additions & 1 deletion bt-daemon/config.json.example
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,6 +14,9 @@
"additional_metadata": {
"team": "platform",
"environment": "development"
}
},
"span_plugins": [
"/absolute/path/to/redact.mjs"
]
}
}
13 changes: 9 additions & 4 deletions bt-daemon/docs/protocol.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -205,7 +205,8 @@ Used for version handover and by tests.
"project_name": "codex"
},
"flush_mode": "fire_and_forget",
"additional_metadata": { "…": "…" }
"additional_metadata": { "…": "…" },
"span_plugins": ["/absolute/path/redact.mjs"]
}
}
```
Expand DownExpand Up@@ -270,7 +271,9 @@ Field notes:

Live credentials returned by the host provider are **never** written to the
journal, logs, status, or RPC response. Envelopes journal only their non-secret
`route`, allowing restart recovery to resolve a fresh lease.
`route`, allowing restart recovery to resolve a fresh lease. Span plugins read
an environment snapshot captured inside their daemon worker process; it is not
part of the envelope or journal schema.

## Daemon lifecycle

Expand DownExpand Up@@ -337,8 +340,10 @@ profiles, organizations, and destinations while sharing one daemon.
`$HOME/.braintrust/state/bt-daemon` on Unix, and
`%LOCALAPPDATA%\Braintrust\bt-daemon` on Windows. On restart the daemon
rebuilds each route's unfinished correlation state independently, replaying
only the journal entries whose `route` matches that pipeline into a fresh
translator. The resulting rows may be resubmitted to repair delivery
only the journal entries whose delivery route matches that pipeline into a
fresh translator. Span plugin paths are ignored for this comparison so raw
events can be replayed through the current plugin chain. The resulting rows
may be resubmitted to repair delivery
interrupted by a crash, but their deterministic ids target the same backend
rows and must never create duplicate spans, and a route never receives
another route's rows. Replay streams the journal and is bounded to the
Expand Down
33 changes: 31 additions & 2 deletions bt-daemon/src/dispatch.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -457,7 +457,36 @@ impl SessionActor {
&ops,
);
}
match sink.emit(&ops).await {
let plugin_paths = ctx
.config
.as_ref()
.map(|config| config.span_plugins.as_slice())
.unwrap_or_default();
let mut processed = Vec::with_capacity(ops.len());
for op in &ops {
match crate::span_processor::process(
plugin_paths,
op,
&self.source,
&self.session_id,
) {
Ok(result) => {
for failure in result.failures {
self.set_error(format!(
"span plugin {} failed; disabled on this worker: {}",
failure.path.display(),
failure.message
));
}
processed.push(result.op);
}
Err(error) => {
self.set_error(format!("span plugin processor failed: {error}"));
processed.push(op.clone());
}
}
}
match sink.emit(&processed).await {
Ok(n) => {
self.counters.spans_emitted.fetch_add(n, Ordering::Relaxed);
}
Expand DownExpand Up@@ -510,7 +539,7 @@ impl SessionActor {
if !entry
.route
.as_ref()
.is_some_and(|candidate| candidate.same_route(&plan.route))
.is_some_and(|candidate| candidate.same_replay_route(&plan.route))
{
continue;
}
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions bt-daemon/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions bt-daemon/Cargo.toml
Original file line numberDiff line numberDiff line change
Expand Up@@ -24,6 +24,8 @@ async-trait = "0.1"
chrono = "0.4"
clap = { version = "4", features = ["derive", "env"] }
regex = "1"
rquickjs = "0.12.2"
rquickjs-serde = "0.6.1"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
sha2 = "0.10"
Expand Down
95 changes: 95 additions & 0 deletions bt-daemon/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -59,6 +59,101 @@ the default `bt` profile. Credentials and backend URLs are never stored here;
production resolves and refreshes them through `bt`. `bt trace run` supplies a
process-local settings overlay and never changes any of these files.

### JavaScript span plugins

`--plugin PATH` registers a synchronous ES module that transforms each
sink-neutral span row after translation and immediately before delivery. Repeat
the flag to compose plugins from left to right. `enable` persists its ordered list
for ordinary agent sessions. Managed runs and imports are isolated from that
list and use only the `--plugin` flags passed to their command. Each path is
canonicalized to an absolute path before it is validated or stored.

Each module must default-export a synchronous function. It receives a span and
`{ operation, source, session_id, env }`, and must return a JSON-compatible span
object. Span, root, and parent identities cannot be changed:

```js
// redact.mjs
function redact(value) {
if (typeof value === "string") {
return value.replace(/sk-[A-Za-z0-9_-]+/g, "[REDACTED]");
}
if (Array.isArray(value)) return value.map(redact);
if (value && typeof value === "object") {
return Object.fromEntries(
Object.entries(value).map(([key, child]) => [key, redact(child)]),
);
}
return value;
}

export default function redactSpan(span) {
const next = { ...span };
for (const field of ["input", "output", "error"]) {
if (field in next) next[field] = redact(next[field]);
}
return next;
}
```

The context can drive a second transform without changing the first one:

```js
// tag-ci.mjs
export default function tagCi(span, context) {
if (!context.env.CI) return span;

return {
...span,
tags: [...new Set([...(span.tags ?? []), "ci"])],
metadata: {
...(span.metadata ?? {}),
deployment: context.env.DEPLOYMENT_ENV ?? "unknown",
trace_source: context.source,
},
};
}
```

Register both transforms persistently for ordinary Codex sessions. The
redactor runs first and its returned span becomes the tagger's input:

```bash
bt trace enable codex --plugin ./redact.mjs --plugin ./tag-ci.mjs
```

`run` and `import` plugins apply only to that command. They replace, rather than
merge with, plugins saved by `enable`:

```bash
# Only local.mjs runs; redact.mjs and tag-ci.mjs remain global enable behavior.
bt trace run --plugin ./local.mjs codex -- "summarize this change"

# Only sanitize-history.mjs transforms spans produced by this import.
bt trace import codex SESSION_ID --plugin ./sanitize-history.mjs
```

The journal stores raw input events, not transformed spans. After daemon
recovery, replayed events therefore pass through the resumed session's current
route: ordinary sessions use the current globally configured plugins, while a
managed session continues using only that run's isolated plugins.

`context.operation` is `"insert"` or `"merge"`; `context.source` and
`context.session_id` identify the translated event stream; and `context.env`
contains the daemon process environment. Environment variable names are
uppercased on Windows so common lookups such as `context.env.PATH` remain
portable.

The environment map is captured from the daemon process when each worker-local
span processor is constructed. Plugins execute in bounded, thread-local
QuickJS runtimes with no filesystem or network host APIs. Modules must be
self-contained and transforms must be stateless: module globals belong to a
worker thread, not a session. If a plugin fails, that worker reports and skips
only that plugin on subsequent spans; the remaining plugins continue to run.
Plugins are trusted local code: although they have no host APIs, they can copy
environment values into spans that are delivered to Braintrust. Read only the
specific variables needed by the transform; never attach `context.env` itself.

### Additional root metadata

`additional_metadata` is a JSON object merged into each traced session's root
Expand Down
5 changes: 4 additions & 1 deletion bt-daemon/config.json.example
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,6 +14,9 @@
"additional_metadata": {
"team": "platform",
"environment": "development"
}
},
"span_plugins": [
"/absolute/path/to/redact.mjs"
]
}
}
13 changes: 9 additions & 4 deletions bt-daemon/docs/protocol.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -205,7 +205,8 @@ Used for version handover and by tests.
"project_name": "codex"
},
"flush_mode": "fire_and_forget",
"additional_metadata": { "…": "…" }
"additional_metadata": { "…": "…" },
"span_plugins": ["/absolute/path/redact.mjs"]
}
}
```
Expand DownExpand Up@@ -270,7 +271,9 @@ Field notes:

Live credentials returned by the host provider are **never** written to the
journal, logs, status, or RPC response. Envelopes journal only their non-secret
`route`, allowing restart recovery to resolve a fresh lease.
`route`, allowing restart recovery to resolve a fresh lease. Span plugins read
an environment snapshot captured inside their daemon worker process; it is not
part of the envelope or journal schema.

## Daemon lifecycle

Expand DownExpand Up@@ -337,8 +340,10 @@ profiles, organizations, and destinations while sharing one daemon.
`$HOME/.braintrust/state/bt-daemon` on Unix, and
`%LOCALAPPDATA%\Braintrust\bt-daemon` on Windows. On restart the daemon
rebuilds each route's unfinished correlation state independently, replaying
only the journal entries whose `route` matches that pipeline into a fresh
translator. The resulting rows may be resubmitted to repair delivery
only the journal entries whose delivery route matches that pipeline into a
fresh translator. Span plugin paths are ignored for this comparison so raw
events can be replayed through the current plugin chain. The resulting rows
may be resubmitted to repair delivery
interrupted by a crash, but their deterministic ids target the same backend
rows and must never create duplicate spans, and a route never receives
another route's rows. Replay streams the journal and is bounded to the
Expand Down
33 changes: 31 additions & 2 deletions bt-daemon/src/dispatch.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -457,7 +457,36 @@ impl SessionActor {
&ops,
);
}
match sink.emit(&ops).await {
let plugin_paths = ctx
.config
.as_ref()
.map(|config| config.span_plugins.as_slice())
.unwrap_or_default();
let mut processed = Vec::with_capacity(ops.len());
for op in &ops {
match crate::span_processor::process(
plugin_paths,
op,
&self.source,
&self.session_id,
) {
Ok(result) => {
for failure in result.failures {
self.set_error(format!(
"span plugin {} failed; disabled on this worker: {}",
failure.path.display(),
failure.message
));
}
processed.push(result.op);
}
Err(error) => {
self.set_error(format!("span plugin processor failed: {error}"));
processed.push(op.clone());
}
}
}
match sink.emit(&processed).await {
Ok(n) => {
self.counters.spans_emitted.fetch_add(n, Ordering::Relaxed);
}
Expand DownExpand Up@@ -510,7 +539,7 @@ impl SessionActor {
if !entry
.route
.as_ref()
.is_some_and(|candidate| candidate.same_route(&plan.route))
.is_some_and(|candidate| candidate.same_replay_route(&plan.route))
{
continue;
}
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions bt-daemon/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions bt-daemon/Cargo.toml
Original file line numberDiff line numberDiff line change
Expand Up@@ -24,6 +24,8 @@ async-trait = "0.1"
chrono = "0.4"
clap = { version = "4", features = ["derive", "env"] }
regex = "1"
rquickjs = "0.12.2"
rquickjs-serde = "0.6.1"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
sha2 = "0.10"
Expand Down
95 changes: 95 additions & 0 deletions bt-daemon/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -59,6 +59,101 @@ the default `bt` profile. Credentials and backend URLs are never stored here;
production resolves and refreshes them through `bt`. `bt trace run` supplies a
process-local settings overlay and never changes any of these files.

### JavaScript span plugins

`--plugin PATH` registers a synchronous ES module that transforms each
sink-neutral span row after translation and immediately before delivery. Repeat
the flag to compose plugins from left to right. `enable` persists its ordered list
for ordinary agent sessions. Managed runs and imports are isolated from that
list and use only the `--plugin` flags passed to their command. Each path is
canonicalized to an absolute path before it is validated or stored.

Each module must default-export a synchronous function. It receives a span and
`{ operation, source, session_id, env }`, and must return a JSON-compatible span
object. Span, root, and parent identities cannot be changed:

```js
// redact.mjs
function redact(value) {
if (typeof value === "string") {
return value.replace(/sk-[A-Za-z0-9_-]+/g, "[REDACTED]");
}
if (Array.isArray(value)) return value.map(redact);
if (value && typeof value === "object") {
return Object.fromEntries(
Object.entries(value).map(([key, child]) => [key, redact(child)]),
);
}
return value;
}

export default function redactSpan(span) {
const next = { ...span };
for (const field of ["input", "output", "error"]) {
if (field in next) next[field] = redact(next[field]);
}
return next;
}
```

The context can drive a second transform without changing the first one:

```js
// tag-ci.mjs
export default function tagCi(span, context) {
if (!context.env.CI) return span;

return {
...span,
tags: [...new Set([...(span.tags ?? []), "ci"])],
metadata: {
...(span.metadata ?? {}),
deployment: context.env.DEPLOYMENT_ENV ?? "unknown",
trace_source: context.source,
},
};
}
```

Register both transforms persistently for ordinary Codex sessions. The
redactor runs first and its returned span becomes the tagger's input:

```bash
bt trace enable codex --plugin ./redact.mjs --plugin ./tag-ci.mjs
```

`run` and `import` plugins apply only to that command. They replace, rather than
merge with, plugins saved by `enable`:

```bash
# Only local.mjs runs; redact.mjs and tag-ci.mjs remain global enable behavior.
bt trace run --plugin ./local.mjs codex -- "summarize this change"

# Only sanitize-history.mjs transforms spans produced by this import.
bt trace import codex SESSION_ID --plugin ./sanitize-history.mjs
```

The journal stores raw input events, not transformed spans. After daemon
recovery, replayed events therefore pass through the resumed session's current
route: ordinary sessions use the current globally configured plugins, while a
managed session continues using only that run's isolated plugins.

`context.operation` is `"insert"` or `"merge"`; `context.source` and
`context.session_id` identify the translated event stream; and `context.env`
contains the daemon process environment. Environment variable names are
uppercased on Windows so common lookups such as `context.env.PATH` remain
portable.

The environment map is captured from the daemon process when each worker-local
span processor is constructed. Plugins execute in bounded, thread-local
QuickJS runtimes with no filesystem or network host APIs. Modules must be
self-contained and transforms must be stateless: module globals belong to a
worker thread, not a session. If a plugin fails, that worker reports and skips
only that plugin on subsequent spans; the remaining plugins continue to run.
Plugins are trusted local code: although they have no host APIs, they can copy
environment values into spans that are delivered to Braintrust. Read only the
specific variables needed by the transform; never attach `context.env` itself.

### Additional root metadata

`additional_metadata` is a JSON object merged into each traced session's root
Expand Down
5 changes: 4 additions & 1 deletion bt-daemon/config.json.example
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,6 +14,9 @@
"additional_metadata": {
"team": "platform",
"environment": "development"
}
},
"span_plugins": [
"/absolute/path/to/redact.mjs"
]
}
}
13 changes: 9 additions & 4 deletions bt-daemon/docs/protocol.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -205,7 +205,8 @@ Used for version handover and by tests.
"project_name": "codex"
},
"flush_mode": "fire_and_forget",
"additional_metadata": { "…": "…" }
"additional_metadata": { "…": "…" },
"span_plugins": ["/absolute/path/redact.mjs"]
}
}
```
Expand DownExpand Up@@ -270,7 +271,9 @@ Field notes:

Live credentials returned by the host provider are **never** written to the
journal, logs, status, or RPC response. Envelopes journal only their non-secret
`route`, allowing restart recovery to resolve a fresh lease.
`route`, allowing restart recovery to resolve a fresh lease. Span plugins read
an environment snapshot captured inside their daemon worker process; it is not
part of the envelope or journal schema.

## Daemon lifecycle

Expand DownExpand Up@@ -337,8 +340,10 @@ profiles, organizations, and destinations while sharing one daemon.
`$HOME/.braintrust/state/bt-daemon` on Unix, and
`%LOCALAPPDATA%\Braintrust\bt-daemon` on Windows. On restart the daemon
rebuilds each route's unfinished correlation state independently, replaying
only the journal entries whose `route` matches that pipeline into a fresh
translator. The resulting rows may be resubmitted to repair delivery
only the journal entries whose delivery route matches that pipeline into a
fresh translator. Span plugin paths are ignored for this comparison so raw
events can be replayed through the current plugin chain. The resulting rows
may be resubmitted to repair delivery
interrupted by a crash, but their deterministic ids target the same backend
rows and must never create duplicate spans, and a route never receives
another route's rows. Replay streams the journal and is bounded to the
Expand Down
33 changes: 31 additions & 2 deletions bt-daemon/src/dispatch.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -457,7 +457,36 @@ impl SessionActor {
&ops,
);
}
match sink.emit(&ops).await {
let plugin_paths = ctx
.config
.as_ref()
.map(|config| config.span_plugins.as_slice())
.unwrap_or_default();
let mut processed = Vec::with_capacity(ops.len());
for op in &ops {
match crate::span_processor::process(
plugin_paths,
op,
&self.source,
&self.session_id,
) {
Ok(result) => {
for failure in result.failures {
self.set_error(format!(
"span plugin {} failed; disabled on this worker: {}",
failure.path.display(),
failure.message
));
}
processed.push(result.op);
}
Err(error) => {
self.set_error(format!("span plugin processor failed: {error}"));
processed.push(op.clone());
}
}
}
match sink.emit(&processed).await {
Ok(n) => {
self.counters.spans_emitted.fetch_add(n, Ordering::Relaxed);
}
Expand DownExpand Up@@ -510,7 +539,7 @@ impl SessionActor {
if !entry
.route
.as_ref()
.is_some_and(|candidate| candidate.same_route(&plan.route))
.is_some_and(|candidate| candidate.same_replay_route(&plan.route))
{
continue;
}
Expand Down
Loading
Loading