mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-30 05:16:50 +01:00
Record runtime activity under Runner authority (#18050)
## Thinking Path > - Paperclip manages agents and tasks. > - Runner owns each authenticated run and its provider process. > - Runtime clients need to report successful activity in that run. > - A retained process can outlive a run, so an operation needs current authority. > - This PR captures that authority before the operation and records activity afterward. > - Core can consume the event through the existing run stream. ## Linked Issues or Issue Description Builds on merged #18129 and targets master. The dependent consumer is #18131. Test stability fixes were separated into merged #18128. Earlier combined-head verification remains in [the archive](https://github.com/paperclipai/paperclip/pull/18050#issuecomment-6489369676). **Subsystem affected** Runner activity recording and provider command environments. **Problem or motivation** A retained provider process can complete an operation after its original run ends. Runtime clients need a host-attributed event and a helper that works inside the provider sandbox. **Proposed solution** Capture opaque Runner context before use. Publish a structured notification after success. Runner checks current authority and records `runtime.activity.recorded`. ## What Changed - Add `notification-context` and `record-activity CONTEXT NAMESPACE ACTIVITY_KEY`. Rotate context on warm attachment and process recovery. Accept no caller run ID. - Use one private named pipe. Reuse Runner's bounded JSON-line reader. Serialize concurrent publishers with a read-only lock. A blocking reader queues at most 128 records; the existing control loop handles at most 16 per pass. - Grant Codex access to the exact helper, descriptor, lock and pipe. Preserve other filesystem and network limits. Preserve host discovery values through provider launch boundaries and reject configured-environment overrides. - Update source identities to Pi 34, Copilot 81 and Grok 12. Keep historical identity fixtures and recovery behavior. ## Verification - Targeted release notification and sandbox-policy tests: seven passed. They cover run rotation, recovery, Unicode limits, concurrent writers, malformed and partial records, bounded queues, failed persistence and fixed-path cleanup. - Real CLI integration and pinned Codex 0.160.0 sandbox checks: passed. The sandbox check uses no model, credential or network call. It verifies activity recording while unrelated state reads and parent-directory writes remain denied. - ACPX profile generator check, formatting and diff checks: passed. - Rebased onto master `31b927e30` at head `64d0248a8`. Master used the same profile versions, so the resolution preserves its Pi 33, Copilot 79 and Grok 11 fixtures byte-for-byte and creates new identities. All identity checks pass. Five targeted provider/environment/recovery files pass 168 tests with one existing skip; the Rust Pi admission regression passes. The notification module and Codex sandbox implementation are byte-identical to the tested pipe head `671373fe0`. - Removed the mailbox traversal reported by security review. Its regression replaces the pipe path with a 100-level tree and an unreadable leaf. Shutdown preserves the leaf, performs no traversal and completes promptly, including with a partial record. - An initial targeted test failed because its run-rotation fixture updated config without updating durable state. The corrected fixture passes. Runner also rejects inconsistent run bindings. - Previous head `26abbf62b` passed Runner, build and browser CI. Server shard 11/12 failed in unchanged `attention-service.test.ts`: cleanup/test timeouts were followed by a duplicate `ATQ` fixture key. The final head needs fresh CI and review. - CI on `64d0248a8` completed with 52 successful checks and four intentional skips. Runner Vitest shard 1/2 failed when retained-state copies encountered the live FIFO; the aggregate also failed. Standard review scored 4/5 and found relative pipe paths plus late context publication. Follow-up `5c3f14d32` places IPC in a private canonical temporary directory and publishes context before welcome commands. Its relative-state CLI test, two authenticated reconnect cases, seven notification/policy cases and real Codex sandbox pass. One affected retained-settlement case passes with the matching release binary; the other 18 cases remain for CI. The sandbox denies sibling-directory and lock writes. All build, test and security checks pass on `5c3f14d32`; standard review is 5/5 with no unresolved threads. The requested Apex review is 5/5 on this exact head, with no findings or unresolved threads. - Earlier broad test results in the archive apply to their recorded source revisions. Per the requested workflow, final local checks stay targeted; GitHub CI supplies broad coverage. - After Apex completed, master advanced again and conflicted on Copilot identity. Head `e94a9359d` is rebased on master `8ac5a128e`; it preserves the published Copilot 80 fixture and uses Copilot 81. The notification module, Codex sandbox and durable control-loop code are byte-identical to Apex-reviewed `5c3f14d32`. All 131 targeted installation/compatibility/recovery cases and identity generator checks pass. All 55 checks pass with four intentional skips on the rebased head. Standard review is 5/5 with no unresolved threads. Apex is 5/5 on `e94a9359d`, with zero findings and no unresolved threads. GitHub confirms this head is conflict-free and ready for review. ## Risks Activity remains an optional hint and grants no access. A successful helper exit means publication to the pipe; Runner persists accepted events through its existing durable outbox. Full queues, unavailable helpers or failed writes can drop a hint without failing the operation. Context reads are local and do not wait for the control loop. Publishers have a 500 ms lock deadline and a 2 KiB record limit. IPC lives outside durable state. Idle transport blocks; cleanup removes its fixed files and empty private directory. The host must provide `/usr/bin/mkfifo`. Rebuild matching Runner/provider artifacts for the updated source identities. No dependency, migration, live provider call or deployment is included. ## Model Used OpenAI Codex, based on GPT-6, with reasoning, repository tools and code execution. The exact serving snapshot is not exposed. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
27fbdb2a74
commit
27be859044
36 files changed
+1416
-45
No files matched your search
@@ -653,3 +653,9 @@ The ordinary authenticated Runner event is the durable record. Normal source-eve
|
||||
acknowledgement and replay deduplicate delivery. Existing run-read authorization
|
||||
and secret redaction apply to saved and live events. Recording reported activity
|
||||
is not independent proof that it happened and grants no resource permission.
|
||||
|
||||
Runner accepts local activity records only with an opaque context
|
||||
captured before the operation. Warm run attachment and process recovery rotate
|
||||
that authority. Runner supplies the run identity from its active authenticated
|
||||
binding. Optional hint failure does not fail the underlying operation. No output
|
||||
text is scanned and no synthetic receipt is written.
|
||||
@@ -595,6 +595,22 @@ Live console provider-backed routes are loopback-only and reject wildcard/LAN
|
||||
binds. Browser mutations require same-origin Fetch Metadata, matching Origin,
|
||||
and JSON content; see the protocol-server tutorial for direct `curl` examples.
|
||||
|
||||
The runtime activity publisher has an offline final-command regression against the
|
||||
qualified Codex executable. Run it on a host that supports Codex's OS sandbox,
|
||||
with the pinned executable and the matching freshly built Runner helper:
|
||||
|
||||
```sh
|
||||
CODEX_NOTIFICATION_SANDBOX_COMMAND=/absolute/path/to/pinned/codex \
|
||||
PAPERCLIP_RUNTIME_NOTIFICATION_HELPER=/absolute/path/to/paperclip-runnerd \
|
||||
cargo test --release --manifest-path runner/Cargo.toml --locked -p paperclip-runner-core --lib \
|
||||
publisher_crosses_real_codex_tool_boundary_without_runner_state_access -- --ignored
|
||||
```
|
||||
|
||||
This uses `app-server command/exec` without credentials or a model call. It checks
|
||||
actual shell discovery, successful durable activity recording, and denial of unrelated
|
||||
Runner state, descriptor writes and ambient secrets. The normal suite covers
|
||||
bounded framing, concurrent publishers, fixed-path cleanup and attachment fencing.
|
||||
|
||||
## Direct live protocol qualification
|
||||
|
||||
The canonical direct live protocol suite lives in the separate
|
||||
|
||||
@@ -5,21 +5,21 @@
|
||||
"protocolVersion": 1,
|
||||
"profiles": {
|
||||
"grok": {
|
||||
"agentProfileVersion": 11,
|
||||
"agentProfileVersion": 12,
|
||||
"agentServerPackage": "builtin:grok-acp",
|
||||
"agentServerVersion": "1",
|
||||
"agentRuntimePackage": "native:grok",
|
||||
"agentRuntimeVersion": "1.0.13",
|
||||
"commandDigest": "sha256:f43a6f5a583972cdcb422784969cd6a97654655b0f135f1cf178317909c404f8",
|
||||
"commandDigest": "sha256:3dc83b2c3493913748cacb09025a8c97b74f3b5bbb1dc204072d1a773fa9af3a",
|
||||
"requiresProviderPolicy": false
|
||||
},
|
||||
"pi": {
|
||||
"agentProfileVersion": 33,
|
||||
"agentProfileVersion": 34,
|
||||
"agentServerPackage": "pi-acp",
|
||||
"agentServerVersion": "0.0.33",
|
||||
"agentRuntimePackage": "@earendil-works/pi-coding-agent",
|
||||
"agentRuntimeVersion": "1.0.0",
|
||||
"commandDigest": "sha256:0260d5810e9247986800ad46c1f9b3511a5abd06cb8f142405d5caacbc3960fc",
|
||||
"commandDigest": "sha256:ed4b32cc8fbf36309d38b3bd378f41f8e7b9e96a2405d275cc55e89042d94c7f",
|
||||
"requiresProviderPolicy": true
|
||||
},
|
||||
"cursor": {
|
||||
@@ -32,12 +32,12 @@
|
||||
"requiresProviderPolicy": true
|
||||
},
|
||||
"copilot": {
|
||||
"agentProfileVersion": 80,
|
||||
"agentProfileVersion": 81,
|
||||
"agentServerPackage": "@github/copilot",
|
||||
"agentServerVersion": "1.0.88",
|
||||
"agentRuntimePackage": null,
|
||||
"agentRuntimeVersion": null,
|
||||
"commandDigest": "sha256:7b8a6a31925256d225ea3d72fdcfd81b70a45c4b67d3eb0ee5073efff9ac8f8b",
|
||||
"commandDigest": "sha256:a51bdf77bba778c4d0265bbfd92a0e6c146f9de5debc3b4a2a268c34e5576e0c",
|
||||
"requiresProviderPolicy": true
|
||||
},
|
||||
"claude": {
|
||||
|
||||
@@ -142,3 +142,11 @@ the run's company, task and agent through its existing authorized run binding;
|
||||
callers cannot supply those identities in the payload. Recording activity does
|
||||
not independently verify the reported action or grant access to any resource.
|
||||
The event uses the ordinary durable outbox, acknowledgement and replay contract.
|
||||
|
||||
Local clients capture an opaque context with `paperclip-runnerd notification-context`
|
||||
before an operation and invoke `record-activity CONTEXT NAMESPACE ACTIVITY_KEY`
|
||||
after success. Runner publishes its matching absolute executable as host-owned
|
||||
`PAPERCLIP_RUNTIME_NOTIFICATION_HELPER` and the stable endpoint descriptor path
|
||||
as `PAPERCLIP_RUNTIME_NOTIFICATION_CONTEXT`. Configured task environment cannot
|
||||
override either value. Context is invalidated on warm attachment and process
|
||||
recovery, so a caller cannot attribute a later completion to a different run.
|
||||
@@ -3246,7 +3246,7 @@ mod tests {
|
||||
#[test]
|
||||
fn pi_profile_matches_published_identity_and_rejects_prior_profiles() {
|
||||
let published: Value = serde_json::from_str(include_str!(
|
||||
"../../../../test-fixtures/pi-acp/profile-v33-identity.json"
|
||||
"../../../../test-fixtures/pi-acp/profile-v34-identity.json"
|
||||
))
|
||||
.unwrap();
|
||||
let mut value = descriptor("codex");
|
||||
@@ -3263,6 +3263,8 @@ mod tests {
|
||||
current.validate(&context()).unwrap();
|
||||
|
||||
for prior in [
|
||||
include_str!("../../../../test-fixtures/pi-acp/profile-v33-identity.json"),
|
||||
include_str!("../../../../test-fixtures/pi-acp/profile-v32-identity.json"),
|
||||
include_str!("../../../../test-fixtures/pi-acp/profile-v31-identity.json"),
|
||||
include_str!("../../../../test-fixtures/pi-acp/profile-v29-identity.json"),
|
||||
include_str!("../../../../test-fixtures/pi-acp/profile-v28-identity.json"),
|
||||
|
||||
@@ -426,12 +426,36 @@ fn run_durable(args: &[String]) -> Result<(), LocalRunnerError> {
|
||||
reconnect_grace: optional_u64(args, "--reconnect-grace-ms")?.map(Duration::from_millis),
|
||||
max_runtime: duration("--max-runtime-ms", 0)?,
|
||||
};
|
||||
#[cfg(unix)]
|
||||
std::env::set_var(
|
||||
paperclip_runner_core::runtime_notifications::CONTEXT_ENV,
|
||||
paperclip_runner_core::runtime_notifications::context_path(
|
||||
&std::path::absolute(&state_dir)
|
||||
.map_err(|error| LocalRunnerError::invalid(error.to_string()))?,
|
||||
),
|
||||
);
|
||||
#[cfg(unix)]
|
||||
{
|
||||
let key = paperclip_runner_core::runtime_notifications::HELPER_ENV;
|
||||
std::env::remove_var(key);
|
||||
if let Ok(executable) = std::env::current_exe() {
|
||||
std::env::set_var(key, executable);
|
||||
}
|
||||
}
|
||||
let executor = NativeProviderCommandExecutor::with_runner_config(state_dir, &config);
|
||||
run_durable_runner(config, ticket, executor)
|
||||
.map_err(|error| LocalRunnerError::invalid(error.to_string()))
|
||||
}
|
||||
|
||||
fn run(args: &[String]) -> Result<(), LocalRunnerError> {
|
||||
#[cfg(unix)]
|
||||
if matches!(
|
||||
args.first().map(String::as_str),
|
||||
Some("notification-context" | "record-activity")
|
||||
) {
|
||||
return paperclip_runner_core::runtime_notifications::run_cli(args)
|
||||
.map_err(|error| LocalRunnerError::invalid(error.to_string()));
|
||||
}
|
||||
if args.first().map(String::as_str) == Some("--stdio-mcp")
|
||||
&& (args.len() == 2 || (args.len() == 3 && args[2] == "--content-images"))
|
||||
{
|
||||
|
||||
@@ -23,6 +23,52 @@ use crate::provider_events::normalized_codex_terminal_event_type;
|
||||
use crate::qualified_launch::verify_launch_artifact;
|
||||
use crate::question_response::validate_question_response;
|
||||
|
||||
#[cfg(unix)]
|
||||
pub(crate) fn runtime_notification_codex_args(
|
||||
args: &[String],
|
||||
profile: &str,
|
||||
paths: &[PathBuf; 4],
|
||||
) -> Vec<String> {
|
||||
let prefix = "shell_environment_policy.include_only=";
|
||||
let Some(keys) = args.iter().rev().find_map(|arg| arg.strip_prefix(prefix)) else {
|
||||
return vec![];
|
||||
};
|
||||
let Ok(mut keys) = serde_json::from_str::<Vec<String>>(keys) else {
|
||||
return vec![];
|
||||
};
|
||||
for key in [
|
||||
crate::runtime_notifications::CONTEXT_ENV,
|
||||
crate::runtime_notifications::HELPER_ENV,
|
||||
] {
|
||||
if !keys.iter().any(|existing| existing == key) {
|
||||
keys.push(key.into());
|
||||
}
|
||||
}
|
||||
let mut extra = vec![
|
||||
"-c".into(),
|
||||
format!("{prefix}{}", serde_json::to_string(&keys).unwrap()),
|
||||
"-c".into(),
|
||||
"shell_environment_policy.inherit=\"all\"".into(),
|
||||
];
|
||||
for profile in BTreeSet::from([profile, "paperclip-runner-workspace-read-only"]) {
|
||||
let prefix = format!("permissions.{profile}.filesystem=");
|
||||
let Some(value) = args.iter().rev().find_map(|arg| arg.strip_prefix(&prefix)) else {
|
||||
continue;
|
||||
};
|
||||
let Ok(mut config) = format!("filesystem={value}").parse::<toml_edit::DocumentMut>() else {
|
||||
continue;
|
||||
};
|
||||
let Some(filesystem) = config["filesystem"].as_inline_table_mut() else {
|
||||
continue;
|
||||
};
|
||||
for (path, mode) in paths.iter().zip(["read", "read", "read", "write"]) {
|
||||
filesystem.insert(path.to_string_lossy().as_ref(), mode.into());
|
||||
}
|
||||
extra.extend(["-c".into(), format!("{prefix}{filesystem}")]);
|
||||
}
|
||||
extra
|
||||
}
|
||||
|
||||
pub const CODEX_APP_SERVER_MAX_FRAME_BYTES: usize = 4 * 1024 * 1024;
|
||||
const QUALIFIED_OPENCODE_VERSION: &str = "1.18.34";
|
||||
const DEFAULT_PROVIDER_TRACE_MAX_BYTES: usize = 64 * 1024 * 1024;
|
||||
@@ -992,6 +1038,18 @@ impl CodexProvider {
|
||||
.map_err(|e| LocalRunnerError::invalid(e.to_string()))?,
|
||||
);
|
||||
}
|
||||
#[cfg(unix)]
|
||||
if config.provider == "codex" {
|
||||
if let Some(paths) = crate::runtime_notifications::access_paths() {
|
||||
let extra =
|
||||
runtime_notification_codex_args(&launch_args, &permission_profile, &paths);
|
||||
let position = launch_args
|
||||
.iter()
|
||||
.position(|arg| arg == "app-server")
|
||||
.unwrap_or(launch_args.len());
|
||||
launch_args.splice(position..position, extra);
|
||||
}
|
||||
}
|
||||
let spawn = (|| {
|
||||
if config.provider == "opencode" {
|
||||
let profile = opencode_launch_profile.ok_or_else(|| {
|
||||
@@ -4176,6 +4234,77 @@ fn codex_question_response(
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
#[cfg(unix)]
|
||||
fn runtime_notification_policy_grants_only_discovery_files_and_ipc() {
|
||||
let mut args = vec![
|
||||
"shell_environment_policy.include_only=[\"PATH\",\"LANG\",\"PAPERCLIP_API_KEY\"]"
|
||||
.into(),
|
||||
];
|
||||
for name in [
|
||||
"paperclip-runner-workspace-only",
|
||||
"paperclip-runner-workspace-read-only",
|
||||
] {
|
||||
args.push(format!(
|
||||
"permissions.{name}.filesystem={{\":root\"=\"none\",\"/state/private\"=\"none\"}}"
|
||||
));
|
||||
}
|
||||
let paths = [
|
||||
PathBuf::from("/runtime/runnerd"),
|
||||
PathBuf::from("/state/private/runtime-notifications.json"),
|
||||
PathBuf::from("/state/private/runtime-activity.lock"),
|
||||
PathBuf::from("/tmp/prn-channel"),
|
||||
];
|
||||
let extra =
|
||||
runtime_notification_codex_args(&args, "paperclip-runner-workspace-only", &paths);
|
||||
let keys: Vec<String> = serde_json::from_str(
|
||||
extra[1]
|
||||
.strip_prefix("shell_environment_policy.include_only=")
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
keys,
|
||||
[
|
||||
"PATH",
|
||||
"LANG",
|
||||
"PAPERCLIP_API_KEY",
|
||||
crate::runtime_notifications::CONTEXT_ENV,
|
||||
crate::runtime_notifications::HELPER_ENV
|
||||
]
|
||||
);
|
||||
let serialized = extra.join("\n");
|
||||
for profile in [
|
||||
"paperclip-runner-workspace-only",
|
||||
"paperclip-runner-workspace-read-only",
|
||||
] {
|
||||
let prefix = format!("permissions.{profile}.filesystem=");
|
||||
let value = extra
|
||||
.iter()
|
||||
.find_map(|arg| arg.strip_prefix(&prefix))
|
||||
.unwrap();
|
||||
let config = format!("filesystem={value}")
|
||||
.parse::<toml_edit::DocumentMut>()
|
||||
.unwrap();
|
||||
let filesystem = config["filesystem"].as_inline_table().unwrap();
|
||||
for (path, mode) in [
|
||||
(":root", "none"),
|
||||
("/state/private", "none"),
|
||||
("/runtime/runnerd", "read"),
|
||||
("/state/private/runtime-notifications.json", "read"),
|
||||
("/state/private/runtime-activity.lock", "read"),
|
||||
("/tmp/prn-channel", "write"),
|
||||
] {
|
||||
assert_eq!(
|
||||
filesystem.get(path).and_then(toml_edit::Value::as_str),
|
||||
Some(mode)
|
||||
);
|
||||
}
|
||||
}
|
||||
assert!(!serialized.contains("network."));
|
||||
assert!(!serialized.contains("PAPERCLIP_RUN_ID"));
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn completion_tail_provider() -> CodexProvider {
|
||||
completion_tail_provider_with_stdout_flood(false)
|
||||
|
||||
@@ -98,6 +98,8 @@ const RESERVED: &[&str] = &[
|
||||
"PAPERCLIP_RUNTIME_SERVICES_JSON",
|
||||
"PAPERCLIP_RUNTIME_SERVICE_INTENTS_JSON",
|
||||
"PAPERCLIP_RUN_ID",
|
||||
"PAPERCLIP_RUNTIME_NOTIFICATION_CONTEXT",
|
||||
"PAPERCLIP_RUNTIME_NOTIFICATION_HELPER",
|
||||
"PAPERCLIP_TASK_ID",
|
||||
"PAPERCLIP_WAKE_COMMENT_ID",
|
||||
"PAPERCLIP_WAKE_PAYLOAD_JSON",
|
||||
@@ -199,6 +201,8 @@ mod tests {
|
||||
"[\"NODE_OPTIONS\"]",
|
||||
"[\"PAPERCLIP_API_KEY\"]",
|
||||
"[\"PAPERCLIP_RUNNER_BOOTSTRAP_TICKET\"]",
|
||||
"[\"PAPERCLIP_RUNTIME_NOTIFICATION_CONTEXT\"]",
|
||||
"[\"PAPERCLIP_RUNTIME_NOTIFICATION_HELPER\"]",
|
||||
"[\"OPENAI_API_KEY\"]",
|
||||
"[\"LD_PRELOAD\"]",
|
||||
"[\"bad=name\"]",
|
||||
|
||||
@@ -415,6 +415,9 @@ pub fn run_durable_runner<E: CommandExecutor>(
|
||||
)?;
|
||||
store.save(&state)?;
|
||||
}
|
||||
#[cfg(unix)]
|
||||
let mut notifications =
|
||||
crate::runtime_notifications::RuntimeNotifications::bind(&config.state_dir).ok();
|
||||
// Bind listener mode or resolve dial mode before processing commands. Dial
|
||||
// reconnects retain the same validated addresses so DNS cannot redirect a
|
||||
// retry after the trust decision.
|
||||
@@ -649,6 +652,10 @@ pub fn run_durable_runner<E: CommandExecutor>(
|
||||
state.lifecycle = "ready".to_owned();
|
||||
state.recoverable_failure = None;
|
||||
store.save(&state)?;
|
||||
#[cfg(unix)]
|
||||
if let Some(notifications) = notifications.as_mut() {
|
||||
notifications.poll(&config, &mut state, &store);
|
||||
}
|
||||
let mut sent_source_seq = state.acked_source_seq;
|
||||
let mut ack_persistence = CumulativeAckPersistence::default();
|
||||
|
||||
@@ -819,6 +826,10 @@ pub fn run_durable_runner<E: CommandExecutor>(
|
||||
"active connection lease expired; durable state is preserved",
|
||||
));
|
||||
}
|
||||
#[cfg(unix)]
|
||||
if let Some(notifications) = notifications.as_mut() {
|
||||
notifications.poll(&config, &mut state, &store);
|
||||
}
|
||||
let now = current_unix_ms()?;
|
||||
if welcome.lease_renewal_version == Some(1) && now >= next_lease_renewal {
|
||||
// A failed write may still have reached the controller.
|
||||
|
||||
@@ -2647,6 +2647,8 @@ mod tests {
|
||||
struct EventExecutor {
|
||||
session_open_calls: Arc<AtomicUsize>,
|
||||
shutdown_calls: Arc<AtomicUsize>,
|
||||
#[cfg(unix)]
|
||||
activity_context_path: std::path::PathBuf,
|
||||
}
|
||||
|
||||
impl super::super::CommandExecutor for EventExecutor {
|
||||
@@ -2655,6 +2657,14 @@ mod tests {
|
||||
command: &Command,
|
||||
) -> Result<super::super::CommandExecution, DurableRunnerError> {
|
||||
if command.command_type == "session.open" {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
let endpoint: Value = serde_json::from_slice(
|
||||
&std::fs::read(&self.activity_context_path).unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(!endpoint["context"].as_str().unwrap().is_empty(), "welcome commands must see active activity context before starting provider work");
|
||||
}
|
||||
let calls = self.session_open_calls.fetch_add(1, Ordering::SeqCst) + 1;
|
||||
return Ok(super::super::CommandExecution {
|
||||
result: json!({"status": "completed", "calls": calls}),
|
||||
@@ -2827,6 +2837,8 @@ mod tests {
|
||||
EventExecutor {
|
||||
session_open_calls: session_open_calls.clone(),
|
||||
shutdown_calls: shutdown_calls.clone(),
|
||||
#[cfg(unix)]
|
||||
activity_context_path: crate::runtime_notifications::context_path(&directory),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
@@ -20,7 +20,7 @@ pub(crate) fn acpx_release_profile(agent: &str) -> Option<AcpxReleaseProfile> {
|
||||
agent_runtime_package: Some("native:grok"),
|
||||
agent_runtime_version: Some("1.0.13"),
|
||||
command_digest:
|
||||
"sha256:f43a6f5a583972cdcb422784969cd6a97654655b0f135f1cf178317909c404f8",
|
||||
"sha256:3dc83b2c3493913748cacb09025a8c97b74f3b5bbb1dc204072d1a773fa9af3a",
|
||||
requires_provider_policy: false,
|
||||
},
|
||||
"pi" => AcpxReleaseProfile {
|
||||
@@ -29,7 +29,7 @@ pub(crate) fn acpx_release_profile(agent: &str) -> Option<AcpxReleaseProfile> {
|
||||
agent_runtime_package: Some("@earendil-works/pi-coding-agent"),
|
||||
agent_runtime_version: Some("1.0.0"),
|
||||
command_digest:
|
||||
"sha256:0260d5810e9247986800ad46c1f9b3511a5abd06cb8f142405d5caacbc3960fc",
|
||||
"sha256:ed4b32cc8fbf36309d38b3bd378f41f8e7b9e96a2405d275cc55e89042d94c7f",
|
||||
requires_provider_policy: true,
|
||||
},
|
||||
"cursor" => AcpxReleaseProfile {
|
||||
@@ -47,7 +47,7 @@ pub(crate) fn acpx_release_profile(agent: &str) -> Option<AcpxReleaseProfile> {
|
||||
agent_runtime_package: None,
|
||||
agent_runtime_version: None,
|
||||
command_digest:
|
||||
"sha256:7b8a6a31925256d225ea3d72fdcfd81b70a45c4b67d3eb0ee5073efff9ac8f8b",
|
||||
"sha256:a51bdf77bba778c4d0265bbfd92a0e6c146f9de5debc3b4a2a268c34e5576e0c",
|
||||
requires_provider_policy: true,
|
||||
},
|
||||
"claude" => AcpxReleaseProfile {
|
||||
|
||||
@@ -369,3 +369,6 @@ mod tests {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
pub mod runtime_notifications;
|
||||
@@ -1034,6 +1034,8 @@ impl SupervisedProcess {
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
for key in [
|
||||
"PAPERCLIP_RUNTIME_NOTIFICATION_CONTEXT",
|
||||
"PAPERCLIP_RUNTIME_NOTIFICATION_HELPER",
|
||||
"PATH",
|
||||
"PATHEXT",
|
||||
"SystemRoot",
|
||||
|
||||
@@ -0,0 +1,784 @@
|
||||
//! Run-scoped activity notifications from provider commands.
|
||||
use crate::durable::{
|
||||
create_private_temporary_file, verify_private_directory, DurableRunnerConfig,
|
||||
DurableRunnerError, DurableState, DurableStateStore, EventPriority,
|
||||
};
|
||||
use crate::process_supervisor::{read_bounded_line, BoundedLine};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use std::collections::HashSet;
|
||||
use std::fs::{self, File, OpenOptions};
|
||||
use std::io::{BufReader, Read, Write};
|
||||
use std::os::unix::fs::{DirBuilderExt, FileTypeExt, OpenOptionsExt, PermissionsExt};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
mpsc::{self, Receiver, SyncSender},
|
||||
Arc,
|
||||
};
|
||||
use std::thread::{self, JoinHandle};
|
||||
use std::time::{Duration, Instant};
|
||||
use uuid::Uuid;
|
||||
|
||||
pub const CONTEXT_ENV: &str = "PAPERCLIP_RUNTIME_NOTIFICATION_CONTEXT";
|
||||
pub const HELPER_ENV: &str = "PAPERCLIP_RUNTIME_NOTIFICATION_HELPER";
|
||||
const MAX_FRAME: usize = 2048;
|
||||
const QUEUE_LIMIT: usize = 128;
|
||||
|
||||
pub fn context_path(state_dir: &Path) -> PathBuf {
|
||||
state_dir.join("runtime-notifications.json")
|
||||
}
|
||||
fn failure(error: impl std::fmt::Display) -> DurableRunnerError {
|
||||
DurableRunnerError::invalid(format!("runtime notification: {error}"))
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct Endpoint {
|
||||
pipe: PathBuf,
|
||||
context: String,
|
||||
}
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
struct Activity {
|
||||
context: String,
|
||||
namespace: String,
|
||||
activity_key: String,
|
||||
}
|
||||
|
||||
// Fixed-path cleanup never traverses provider-created contents.
|
||||
struct ActivityPipe(PathBuf);
|
||||
impl Drop for ActivityPipe {
|
||||
fn drop(&mut self) {
|
||||
let _ = fs::remove_file(&self.0);
|
||||
let _ = fs::remove_file(self.0.with_extension("lock"));
|
||||
let _ = fs::remove_dir(self.0.parent().unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
fn private_file(path: &Path) -> Result<File, DurableRunnerError> {
|
||||
let file = OpenOptions::new()
|
||||
.read(true)
|
||||
.custom_flags((rustix::fs::OFlags::NOFOLLOW | rustix::fs::OFlags::NONBLOCK).bits() as i32)
|
||||
.open(path)
|
||||
.map_err(failure)?;
|
||||
let metadata = file.metadata().map_err(failure)?;
|
||||
if !metadata.is_file() || metadata.permissions().mode() & 0o077 != 0 || metadata.len() > 8192 {
|
||||
return Err(failure("invalid endpoint descriptor"));
|
||||
}
|
||||
Ok(file)
|
||||
}
|
||||
|
||||
fn descriptor(path: &Path) -> Result<Endpoint, DurableRunnerError> {
|
||||
let mut file = private_file(path)?;
|
||||
let mut bytes = Vec::new();
|
||||
(&mut file)
|
||||
.take(8193)
|
||||
.read_to_end(&mut bytes)
|
||||
.map_err(failure)?;
|
||||
serde_json::from_slice(&bytes).map_err(failure)
|
||||
}
|
||||
|
||||
fn write_descriptor(path: &Path, endpoint: &Endpoint) -> Result<(), DurableRunnerError> {
|
||||
let bytes = serde_json::to_vec(endpoint).map_err(failure)?;
|
||||
let (temporary, mut file) = create_private_temporary_file(path)?;
|
||||
let result = file
|
||||
.write_all(&bytes)
|
||||
.and_then(|()| fs::rename(&temporary, path));
|
||||
let _ = fs::remove_file(temporary);
|
||||
result.map_err(failure)
|
||||
}
|
||||
|
||||
// Check between reads so shutdown also ends an unterminated full-pipe record.
|
||||
struct StoppableReader<'a, R>(R, &'a AtomicBool);
|
||||
impl<R: Read> Read for StoppableReader<'_, R> {
|
||||
fn read(&mut self, bytes: &mut [u8]) -> std::io::Result<usize> {
|
||||
if self.1.load(Ordering::Acquire) {
|
||||
return Ok(0);
|
||||
}
|
||||
self.0.read(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
fn receive_records(mut reader: impl Read, sender: SyncSender<Activity>, stopped: &AtomicBool) {
|
||||
let mut reader = BufReader::with_capacity(MAX_FRAME, StoppableReader(&mut reader, stopped));
|
||||
while !stopped.load(Ordering::Acquire) {
|
||||
match read_bounded_line(&mut reader, MAX_FRAME - 2) {
|
||||
Ok(BoundedLine::Line(line)) => {
|
||||
if let Ok(activity) = serde_json::from_str(&line) {
|
||||
let _ = sender.try_send(activity);
|
||||
}
|
||||
}
|
||||
Err(error)
|
||||
if !matches!(
|
||||
error.kind(),
|
||||
std::io::ErrorKind::Interrupted | std::io::ErrorKind::InvalidData
|
||||
) =>
|
||||
{
|
||||
break
|
||||
}
|
||||
Ok(BoundedLine::Eof) => break,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn publish(path: &Path, activity: &Activity) -> Result<(), DurableRunnerError> {
|
||||
let bytes = serde_json::to_vec(activity).map_err(failure)?;
|
||||
if bytes.len() + 2 > MAX_FRAME {
|
||||
return Err(failure("oversized activity"));
|
||||
}
|
||||
let endpoint = descriptor(path)?;
|
||||
if endpoint.context.is_empty() || endpoint.context != activity.context {
|
||||
return Err(failure("runtime authority is inactive or changed"));
|
||||
}
|
||||
// A stable read-only lock serializes publishers; the descriptor can rotate atomically.
|
||||
let guard = private_file(&endpoint.pipe.with_extension("lock"))?;
|
||||
let deadline = Instant::now() + Duration::from_millis(500);
|
||||
loop {
|
||||
match guard.try_lock() {
|
||||
Ok(()) => break,
|
||||
Err(fs::TryLockError::WouldBlock) if Instant::now() < deadline => {
|
||||
thread::sleep(Duration::from_millis(1))
|
||||
}
|
||||
Err(error) => return Err(failure(error)),
|
||||
}
|
||||
}
|
||||
let mut pipe = OpenOptions::new()
|
||||
.write(true)
|
||||
.custom_flags((rustix::fs::OFlags::NOFOLLOW | rustix::fs::OFlags::NONBLOCK).bits() as i32)
|
||||
.open(endpoint.pipe)
|
||||
.map_err(failure)?;
|
||||
if !pipe.metadata().map_err(failure)?.file_type().is_fifo() {
|
||||
return Err(failure("invalid activity pipe"));
|
||||
}
|
||||
// A leading newline terminates any incomplete previous write.
|
||||
let mut frame = vec![b'\n'];
|
||||
frame.extend(bytes);
|
||||
frame.push(b'\n');
|
||||
pipe.write_all(&frame).map_err(failure)
|
||||
}
|
||||
|
||||
pub struct RuntimeNotifications {
|
||||
pipe: ActivityPipe,
|
||||
wake: File,
|
||||
stopped: Arc<AtomicBool>,
|
||||
worker: Option<JoinHandle<()>>,
|
||||
incoming: Receiver<Activity>,
|
||||
run_id: Option<String>,
|
||||
context: String,
|
||||
recorded: HashSet<(String, String)>,
|
||||
}
|
||||
|
||||
impl RuntimeNotifications {
|
||||
pub fn bind(state_dir: &Path) -> Result<Self, DurableRunnerError> {
|
||||
verify_private_directory(state_dir)?;
|
||||
let directory = fs::canonicalize(std::env::temp_dir())
|
||||
.map_err(failure)?
|
||||
.join(format!("runtime-activity-{}", Uuid::new_v4().simple()));
|
||||
fs::DirBuilder::new()
|
||||
.mode(0o700)
|
||||
.create(&directory)
|
||||
.map_err(failure)?;
|
||||
let pipe = ActivityPipe(directory.join("activity.fifo"));
|
||||
OpenOptions::new()
|
||||
.write(true)
|
||||
.create_new(true)
|
||||
.mode(0o600)
|
||||
.open(pipe.0.with_extension("lock"))
|
||||
.map_err(failure)?;
|
||||
let status = std::process::Command::new("/usr/bin/mkfifo")
|
||||
.args(["-m", "600"])
|
||||
.arg(&pipe.0)
|
||||
.env_clear()
|
||||
.stdout(std::process::Stdio::null())
|
||||
.stderr(std::process::Stdio::null())
|
||||
.status()
|
||||
.map_err(failure)?;
|
||||
if !status.success() {
|
||||
return Err(failure("cannot create activity pipe"));
|
||||
}
|
||||
let reader = OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.custom_flags(rustix::fs::OFlags::NOFOLLOW.bits() as i32)
|
||||
.open(&pipe.0)
|
||||
.map_err(failure)?;
|
||||
if !reader.metadata().map_err(failure)?.file_type().is_fifo() {
|
||||
return Err(failure("invalid activity pipe"));
|
||||
}
|
||||
let wake = OpenOptions::new()
|
||||
.write(true)
|
||||
.custom_flags(
|
||||
(rustix::fs::OFlags::NOFOLLOW | rustix::fs::OFlags::NONBLOCK).bits() as i32,
|
||||
)
|
||||
.open(&pipe.0)
|
||||
.map_err(failure)?;
|
||||
write_descriptor(
|
||||
&context_path(state_dir),
|
||||
&Endpoint {
|
||||
pipe: pipe.0.clone(),
|
||||
context: String::new(),
|
||||
},
|
||||
)?;
|
||||
let (sender, incoming) = mpsc::sync_channel(QUEUE_LIMIT);
|
||||
let stopped = Arc::new(AtomicBool::new(false));
|
||||
let stop = Arc::clone(&stopped);
|
||||
let worker = thread::Builder::new()
|
||||
.name("runner-activity".into())
|
||||
.spawn(move || receive_records(reader, sender, &stop))
|
||||
.map_err(failure)?;
|
||||
Ok(Self {
|
||||
pipe,
|
||||
wake,
|
||||
stopped,
|
||||
worker: Some(worker),
|
||||
incoming,
|
||||
run_id: None,
|
||||
context: String::new(),
|
||||
recorded: HashSet::new(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn poll(
|
||||
&mut self,
|
||||
config: &DurableRunnerConfig,
|
||||
state: &mut DurableState,
|
||||
store: &DurableStateStore,
|
||||
) {
|
||||
let run_id = (state.lifecycle == "ready"
|
||||
&& state.warm_transition.is_none()
|
||||
&& state.run_id == config.run_id)
|
||||
.then_some(config.run_id.as_str());
|
||||
if self.run_id.as_deref() != run_id {
|
||||
self.run_id = run_id.map(str::to_owned);
|
||||
self.recorded.clear();
|
||||
self.context = if run_id.is_some() {
|
||||
Uuid::new_v4().to_string()
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
if write_descriptor(
|
||||
&context_path(&config.state_dir),
|
||||
&Endpoint {
|
||||
pipe: self.pipe.0.clone(),
|
||||
context: self.context.clone(),
|
||||
},
|
||||
)
|
||||
.is_err()
|
||||
{
|
||||
self.context.clear();
|
||||
}
|
||||
}
|
||||
for _ in 0..16 {
|
||||
let Ok(activity) = self.incoming.try_recv() else {
|
||||
break;
|
||||
};
|
||||
let _ = self.record(activity, config, state, store);
|
||||
}
|
||||
}
|
||||
|
||||
fn record(
|
||||
&mut self,
|
||||
activity: Activity,
|
||||
config: &DurableRunnerConfig,
|
||||
state: &mut DurableState,
|
||||
store: &DurableStateStore,
|
||||
) -> Result<(), DurableRunnerError> {
|
||||
let valid = |value: &str| {
|
||||
!value.is_empty()
|
||||
&& value.chars().count() <= 160
|
||||
&& !value.chars().any(|ch| ch.is_ascii_control())
|
||||
};
|
||||
if self.context.is_empty()
|
||||
|| state.run_id != config.run_id
|
||||
|| activity.context != self.context
|
||||
|| !valid(&activity.namespace)
|
||||
|| !valid(&activity.activity_key)
|
||||
{
|
||||
return Err(failure("invalid activity context or identifiers"));
|
||||
}
|
||||
let key = (activity.namespace, activity.activity_key);
|
||||
if !self.recorded.contains(&key) {
|
||||
if self.recorded.len() >= QUEUE_LIMIT {
|
||||
return Err(failure("activity limit reached"));
|
||||
}
|
||||
let mut next = state.clone();
|
||||
next.enqueue_event(
|
||||
config,
|
||||
"runtime.activity.recorded",
|
||||
EventPriority::P1,
|
||||
json!({"namespace":key.0,"activityKey":key.1}),
|
||||
)?;
|
||||
store.save(&next)?;
|
||||
*state = next;
|
||||
self.recorded.insert(key);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for RuntimeNotifications {
|
||||
fn drop(&mut self) {
|
||||
self.stopped.store(true, Ordering::Release);
|
||||
// A nonblocking, sub-PIPE_BUF write wakes an idle reader; a full pipe is already readable.
|
||||
loop {
|
||||
match self.wake.write(b"\n") {
|
||||
Err(error) if error.kind() == std::io::ErrorKind::Interrupted => continue,
|
||||
_ => break,
|
||||
}
|
||||
}
|
||||
if let Some(worker) = self.worker.take() {
|
||||
let _ = worker.join();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn access_paths() -> Option<[PathBuf; 4]> {
|
||||
let path = PathBuf::from(std::env::var_os(CONTEXT_ENV)?);
|
||||
let endpoint = descriptor(&path).ok()?;
|
||||
Some([
|
||||
PathBuf::from(std::env::var_os(HELPER_ENV)?),
|
||||
path,
|
||||
endpoint.pipe.with_extension("lock"),
|
||||
endpoint.pipe,
|
||||
])
|
||||
}
|
||||
|
||||
pub fn run_cli(args: &[String]) -> Result<(), DurableRunnerError> {
|
||||
let path = PathBuf::from(
|
||||
std::env::var_os(CONTEXT_ENV).ok_or_else(|| failure("activity endpoint is unavailable"))?,
|
||||
);
|
||||
match args {
|
||||
[command] if command == "notification-context" => {
|
||||
let endpoint = descriptor(&path)?;
|
||||
if endpoint.context.is_empty() {
|
||||
return Err(failure("runtime authority is inactive"));
|
||||
}
|
||||
println!("{}", endpoint.context);
|
||||
Ok(())
|
||||
}
|
||||
[command, context, namespace, activity_key] if command == "record-activity" => publish(
|
||||
&path,
|
||||
&Activity {
|
||||
context: context.clone(),
|
||||
namespace: namespace.clone(),
|
||||
activity_key: activity_key.clone(),
|
||||
},
|
||||
),
|
||||
_ => Err(failure("invalid activity arguments")),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::io::Cursor;
|
||||
use std::os::unix::fs::DirBuilderExt;
|
||||
struct TestDirectory {
|
||||
directory: PathBuf,
|
||||
}
|
||||
impl TestDirectory {
|
||||
fn create(parent: &Path) -> std::io::Result<Self> {
|
||||
let directory = parent.join(format!("activity-test-{}", Uuid::new_v4()));
|
||||
fs::DirBuilder::new().mode(0o700).create(&directory)?;
|
||||
Ok(Self { directory })
|
||||
}
|
||||
}
|
||||
impl Drop for TestDirectory {
|
||||
fn drop(&mut self) {
|
||||
let _ = fs::remove_dir_all(&self.directory);
|
||||
}
|
||||
}
|
||||
fn config(path: PathBuf) -> DurableRunnerConfig {
|
||||
DurableRunnerConfig {
|
||||
connect_url: "ws://127.0.0.1/runner".into(),
|
||||
ca_bundle_path: None,
|
||||
state_dir: path,
|
||||
runner_instance_id: "runner-1".into(),
|
||||
environment_lease_id: "lease-1".into(),
|
||||
run_id: "run-1".into(),
|
||||
normalized_session_id: "session-1".into(),
|
||||
turn_id: "turn-1".into(),
|
||||
item_id: "item-1".into(),
|
||||
runner_version: "1".into(),
|
||||
runner_digest: "sha256:digest".into(),
|
||||
acpx_launch_profile: None,
|
||||
opencode_launch_profile: None,
|
||||
max_outbox_bytes: 1024 * 1024,
|
||||
p0_reserve_bytes: 64 * 1024,
|
||||
max_frame_bytes: 64 * 1024,
|
||||
reconnect_delay: Duration::from_millis(1),
|
||||
reconnect_grace: None,
|
||||
max_runtime: Duration::ZERO,
|
||||
}
|
||||
}
|
||||
|
||||
fn activity(context: &str, key: &str) -> Activity {
|
||||
Activity {
|
||||
context: context.into(),
|
||||
namespace: "example.runtime".into(),
|
||||
activity_key: key.into(),
|
||||
}
|
||||
}
|
||||
fn setup() -> (
|
||||
TestDirectory,
|
||||
DurableRunnerConfig,
|
||||
DurableStateStore,
|
||||
DurableState,
|
||||
RuntimeNotifications,
|
||||
) {
|
||||
let directory = TestDirectory::create(&std::env::temp_dir()).unwrap();
|
||||
let config = config(directory.directory.clone());
|
||||
let store = DurableStateStore::new(&config.state_dir).unwrap();
|
||||
let (mut state, _) = store.load_or_create(&config).unwrap();
|
||||
state.lifecycle = "ready".into();
|
||||
let mut server = RuntimeNotifications::bind(&config.state_dir).unwrap();
|
||||
server.poll(&config, &mut state, &store);
|
||||
(directory, config, store, state, server)
|
||||
}
|
||||
#[test]
|
||||
fn capture_is_local_and_authority_rotation_rejects_late_completion() {
|
||||
let (_directory, mut config, store, mut state, mut server) = setup();
|
||||
let first = descriptor(&context_path(&config.state_dir)).unwrap();
|
||||
for _ in 0..100 {
|
||||
assert_eq!(
|
||||
descriptor(&context_path(&config.state_dir))
|
||||
.unwrap()
|
||||
.context,
|
||||
first.context
|
||||
);
|
||||
}
|
||||
config.run_id = "next-run".into();
|
||||
state.run_id.clone_from(&config.run_id);
|
||||
server.poll(&config, &mut state, &store);
|
||||
assert!(server
|
||||
.record(
|
||||
activity(&first.context, "desktop.used"),
|
||||
&config,
|
||||
&mut state,
|
||||
&store
|
||||
)
|
||||
.is_err());
|
||||
let current = server.context.clone();
|
||||
server
|
||||
.record(
|
||||
activity(¤t, "desktop.used"),
|
||||
&config,
|
||||
&mut state,
|
||||
&store,
|
||||
)
|
||||
.unwrap();
|
||||
server
|
||||
.record(
|
||||
activity(¤t, "desktop.used"),
|
||||
&config,
|
||||
&mut state,
|
||||
&store,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(state.outbox.len(), 1);
|
||||
assert_eq!(state.outbox[0].envelope["payload"]["runId"], "next-run");
|
||||
state.lifecycle = "closed".into();
|
||||
server.poll(&config, &mut state, &store);
|
||||
assert!(server
|
||||
.record(
|
||||
activity(¤t, "desktop.used"),
|
||||
&config,
|
||||
&mut state,
|
||||
&store
|
||||
)
|
||||
.is_err());
|
||||
assert!(descriptor(&context_path(&config.state_dir))
|
||||
.unwrap()
|
||||
.context
|
||||
.is_empty());
|
||||
drop(server);
|
||||
state.lifecycle = "ready".into();
|
||||
let mut recovered = RuntimeNotifications::bind(&config.state_dir).unwrap();
|
||||
recovered.poll(&config, &mut state, &store);
|
||||
assert!(recovered
|
||||
.record(
|
||||
activity(¤t, "desktop.used"),
|
||||
&config,
|
||||
&mut state,
|
||||
&store
|
||||
)
|
||||
.is_err());
|
||||
}
|
||||
#[test]
|
||||
fn unicode_limits_and_failed_persistence_leave_authority_usable() {
|
||||
let (_directory, config, store, mut state, mut server) = setup();
|
||||
let context = server.context.clone();
|
||||
for key in ["视".repeat(60), "😀".repeat(160), "\u{85}".into()] {
|
||||
server
|
||||
.record(activity(&context, &key), &config, &mut state, &store)
|
||||
.unwrap();
|
||||
}
|
||||
for key in [
|
||||
"".into(),
|
||||
"😀".repeat(161),
|
||||
"\u{1f}".into(),
|
||||
"\u{7f}".into(),
|
||||
] {
|
||||
assert!(server
|
||||
.record(activity(&context, &key), &config, &mut state, &store)
|
||||
.is_err());
|
||||
}
|
||||
let saved = config.state_dir.join("runner-state.json");
|
||||
let backup = saved.with_extension("backup");
|
||||
fs::rename(&saved, &backup).unwrap();
|
||||
fs::create_dir(&saved).unwrap();
|
||||
let before = state.outbox.len();
|
||||
assert!(server
|
||||
.record(
|
||||
activity(&context, "retry.used"),
|
||||
&config,
|
||||
&mut state,
|
||||
&store
|
||||
)
|
||||
.is_err());
|
||||
assert_eq!(state.outbox.len(), before);
|
||||
fs::remove_dir(&saved).unwrap();
|
||||
fs::rename(backup, saved).unwrap();
|
||||
server
|
||||
.record(
|
||||
activity(&context, "retry.used"),
|
||||
&config,
|
||||
&mut state,
|
||||
&store,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(state.outbox.len(), before + 1);
|
||||
}
|
||||
#[test]
|
||||
fn malformed_partial_and_oversized_records_resynchronize_with_bounded_queue() {
|
||||
let good = serde_json::to_vec(&activity("context", "desktop.used")).unwrap();
|
||||
let mut bytes = vec![b'x'; MAX_FRAME * 2];
|
||||
bytes.extend(b"\n{broken\n\xff\n{\"partial\":");
|
||||
for _ in 0..QUEUE_LIMIT + 10 {
|
||||
bytes.push(b'\n');
|
||||
bytes.extend(&good);
|
||||
bytes.push(b'\n');
|
||||
}
|
||||
let (tx, rx) = mpsc::sync_channel(QUEUE_LIMIT);
|
||||
receive_records(Cursor::new(bytes), tx, &AtomicBool::new(false));
|
||||
let records: Vec<_> = rx.try_iter().collect();
|
||||
assert_eq!(records.len(), QUEUE_LIMIT);
|
||||
assert_eq!(records[0].activity_key, "desktop.used");
|
||||
assert!(serde_json::from_value::<Activity>(json!({"context":"context","namespace":"example.runtime","activityKey":"desktop.used","runId":"caller"})).is_err());
|
||||
}
|
||||
#[test]
|
||||
fn publish_reaches_the_durable_run_without_a_reply_or_idle_filesystem_work() {
|
||||
let (_directory, config, store, mut state, mut server) = setup();
|
||||
let path = context_path(&config.state_dir);
|
||||
let modified = fs::metadata(&path).unwrap().modified().unwrap();
|
||||
let context = server.context.clone();
|
||||
publish(&path, &activity(&context, "desktop.used")).unwrap();
|
||||
let deadline = Instant::now() + Duration::from_secs(2);
|
||||
while state.outbox.is_empty() && Instant::now() < deadline {
|
||||
server.poll(&config, &mut state, &store);
|
||||
thread::sleep(Duration::from_millis(1));
|
||||
}
|
||||
assert_eq!(state.outbox.len(), 1);
|
||||
for _ in 0..100 {
|
||||
server.poll(&config, &mut state, &store);
|
||||
}
|
||||
assert_eq!(fs::metadata(path).unwrap().modified().unwrap(), modified);
|
||||
assert_eq!(
|
||||
state.outbox[0].envelope["payload"]["payload"],
|
||||
json!({"namespace":"example.runtime","activityKey":"desktop.used"})
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn concurrent_unicode_records_stay_framed_and_rotation_does_not_wait_for_publishers() {
|
||||
let (_directory, mut config, store, mut state, mut server) = setup();
|
||||
let path = context_path(&config.state_dir);
|
||||
let old = descriptor(&path).unwrap();
|
||||
let guard = private_file(&old.pipe.with_extension("lock")).unwrap();
|
||||
guard.try_lock().unwrap();
|
||||
config.run_id = "next-run".into();
|
||||
state.run_id.clone_from(&config.run_id);
|
||||
server.poll(&config, &mut state, &store);
|
||||
let current = descriptor(&path).unwrap();
|
||||
assert_ne!(old.context, current.context);
|
||||
assert!(publish(&path, &activity(&old.context, "desktop.used")).is_err());
|
||||
assert!(publish(&path, &activity(¤t.context, "desktop.used")).is_err());
|
||||
drop(guard);
|
||||
let barrier = Arc::new(std::sync::Barrier::new(8));
|
||||
let writers: Vec<_> = (0..8)
|
||||
.map(|index| {
|
||||
let path = path.clone();
|
||||
let barrier = Arc::clone(&barrier);
|
||||
let activity = Activity {
|
||||
context: current.context.clone(),
|
||||
namespace: "😀".repeat(160),
|
||||
activity_key: format!("{index}{}", "😀".repeat(159)),
|
||||
};
|
||||
thread::spawn(move || {
|
||||
barrier.wait();
|
||||
publish(&path, &activity).unwrap();
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
for writer in writers {
|
||||
writer.join().unwrap();
|
||||
}
|
||||
let deadline = Instant::now() + Duration::from_secs(2);
|
||||
while state.outbox.len() < 8 && Instant::now() < deadline {
|
||||
server.poll(&config, &mut state, &store);
|
||||
thread::sleep(Duration::from_millis(1));
|
||||
}
|
||||
assert_eq!(state.outbox.len(), 8);
|
||||
assert!(state
|
||||
.outbox
|
||||
.iter()
|
||||
.all(|event| event.envelope["payload"]["runId"] == "next-run"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn endpoint_replacement_cannot_redirect_writes_or_make_shutdown_traverse_a_tree() {
|
||||
let (_directory, config, store, mut state, mut server) = setup();
|
||||
let pipe = server.pipe.0.clone();
|
||||
fs::remove_file(&pipe).unwrap();
|
||||
fs::create_dir(&pipe).unwrap();
|
||||
let mut nested = pipe.clone();
|
||||
for _ in 0..100 {
|
||||
nested = nested.join("deep");
|
||||
fs::create_dir(&nested).unwrap();
|
||||
}
|
||||
fs::write(nested.join("protected"), b"unchanged").unwrap();
|
||||
fs::set_permissions(&nested, fs::Permissions::from_mode(0)).unwrap();
|
||||
assert!(publish(
|
||||
&context_path(&config.state_dir),
|
||||
&activity(&server.context, "desktop.used")
|
||||
)
|
||||
.is_err());
|
||||
for _ in 0..100 {
|
||||
server.poll(&config, &mut state, &store);
|
||||
}
|
||||
server.wake.write_all(b"{\"partial\":").unwrap();
|
||||
let started = Instant::now();
|
||||
drop(server);
|
||||
assert!(started.elapsed() < Duration::from_secs(2));
|
||||
fs::set_permissions(&nested, fs::Permissions::from_mode(0o700)).unwrap();
|
||||
assert_eq!(fs::read(nested.join("protected")).unwrap(), b"unchanged");
|
||||
fs::remove_dir_all(pipe.parent().unwrap()).unwrap();
|
||||
}
|
||||
#[test]
|
||||
#[ignore = "requires CODEX_NOTIFICATION_SANDBOX_COMMAND and PAPERCLIP_RUNTIME_NOTIFICATION_HELPER"]
|
||||
fn publisher_crosses_real_codex_tool_boundary_without_runner_state_access() {
|
||||
use serde_json::Value;
|
||||
use std::io::{BufRead, BufReader};
|
||||
use std::process::{Command, Stdio};
|
||||
use std::sync::mpsc;
|
||||
let command = std::env::var_os("CODEX_NOTIFICATION_SANDBOX_COMMAND").unwrap();
|
||||
let helper = std::env::var_os(HELPER_ENV).unwrap();
|
||||
let directory = TestDirectory::create(&std::env::temp_dir()).unwrap();
|
||||
let config = config(directory.directory.clone());
|
||||
let store = DurableStateStore::new(&config.state_dir).unwrap();
|
||||
let (mut state, _) = store.load_or_create(&config).unwrap();
|
||||
state.lifecycle = "ready".into();
|
||||
let mut server = RuntimeNotifications::bind(&config.state_dir).unwrap();
|
||||
server.poll(&config, &mut state, &store);
|
||||
let paths = [
|
||||
PathBuf::from(&helper),
|
||||
context_path(&config.state_dir),
|
||||
server.pipe.0.with_extension("lock"),
|
||||
server.pipe.0.clone(),
|
||||
];
|
||||
let workspace = config.state_dir.join("workspace");
|
||||
fs::create_dir(&workspace).unwrap();
|
||||
let home = workspace.join("home");
|
||||
fs::create_dir(&home).unwrap();
|
||||
let profile = "paperclip-runner-workspace-only";
|
||||
let mut args = vec![
|
||||
"-c".into(),
|
||||
format!("default_permissions=\"{profile}\""),
|
||||
"-c".into(),
|
||||
"shell_environment_policy.include_only=[\"PATH\",\"LANG\"]".into(),
|
||||
];
|
||||
for name in [profile, "paperclip-runner-workspace-read-only"] {
|
||||
args.extend(["-c".into(), format!("permissions.{name}.filesystem={{\":root\"=\"none\",\":minimal\"=\"read\",\":tmpdir\"=\"none\",\":workspace_roots\"={{\".\"=\"read\"}}}}"),
|
||||
"-c".into(), format!("permissions.{name}.network.enabled=false")]);
|
||||
}
|
||||
let extra = crate::codex_provider::runtime_notification_codex_args(&args, profile, &paths);
|
||||
let script = r#"
|
||||
[ -z "${UNRELATED_SECRET:-}" ] || exit 70
|
||||
if cat "$1" >/dev/null 2>&1; then exit 71; fi
|
||||
if /bin/sh -c ': >> "$PAPERCLIP_RUNTIME_NOTIFICATION_CONTEXT"' 2>/dev/null; then exit 72; fi
|
||||
if touch "$2/probe" 2>/dev/null; then exit 75; fi
|
||||
if touch "$3/probe" 2>/dev/null; then exit 76; fi
|
||||
if /bin/sh -c ': >> "$1"' ignored "$4" 2>/dev/null; then exit 77; fi
|
||||
context=$("$PAPERCLIP_RUNTIME_NOTIFICATION_HELPER" notification-context) || exit 73
|
||||
"$PAPERCLIP_RUNTIME_NOTIFICATION_HELPER" record-activity "$context" example.runtime desktop.used || exit 74
|
||||
"#;
|
||||
let mut child = Command::new(command)
|
||||
.args(args)
|
||||
.args(extra)
|
||||
.args(["app-server", "--stdio", "--strict-config"])
|
||||
.current_dir(&workspace)
|
||||
.env_clear()
|
||||
.env("PATH", "/usr/bin:/bin")
|
||||
.env("LANG", "C.UTF-8")
|
||||
.env("HOME", &home)
|
||||
.env("CODEX_HOME", &home)
|
||||
.env("UNRELATED_SECRET", "must-not-reach-command")
|
||||
.env(CONTEXT_ENV, &paths[1])
|
||||
.env(HELPER_ENV, helper)
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
.unwrap();
|
||||
let mut input = child.stdin.take().unwrap();
|
||||
let output = child.stdout.take().unwrap();
|
||||
let (tx, rx) = mpsc::channel();
|
||||
let reader = thread::spawn(move || {
|
||||
for line in BufReader::new(output).lines().map_while(Result::ok) {
|
||||
if let Ok(value) = serde_json::from_str::<Value>(&line) {
|
||||
let _ = tx.send(value);
|
||||
}
|
||||
}
|
||||
});
|
||||
writeln!(input, "{}", json!({"id":1,"method":"initialize","params":{"clientInfo":{"name":"notification-probe","version":"1"},"capabilities":{"experimentalApi":true}}})).unwrap();
|
||||
let deadline = Instant::now() + Duration::from_secs(20);
|
||||
let mut response = None;
|
||||
while child.try_wait().unwrap().is_none() && Instant::now() < deadline && response.is_none()
|
||||
{
|
||||
server.poll(&config, &mut state, &store);
|
||||
while let Ok(message) = rx.try_recv() {
|
||||
if message["id"] == 1 {
|
||||
assert!(message.get("error").is_none(), "initialize: {message}");
|
||||
writeln!(input, "{}", json!({"method":"initialized","params":{}})).unwrap();
|
||||
writeln!(input, "{}", json!({"id":2,"method":"command/exec","params":{
|
||||
"command":["/bin/sh","-c",script,"publisher",config.state_dir.join("runner-state.json"),config.state_dir,server.pipe.0.parent().unwrap(),server.pipe.0.with_extension("lock")],
|
||||
"cwd":workspace,"timeoutMs":10000}})).unwrap();
|
||||
} else if message["id"] == 2 {
|
||||
response = Some(message);
|
||||
}
|
||||
}
|
||||
thread::sleep(Duration::from_millis(1));
|
||||
}
|
||||
if child.try_wait().unwrap().is_none() {
|
||||
child.kill().unwrap();
|
||||
}
|
||||
let result = child.wait_with_output().unwrap();
|
||||
reader.join().unwrap();
|
||||
let response = response.unwrap_or_else(|| {
|
||||
panic!(
|
||||
"app-server did not reply: {}",
|
||||
String::from_utf8_lossy(&result.stderr)
|
||||
)
|
||||
});
|
||||
assert!(response.get("error").is_none(), "command: {response}");
|
||||
assert_eq!(response["result"]["exitCode"], 0, "publisher: {response}");
|
||||
let deadline = Instant::now() + Duration::from_secs(2);
|
||||
while state.outbox.is_empty() && Instant::now() < deadline {
|
||||
server.poll(&config, &mut state, &store);
|
||||
thread::sleep(Duration::from_millis(1));
|
||||
}
|
||||
assert_eq!(state.outbox.len(), 1);
|
||||
assert_eq!(state.outbox[0].envelope["payload"]["runId"], config.run_id);
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -17,7 +17,7 @@ use sha2::{Digest, Sha256};
|
||||
const CODEX_ACPX_DIGEST: &str =
|
||||
"sha256:443491f4e814501a5dc47347e4510e84faeec9c9382e2d4e53bfe69386d5aabc";
|
||||
const PI_ACPX_DIGEST: &str =
|
||||
"sha256:0260d5810e9247986800ad46c1f9b3511a5abd06cb8f142405d5caacbc3960fc";
|
||||
"sha256:ed4b32cc8fbf36309d38b3bd378f41f8e7b9e96a2405d275cc55e89042d94c7f";
|
||||
|
||||
fn temporary_directory(label: &str) -> PathBuf {
|
||||
let nonce = SystemTime::now()
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
#![cfg(unix)]
|
||||
|
||||
use std::fs;
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
use std::path::PathBuf;
|
||||
use std::process::{Command, Output};
|
||||
use std::thread;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use paperclip_runner_core::durable::{DurableRunnerConfig, DurableStateStore};
|
||||
use paperclip_runner_core::runtime_notifications::{
|
||||
context_path, RuntimeNotifications, CONTEXT_ENV, HELPER_ENV,
|
||||
};
|
||||
|
||||
fn helper(config: &DurableRunnerConfig, args: &[&str]) -> Output {
|
||||
Command::new(std::env::var_os(HELPER_ENV).expect("Runner publishes its matching helper"))
|
||||
.env("PATH", "/usr/bin:/bin")
|
||||
.env(
|
||||
CONTEXT_ENV,
|
||||
fs::canonicalize(context_path(&config.state_dir)).unwrap(),
|
||||
)
|
||||
.env("PAPERCLIP_RUN_ID", "stale-process-run")
|
||||
.current_dir(std::env::temp_dir())
|
||||
.args(args)
|
||||
.stdout(std::process::Stdio::piped())
|
||||
.stderr(std::process::Stdio::piped())
|
||||
.output()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn real_cli_uses_relative_state_from_another_directory_and_never_process_run_identity() {
|
||||
let path = std::env::current_dir()
|
||||
.unwrap()
|
||||
.join(format!("notification-cli-{}", uuid::Uuid::new_v4()));
|
||||
fs::create_dir(&path).unwrap();
|
||||
fs::set_permissions(&path, fs::Permissions::from_mode(0o700)).unwrap();
|
||||
let mut config = DurableRunnerConfig {
|
||||
connect_url: "ws://127.0.0.1/runner".into(),
|
||||
ca_bundle_path: None,
|
||||
state_dir: PathBuf::from(path.file_name().unwrap()),
|
||||
runner_instance_id: "runner".into(),
|
||||
environment_lease_id: "lease".into(),
|
||||
run_id: "active-run".into(),
|
||||
normalized_session_id: "session".into(),
|
||||
turn_id: "turn".into(),
|
||||
item_id: "item".into(),
|
||||
runner_version: "1".into(),
|
||||
runner_digest: "sha256:digest".into(),
|
||||
acpx_launch_profile: None,
|
||||
opencode_launch_profile: None,
|
||||
max_outbox_bytes: 1024 * 1024,
|
||||
p0_reserve_bytes: 65536,
|
||||
max_frame_bytes: 65536,
|
||||
reconnect_delay: Duration::from_millis(1),
|
||||
reconnect_grace: None,
|
||||
max_runtime: Duration::ZERO,
|
||||
};
|
||||
let store = DurableStateStore::new(&path).unwrap();
|
||||
let (mut state, _) = store.load_or_create(&config).unwrap();
|
||||
state.lifecycle = "ready".into();
|
||||
let mut server = RuntimeNotifications::bind(&config.state_dir).unwrap();
|
||||
server.poll(&config, &mut state, &store);
|
||||
std::env::set_var(CONTEXT_ENV, fs::canonicalize(context_path(&path)).unwrap());
|
||||
std::env::set_var(HELPER_ENV, env!("CARGO_BIN_EXE_paperclip-runnerd"));
|
||||
let mut provider = paperclip_runner_core::process_supervisor::SupervisedProcess::spawn(
|
||||
std::path::Path::new("/bin/sh"),
|
||||
&[
|
||||
"-c".into(),
|
||||
"printf '%s\\n' \"$PAPERCLIP_RUNTIME_NOTIFICATION_CONTEXT\" \"$PAPERCLIP_RUNTIME_NOTIFICATION_HELPER\"".into(),
|
||||
],
|
||||
Duration::from_millis(100),
|
||||
2048,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
provider
|
||||
.receive_stdout_line(Duration::from_secs(1))
|
||||
.unwrap(),
|
||||
Some(
|
||||
fs::canonicalize(context_path(&path))
|
||||
.unwrap()
|
||||
.to_string_lossy()
|
||||
.into_owned()
|
||||
)
|
||||
);
|
||||
assert_eq!(
|
||||
provider
|
||||
.receive_stdout_line(Duration::from_secs(1))
|
||||
.unwrap(),
|
||||
Some(env!("CARGO_BIN_EXE_paperclip-runnerd").to_owned())
|
||||
);
|
||||
provider.wait().unwrap();
|
||||
let captured = helper(&config, &["notification-context"]);
|
||||
assert!(captured.status.success(), "{:?}", captured);
|
||||
let context = String::from_utf8(captured.stdout).unwrap();
|
||||
let args = [
|
||||
"record-activity",
|
||||
context.trim(),
|
||||
"example.runtime",
|
||||
"desktop.used",
|
||||
];
|
||||
let activated = helper(&config, &args);
|
||||
assert!(activated.status.success(), "{:?}", activated);
|
||||
assert!(activated.stdout.is_empty());
|
||||
assert!(activated.stderr.is_empty());
|
||||
let deadline = Instant::now() + Duration::from_secs(2);
|
||||
while state.outbox.is_empty() && Instant::now() < deadline {
|
||||
server.poll(&config, &mut state, &store);
|
||||
thread::sleep(Duration::from_millis(1));
|
||||
}
|
||||
assert_eq!(state.outbox[0].envelope["payload"]["runId"], "active-run");
|
||||
config.run_id = "next-run".into();
|
||||
state.run_id.clone_from(&config.run_id);
|
||||
server.poll(&config, &mut state, &store);
|
||||
let stale = helper(&config, &args);
|
||||
assert!(!stale.status.success());
|
||||
assert_eq!(state.outbox.len(), 1);
|
||||
std::env::remove_var(CONTEXT_ENV);
|
||||
std::env::remove_var(HELPER_ENV);
|
||||
drop(server);
|
||||
fs::remove_dir_all(path).unwrap();
|
||||
}
|
||||
@@ -7,9 +7,9 @@ import { fileURLToPath } from "node:url";
|
||||
import { resolve, dirname } from "node:path";
|
||||
|
||||
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const fixture = resolve(root, "test/fixtures/copilot-profile-v80-identity.json");
|
||||
const fixture = resolve(root, "test/fixtures/copilot-profile-v81-identity.json");
|
||||
const historical = JSON.parse(readFileSync(resolve(root, "test/fixtures/copilot-profile-v12-identity.json"), "utf8"));
|
||||
const declaration = { ...historical.declaration, agentProfileVersion: 80 };
|
||||
const declaration = { ...historical.declaration, agentProfileVersion: 81 };
|
||||
delete declaration.permissionIdentitySourceSha256;
|
||||
const sources = {
|
||||
displayEventSourceSha256: "src/drivers/acpx/copilot-events.ts",
|
||||
|
||||
@@ -7,9 +7,9 @@ import { fileURLToPath } from "node:url";
|
||||
|
||||
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const repositoryRoot = resolve(packageRoot, "../..");
|
||||
// Versions 1–10 remain decodable historical identities. Versioned fixtures are
|
||||
// Versions 1–11 remain decodable historical identities. Versioned fixtures are
|
||||
// immutable; advance this version before changing a versioned declaration.
|
||||
export const GROK_PROFILE_VERSION = 11;
|
||||
export const GROK_PROFILE_VERSION = 12;
|
||||
// Contract surfaces: ACPX wire patch; sidecar parsing and error translation;
|
||||
// native effort/search/credential admission; host/sandbox/tool-bridge execution;
|
||||
// launcher integrity and platform pins; Rust descriptor writes, session state,
|
||||
|
||||
@@ -660,6 +660,9 @@ describe("native backend factory", () => {
|
||||
["copilot", "../../test/fixtures/copilot-profile-v75-identity.json"],
|
||||
["copilot", "../../test/fixtures/copilot-profile-v76-identity.json"],
|
||||
["copilot", "../../test/fixtures/copilot-profile-v77-identity.json"],
|
||||
["copilot", "../../test/fixtures/copilot-profile-v78-identity.json"],
|
||||
["pi", "../../test-fixtures/pi-acp/profile-v32-identity.json"],
|
||||
["grok", "../../test/fixtures/grok-profile-v10-identity.json"],
|
||||
["cursor", "../../test/fixtures/cursor-acp/profile-v10-identity.json"],
|
||||
["copilot", "../../test/fixtures/copilot-profile-v14-identity.json"],
|
||||
["copilot", "../../test/fixtures/copilot-profile-v60-identity.json"],
|
||||
|
||||
@@ -10,7 +10,7 @@ export const GENERATED_RUNTIME_ENVIRONMENT_KEYS: ReadonlySet<string> = new Set([
|
||||
...AGENT_OBSERVABILITY_ENV_KEYS,
|
||||
"PAPERCLIP_AGENT_ID", "PAPERCLIP_AGENT_KEY_ID", "PAPERCLIP_AGENT_PUBLIC_KEY", "PAPERCLIP_AGENT_PRIVATE_KEY",
|
||||
"PAPERCLIP_API_KEY", "PAPERCLIP_API_URL", "PAPERCLIP_API_BRIDGE_MODE", "PAPERCLIP_COMPANY_ID", "PAPERCLIP_INSTANCE_ID",
|
||||
"PAPERCLIP_RUN_ID", "PAPERCLIP_NORMALIZED_SESSION_ID", "PAPERCLIP_RUNNER_INSTANCE_ID", "PAPERCLIP_RUNNER_BOOTSTRAP_TICKET", "PAPERCLIP_TASK_ID", "PAPERCLIP_APPROVAL_ID", "PAPERCLIP_APPROVAL_STATUS", "PAPERCLIP_LINKED_ISSUE_IDS",
|
||||
"PAPERCLIP_RUNTIME_NOTIFICATION_HELPER", "PAPERCLIP_RUNTIME_NOTIFICATION_CONTEXT", "PAPERCLIP_RUN_ID", "PAPERCLIP_NORMALIZED_SESSION_ID", "PAPERCLIP_RUNNER_INSTANCE_ID", "PAPERCLIP_RUNNER_BOOTSTRAP_TICKET", "PAPERCLIP_TASK_ID", "PAPERCLIP_APPROVAL_ID", "PAPERCLIP_APPROVAL_STATUS", "PAPERCLIP_LINKED_ISSUE_IDS",
|
||||
"PAPERCLIP_WAKE_REASON", "PAPERCLIP_WAKE_COMMENT_ID", "PAPERCLIP_WAKE_PAYLOAD_JSON", "PAPERCLIP_EXECUTION_MODE",
|
||||
"PAPERCLIP_WORKSPACES_JSON", "PAPERCLIP_WORKSPACE_ID", "PAPERCLIP_WORKSPACE_CWD", "PAPERCLIP_WORKSPACE_SOURCE",
|
||||
"PAPERCLIP_WORKSPACE_REPO_URL", "PAPERCLIP_WORKSPACE_REPO_REF", "PAPERCLIP_WORKSPACE_BRANCH",
|
||||
|
||||
@@ -87,9 +87,9 @@ vi.mock("./installation-integrity.js", () => ({ verifyNativeAcpxInstallation: vi
|
||||
|
||||
describe("Copilot build-owned installation", () => {
|
||||
it("admits the current profile with environment launch preparation", () => {
|
||||
expect(QUALIFIED_ACPX_PROFILES.copilot.agentProfileVersion).toBe(80);
|
||||
expect(QUALIFIED_ACPX_PROFILES.copilot.agentProfileVersion).toBe(81);
|
||||
expect(QUALIFIED_ACPX_PROFILES.copilot.qualificationStatus).toBeUndefined();
|
||||
expect(QUALIFIED_ACPX_PROFILES.copilot.commandDigest).toBe("sha256:7b8a6a31925256d225ea3d72fdcfd81b70a45c4b67d3eb0ee5073efff9ac8f8b");
|
||||
expect(QUALIFIED_ACPX_PROFILES.copilot.commandDigest).toBe("sha256:a51bdf77bba778c4d0265bbfd92a0e6c146f9de5debc3b4a2a268c34e5576e0c");
|
||||
});
|
||||
|
||||
it("preserves published v65 while binding retained-maintenance waits in v66", async () => {
|
||||
|
||||
@@ -22,6 +22,12 @@ describe("ACPX launch environment", () => {
|
||||
expect(launch.ACPX_CLAUDE_INCLUDE_USER_SETTINGS).toBeUndefined();
|
||||
expect(environment.CLAUDE_CONFIG_DIR).toBe(configured);
|
||||
});
|
||||
it("retains Runner notification discovery while denying task-supplied overrides", () => {
|
||||
for (const key of ["PAPERCLIP_RUNTIME_NOTIFICATION_CONTEXT", "PAPERCLIP_RUNTIME_NOTIFICATION_HELPER"]) {
|
||||
expect(createSanitizedAcpxSpawnInput({ [key]: "/private/runner/context" }, "claude").env[key]).toBe("/private/runner/context");
|
||||
expect(configuredEnvironmentProjection({ [key]: "/caller/override" })).not.toHaveProperty(key);
|
||||
}
|
||||
});
|
||||
it("retains an explicit Claude MCP timeout without inheriting an ambient or sibling setting", () => {
|
||||
vi.stubEnv("MCP_TOOL_TIMEOUT", "999999");
|
||||
expect(createSanitizedAcpxSpawnInput(undefined, "claude").env).not.toHaveProperty("MCP_TOOL_TIMEOUT");
|
||||
|
||||
@@ -129,6 +129,8 @@ export function createSanitizedAcpxSpawnInput(
|
||||
"all_proxy",
|
||||
"RUST_BACKTRACE",
|
||||
"PAPERCLIP_NATIVE_MCP_NAME",
|
||||
"PAPERCLIP_RUNTIME_NOTIFICATION_CONTEXT",
|
||||
"PAPERCLIP_RUNTIME_NOTIFICATION_HELPER",
|
||||
"PAPERCLIP_NATIVE_MCP_URL",
|
||||
...(environment === undefined ? [] : ["PAPERCLIP_AGENT_KEY_ID", "PAPERCLIP_AGENT_PUBLIC_KEY", "PAPERCLIP_AGENT_PRIVATE_KEY"]),
|
||||
...credentialNames,
|
||||
|
||||
@@ -9,12 +9,12 @@ export const QUALIFIED_ACPX_PROFILE_DATA = {
|
||||
"protocolVersion": 1,
|
||||
"acpxVersion": "0.13.1",
|
||||
"agent": "grok",
|
||||
"agentProfileVersion": 11,
|
||||
"agentProfileVersion": 12,
|
||||
"agentServerPackage": "builtin:grok-acp",
|
||||
"agentServerVersion": "1",
|
||||
"agentRuntimePackage": "native:grok",
|
||||
"agentRuntimeVersion": "1.0.13",
|
||||
"commandDigest": "sha256:f43a6f5a583972cdcb422784969cd6a97654655b0f135f1cf178317909c404f8",
|
||||
"commandDigest": "sha256:3dc83b2c3493913748cacb09025a8c97b74f3b5bbb1dc204072d1a773fa9af3a",
|
||||
"permissionPolicy": "interactive"
|
||||
},
|
||||
"pi": {
|
||||
@@ -22,12 +22,12 @@ export const QUALIFIED_ACPX_PROFILE_DATA = {
|
||||
"protocolVersion": 1,
|
||||
"acpxVersion": "0.13.1",
|
||||
"agent": "pi",
|
||||
"agentProfileVersion": 33,
|
||||
"agentProfileVersion": 34,
|
||||
"agentServerPackage": "pi-acp",
|
||||
"agentServerVersion": "0.0.33",
|
||||
"agentRuntimePackage": "@earendil-works/pi-coding-agent",
|
||||
"agentRuntimeVersion": "1.0.0",
|
||||
"commandDigest": "sha256:0260d5810e9247986800ad46c1f9b3511a5abd06cb8f142405d5caacbc3960fc",
|
||||
"commandDigest": "sha256:ed4b32cc8fbf36309d38b3bd378f41f8e7b9e96a2405d275cc55e89042d94c7f",
|
||||
"permissionPolicy": "interactive"
|
||||
},
|
||||
"cursor": {
|
||||
@@ -48,12 +48,12 @@ export const QUALIFIED_ACPX_PROFILE_DATA = {
|
||||
"protocolVersion": 1,
|
||||
"acpxVersion": "0.13.1",
|
||||
"agent": "copilot",
|
||||
"agentProfileVersion": 80,
|
||||
"agentProfileVersion": 81,
|
||||
"agentServerPackage": "@github/copilot",
|
||||
"agentServerVersion": "1.0.88",
|
||||
"agentRuntimePackage": null,
|
||||
"agentRuntimeVersion": null,
|
||||
"commandDigest": "sha256:7b8a6a31925256d225ea3d72fdcfd81b70a45c4b67d3eb0ee5073efff9ac8f8b",
|
||||
"commandDigest": "sha256:a51bdf77bba778c4d0265bbfd92a0e6c146f9de5debc3b4a2a268c34e5576e0c",
|
||||
"permissionPolicy": "interactive"
|
||||
},
|
||||
"claude": {
|
||||
|
||||
@@ -175,6 +175,14 @@ describe("Pi installation factory", () => {
|
||||
expect(isSupportedAcpxProfileVersion("pi", 29)).toBe(true);
|
||||
});
|
||||
|
||||
it("preserves profile 33 while binding host-owned notification discovery in 34", async () => {
|
||||
const prior = JSON.parse(await readFile(new URL("../../../test-fixtures/pi-acp/profile-v33-identity.json", import.meta.url), "utf8"));
|
||||
const current = JSON.parse(await readFile(new URL("../../../test-fixtures/pi-acp/profile-v34-identity.json", import.meta.url), "utf8"));
|
||||
expect(Object.keys(current.declaration).filter(key => JSON.stringify(current.declaration[key]) !== JSON.stringify(prior.declaration[key])).sort())
|
||||
.toEqual(["agentProfileVersion", "credentialEnvironmentSourceSha256"]);
|
||||
expect(isSupportedAcpxProfileVersion("pi", 33)).toBe(true);
|
||||
});
|
||||
|
||||
it("binds the profile declaration to the reviewed patch and platform closure pins", async () => {
|
||||
const hash = (bytes: string | Uint8Array) => createHash("sha256").update(bytes).digest("hex");
|
||||
const canonical = (value: any): string => Array.isArray(value) ? `[${value.map(canonical).join(",")}]`
|
||||
@@ -210,21 +218,21 @@ describe("Pi installation factory", () => {
|
||||
|
||||
it("rejects legacy profiles and tampered executable identities", () => {
|
||||
expect(() => assertPiInstallationProfile(candidate())).not.toThrow();
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 1 })).toThrow("version 33");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 2 })).toThrow("version 33");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 3 })).toThrow("version 33");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 4 })).toThrow("version 33");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 5 })).toThrow("version 33");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 6 })).toThrow("version 33");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 7 })).toThrow("version 33");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 8 })).toThrow("version 33");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 9 })).toThrow("version 33");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 10 })).toThrow("version 33");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 11 })).toThrow("version 33");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 12 })).toThrow("version 33");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 13 })).toThrow("version 33");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 14 })).toThrow("version 33");
|
||||
for (const priorVersion of [15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31]) expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: priorVersion })).toThrow("version 33");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 1 })).toThrow("version 34");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 2 })).toThrow("version 34");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 3 })).toThrow("version 34");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 4 })).toThrow("version 34");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 5 })).toThrow("version 34");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 6 })).toThrow("version 34");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 7 })).toThrow("version 34");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 8 })).toThrow("version 34");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 9 })).toThrow("version 34");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 10 })).toThrow("version 34");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 11 })).toThrow("version 34");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 12 })).toThrow("version 34");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 13 })).toThrow("version 34");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 14 })).toThrow("version 34");
|
||||
for (const priorVersion of [15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33]) expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: priorVersion })).toThrow("version 34");
|
||||
for (const changed of [{ agentServerVersion: "latest" }, { commandDigest: `sha256:${"0".repeat(64)}` }, { agentRuntimePackage: "ambient-pi" }]) {
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), ...changed })).toThrow("trusted declaration");
|
||||
}
|
||||
|
||||
@@ -2,13 +2,13 @@ import { QUALIFIED_ACPX_PROFILE_DATA } from "./generated-profiles.js";
|
||||
|
||||
/** Decodable historical profile revisions, not permission to launch them.
|
||||
* Exact current profile/package/digest admission is checked separately. */
|
||||
type HistoricalAcpxProfileVersion = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79;
|
||||
type HistoricalAcpxProfileVersion = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80;
|
||||
type CurrentAcpxProfileVersion = typeof QUALIFIED_ACPX_PROFILE_DATA[keyof typeof QUALIFIED_ACPX_PROFILE_DATA]["agentProfileVersion"];
|
||||
export type AcpxProfileVersion = HistoricalAcpxProfileVersion | CurrentAcpxProfileVersion;
|
||||
|
||||
const historicalVersions: Readonly<Record<string, readonly HistoricalAcpxProfileVersion[]>> = {
|
||||
pi: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32], claude: [1, 2, 3, 4, 5], codex: [1, 2, 3, 4, 5],
|
||||
grok: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], copilot: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79],
|
||||
pi: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33], claude: [1, 2, 3, 4, 5], codex: [1, 2, 3, 4, 5],
|
||||
grok: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], copilot: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80],
|
||||
cursor: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
|
||||
};
|
||||
|
||||
|
||||
@@ -31,6 +31,13 @@ describe("ACPX recovery identity", () => {
|
||||
["copilot", "../../../test/fixtures/copilot-profile-v75-identity.json"],
|
||||
["copilot", "../../../test/fixtures/copilot-profile-v76-identity.json"],
|
||||
["copilot", "../../../test/fixtures/copilot-profile-v77-identity.json"],
|
||||
["copilot", "../../../test/fixtures/copilot-profile-v78-identity.json"],
|
||||
["copilot", "../../../test/fixtures/copilot-profile-v79-identity.json"],
|
||||
["copilot", "../../../test/fixtures/copilot-profile-v80-identity.json"],
|
||||
["pi", "../../../test-fixtures/pi-acp/profile-v32-identity.json"],
|
||||
["pi", "../../../test-fixtures/pi-acp/profile-v33-identity.json"],
|
||||
["grok", "../../../test/fixtures/grok-profile-v10-identity.json"],
|
||||
["grok", "../../../test/fixtures/grok-profile-v11-identity.json"],
|
||||
["cursor", "../../../test/fixtures/cursor-acp/profile-v10-identity.json"],
|
||||
["copilot", "../../../test/fixtures/copilot-profile-v14-identity.json"],
|
||||
["pi", "../../../test-fixtures/pi-acp/profile-v13-identity.json"],
|
||||
|
||||
@@ -894,6 +894,8 @@ describe("OpenCodeServerDriver", () => {
|
||||
environment: {
|
||||
PATH: process.env.PATH,
|
||||
OPENROUTER_API_KEY: "test-openrouter-key",
|
||||
PAPERCLIP_RUNTIME_NOTIFICATION_CONTEXT: join(root, "runtime-notifications.json"),
|
||||
PAPERCLIP_RUNTIME_NOTIFICATION_HELPER: join(root, "runnerd"),
|
||||
PAPERCLIP_API_KEY: "must-not-leak",
|
||||
UNRELATED_SECRET: "must-not-leak",
|
||||
PAPERCLIP_PROVIDER_TRACE_PATH: tracePath,
|
||||
@@ -1037,6 +1039,8 @@ describe("OpenCodeServerDriver", () => {
|
||||
"utf8",
|
||||
),
|
||||
);
|
||||
expect(environment.notificationContext).toBe(join(root, "runtime-notifications.json"));
|
||||
expect(environment.notificationHelper).toBe(join(root, "runnerd"));
|
||||
expect(environment.keys).not.toContain("OPENROUTER_API_KEY");
|
||||
expect(environment.keys).not.toContain("PAPERCLIP_API_KEY");
|
||||
expect(environment.keys).not.toContain("UNRELATED_SECRET");
|
||||
|
||||
@@ -2967,6 +2967,7 @@ function sanitizedEnvironment(
|
||||
overrides: Record<string, string>,
|
||||
): NodeJS.ProcessEnv {
|
||||
const allowed = [
|
||||
"PAPERCLIP_RUNTIME_NOTIFICATION_CONTEXT", "PAPERCLIP_RUNTIME_NOTIFICATION_HELPER",
|
||||
"PAPERCLIP_AGENT_KEY_ID", "PAPERCLIP_AGENT_PUBLIC_KEY", "PAPERCLIP_AGENT_PRIVATE_KEY",
|
||||
"PATH",
|
||||
"LANG",
|
||||
@@ -2989,6 +2990,7 @@ function sanitizedEnvironment(
|
||||
|
||||
function sanitizedEnvironmentKeys(): string[] {
|
||||
return [
|
||||
"PAPERCLIP_RUNTIME_NOTIFICATION_CONTEXT", "PAPERCLIP_RUNTIME_NOTIFICATION_HELPER",
|
||||
"PATH",
|
||||
"LANG",
|
||||
"LC_ALL",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Grok admission source identity: sha256:c5a4628adcdd9190c45558f42014b0055698b8f8c7eb301417238f38d913f503
|
||||
// Grok admission source identity: sha256:79188d089f3f6b575726af77fe055a58449144a373a0745dcad4be414cba8cd0
|
||||
// This launcher is admitted by digest and receives a verified native executable.
|
||||
// Never resolve an executable through PATH or accept command-line overrides.
|
||||
const { isAbsolute, join } = require("node:path");
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"commandDigest": "sha256:ed4b32cc8fbf36309d38b3bd378f41f8e7b9e96a2405d275cc55e89042d94c7f",
|
||||
"declaration": {
|
||||
"schema": "paperclip.acpx_profile_declaration.v1",
|
||||
"agent": "pi",
|
||||
"agentProfileVersion": 34,
|
||||
"acpxVersion": "0.13.1",
|
||||
"agentServerVersion": "0.0.33",
|
||||
"agentRuntimeVersion": "1.0.0",
|
||||
"nodeVersion": "24.21.0",
|
||||
"closure": {
|
||||
"darwin-arm64": "282022db10150c6632b3444df421342e7d534bdf5d5fb1097a2e79d0625a2bcf",
|
||||
"darwin-x64": "64e251e19009f755c0b04f73ce2138246faab71a961b0f13d75ebfcc34bef12e",
|
||||
"linux-x64": "713b1fdff42fb56a1518bdc084f181d70bee8ebadc3e4b1d76321ed9108c8410"
|
||||
},
|
||||
"wrapperSha256": "9d129b3d38772e93e97080aa6c4e574ac5df4e47ce5bc331a484a9fbf8188b36",
|
||||
"helperSha256": "41e0490b617da0d60e0c8ec58ef311236f945129d99f816cff6897f78da7f91d",
|
||||
"extensionSha256": "f5818573a355702388072b28926897db546a5cf346b5171f646582ce5f9102a4",
|
||||
"agentFilesBinding": "registered-runtime-context-v1",
|
||||
"nativeQuestions": "select-confirm-input-editor-v4-root-field-types",
|
||||
"pathPolicy": "sdk-normalization-v1",
|
||||
"mcpToolNames": "provider-safe-exact-source-v1",
|
||||
"acpxPatchSha256": "1e7dae8c83bf8620f53f2bea13c175eeff95e7e955783f6f529bb1dcbdcdb6d2",
|
||||
"nativeAssistantMessages": "pi1-rpc-deltas-v3-bound-message-and-tool-receipts",
|
||||
"nativeNotices": "session-extension-v2-bounded-runtime-failure",
|
||||
"messageProjectionSha256": "17d1e91b948dfa6a56e1905645add52b502041ad843c8d50a13b07956f22aadd",
|
||||
"noticeProjectionSha256": "014485adc690998e395d334ea3ae67f72be6060a2fc78a1185df7b8d1da28fbf",
|
||||
"sharedRuntimeContract": "paperclip.acpx-runtime-contract.v1",
|
||||
"nativeInputDisposition": "queued-only-v1",
|
||||
"cacheWarming": "disabled-owned-decision-v1",
|
||||
"thinkingLevel": {
|
||||
"supported": [
|
||||
"off",
|
||||
"low",
|
||||
"high",
|
||||
"max"
|
||||
],
|
||||
"newConfigurationDefault": "low",
|
||||
"admission": "explicit-native-effective-before-prompt-v1",
|
||||
"recovery": "mode-bound-v1"
|
||||
},
|
||||
"modelSelection": "caller-selected-native-verified-v1",
|
||||
"providerConfigurationSourceSha256": "75a9a13f13f224dc092bd8ebf49e8747162172943761e44476c8eb18d80c52d4",
|
||||
"credentialEnvironmentSourceSha256": "fbc9fc037dab8134d090ac7c3eb23f80026cd84085d56a9704fcf9ceb0478926",
|
||||
"runtimeSandboxSourceSha256": "04a49a24e3828ce8fca908de15e31ae932b8b1b4f7ecec0308627f6a8da9ba20",
|
||||
"recoveryIdentitySourceSha256": "f74d061cc07b0c7ca04df62ca991d940a733d9ef6594b3362451769fc10030b8",
|
||||
"nativeCancellation": "acknowledged-cancelled-terminal-preserves-usage-v1",
|
||||
"dependencySecurityPatchSha256": "5273a195b952f233336122842faaf650193c04f36b9359d20d4d63079b0ce814",
|
||||
"openRouterAttributionSourceSha256": "1211191529d6073b733b311c3d69cada4973eede0f3e540c46e261f78e0b90f9",
|
||||
"preparedHarnessHomeSourceSha256": "bb286f7ddc6e9597c56d46bf7ff6e3b556c5b680d7e147fd46518724432b083e"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
{
|
||||
"commandDigest": "sha256:a51bdf77bba778c4d0265bbfd92a0e6c146f9de5debc3b4a2a268c34e5576e0c",
|
||||
"declaration": {
|
||||
"schema": "paperclip.acpx_profile_declaration.v1",
|
||||
"agent": "copilot",
|
||||
"agentProfileVersion": 81,
|
||||
"acpxVersion": "0.13.1",
|
||||
"agentServerVersion": "1.0.88",
|
||||
"protocolPolicyRevision": "copilot-agent-manual-v1",
|
||||
"acpxPatchSha256": "1e7dae8c83bf8620f53f2bea13c175eeff95e7e955783f6f529bb1dcbdcdb6d2",
|
||||
"policySha256": "a4be69be43db84052560420a3ca546a665c9165573c24d543eb33d144ba138a3",
|
||||
"distributionSourceSha256": "c0a65e247fc47b263bd369a3d5ab505c4b323d4e6493be8a9ecbeb0e9dbb9107",
|
||||
"agentFilesBinding": "registered-runtime-context-v1",
|
||||
"systemInstructionDelivery": "COPILOT_HOME/copilot-instructions.md:replace-under-lifetime-lease-before-launch:v1",
|
||||
"sharedRuntimeContract": "paperclip.acpx-runtime-contract.v1",
|
||||
"innerDistributionSourceSha256": "9bb08347eed01f1ab44e065c8b06ead9be3a25973d6e6329dc4c0a099096279a",
|
||||
"upstreamAppSha256": "7b48282a19b5b0814a0c96ad5e3a125173d792cc55f9525b2effea962f6063c0",
|
||||
"patchedAppSha256": "61f8eebe0c30cc03b311c6818b2acb158a6d16ca58c8bfac5ebd42db095c2679",
|
||||
"messageIdentityContract": "copilot-native-message-id-v1",
|
||||
"ownedDistributionDelivery": "COPILOT_CLI_DIST_DIR:lease-owned-guarded-inner-distribution:v1",
|
||||
"permissionContextContract": "copilot-edit-permission-context-v1",
|
||||
"permissionContextSourceSha256": "de2d38d702b3c574161af0c0c04790fb6f6347916eb3916ce7a131a8c229121b",
|
||||
"permissionAdapterSourceSha256": "ee5929ed2ee647ad0369465dee9d51a00b2dd28e1ff735b43e969ddc9c164668",
|
||||
"permissionLocationsSourceSha256": "f39a81da395b6e23aa2a1a45572a28846e5e81bfd3d764d9a456f74a79c752e9",
|
||||
"permissionRedactionSourceSha256": "ad6770c69269087951790e2e87e22ac088e3d432769adf17ebc865ed512e6c4f",
|
||||
"permissionClassifierSourceSha256": "1161eabd82e26133e3b3db3ab567f65d52808b03f176e93497e02c8330349fb4",
|
||||
"semanticToolReceiptContract": "paperclip.semantic_tool_receipt.v2",
|
||||
"semanticReceiptSourceSha256": "b2795726b2234a663fec1a6ca11f0551ae69882fe9e82aa39686e952d036e791",
|
||||
"semanticBridgeSourceSha256": "8764b8c9032fdc7d1415b01e75a9fc57c550e7a5e11da4bed7d9100082872ae5",
|
||||
"toolEvidenceSourceSha256": "6eef5039afc75bb851eaf9cd82a6f099e4bd398ca2deb59dd66f5b2a81a7a706",
|
||||
"semanticNormalizedInputContract": "validated-forwarded-input-commit-v1",
|
||||
"semanticSidecarSourceSha256": "7d6e948b7cff4df52a9afc916db46ace0f42b4b2d3134ba0c21ee0a928857e62",
|
||||
"semanticDirectDriverSourceSha256": "d683429427dd8a1a5efe50952018ea486f808bdbf0a92fc5946ee267fe2c72ee",
|
||||
"semanticValidationSourceSha256": "02fbb92681c78c5de5d2f1aae25aabc25a0e5df6b0ff4baccac652192a8da975",
|
||||
"semanticNormalizationSourceSha256": "8b628b82e8eacc91cdcc6fdca2210ba4cf282f01e9c6f74e95ff271655b5047e",
|
||||
"semanticCompletionContractSourceSha256": "f49398b682a5623812f5d3c33d6bb9a749008e47cc0ed969072041615f66df59",
|
||||
"semanticValidatorsSourceSha256": "b20ec8ee6e7165117233ce4421a975bb9d3f276220ce31655c0b9a717cb2d9cb",
|
||||
"semanticSchemaBundleSourceSha256": "81ef98ca7b34896a0f76390ac7af3dd8ef7609fe6f38d54bd6a69373d50fe40b",
|
||||
"semanticSidecarProtocolSourceSha256": "26dd0cfbef8b2b401de6b7862a4af7f243fb703e23678a1a5ed8fa68d4784d42",
|
||||
"semanticSidecarReceiptCommitContract": "correlated-tool-resolve-v1",
|
||||
"displayEventSourceSha256": "d72f9a6035c247bc6a42c067c05753076c54322c4b58984e93922f3325dc6877",
|
||||
"displayProjectionSourceSha256": "75952606b26dd93bbc582c1f0a4424487377272ea745fb3a27ceae031fd5dc7e",
|
||||
"configuredEnvironmentSourceSha256": "ecef0ada320a762e0a3d91c38c72d4e19e2c8e8be8c060938a47e7f1a029058a",
|
||||
"configuredEnvironmentRustSourceSha256": "054ce778765690c35a4bbb4e73d923d7dd181226865b7a2e76e7952b4a0f44a3",
|
||||
"profileActivitySourceSha256": "4fdb6b0f9125fd8372ba6f27945fa5c8a3d3b2ec891df544092e3dc7a75d5282",
|
||||
"profileExtensionsSourceSha256": "a80ddc95eb4505095bfdbbf85c758199949ab20be9c84b34c77f762eb7180571",
|
||||
"profileInstallationSourceSha256": "3e61c4cdd4b78e38fcb16eaadae4f1fd65ce3c7127184ea7a80948c8e5d77f88",
|
||||
"attachmentPolicySourceSha256": "4f7638b7cc64e18f6a4ba33fd46de2b3d49225e32aeab41f5c695b268fa4f8e2",
|
||||
"singleReadEvidenceSourceSha256": "4069393405089bde613cecec04f5123c426594811da4c668bd96f0e7e283550f",
|
||||
"nativeSessionRuntimeSourceSha256": "1fe6fa9e6b96d84d70432b85a57c473f833606d4a521563d5022b4d3c41576ce",
|
||||
"nativeRuntimeContextSourceSha256": "71607f80dbb8dc5cc6c123d0b8f3995d6c5407277695a8c82cbedc90fb8f4842",
|
||||
"nativeRuntimePromptsSourceSha256": "fff5755dba9b7dce8a5f10ff6ac6d5ed06b92ed079ce1e0a44ac72f2ce7869c1",
|
||||
"nativeSessionContractSourceSha256": "22a8c96ce41ba4f03e976fc37f0cc7c5d05cee99fa789941da31ee1dab440b90",
|
||||
"harnessSessionContractSourceSha256": "baba1a210b863518f01e0c875606ed37f9467c433222251438578bd999bc43b5",
|
||||
"harnessBackendSourceSha256": "dc827e281ae6731545b166ee4d709a4f67f594a556dcbe63c70fe8f1aca26249",
|
||||
"harnessSessionSourceSha256": "81361733e86b56dd5a66415aefc6f72e69be24acc8d3417370154ebb7fef6e94",
|
||||
"transportContractSourceSha256": "03fca93d36a89b0cc0917335e293ad097fdf04356d21b307827bb0ea4c13f523",
|
||||
"nativeBackendSourceSha256": "7495d9898d22dca1ebf6336361d332bafefc4498f6e49f1135025fad846c8586",
|
||||
"nativeBackendFactorySourceSha256": "7e431c926b76d53c4c08e3a4105353dcd34bd940f6588a1a68f79631f6feeb44",
|
||||
"nativeInstructionsSourceSha256": "82ac6e5f7275b4ef79fb166c7330efc504e80cab3d7e9081e4e40a8b570bc50b",
|
||||
"providerLaunchSourceSha256": "c744235b3067ea75973523f78f1e13399337bc155f1775af694e34782d5c9bef",
|
||||
"launchPreparationStoreSourceSha256": "ff6025568bbc0aa6ce545772d73dfca2d3da226740bd3a7b386b069e76cff296",
|
||||
"runtimeRequestBridgeSourceSha256": "2d167741defe23d6b2b0647a54405331ae4457d298936f0796733628ad79d7bc",
|
||||
"runtimeRequestFacadeSourceSha256": "d0ed28e0c5ddad34db8f437e704c8a755eb0b47f27e08e44185f66d1d2205295",
|
||||
"runtimeRequestNotificationSourceSha256": "6196c6175f11c69295dc00fc773201f4fdeca26c500653870253c43b81e65211",
|
||||
"guardedInstallationSourceSha256": "56a177d3e702914818dca4ca50e061f0d25f988bfc4c0582c36ce23c1696d82f",
|
||||
"nativeExecutionSourceSha256": "d60b8ea63bad975c0144f0702b2749d7db9a7da50680a6147f20d4d11f2d5297",
|
||||
"profileCompatibilitySourceSha256": "a9fca95a16c45b92fbe7d7f19d9a5b97602530580901afdb46ec3999729692b0",
|
||||
"installationSourceSha256": "a1c7c6f103b898216973e77ffbac0ef7572e250aa574d1a4065270e9dfa0dc2d",
|
||||
"assetOwnershipSourceSha256": "1785e3f31e5dad5a13b53e25d8cf9482a9bd89a27b6c127830aed6b81acc605c",
|
||||
"assetInstallerSourceSha256": "d3bd9514e5920d0cc3567aa50487fb534c57a6f66b8da0d3f22c5f6e499c841f",
|
||||
"credentialBindingSourceSha256": "fbc9fc037dab8134d090ac7c3eb23f80026cd84085d56a9704fcf9ceb0478926",
|
||||
"admissionDiagnosticsSourceSha256": "df8f1aa4f2d1bc63b2635bdd09cb8165c8b7681ddcc2618435c7c08ca90475af",
|
||||
"grokAdmissionSourceSha256": "f1e46cf2e1e36b7704bd797b678e3db8db2003f97bdb9d827cfa3d7662698777",
|
||||
"sidecarErrorSourceSha256": "17948ad74350dc487ab20d24a0fb89daf0b426b98396454fff0e3856e922d82c",
|
||||
"profileSourceSha256": "f5538b3dbefa7634a332b87d5ededcefb56aa7e5790cede1c7a0d948a548a456",
|
||||
"metadataProbeSourceSha256": "be4c8416edd27061109bc1f33d0c26071b4facbb3f651beaae6b3c0275cce727",
|
||||
"permissionToolIdentitySourceSha256": "770b47305e571a81f344883057d0e4a482a1cd59ac77d045836593a8e739deb1",
|
||||
"semanticContinuationSourceSha256": "c9cc0fd516b3df92a6655f56ee9bef37615a889c371e2e137d8ac5c1176de811",
|
||||
"preparedHarnessHomeSourceSha256": "bb286f7ddc6e9597c56d46bf7ff6e3b556c5b680d7e147fd46518724432b083e",
|
||||
"runtimeHostSourceSha256": "13f9ac382a279a67adb8974c36bbed38fe43d9b93b87bd7a05e879ead7f03d63",
|
||||
"runtimeAdapterSourceSha256": "7c47c05306ebe7d1ca6b5d046a96f5726a48adec4d27e7c16788f505f25b5db2",
|
||||
"runtimeSandboxSourceSha256": "04a49a24e3828ce8fca908de15e31ae932b8b1b4f7ecec0308627f6a8da9ba20",
|
||||
"cancellationSourceSha256": "a07d1a9e982dec0ae130c1ad1efa5bc75c6a39e013b730ba3ee45256f09ceeca",
|
||||
"agentFilesSourceSha256": "fb32f6badc13dfa22f231479509b631b2f0eba0c99b1ab9f3cd5c0c601aa03ba",
|
||||
"nativeDistributionSourceSha256": "aed327c389e957483a503cb1bb8b247ee7cd7ff4b528fb4567c39bcb4577690b"
|
||||
}
|
||||
}
|
||||
@@ -101,6 +101,8 @@ await writeFile(
|
||||
join(process.env.XDG_DATA_HOME, "fake-environment.json"),
|
||||
JSON.stringify({
|
||||
keys: Object.keys(process.env).sort(),
|
||||
notificationContext: process.env.PAPERCLIP_RUNTIME_NOTIFICATION_CONTEXT,
|
||||
notificationHelper: process.env.PAPERCLIP_RUNTIME_NOTIFICATION_HELPER,
|
||||
noProxy: process.env.NO_PROXY,
|
||||
home: process.env.HOME,
|
||||
configHome: process.env.XDG_CONFIG_HOME,
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"commandDigest": "sha256:3dc83b2c3493913748cacb09025a8c97b74f3b5bbb1dc204072d1a773fa9af3a",
|
||||
"sourceDigest": "sha256:79188d089f3f6b575726af77fe055a58449144a373a0745dcad4be414cba8cd0",
|
||||
"declaration": {
|
||||
"schema": "paperclip.grok-profile-identity.v1",
|
||||
"agent": "grok",
|
||||
"agentProfileVersion": 12,
|
||||
"agentServerPackage": "builtin:grok-acp",
|
||||
"agentServerVersion": "1",
|
||||
"agentRuntimePackage": "native:grok",
|
||||
"agentRuntimeVersion": "1.0.13",
|
||||
"requiresProviderPolicy": false,
|
||||
"acpxVersion": "0.13.1",
|
||||
"sources": {
|
||||
"patches/acpx@0.13.1.patch": "sha256:1e7dae8c83bf8620f53f2bea13c175eeff95e7e955783f6f529bb1dcbdcdb6d2",
|
||||
"packages/paperclip-runner/src/cli/acpx-runtime-sidecar.ts": "sha256:7d6e948b7cff4df52a9afc916db46ace0f42b4b2d3134ba0c21ee0a928857e62",
|
||||
"packages/paperclip-runner/src/cli/acpx-sidecar-input.ts": "sha256:17948ad74350dc487ab20d24a0fb89daf0b426b98396454fff0e3856e922d82c",
|
||||
"packages/paperclip-runner/src/drivers/acpx/admission-diagnostics.ts": "sha256:df8f1aa4f2d1bc63b2635bdd09cb8165c8b7681ddcc2618435c7c08ca90475af",
|
||||
"packages/paperclip-runner/src/drivers/acpx/grok-effort.ts": "sha256:f1e46cf2e1e36b7704bd797b678e3db8db2003f97bdb9d827cfa3d7662698777",
|
||||
"packages/paperclip-runner/src/drivers/acpx/grok-web-search.ts": "sha256:754b5ca2345c045421f6007f34999508d996e1cea035f0f1952b3d5f00072b9c",
|
||||
"packages/paperclip-runner/src/drivers/acpx/grok-credentials.ts": "sha256:1c4ff0be6da798b6fcc604d20a9198dcc8523a3806d563b5b5345b7260549f73",
|
||||
"packages/paperclip-runner/src/drivers/acpx/codex-runtime-adapter.ts": "sha256:7c47c05306ebe7d1ca6b5d046a96f5726a48adec4d27e7c16788f505f25b5db2",
|
||||
"packages/paperclip-runner/src/drivers/acpx/runtime-host.ts": "sha256:13f9ac382a279a67adb8974c36bbed38fe43d9b93b87bd7a05e879ead7f03d63",
|
||||
"packages/paperclip-runner/src/drivers/preconfigured-home.ts": "sha256:bb286f7ddc6e9597c56d46bf7ff6e3b556c5b680d7e147fd46518724432b083e",
|
||||
"packages/paperclip-runner/src/drivers/acpx/runtime-sandbox.ts": "sha256:04a49a24e3828ce8fca908de15e31ae932b8b1b4f7ecec0308627f6a8da9ba20",
|
||||
"packages/paperclip-runner/src/drivers/acpx/sidecar-protocol.ts": "sha256:26dd0cfbef8b2b401de6b7862a4af7f243fb703e23678a1a5ed8fa68d4784d42",
|
||||
"packages/paperclip-runner/src/drivers/acpx/native-distribution-integrity.ts": "sha256:aed327c389e957483a503cb1bb8b247ee7cd7ff4b528fb4567c39bcb4577690b",
|
||||
"packages/paperclip-runner/src/providers/grok/launcher.cjs": "sha256:f0b698395a3704ed2ffaf84ea19bdb20c36c8a0a70b7c629c7b6ffe144e59e55",
|
||||
"packages/paperclip-runner/src/providers/grok/platforms.json": "sha256:931eed3abbf2bf041e6c72e6e9486d976f4352613cf069b216ddf010c9a8e704",
|
||||
"packages/paperclip-runner/runner/crates/runner-core/src/acpx_provider_backend.rs": "sha256:cdba0cbca6ea0e28928cae4817055dc38c527308570c6a03f0736fef0210fe9e",
|
||||
"packages/paperclip-runner/runner/crates/runner-core/src/acpx_provider_session.rs": "sha256:e97a7c4d2cf9be9c6e0ada78076a5551b01401518597ab81cb6fbd5a1faf4a5e",
|
||||
"packages/paperclip-runner/runner/crates/runner-core/src/acpx_sidecar_transport.rs": "sha256:f711428ec3bb9bcf723a5fd1382a3d266ed14e7ed5340b8a32eafe7357940144"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -271,6 +271,44 @@ views, access failures, inactive runs and missing plugins contribute no task tab
|
||||
Closing a tab preserves dismissal during polling; available panels remain in the
|
||||
launcher. Only tab identity is persisted, never availability responses or secrets.
|
||||
|
||||
#### Runtime activity recording
|
||||
|
||||
A runtime client captures Runner's opaque activity context before an eligible
|
||||
operation, then sends a structured notification after successful use:
|
||||
|
||||
```sh
|
||||
context=$("$PAPERCLIP_RUNTIME_NOTIFICATION_HELPER" notification-context)
|
||||
# Perform the operation; notify only on success.
|
||||
"$PAPERCLIP_RUNTIME_NOTIFICATION_HELPER" record-activity "$context" example.runtime desktop.used
|
||||
```
|
||||
|
||||
Runner supplies the matching absolute executable as
|
||||
`PAPERCLIP_RUNTIME_NOTIFICATION_HELPER` and an atomic endpoint descriptor as
|
||||
`PAPERCLIP_RUNTIME_NOTIFICATION_CONTEXT`. Configured task environment cannot
|
||||
override them. Provider launch boundaries preserve both values. Codex receives
|
||||
read access to the helper, descriptor and publisher lock, plus write access to
|
||||
one named pipe. The pipe and lock live in a private temporary directory, outside
|
||||
durable Runner state. The enclosing directories remain protected.
|
||||
|
||||
The helper captures context locally. After success, it publishes newline-delimited
|
||||
JSON through a nonblocking pipe. A leading newline terminates an interrupted
|
||||
previous write. A read-only file lock serializes publishers with a 500 ms deadline.
|
||||
Runner reuses its bounded line reader: 2 KiB per record, 128 queued records and
|
||||
16 records per control-loop iteration. Idle transport blocks; cleanup removes
|
||||
only its pipe, lock and empty private directory.
|
||||
|
||||
A successful helper exit means publication to the pipe. Runner validates context
|
||||
against the active run, deduplicates up to 128 activity pairs per authority, and
|
||||
persists `runtime.activity.recorded` through its authenticated event stream.
|
||||
Warm attachment and process recovery rotate context. The helper accepts no run
|
||||
identity. Clients treat this hint as best effort, including with older runtimes.
|
||||
Notify only after successful use; exclude help, schema discovery, status probes
|
||||
and failures. No agent instruction is required.
|
||||
|
||||
Identifiers are 1–160 Unicode code points, excluding C0 controls and DEL.
|
||||
Core maps declared activity identifiers to its consumers and applies their
|
||||
existing discovery, authorization and availability checks.
|
||||
|
||||
#### `taskDetailView`
|
||||
|
||||
A specialized slot rendered in the context of a task or issue detail view. Similar to `detailTab` but designed for inline content within the task detail layout rather than a separate tab. Receives `context.companyId`, `context.entityId`, and `context.entityType` like `detailTab`. Requires the `ui.detailTab.register` capability.
|
||||
|
||||
@@ -52,8 +52,8 @@ assert.equal(JSON.parse(await readFile(join(server, 'package.json'), 'utf8')).na
|
||||
const { resolvePaperclipRunnerBinary } = await import(pathToFileURL(join(server, 'dist/services/native-runtime/native-codex-runner.js')));
|
||||
const { createCapabilityRunnerdCodexTransport } = await import(pathToFileURL(join(server, 'dist/vendor/paperclip-runner/live/runnerd-codex-transport.js')));
|
||||
const { QUALIFIED_ACPX_PROFILES } = await import(pathToFileURL(join(server, 'dist/vendor/paperclip-runner/drivers/acpx/qualified-profiles.js')));
|
||||
assert.equal(QUALIFIED_ACPX_PROFILES.pi.agentProfileVersion, 33);
|
||||
assert.equal(QUALIFIED_ACPX_PROFILES.pi.commandDigest, 'sha256:0260d5810e9247986800ad46c1f9b3511a5abd06cb8f142405d5caacbc3960fc');
|
||||
assert.equal(QUALIFIED_ACPX_PROFILES.pi.agentProfileVersion, 34);
|
||||
assert.equal(QUALIFIED_ACPX_PROFILES.pi.commandDigest, 'sha256:ed4b32cc8fbf36309d38b3bd378f41f8e7b9e96a2405d275cc55e89042d94c7f');
|
||||
assert.equal(Object.hasOwn(QUALIFIED_ACPX_PROFILES.pi, 'qualificationModel'), false);
|
||||
assert.equal(Object.hasOwn(QUALIFIED_ACPX_PROFILES.pi, 'reportedModelId'), false);
|
||||
const daemon = resolvePaperclipRunnerBinary();
|
||||
|
||||
Reference in new issue
Block a user