Skip to content

v0.12.0 - #19

Open
dzikowski wants to merge 18 commits into
mainfrom
nightly
Open

v0.12.0#19
dzikowski wants to merge 18 commits into
mainfrom
nightly

Conversation

@dzikowski

Copy link
Copy Markdown
Contributor

No description provided.

dzikowski and others added 18 commits July 24, 2026 08:11
Add a new `jaiph serve` command that exposes workflows over HTTP instead
of only stdio JSON-RPC, so jaiph can run as a deployed, network-callable
service. Built hand-rolled on node:http with no new runtime dependencies.

The server mirrors `jaiph mcp` startup (graph load, diagnostics, --env,
Docker image prep, credential preflight) and serves run-resource endpoints
under /v1, a browser-usable /docs Swagger UI (CDN assets pinned with SRI),
and a per-request OpenAPI 3.1 document generated by a pure buildOpenApi
from the same deriveTools exposure rules as MCP. Workflow execution reuses
the MCP call layer, now moved to src/cli/exec/call.ts (McpCallResult
renamed to WorkflowCallResult, caller-supplied runId), and the generation
hot-reload machinery is extracted to src/cli/shared/generation.ts shared
by both commands. Adds bearer auth (JAIPH_SERVE_TOKEN, required for /v1
and for non-loopback binds), a concurrency cap (JAIPH_SERVE_MAX_CONCURRENT),
body/content-type limits, graceful drain-then-cancel shutdown, docs, and
unit/integration/e2e coverage.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add run-inspection endpoints to `jaiph serve` so clients can watch a run
in flight and retrieve what it produced, closing the API half of the
"inspect what it is doing" feedback.

GET /v1/runs/{id}/events serves the run's hash-chained, credential-redacted
run_summary.jsonl verbatim: NDJSON by default, or SSE (Accept:
text/event-stream) that replays existing journal lines, follows the file
via ~250ms polling for appended lines, sends :ka keep-alives every 15s,
and emits `event: end` once the registry marks the run terminal (works for
already-terminal runs too). GET /v1/runs/{id}/artifacts lists files under
the run dir's artifacts/, and GET /v1/runs/{id}/artifacts/{path} downloads
one as application/octet-stream. Downloads are traversal-proof: the request
is resolved and realpath-contained against the artifacts dir, rejecting
`..`, absolute paths, and symlinks escaping it with 404. Raw %06d-*.out/.err
capture files are never exposed by any route. Docker-mode runs read the
host-side run dir via the existing discoverDockerRunDir/remapContainerPath
layer. Docs and integration/e2e coverage (SSE mid-run, redaction, artifact
round-trip, traversal battery) included.
A completed run now exports one OpenTelemetry span tree (workflow → steps
→ prompts) to any OTLP/HTTP collector — a local otel-collector, Grafana
Tempo, Honeycomb, Datadog — enabled by the standard OTEL_EXPORTER_OTLP_*
environment variables. Export happens host-side after the run reaches
terminal state by reading the run's already credential-redacted
run_summary.jsonl, so nothing new crosses the sandbox boundary and no
OTEL_* env is forwarded into the container.

The new src/cli/telemetry/otlp.ts adds zero runtime dependencies (a
node:https/node:http request is the whole transport). The pure
runSummaryToOtlp() maps journal lines to a JSON ExportTraceServiceRequest
with deterministic trace/span ids, and a single shared exportRunTelemetry()
hook fires at every host terminal state (jaiph run completion and the
shared workflow-call layer covering MCP and jaiph serve). Telemetry is
never load-bearing: an unreachable or erroring collector produces exactly
one stderr warning and leaves the run's exit code, output, and journal
untouched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A run that terminates unsuccessfully (nonzero exit or a signal) is now
pushed to a Sentry error tracker as a single error event, enabled iff
SENTRY_DSN is set. Operators running workflows on a schedule or as a
service get alerting and grouping without scraping run directories.

The new dependency-free src/cli/telemetry/sentry.ts hand-rolls a Sentry
envelope POST over node:https, sharing the timeout-guarded POST helper
extracted into src/cli/telemetry/http.ts (otlp.ts now reuses it too).
Reporting fires from the single shared post-run hook that all modes
funnel through, so it is one choke point across jaiph run and the
MCP/HTTP call layer. Event content — message, tags, extra, fingerprint,
release, environment — is sourced only from the already credential-
redacted run_summary.jsonl; re-occurrences group per workflow + failing
step. Like trace export it is host-side, end-of-run, and never load-
bearing: unreachable Sentry, a non-2xx response, or a timeout produces
exactly one stderr warning and leaves the run's exit code and output
untouched. Successful runs send nothing.

SENTRY_DSN/SENTRY_ENVIRONMENT/SENTRY_RELEASE are documented in the
env-vars telemetry section and docs/observability.md. No JAIPH_* vars
added; dependencies remain empty.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The ghcr.io/jaiphlang/jaiph-runtime image already bundles jaiph and the
claude/cursor/codex backends, but it was only usable as a sandbox rootfs
orchestrated by a host jaiph process. Running it directly failed because
jaiph defaults to Docker sandboxing on Linux and there is no daemon
inside the container.

Bake ENV JAIPH_UNSAFE=true into runtime/Dockerfile so the container acts
as its own sandbox in host mode; the inner `jaiph run --raw` invocation
never launches Docker, so host-orchestrated sandbox behavior is
unchanged (pinned by test). When jaiph would launch Docker but the CLI
is missing and a container indicator (/.dockerenv or /run/.containerenv)
is present, the error now tells the user to set JAIPH_UNSAFE=true.

Add docs/deploy.md (one-shot docker run, CI note, Kubernetes) with a
real docs/deploy/k8s.yaml manifest and an explicit no-jaiph-sandbox
security posture, linked from README, sandboxing, and setup docs. Add a
gated standalone-image e2e smoke test and a kubectl dry-run manifest
gate in CI.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Record the deployment review as executable tasks and use Fable for their implementation.

Co-authored-by: Cursor <cursoragent@cursor.com>
composeResult in src/cli/exec/call.ts built a failed call's text from
the run's live captured output, so jaiph serve (result_text) and jaiph
mcp (tool errors) could return a secret verbatim even though the event
journal, OTLP, and Sentry all redacted it. The credential rule is now a
single shared helper, redactCredentials, extracted from
RuntimeEventEmitter into src/runtime/kernel/redact.ts and imported by
both the journal writer and composeResult. composeResult redacts the
fully assembled failure text once, so failed-step detail, raw
stderr/stdout, and collected log messages all pass the same boundary,
and the Docker --env passthrough is merged back in for redaction.
Successful return values stay verbatim as intentional API output.
Covered by src/cli/exec/call.test.ts and integration tests that a
failing workflow echoing a credential never leaks it through ?wait=true,
GET /v1/runs/{id}, or an MCP tool result.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
jaiph mcp handled tool calls concurrently, but a hot reload deleted the
previous generation's out dir (emitted scripts + serialized graph) the
instant it swapped, so a call started just before the reload lost the
scripts its remaining steps spawn from. Signal shutdown also removed the
shared temp root without draining or cancelling active calls.

Extract the LiveGeneration refcount model that jaiph serve already used
into createGenerationTracker in src/cli/shared/generation.ts and share it
between both commands. Each call acquires a lease on the generation live
at call start; a superseded generation's out dir is deleted only when its
last lease releases, so a call spanning one or more reloads always runs
from the generation it captured and no directory is left behind.

Shutdown is now drain-then-cancel: stdin close or the first SIGINT/SIGTERM
stops accepting input and awaits in-flight calls before cleanup, exiting
0. A second signal invokes McpServer.cancelAll(), terminating each run's
child process tree (SIGINT then SIGKILL) and force-removing its Docker
container, so no child process or container is orphaned; killed runs
settle with normal isError responses so the drain still completes.

Adds generation.test.ts, server.test.ts cancelAll coverage, and e2e test
149_mcp_generation_lifecycle.sh; updates CLI and MCP docs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wait visibly between usage checks before selecting queue work, and keep Fable as the implementation model.

Co-authored-by: Cursor <cursoragent@cursor.com>
The serve concurrency cap limited active children but not process
memory, so a long-lived server could be exhausted three ways: the
in-memory run map grew forever, each completed run's result_text stayed
resident, and a single run's raw stdout/stderr/log capture accumulated
unbounded.

Add per-run UTF-8 byte caps (OutputCaps) threaded through the call
layer: attachOutputCollector stops accumulating stdout, stderr, and log
output at the cap and composeResult caps result_text as a backstop, with
overflow dropped and replaced by a deterministic truncation marker
(capBytes slices on a byte boundary to keep valid UTF-8). Defaults stay
effectively unbounded so jaiph mcp and direct callers are unchanged;
only jaiph serve passes finite caps via JAIPH_SERVE_MAX_OUTPUT_BYTES
(default 1 MiB).

Add completed-run retention: evictCompleted runs after every finalize,
dropping terminal records past JAIPH_SERVE_RETAIN_RUNS (default 500,
oldest first) and older than JAIPH_SERVE_RETAIN_AGE_SEC (default 86400,
0 disables). Active runs are never evicted, and eviction removes only
the in-memory record — durable journals and artifacts on disk remain the
operator's to prune.

Paginate GET /v1/runs via ?limit (default 100, clamped to 1000) and
?offset, returning {runs, total, limit, offset} so the response is never
unbounded and newest-first paging stays stable. Update the OpenAPI
document, docs, and add unit and e2e coverage.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Pause engineer queue processing once current-session usage exceeds 90 percent.

Co-authored-by: Cursor <cursoragent@cursor.com>
Make the Kubernetes example operationally safe and keep HTTP request, event, and artifact handling bounded by bytes transferred.

Co-authored-by: Cursor <cursoragent@cursor.com>
Route run, serve, and MCP through one shared execution-policy parser so
sandbox posture, workspace, repeatable --env, --inplace, --unsafe, and
--yes behave identically in every mode. Reject mutually exclusive
postures and command-specific unsupported flags as usage errors before
spawning, keeping only display (--raw) and transport (--host/--port)
options command-specific.

Define a single precedence order across CLI flags, JAIPH_* env vars, and
workflow runtime metadata, resolve and print the effective posture once
at server startup, and apply it to every call while preserving the
standalone-container exception. Adopt one lifecycle-hook contract for
direct, HTTP, and MCP invocations, backed by a table-driven integration
suite and hook tests, with matching docs and env-var reference updates.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Standalone `jaiph run --raw` previously bypassed the shared OTLP/Sentry
export hook, and the two exporters were awaited sequentially so two
unreachable backends could hold a completed run — and a server's
execution-concurrency slot — for up to two full timeouts.

`runWorkflowRaw` now fires the shared export hook after the child exits,
gated by `shouldExportRawTelemetry` so a user-invoked raw run exports
exactly like a normal run while the inner raw process of a Docker run
skips it (the host stays the single exporter, so a container run exports
exactly once). The OTLP and Sentry exporters now run concurrently under
one flush budget configurable via `JAIPH_TELEMETRY_FLUSH_MS`. On
long-lived `jaiph serve` / `jaiph mcp` processes the call layer detaches
delivery via `deliverRunTelemetryDetached`, marking the run terminal and
releasing its concurrency slot before best-effort export; detached
failures are tracked in bounded cumulative metrics with warnings capped.
`OTEL_*` / `SENTRY_*` stay operator-side — dropped by the prompt-env
allowlist and excluded from Docker forwarding, now pinned by tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Company deployments needed both protocols without running two processes;
POST /mcp now shares jaiph serve's tools, run registry, concurrency,
auth, and cancel contract with the existing REST API.

Co-authored-by: Cursor <cursoragent@cursor.com>
HTTP run discovery was an in-memory map, so restarting jaiph serve made
completed run ids unreachable, lost in-flight state, and let client
retries spawn duplicate expensive workflows.

Persist each run's public record as run.json beside its journal (atomic
temp-file + rename at finalize) and reconstruct terminal runs on startup
via loadPersistedRuns, so list/get/events/artifacts keep working across a
restart. A run with a journal but no run.json was running when the
process died; it is reconciled from its WORKFLOW_START line into a new
explicit terminal status "interrupted" and never reported as permanently
running. POST /v1/workflows/{name}/runs now honors an Idempotency-Key
scoped to the workflow plus authenticated principal: the key is reserved
synchronously before any await, a repeat with identical args returns the
original run, and a reused key with different args is
409 E_IDEMPOTENCY_CONFLICT and spawns nothing. The idempotency index is
rebuilt from reconstructed records at startup and evicted with the run,
so it survives restarts and cannot outgrow the registry.

jaiph serve is documented as single-replica by design (per-process
registry, concurrency cap, and idempotency index); k8s.yaml pins
replicas: 1 with a Recreate strategy. Covered by run-store and handler
unit tests plus an integration test that survives a real process restart.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The static JAIPH_SERVE_TOKEN gate is fail-closed but anonymous: no user
identity, revocation, per-action authorization, or attribution, which is
unsafe once multiple users can invoke arbitrary engineering workflows.

Keep the shared bearer token as an explicit single-operator mode and add
a standard OIDC/JWT mode configured by issuer, audience, and JWKS
discovery, verified with a maintained JWT library rather than custom
crypto. Authorize invoke, inspect/artifact, and cancel as separate
capabilities so a principal cannot inspect or cancel runs outside its
policy. Attach the authenticated principal and a request/correlation ID
to run metadata, logs, OTLP resources, and Sentry tags while keeping
bearer tokens and secret-bearing claims out of journals. Make /docs and
/openapi.json exposure configurable and keep health probes free of
credentials. Integration and unit tests cover valid, expired,
wrong-audience, wrong-issuer, unknown-key, and insufficient-scope tokens
plus audit attribution.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant