Skip to content

Commit 8ebc492

Browse files
committed
llm-runner 0.2.1: session_ref helper for contract section 14.1
Every module that keeps state for a runner session needs the same reference, so the derivation lives in the contract crate instead of being reimplemented. The JCS object is built by hand: its three keys are already in JCS order, and building it this way keeps the bytes independent of serde_json map ordering, which a workspace build can change. Tests check it against the pinned vectors, and the new vector file is listed with the others. The contract now says the reference is a correlation key, not proof of identity.
1 parent 30cf6b3 commit 8ebc492

6 files changed

Lines changed: 122 additions & 3 deletions

File tree

‎Cargo.lock‎

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Changelog
22

3+
## 0.2.1
4+
5+
- Add `session_ref(harness, project_root, session)`, the session reference from contract section 14.1, so every module that keeps state for a runner session derives the same value. It is checked against `test-vectors/llm-runner-v1/session-ref.json`.
6+
37
## 0.1.1
48

59
- Clarify that compaction conformance accepts runner-driven recovery or an interrupted run resumed by a sender's new send; runtime rules are unchanged.

‎crates/cortexkit-role-llm-runner/CONTRACT.md‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1128,7 +1128,11 @@ real process kill.
11281128
provider, a backup or move participant) derives the same `session_ref`
11291129
from the same bind identity, without asking the runner.
11301130
`test-vectors/llm-runner-v1/session-ref.json` pins the derivation,
1131-
including a trailing slash and non-ASCII escaping.
1131+
including a trailing slash and non-ASCII escaping, and the crate's
1132+
`session_ref` function implements it.
1133+
- [pinned] `session_ref` is a correlation key, not proof of identity: a
1134+
direct caller chooses its own bind fields. Authorization still rests on
1135+
the daemon-stamped principal and scope.
11321136

11331137
## 15. Conformance suite (planned)
11341138

‎crates/cortexkit-role-llm-runner/Cargo.toml‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
# suite.
1212
[package]
1313
name = "cortexkit-role-llm-runner"
14-
version = "0.2.0"
14+
version = "0.2.1"
1515
edition.workspace = true
1616
license.workspace = true
1717
repository.workspace = true
@@ -22,3 +22,4 @@ publish = true
2222
[dependencies]
2323
serde = { version = "1", features = ["derive"] }
2424
serde_json = "1"
25+
sha2 = "0.10"

‎crates/cortexkit-role-llm-runner/src/lib.rs‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,11 @@ pub mod read;
3030
pub mod retract;
3131
pub mod run;
3232
pub mod send;
33+
pub mod session_ref;
3334
pub mod subscribe;
3435

36+
pub use session_ref::session_ref;
37+
3538
/// The role name as `role.describe` reports it.
3639
pub const ROLE: &str = "llm-runner";
3740

@@ -278,6 +281,7 @@ pub(crate) mod vectors {
278281
"run-result.json",
279282
"send.json",
280283
"session-change.json",
284+
"session-ref.json",
281285
"subscribe.json",
282286
];
283287

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
//! The session reference (contract section 14.1).
2+
//!
3+
//! Every module that keeps state for a runner session derives the same short
4+
//! reference from the route bind's identity, so they agree without asking the
5+
//! runner. The reference is a correlation key, not proof of identity: a direct
6+
//! caller chooses its own bind fields.
7+
8+
use sha2::{Digest, Sha256};
9+
10+
/// The first 16 bytes of SHA-256 over the RFC 8785 (JCS) encoding of
11+
/// `{"harness", "project_root", "session"}`, as 32 lowercase hex characters.
12+
///
13+
/// Pass the three fields exactly as the route bind carried them. In particular
14+
/// `project_root` must not be canonicalised: a runner keys its durable state by
15+
/// those bytes, so a resolved symlink or a dropped trailing slash names a
16+
/// different session.
17+
pub fn session_ref(harness: &str, project_root: &str, session: &str) -> String {
18+
// JCS orders members by key; these three keys are already in that order, and
19+
// JCS string escaping matches serde_json's string encoding (quote,
20+
// backslash and control characters only). Building the object by hand keeps
21+
// the bytes independent of serde_json's map ordering, which a workspace build
22+
// can change through the `preserve_order` feature.
23+
let jcs = format!(
24+
r#"{{"harness":{},"project_root":{},"session":{}}}"#,
25+
json_string(harness),
26+
json_string(project_root),
27+
json_string(session),
28+
);
29+
let digest = Sha256::digest(jcs.as_bytes());
30+
digest[..16]
31+
.iter()
32+
.map(|byte| format!("{byte:02x}"))
33+
.collect()
34+
}
35+
36+
fn json_string(value: &str) -> String {
37+
serde_json::to_string(value).expect("a string always serializes")
38+
}
39+
40+
#[cfg(test)]
41+
mod tests {
42+
use super::session_ref;
43+
use serde::Deserialize;
44+
45+
#[derive(Deserialize)]
46+
struct Vectors {
47+
cases: Vec<Case>,
48+
}
49+
50+
#[derive(Deserialize)]
51+
struct Case {
52+
name: String,
53+
bind: Bind,
54+
jcs: String,
55+
session_ref: String,
56+
}
57+
58+
#[derive(Deserialize)]
59+
struct Bind {
60+
harness: String,
61+
project_root: String,
62+
session: String,
63+
}
64+
65+
#[test]
66+
fn matches_the_pinned_vectors() {
67+
let vectors: Vectors = serde_json::from_str(include_str!(
68+
"../../../test-vectors/llm-runner-v1/session-ref.json"
69+
))
70+
.unwrap();
71+
assert!(vectors.cases.len() >= 4);
72+
for case in vectors.cases {
73+
let Bind {
74+
harness,
75+
project_root,
76+
session,
77+
} = &case.bind;
78+
assert_eq!(
79+
format!(
80+
r#"{{"harness":{},"project_root":{},"session":{}}}"#,
81+
super::json_string(harness),
82+
super::json_string(project_root),
83+
super::json_string(session)
84+
),
85+
case.jcs,
86+
"{}: JCS bytes",
87+
case.name
88+
);
89+
assert_eq!(
90+
session_ref(harness, project_root, session),
91+
case.session_ref,
92+
"{}",
93+
case.name
94+
);
95+
}
96+
}
97+
98+
#[test]
99+
fn project_root_bytes_are_not_normalised() {
100+
assert_ne!(
101+
session_ref("runner", "/tmp/project/", "s1"),
102+
session_ref("runner", "/tmp/project", "s1")
103+
);
104+
}
105+
}

0 commit comments

Comments
 (0)