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
28 changes: 28 additions & 0 deletions docs/probes/pr-demos/command-client/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Command-level request attribution

The recorded real CLI requests change from `X-Quipu-Client: seeds` to
`seeds/show`. A different subject argument keeps the same bounded label.
All three invocations intentionally return exit 3 for a missing synthetic subject.

The before binary is the retained actor-compatibility proposal at `6f9300ad`;
its actor changes are not exercised by this read-only fixture. It is not a
build of the current base. The after binary is built from `ac0e758a` after the
command-label change and current-main integration. Exact binary SHA256 values
appear in the transcript. This fixture proves actual HTTP headers, not real
server authentication, deployment, release, or canary acceptance.

The output and timing were captured together with util-linux `script`.
`transcript.txt` only normalizes terminal CRLF to LF for reading.

Replay the original bytes:

```sh
gzip -dc output.txt.gz > /tmp/command-client-output.txt
scriptreplay --log-timing timing.txt --log-out /tmp/command-client-output.txt
```

Reproduce with two existing CLI binaries (Python standard library only):

```sh
python3 reproduce.py /path/to/before/sd /path/to/after/sd
```
Binary file added docs/probes/pr-demos/command-client/output.txt.gz
Binary file not shown.
93 changes: 93 additions & 0 deletions docs/probes/pr-demos/command-client/reproduce.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
#!/usr/bin/env python3
"""Compare real CLI headers against one private, synthetic HTTP read fixture."""
import hashlib
import http.server
import json
import os
from pathlib import Path
import subprocess
import sys
import tempfile
import threading

seen = []


class Fixture(http.server.BaseHTTPRequestHandler):
def log_message(self, *_):
pass

def reply(self):
size = int(self.headers.get('Content-Length', '0'))
if size > 1024 * 1024:
raise RuntimeError('oversized fixture request')
body = self.rfile.read(size).decode()
seen.append((self.command, self.path, self.headers.get('X-Quipu-Client')))
if self.path == '/query':
if 'SELECT ?probe WHERE' in body:
value = {'head': {'vars': ['probe']}, 'results': {'bindings': [
{'probe': {'type': 'literal', 'value': 'complete'}}]}, 'truncated': False}
elif 'ASK' in body:
value = {'head': {}, 'boolean': False, 'result': False}
else:
value = {'head': {'vars': []}, 'results': {'bindings': []},
'rows': [], 'count': 0, 'truncated': False}
elif self.path == '/health':
value = {'status': 'ok', 'graphs': []}
else:
raise RuntimeError('unexpected fixture endpoint')
data = json.dumps(value).encode()
self.send_response(200)
self.send_header('Content-Type', 'application/json')
self.send_header('Content-Length', str(len(data)))
self.end_headers()
self.wfile.write(data)

do_GET = reply
do_POST = reply


def run(binary, label, expected, url, subject):
begin = len(seen)
print(f'{label}: sd --quipu {url} --graph https://example.test/project/demo '
f'show {subject} --json', flush=True)
with tempfile.TemporaryDirectory(prefix='command-client-demo-') as home:
result = subprocess.run(
[str(Path(binary).resolve()), '--quipu', url, '--graph',
'https://example.test/project/demo', 'show', subject, '--json'],
env={'PATH': '/usr/bin:/bin', 'HOME': home, 'XDG_CONFIG_HOME': home,
'XDG_STATE_HOME': home},
capture_output=True, text=True, timeout=10)
if result.returncode != 3:
raise RuntimeError(f'expected synthetic missing-subject exit3, got {result.returncode}')
requests = seen[begin:]
labels = sorted({client for _, _, client in requests})
if len(requests) != 5 or labels != [expected]:
raise RuntimeError(f'unexpected fixture request attribution: {requests!r}')
print(f' exit={result.returncode} (synthetic subject deliberately absent)')
print(f' actual requests={len(requests)}; X-Quipu-Client={labels!r}', flush=True)


def main():
if len(sys.argv) != 3:
raise SystemExit('usage: reproduce.py BEFORE_SD AFTER_SD')
before, after = sys.argv[1:]
for label, binary in [('before', before), ('after', after)]:
print(f'{label} binary SHA256={hashlib.sha256(Path(binary).read_bytes()).hexdigest()}')
server = http.server.HTTPServer(('127.0.0.1', 0), Fixture)
thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
try:
url = f'http://127.0.0.1:{server.server_port}'
run(before, 'before', 'seeds', url, 'sample-1')
run(after, 'after', 'seeds/show', url, 'sample-1')
run(after, 'argument-control', 'seeds/show', url, 'different-2')
print('PASS: command label changes; different arguments do not change it.')
finally:
server.shutdown()
server.server_close()
thread.join(timeout=2)


if __name__ == '__main__':
main()
7 changes: 7 additions & 0 deletions docs/probes/pr-demos/command-client/timing.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
0.244593 87
0.052106 86
0.001198 106
0.008542 202
0.008174 102
0.000004 119
0.005268 170
15 changes: 15 additions & 0 deletions docs/probes/pr-demos/command-client/transcript.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
Script started on 2026-10-24 10:34:43-04:00 [COMMAND="python3 reproduce.py "$BEFORE_SD" "$AFTER_SD"" <not executed on terminal>]
before binary SHA256=d516378a79352816b02d32cba96ced88b1a0e203b8348d12aab81a7833a42e2d
after binary SHA256=4f7d5de23e74a6cc0e3b673c2f9bee4030298dfe7a156ef3cc751ac8c838376a
before: sd --quipu http://127.0.0.1:35531 --graph https://example.test/project/demo show sample-1 --json
exit=3 (synthetic subject deliberately absent)
actual requests=5; X-Quipu-Client=['seeds']
after: sd --quipu http://127.0.0.1:35531 --graph https://example.test/project/demo show sample-1 --json
exit=3 (synthetic subject deliberately absent)
actual requests=5; X-Quipu-Client=['seeds/show']
argument-control: sd --quipu http://127.0.0.1:35531 --graph https://example.test/project/demo show different-2 --json
exit=3 (synthetic subject deliberately absent)
actual requests=5; X-Quipu-Client=['seeds/show']
PASS: command label changes; different arguments do not change it.

Script done on 2026-10-24 10:34:44-04:00 [COMMAND_EXIT_CODE="0"]
86 changes: 82 additions & 4 deletions src/native/command_context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,27 @@
use std::cell::Cell;

thread_local! {
static CLIENT_COMMAND: Cell<Option<&'static str>> = const { Cell::new(None) };
static SOURCE: Cell<&'static str> = const { Cell::new("seeds") };
}

pub(super) fn source() -> &'static str {
SOURCE.with(Cell::get)
}

pub(super) struct Scope(&'static str);
/// The metric label uses only the parsed top-level command. Library calls
/// outside a CLI scope retain the historical producer label.
pub(super) fn client() -> String {
CLIENT_COMMAND.with(|slot| match slot.get() {
Some(command) => format!("seeds/{command}"),
None => "seeds".into(),
})
}

pub(super) struct Scope {
source: &'static str,
client: Option<&'static str>,
}

impl Scope {
pub(super) fn new(command: &super::cli::Command) -> Self {
Expand All @@ -29,17 +42,31 @@ impl Scope {
Command::Info(_) => "sd-info",
_ => "sd-other",
};
Self(SOURCE.with(|slot| {
let source = SOURCE.with(|slot| {
let outer = slot.get();
slot.set(if outer == "seeds" { source } else { outer });
outer
}))
});
// Command::name is an exhaustive enum match of static names. No
// argument, subject, actor, raw argv or environment enters this label.
let command = command
.name()
.split_ascii_whitespace()
.next()
.expect("command name");
let client = CLIENT_COMMAND.with(|slot| {
let outer = slot.get();
slot.set(outer.or(Some(command)));
outer
});
Self { source, client }
}
}

impl Drop for Scope {
fn drop(&mut self) {
SOURCE.with(|slot| slot.set(self.0));
SOURCE.with(|slot| slot.set(self.source));
CLIENT_COMMAND.with(|slot| slot.set(self.client));
}
}

Expand All @@ -48,6 +75,57 @@ mod tests {
use super::*;
use clap::Parser;

#[test]
fn metric_client_is_command_only_for_every_request_kind() {
let cli =
super::super::cli::Cli::try_parse_from(["sd", "show", "private-subject"]).unwrap();
assert_eq!(client(), "seeds");
{
let _scope = Scope::new(&cli.command);
assert_eq!(client(), "seeds/show");
let requests = [
super::super::provenance::apply(ureq::get("http://localhost/unused")),
super::super::provenance::apply_read(ureq::get("http://localhost/unused"), "query"),
super::super::provenance::apply_signed(ureq::get("http://localhost/unused")),
];
for request in requests {
assert_eq!(request.header("X-Quipu-Client"), Some("seeds/show"));
}
}
assert_eq!(client(), "seeds");
}

#[test]
fn nested_command_labels_and_parallel_scopes_do_not_leak() {
let cli = super::super::cli::Cli::try_parse_from(["sd", "list"]).unwrap();
let _outer = Scope::new(&cli.command);
let inner = super::super::cli::Cli::try_parse_from(["sd", "show", "private-id"]).unwrap();
{
let _inner = Scope::new(&inner.command);
assert_eq!(client(), "seeds/list");
}
let other = std::thread::spawn(|| {
assert_eq!(client(), "seeds");
let cli = super::super::cli::Cli::try_parse_from(["sd", "ready"]).unwrap();
let _scope = Scope::new(&cli.command);
assert_eq!(client(), "seeds/ready");
});
other.join().unwrap();
assert_eq!(client(), "seeds/list");
}

#[test]
fn nested_verbs_share_one_bounded_top_level_label() {
for argv in [
vec!["sd", "comments", "add", "private-id", "private-body"],
vec!["sd", "comments", "list", "other-private-id"],
] {
let cli = super::super::cli::Cli::try_parse_from(argv).unwrap();
let _scope = Scope::new(&cli.command);
assert_eq!(client(), "seeds/comments");
}
}

#[test]
fn demo_command_attribution_before_and_after() {
let before = super::super::provenance::read_headers_from(|_| None, || None, "query");
Expand Down
5 changes: 4 additions & 1 deletion src/native/provenance.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! Structured write provenance for every request seeds sends to quipu.
//!
//! Who wrote a fact must be machine-readable. Beside `X-Quipu-Client: seeds`,
//! Who wrote a fact must be machine-readable. Beside the bounded `X-Quipu-Client: seeds/<command>` metric label,
//! each request carries up to six headers from the environment. The task
//! label is explicitly declared diagnostic metadata; no task is inferred:
//!
Expand Down Expand Up @@ -130,6 +130,7 @@ pub fn headers() -> &'static [(&'static str, String)] {

/// Set this process's provenance headers on a request.
pub fn apply(mut req: ureq::Request) -> ureq::Request {
req = req.set("X-Quipu-Client", &super::command_context::client());
for (name, value) in headers() {
req = req.set(name, value);
}
Expand Down Expand Up @@ -175,6 +176,7 @@ pub fn read_headers_from(

/// Apply read metadata only. Writer provenance/signatures use `apply` unchanged.
pub fn apply_read(mut req: ureq::Request, operation: &str) -> ureq::Request {
req = req.set("X-Quipu-Client", &super::command_context::client());
// Preserve the writer's legacy first-request snapshot, even when that
// request is a read. Read declarations remain separate from that snapshot.
let _ = headers();
Expand Down Expand Up @@ -206,6 +208,7 @@ pub(super) fn hostname() -> Option<String> {
/// Diagnostic metadata cannot assert the agent/session on a signed request.
/// The receiving protected registry supplies both fields from the verified key.
pub fn apply_signed(mut req: ureq::Request) -> ureq::Request {
req = req.set("X-Quipu-Client", &super::command_context::client());
for (name, value) in headers() {
if !matches!(*name, "X-Quipu-Agent" | "X-Quipu-Session") {
req = req.set(name, value);
Expand Down
9 changes: 4 additions & 5 deletions src/native/remote.rs
Original file line number Diff line number Diff line change
Expand Up @@ -229,11 +229,11 @@ pub struct RemoteBackend {
/// process's structured write provenance (`X-Quipu-Agent`, `-Harness`,
/// `-Model`, `-Session`, `-Host`; see [`super::provenance`]).
fn quipu_request(req: ureq::Request) -> ureq::Request {
super::provenance::apply(req.set("X-Quipu-Client", "seeds"))
super::provenance::apply(req)
}

fn quipu_read_request(req: ureq::Request, operation: &str) -> ureq::Request {
super::provenance::apply_read(req.set("X-Quipu-Client", "seeds"), operation)
super::provenance::apply_read(req, operation)
}

fn transport(url: &str, e: &ureq::Transport) -> SdError {
Expand Down Expand Up @@ -409,8 +409,7 @@ impl RemoteBackend {
body: &[u8],
) -> Result<ureq::Request> {
let request = self.agent.request(method, &format!("{}{path}", self.base));
let mut req = super::provenance::apply_signed(request.set("X-Quipu-Client", "seeds"))
.set("Content-Type", content_type);
let mut req = super::provenance::apply_signed(request).set("Content-Type", content_type);
if let Some(signer) = &self.signer {
req = req.set(
super::attest::HEADER,
Expand Down Expand Up @@ -2619,7 +2618,7 @@ mod tests {
.map(|(_, v)| v.as_str())
.collect::<Vec<_>>()
};
assert_eq!(get("X-Quipu-Client"), ["seeds"], "{line}");
assert_eq!(get("X-Quipu-Client"), ["seeds/show"], "{line}");
for (name, value) in expected {
assert_eq!(get(name), [value.as_str()], "{line}: {name}");
}
Expand Down
2 changes: 1 addition & 1 deletion tests/read_attribution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ fn probe(extra: &[(&str, &str)]) -> Vec<Headers> {
discovery, the facts reads and the ephemeral existence check (aegis-83l)"
);
for headers in &result {
assert_eq!(headers["x-quipu-client"], "seeds");
assert_eq!(headers["x-quipu-client"], "seeds/show");
let expected_source = extra
.iter()
.find(|(key, _)| *key == "QUIPU_SOURCE")
Expand Down
Loading