Skip to content

Commit 36b1dcd

Browse files
committed
feat: Custom Software/Agents docs + native options schema (zod) + sidecar logfmt observability
Combines: - docs: Custom Software section + Custom Agents guide - feat: native AgentOs options schema (zod/v4) + defineSoftware re-export - feat(sidecar): configurable logfmt logging + ext stall-watchdog tracing
2 parents f607add + 90c7b50 commit 36b1dcd

14 files changed

Lines changed: 440 additions & 105 deletions

File tree

‎CLAUDE.md‎

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,40 @@ Agent OS is the agent-facing wrapper around secure-exec. It provides ACP session
55
## Boundaries
66

77
- secure-exec dependency workflow. Manage the secure-exec dependency ONLY through `scripts/secure-exec-dep.mjs` (the `just secure-exec-*` recipes); never hand-edit the `path` / `version` / `catalog:` pins.
8-
- Testing against local secure-exec changes: run `just secure-exec-local` to repoint npm (`link:`) and crates (`path = "../secure-exec/..."`) at the sibling checkout, then `node scripts/secure-exec-dep.mjs set-crate-version <sibling-version>` so the Cargo version requirement matches the sibling crate version (otherwise cargo cannot resolve the path deps). Use `just secure-exec-status` to inspect. This mode is for local builds/tests ONLY.
8+
- Testing against local secure-exec changes: run `just secure-exec-local` to repoint npm (`link:`) and crates (`path = "../secure-exec/..."`) at the sibling checkout, then `node scripts/secure-exec-dep.mjs set-crate-version <sibling-version>` so the Cargo version requirement matches the sibling crate version (otherwise cargo cannot resolve the path deps). Also run `pnpm install` in `../secure-exec` first, or cargo panics in `v8-runtime/build.rs` with "missing Node dependencies at .../packages/build-tools/node_modules" (the V8 bridge assets are built from there). Use `just secure-exec-status` to inspect. This mode is for local builds/tests ONLY.
99
- Pushing changes that depend on secure-exec changes: NEVER push with local (`path:` / `link:`) dependencies. First preview-publish the secure-exec changes to their own secure-exec branch (the `preview-publish-secure-exec` flow), then point agent-os back at that exact published version with `just secure-exec-pinned` + `just secure-exec-set-version <version>` (and `set-crate-version <version>` for the crates). Only commit/push the pinned-to-remote state.
1010
- Keep generic runtime, kernel, VFS, language execution, and registry software behavior in secure-exec.
1111
- Agent OS owns ACP, sessions, agent adapters, toolkit semantics, quickstarts, and the AgentOs facade.
1212
- Call OS instances VMs, never sandboxes.
1313
- The protocol has no backwards compatibility. Clients and the sidecar ship in same-version lockstep, so never add protocol or config versioning, runtime negotiation, fallbacks, or converters. Configs such as `CreateVmConfig` carry no `version` field; the single same-version wire handshake is the only version check. Change the protocol freely and update both sides together.
1414

15+
## Development
16+
17+
### secure-exec dependency versions (`just`)
18+
19+
Two independent version tracks:
20+
- **secure-exec** — the `@secure-exec/*` npm packages and the `secure-exec-*` Cargo crates always share **one** version (npm and crates are kept in sync; pin both to the same `<v>`).
21+
- **`@agentos-software/*`** software packages (registry agents / WASM commands) are on a **separate** track and version independently of secure-exec.
22+
23+
Manage them ONLY via these recipes (never hand-edit `path`/`version`/`catalog:` pins):
24+
- `just secure-exec-local` — point deps at the sibling `../secure-exec` checkout for local hacking.
25+
- `just secure-exec-set-version <v>` — pin secure-exec to a published version: sets the `@secure-exec/*` npm packages **and** the `secure-exec-*` crates (same `<v>`, they're in sync) and switches to pinned mode.
26+
- `just agentos-pkgs-set-version <v>` — pin the `@agentos-software/*` software packages (separate version track).
27+
28+
### Depending on unreleased secure-exec changes
29+
30+
agent-os builds against secure-exec crates + npm packages, so a secure-exec change must reach agent-os before it can be pushed. NEVER push with local (`path:`/`link:`) deps. Flow: preview-publish the secure-exec branch (the `preview-publish-secure-exec` skill), then `just secure-exec-set-version <published-version>` (pins npm + crates + switches to pinned mode), and push only that pinned state. Caveat: a preview publishes npm but the crates.io job is dry-run/skipped — a secure-exec *crate* change only flows locally (`secure-exec-local`) or via a real crates.io release.
31+
32+
### Preview-publishing agent-os
33+
34+
`just preview-publish <branch>` dispatches `.github/workflows/publish.yaml` to cut a **preview** (debug build, npm-only, dist-tag = sanitized branch name) — for handing a build to an external project. **Preview-publish is for previews ONLY; never cut a release with it.** Releases go through `just release` (the `scripts/publish` flow).
35+
36+
### Testing a local build from an external project (same machine)
37+
38+
To consume an unpublished agent-os build in another project on this machine:
39+
- **npm:** `pnpm -r build`, then either `pnpm pack` the package(s) and `npm install ./rivet-dev-agentos-*.tgz` in the external project, or add a `link:`/`file:` override (e.g. `"@rivet-dev/agentos": "link:/abs/path/agent-os/packages/agentos"`). The sidecar binary ships as `@rivet-dev/agentos-sidecar`.
40+
- **cargo:** point the external Cargo project at the local crate via a path dep or `[patch.crates-io]` override (e.g. `[patch.crates-io] agentos-sidecar = { path = "/abs/path/agent-os/crates/agentos-sidecar" }`).
41+
1542
## Security Model
1643

1744
Trust model (decide which side of the boundary something is on before judging whether it is a security bug). Three components:
@@ -41,6 +68,7 @@ Trust model (decide which side of the boundary something is on before judging wh
4168

4269
## Website And Docs
4370

71+
- External/consumer usage (installing `@rivet-dev/agentos` and using it in your own project) is documented in the website quickstart + Agents/Custom Software pages under `website/`, not in this file. This `CLAUDE.md` is contributor/maintainer-only.
4472
- The Agent OS website and docs live in `website/` (Astro + Starlight) and deploy to `agentos-sdk.dev` (docs at `agentos-sdk.dev/docs`). The marketing pages and docs were migrated out of `rivet.dev/agent-os` and `rivet.dev/docs/agent-os`, which now 301-redirect to this domain.
4573
- Docs styling is owned by the shared **`@rivet-dev/docs-theme`** repo (`github.com/rivet-dev/docs-theme`), consumed via `github:rivet-dev/docs-theme#<tag>` and wired in via `...docsTheme(starlight, siteConfig)`. To change any docs styling (palette, header, sidebar, code blocks, fonts), edit that repo and follow its CLAUDE.md release workflow — never restyle docs in `website/src`. This site owns only content + `website/docs.config.mjs` (sidebar icons via each item's `attrs['data-icon']`).
4674
- Architecture reference docs live in `website/src/content/docs/docs/architecture/` and are surfaced in `website/docs.config.mjs` under Reference → Advanced → Architecture. Treat these pages as the canonical human-facing architecture reference. When architecture behavior changes or new architecture is added, recommend the corresponding docs update to the user; do not proactively edit the docs unless the user asks for docs work or the task explicitly includes it.

‎Cargo.lock‎

Lines changed: 47 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎crates/agentos-sidecar/Cargo.toml‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,11 @@ agentos-protocol = { workspace = true }
1818
serde_json = "1.0"
1919
serde_bare = "0.5"
2020
secure-exec-sidecar = { workspace = true }
21-
tokio = { version = "1", features = ["sync", "time"] }
21+
tokio = { version = "1", features = ["sync", "time", "macros"] }
2222
tracing = "0.1"
23-
tracing-subscriber = { version = "0.3", features = ["fmt"] }
23+
tracing-subscriber = { version = "0.3", features = ["fmt", "env-filter"] }
24+
tracing-logfmt = { version = "0.3", features = ["ansi_logs"] }
25+
tracing-appender = "0.2"
2426

2527
[dev-dependencies]
2628
agentos-bridge = { workspace = true }

‎crates/agentos-sidecar/src/acp_extension.rs‎

Lines changed: 60 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -109,22 +109,74 @@ impl AcpExtension {
109109
ctx: ExtensionContext<'_>,
110110
payload: &[u8],
111111
) -> Result<ExtensionResponse, SidecarError> {
112+
use tracing::Instrument as _;
112113
let request = decode_request(payload)?;
113-
let response = match request {
114-
AcpRequest::AcpCreateSessionRequest(request) => self.create_session(ctx, request).await,
115-
AcpRequest::AcpGetSessionStateRequest(request) => {
116-
AcpHandlerOutput::response(self.get_session_state(ctx, request).await)
114+
let kind = Self::acp_request_kind(&request);
115+
let start = std::time::Instant::now();
116+
tracing::info!(target: "agentos_sidecar::acp_extension", kind, "ext request received");
117+
118+
let work = async move {
119+
match request {
120+
AcpRequest::AcpCreateSessionRequest(request) => {
121+
self.create_session(ctx, request).await
122+
}
123+
AcpRequest::AcpGetSessionStateRequest(request) => {
124+
AcpHandlerOutput::response(self.get_session_state(ctx, request).await)
125+
}
126+
AcpRequest::AcpCloseSessionRequest(request) => {
127+
AcpHandlerOutput::response(self.close_session(ctx, request).await)
128+
}
129+
AcpRequest::AcpSessionRequest(request) => self.session_request(ctx, request).await,
130+
AcpRequest::AcpResumeSessionRequest(request) => {
131+
self.resume_session(ctx, request).await
132+
}
117133
}
118-
AcpRequest::AcpCloseSessionRequest(request) => {
119-
AcpHandlerOutput::response(self.close_session(ctx, request).await)
134+
}
135+
.instrument(tracing::info_span!(
136+
target: "agentos_sidecar::acp_extension",
137+
"ext.request",
138+
kind
139+
));
140+
141+
// Stall watchdog: while the request is in flight, warn periodically so a
142+
// hang surfaces as a breadcrumb long before the host's 120s frame
143+
// timeout. This never interrupts the work itself.
144+
tokio::pin!(work);
145+
let response = loop {
146+
tokio::select! {
147+
result = &mut work => break result,
148+
_ = tokio::time::sleep(std::time::Duration::from_secs(10)) => {
149+
tracing::warn!(
150+
target: "agentos_sidecar::acp_extension",
151+
kind,
152+
elapsed_ms = start.elapsed().as_millis() as u64,
153+
"ext request still pending — possible stall before response frame",
154+
);
155+
}
120156
}
121-
AcpRequest::AcpSessionRequest(request) => self.session_request(ctx, request).await,
122-
AcpRequest::AcpResumeSessionRequest(request) => self.resume_session(ctx, request).await,
123157
};
158+
159+
tracing::info!(
160+
target: "agentos_sidecar::acp_extension",
161+
kind,
162+
elapsed_ms = start.elapsed().as_millis() as u64,
163+
"ext request handled",
164+
);
124165
let payload = encode_response(response.response.unwrap_or_else(error_response))?;
125166
ExtensionResponse::with_wire_events(payload, response.events)
126167
}
127168

169+
/// Stable label for an ACP request kind, used as a tracing field.
170+
fn acp_request_kind(request: &AcpRequest) -> &'static str {
171+
match request {
172+
AcpRequest::AcpCreateSessionRequest(_) => "create_session",
173+
AcpRequest::AcpGetSessionStateRequest(_) => "get_session_state",
174+
AcpRequest::AcpCloseSessionRequest(_) => "close_session",
175+
AcpRequest::AcpSessionRequest(_) => "session_request",
176+
AcpRequest::AcpResumeSessionRequest(_) => "resume_session",
177+
}
178+
}
179+
128180
async fn create_session(
129181
&self,
130182
mut ctx: ExtensionContext<'_>,

‎crates/agentos-sidecar/src/main.rs‎

Lines changed: 82 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,90 @@
1+
use tracing_subscriber::fmt::writer::BoxMakeWriter;
2+
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
3+
14
fn main() {
2-
// The sidecar speaks its binary protocol over stdout (see
3-
// `secure_exec_sidecar::stdio`), so tracing output MUST go to stderr —
4-
// otherwise log lines are interleaved into the frame stream and the client
5-
// misreads them as (garbage-length) frames. See crates/CLAUDE.md:
6-
// "Control channels must be out-of-band."
7-
tracing_subscriber::fmt()
8-
.with_writer(std::io::stderr)
9-
.with_max_level(tracing::Level::ERROR)
10-
.init();
5+
init_tracing();
116
if let Err(error) =
127
secure_exec_sidecar::stdio::run_with_extensions(agentos_sidecar_wrapper::extensions())
138
{
149
tracing::error!(?error, "agentos-sidecar startup failed");
1510
std::process::exit(1);
1611
}
1712
}
13+
14+
/// `1` => true, anything else => false. Mirrors rivet's `env_flag`.
15+
fn env_flag(name: &str) -> bool {
16+
std::env::var(name).map_or(false, |v| v == "1")
17+
}
18+
19+
/// Initialize tracing for the sidecar.
20+
///
21+
/// Mirrors the rivet logging setup (`rivetkit-napi::init_tracing`): an
22+
/// `EnvFilter`-gated subscriber with a logfmt formatter, so log level and
23+
/// verbosity are runtime-configurable instead of hardcoded.
24+
///
25+
/// Configuration (all optional):
26+
/// - level: `AGENTOS_LOG_LEVEL` > `LOG_LEVEL` > `RUST_LOG` > `"info"`.
27+
/// - format: `RUST_LOG_FORMAT=logfmt` (default) or `text`.
28+
/// - sink: `AGENTOS_LOG_FILE` (append to that file) else stderr.
29+
/// - field toggles: `RUST_LOG_{SPAN_NAME,SPAN_PATH,TARGET,LOCATION,MODULE_PATH,ANSI_COLOR}`.
30+
///
31+
/// The sink MUST be stderr or a file — NEVER stdout, which carries the
32+
/// sidecar's binary frame protocol (see crates/CLAUDE.md: "Control channels
33+
/// must be out-of-band").
34+
fn init_tracing() {
35+
// Level priority: AGENTOS_LOG_LEVEL > LOG_LEVEL > RUST_LOG > "info".
36+
let directive = std::env::var("AGENTOS_LOG_LEVEL")
37+
.ok()
38+
.or_else(|| std::env::var("LOG_LEVEL").ok())
39+
.or_else(|| std::env::var("RUST_LOG").ok())
40+
.unwrap_or_else(|| "info".to_string());
41+
let env_filter = EnvFilter::try_new(&directive).unwrap_or_else(|_| EnvFilter::new("info"));
42+
43+
// Sink: a file if AGENTOS_LOG_FILE is set, else stderr. Never stdout.
44+
let writer: BoxMakeWriter = match std::env::var("AGENTOS_LOG_FILE") {
45+
Ok(path) if !path.is_empty() => {
46+
let path = std::path::PathBuf::from(path);
47+
let dir = path
48+
.parent()
49+
.filter(|p| !p.as_os_str().is_empty())
50+
.map(|p| p.to_path_buf())
51+
.unwrap_or_else(|| std::path::PathBuf::from("."));
52+
let name = path
53+
.file_name()
54+
.map(std::ffi::OsString::from)
55+
.unwrap_or_else(|| std::ffi::OsString::from("agentos-sidecar.log"));
56+
BoxMakeWriter::new(tracing_appender::rolling::never(dir, name))
57+
}
58+
_ => BoxMakeWriter::new(std::io::stderr),
59+
};
60+
61+
let registry = tracing_subscriber::registry().with(env_filter);
62+
63+
let text_format = std::env::var("RUST_LOG_FORMAT")
64+
.map(|v| v.eq_ignore_ascii_case("text"))
65+
.unwrap_or(false);
66+
67+
if text_format {
68+
registry
69+
.with(
70+
tracing_subscriber::fmt::layer()
71+
.with_ansi(false)
72+
.with_writer(writer),
73+
)
74+
.init();
75+
} else {
76+
registry
77+
.with(
78+
tracing_logfmt::builder()
79+
.with_span_name(env_flag("RUST_LOG_SPAN_NAME"))
80+
.with_span_path(env_flag("RUST_LOG_SPAN_PATH"))
81+
.with_target(env_flag("RUST_LOG_TARGET"))
82+
.with_location(env_flag("RUST_LOG_LOCATION"))
83+
.with_module_path(env_flag("RUST_LOG_MODULE_PATH"))
84+
.with_ansi_color(env_flag("RUST_LOG_ANSI_COLOR"))
85+
.layer()
86+
.with_writer(writer),
87+
)
88+
.init();
89+
}
90+
}

‎justfile‎

Lines changed: 7 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,34 +6,22 @@ release *args:
66
preview-publish REF:
77
gh workflow run .github/workflows/publish.yaml --ref "{{ REF }}"
88

9-
# Point the workspace at PUBLISHED secure-exec versions (CI/release default).
10-
secure-exec-pinned:
11-
node scripts/secure-exec-dep.mjs pinned
12-
13-
# Point the workspace at the sibling ../secure-exec checkout for local hacking.
9+
# Point deps at the sibling ../secure-exec checkout for local hacking.
1410
secure-exec-local:
1511
node scripts/secure-exec-dep.mjs local
1612

17-
# Bump the pinned @secure-exec/* npm version (core/s3/google-drive/sandbox).
13+
# Pin secure-exec to a published version. The @secure-exec/* npm packages and the
14+
# secure-exec-* crates are always the same version, so this sets both and switches
15+
# to pinned mode.
1816
secure-exec-set-version VERSION:
17+
node scripts/secure-exec-dep.mjs pinned
1918
node scripts/secure-exec-dep.mjs set-secure-exec-version "{{ VERSION }}"
19+
node scripts/secure-exec-dep.mjs set-crate-version "{{ VERSION }}"
2020

21-
# Bump the pinned @agentos-software/* software-package npm version.
21+
# Pin the @agentos-software/* software packages (separate version track).
2222
agentos-pkgs-set-version VERSION:
2323
node scripts/secure-exec-dep.mjs set-agentos-pkgs-version "{{ VERSION }}"
2424

25-
# Bump BOTH scopes at once (only when secure-exec + software publish in lockstep).
26-
secure-exec-set-all-versions VERSION:
27-
node scripts/secure-exec-dep.mjs set-version "{{ VERSION }}"
28-
29-
# Bump the @secure-exec/* crate version requirement (must match the sibling crate version).
30-
secure-exec-set-crate-version VERSION:
31-
node scripts/secure-exec-dep.mjs set-crate-version "{{ VERSION }}"
32-
33-
# Show the current secure-exec dependency mode + pinned versions.
34-
secure-exec-status:
35-
node scripts/secure-exec-dep.mjs status
36-
3725
dev-shell *args:
3826
pnpm --filter @rivet-dev/agentos-dev-shell dev-shell -- "$@"
3927

‎packages/core/package.json‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,8 @@
6666
"isolated-vm": "^6.0.0",
6767
"long-timeout": "^0.1.1",
6868
"minimatch": "^10.2.4",
69-
"zod": "^4.1.11"
69+
"zod": "^4.1.11",
70+
"zod-to-json-schema": "^3.25.2"
7071
},
7172
"devDependencies": {
7273
"@anthropic-ai/claude-agent-sdk": "^0.2.87",
@@ -107,6 +108,7 @@
107108
"sandbox-agent": "^0.4.2",
108109
"typescript": "^5.7.2",
109110
"vitest": "^2.1.8",
110-
"ws": "^8.18.0"
111+
"ws": "^8.18.0",
112+
"zod3": "npm:zod@^3.25.76"
111113
}
112114
}

0 commit comments

Comments
 (0)