Skip to content

Rya core (Track A): retry/repair, credentials, streaming chat, Langfuse datasets - #2

Merged
lokesh-danu merged 33 commits into
mainfrom
track-a-core
Jul 31, 2026
Merged

Rya core (Track A): retry/repair, credentials, streaming chat, Langfuse datasets#2
lokesh-danu merged 33 commits into
mainfrom
track-a-core

Conversation

@lokesh-danu

@lokesh-danu lokesh-danu commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator

Re-scoped from #1 to contain ONLY the Track A core-runtime primitives:

  • A1 declarative tool-call retry + repair
  • A2 id-secrecy output guard (scrub master ids)
  • A3 per-user connection credential + reconnect-on-401 (store upsert, plaintext-credential deploy gate, needs_reconnect terminal outcome)
  • A5 in-turn deterministic student-key adoption

Commits (4), ordered guard → store → runtime → sdk; all changes are under src/ + root tests/.

The csa-counsellor example (Track B) is being moved to another repo, so it is intentionally not part of this PR. Supersedes #1.

lokesh-danu and others added 4 commits July 23, 2026 09:38
Add a secrecy output guard that rewrites configured secret-id patterns to a
safe token instead of blocking the turn — the runtime form of production's
scrubMasterIds(). Unlike grounding (which fails closed), secrecy scrubs and
lets the redacted value through so the model keeps working.

- secrecy_scrub_text / secrecy_scrub (per-leaf over dicts/lists) / secrecy_check
- _compile_secrecy with a policy-keyed cache; a malformed pattern is skipped,
  never fatal, so one broken rule can't take down every tool call
- opt-in via `secrecy.enabled` in rya.guard.yaml; applied by callers at the
  tool boundary and on outbound (wired in a later commit)

Tests port chatstudyabroad/lib/utils.test.ts (scrubMasterIds): a Crizac master
id (3-8 letters + 8+ digits) is scrubbed; numeric CAMS ids, passports, phones,
years, and money are preserved.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add upsert_connection to both FileStore and PostgresStore, keyed on
(provider, owner). A login handler that re-mints a per-user bearer must not
leave a stale duplicate the runtime could later inject, so the upsert rewrites
the existing active connection in place — preserving id/createdAt, stamping
updatedAt — rather than appending a new one.

- FileStore: scan connections_dir for the existing (provider, owner, active)
  doc and rewrite its file; else create.
- PostgresStore: SELECT ... FOR UPDATE on `owner IS NOT DISTINCT FROM %s`, then
  UPDATE or INSERT.

Backs the ctx.connections.upsert SDK path (wired in a later commit).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…oy gate (A3)

A per-user connection can expire mid-turn. Surface that as a clean, distinct
outcome the caller can act on (log in again and retry) instead of a generic
failure — mirroring production's CrizacAuthError -> reconnect prompt. No
auto-refresh.

- engine: an E_CONNECTION_EXPIRED RyaError sets run.status = needs_reconnect
  with its own run.needs_reconnect trace step (not run.failed); status added to
  the observability export gate.
- turns: needs_reconnect added to TERMINAL_RUN_STATUSES.
- api/app: needs_reconnect added to the manual run-status allow-list.
- readiness: `rya deploy --check` now blocks E_PLAINTEXT_SECRET_AT_REST when a
  require_user provider has a stored connection sitting unencrypted at rest —
  seal() degrades to plaintext without key material, so this fails closed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…edential enforcement (A1/A2/A5/A3)

Rewrite the _Tools.call backend path so retry, self-heal, secrecy, and adoption
all flow through one journaled step, backend-agnostic across agent/http/mock —
so a replay after an approval pause returns the memoized result and never
re-runs a retry or repair.

A1 — declarative retry + repair:
- manifest RetryDecl (max_attempts / backoff / on), with a YAML-1.1 `on: true`
  rescue so an unquoted `on:` key is never a silent no-op.
- @agent.repair("<tool>") callback; a RyaRecoverableToolError self-heals once
  with a patched input (tool.repair trace step). Repair (domain) and retry
  (transient) are orthogonal budgets.
- _http_tool tags a 5xx (http_status) and a socket timeout (E_TIMEOUT) so the
  retry classifier can see them; RyaError carries http_status.

A2 — secrecy wiring: the scrub runs on every tool result before it re-enters
the loop / journal / trace, and on every outbound message before it leaves.
ctx.guard.check_secrecy exposed for handler-side assertions.

A5 — adoption: a manifest `adopt: {field: scope.key}` copies a successful
result field into scoped memory so a later pinned tool in the same turn adopts
it (e.g. create_lead -> student_state.camsId).

A3 — per-user credential: credential resolution is scoped to egress backends
only (a local @agent.tool leaf never receives the secret, so provider: on it is
governance metadata); require_user fails closed with E_NO_IDENTITY when no
verified sub; _http_tool maps a 401 on a credentialed request to
E_CONNECTION_EXPIRED; ctx.connections.upsert mints/refreshes the caller's
connection.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
lokesh-danu and others added 2 commits July 23, 2026 17:47
rya is a general-purpose agent runtime, so the core primitives and their
tests should not carry a specific customer's business vocabulary. Replace
Crizac/CAMS/counsellor/chatstudyabroad references (comments, test provider
names, fixture field names, a secrecy-policy id) with neutral equivalents
(external CRM, account id, user, opaque_master_id, session_state). Behaviour
is unchanged: comment-only in src, self-contained renames in tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The governed agent loop seeded itself with only the current message, so
follow-ups ("compare those", "#2") had no earlier context to resolve
against. Add an optional keyword-only `history` param to _LLM.run that
seeds the loop with prior user/assistant turns ahead of the current
message. Blank-content and non-user/assistant rows are filtered so the
provider never receives an empty or unnormalizable message.

Backward-compatible: defaults to None (existing callers and journals
unchanged; the number/order of _step calls is unaffected). The caller
owns the window size, e.g. via ctx.sessions.history(...).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@lokesh-danu
lokesh-danu marked this pull request as ready for review July 24, 2026 06:14
@lokesh-danu
lokesh-danu requested review from arshadshk and vaibs-d July 24, 2026 06:14
@lokesh-danu

Copy link
Copy Markdown
Collaborator Author

support conversation history in ctx.llm.run

lokesh-danu and others added 3 commits July 24, 2026 17:39
Handlers that need to call an upstream API directly (not through a declared
`url:` tool) had no way to get at a connection's bearer - only leaf tools got
credential injection. Add ctx.connections.secret(provider, scopes=...), the
handler-side analogue: same per-user resolution and scope-intersection
enforcement as tool injection, and the resolved secret is seeded into the
redaction vault so it can never surface in a trace or log.

Deliberately not journaled - a plaintext credential must never touch the run
journal - so a replay re-resolves it live instead of memoizing it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
chat() gained an optional on_token callback: when set, the anthropic provider
streams the response over SSE and emits assistant text deltas as they arrive,
instead of blocking for the full completion. Tool-use blocks are rebuilt from
input_json_delta fragments so a tool-calling step behaves identically to the
non-streaming path - only the final text is additionally streamed. Providers
without a streaming chat path ignore on_token and return the full result at
once, so passing it is always safe.

_anthropic_messages/_anthropic_tools are pulled out of _anthropic_chat so the
streaming and non-streaming paths build the request body from the same code
and can never diverge.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…taset

Rya could only push runs/scores to Langfuse; there was no way to run the
agent over an existing Langfuse Dataset. Add `rya eval --langfuse-dataset
<name>`: fetches the dataset's items, fires a real engine run per item
(--trigger-type/--payload-defaults control how an item's `input` maps to an
event), and links each run's trace back to its dataset item as a
dataset-run-item under a named run - so the run shows up under
Datasets -> runs in the Langfuse UI, one row per item.

An item may carry a Rya `expect` block under its metadata to be scored with
the same SCORERS as local eval cases (extracted local-suite scoring into
_score_expect so both paths share it); a failing item still exits non-zero
like the local suite, so a Langfuse dataset can gate a deploy too.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@lokesh-danu lokesh-danu changed the title Rya core primitives (Track A): retry+repair, id-secrecy scrub, per-user connection creds, in-turn adoption Rya core (Track A): retry/repair, credentials, streaming chat, Langfuse datasets Jul 24, 2026
@lokesh-danu

Copy link
Copy Markdown
Collaborator Author

handler-side credentials, streaming Anthropic chat, Langfuse dataset evals

  • ctx.connections.secret(provider, scopes=...) — handler-side analogue of tool
    credential injection: same per-user resolution + scope-intersection enforcement
    as declared url: tools, and the resolved secret is vaulted so it can never
    surface in a trace or log. Deliberately not journaled — a replay re-resolves it
    live rather than memoizing a plaintext credential.
  • chat(..., on_token=...) — optional streaming callback on the LLM provider
    layer; the anthropic provider now streams assistant text deltas over SSE while
    assembling the identical {text, toolCalls, model, provider, usage} shape.
    Providers without a streaming path ignore on_token and return the full result
    at once, so passing it is always safe.
  • rya eval --langfuse-dataset <name> — pulls an existing Langfuse dataset's
    items, fires a real engine run per item, and links each run's trace back to its
    dataset item as a dataset-run-item (shows up under Datasets → runs in the
    Langfuse UI). An item may carry a Rya expect block under its metadata to be
    scored with the same scorers as local eval cases; a failing item still exits
    non-zero, so a Langfuse dataset can gate a deploy too.

The repo had no linting at all - style and import hygiene were whatever each
file happened to land on. Add ruff (one tool for both lint and format),
configured entirely in pyproject.toml: line-length 120 (the tree only has 31
lines over it), target py310 to match requires-python, and the F/E/W/I/UP/B/
C4/SIM/RUF rule set. Three ignores, each with a stated reason - E501 (the
formatter owns wrapping), B008 (Typer/FastAPI declare params as calls in
defaults, 48 intentional hits), SIM105 - plus per-file E402 for tests/ (conftest
sets sys.path before importing rya) and examples/.

Config only: no source file is touched by this commit. The tree is NOT clean
against it yet - `ruff check` reports 739 findings (562 safe-autofixable,
mostly UP045 Optional -> `| None`) and `ruff format --check` would rewrite 88
of 136 files. Fixing those is deliberately deferred and will happen gradually,
file by file as they're touched, so real changes aren't buried under thousands
of lines of churn.

CI is also pending: there is no .github/ in this repo, and wiring a workflow
now would only fail red. Once the tree is closer to clean - or as a
changed-files-only gate - lint/format/pytest should run there.

docs/devex.md documents the commands and this convergence plan.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@lokesh-danu

Copy link
Copy Markdown
Collaborator Author

ruff as linter + formatter — config only, cleanup and CI still pending

The repo had no linting at all, so style and import hygiene were whatever each
file happened to land on. 8def09f adds ruff
(one tool for both lint and format), configured entirely in pyproject.toml:

  • line-length = 120 — the tree only has 31 lines over it
  • target-version = "py310" to match requires-python
  • rule set F, E, W, I, UP, B, C4, SIM, RUF
  • three ignores, each with a stated reason: E501 (the formatter owns
    wrapping), B008 (Typer/FastAPI declare params as calls in defaults — 48
    intentional hits), SIM105
  • per-file E402 for tests/ (conftest sets sys.path before importing rya)
    and examples/
  • ruff>=0.16 in the dev extra; docs/devex.md documents the commands

No source file is touched by this commit — nothing in the diff is
behavioural, so it doesn't affect review of the Track A commits.

Deliberately deferred

The tree is not clean against this config yet. Current baseline:

check result
ruff check 739 findings, 562 safe-autofixable
ruff format --check 88 of 136 files would be rewritten

Largest buckets: UP045 Optional[X]X \| None (364), UP006 (95),
E702 semicolon one-liners (78), I001 unsorted imports (53), B904
raise-without-from (48).

Fixing these is intentionally not in this PR. A repo-wide autofix +
reformat would be a ~7k-line diff that buries the actual runtime changes here.
The plan is to converge file by file as they're touched.

CI is also pending. There is no .github/ in this repo, and wiring a
workflow now would only fail red. Once the tree is closer to clean — or as a
changed-files-only gate — lint/format/pytest should run there. Happy to do that
as a follow-up PR.

lokesh-danu and others added 11 commits July 26, 2026 15:33
A real agent turn exported a trace that was hard to read: the model steps
themselves were contentless, most observations were empty rows, everything
was flat, and the reported latency was wrong.

Root causes, all in the export path:

- The governed agent loop (ctx.llm.run) emits its model steps with kind
  `llm.chat`, but `_LLM_KINDS` only listed `llm.respond`/`model.call`. Those
  steps fell through to the generic branch and were exported as contentless
  EVENTs — no prompt, no response, no model, no tokens. Only the single-shot
  sidecar (llm.respond) rendered as a GENERATION, so the interesting calls
  were the ones missing. The same mismatch made run_usage skip the whole
  compose loop, silently under-reporting tokens and cost.
- Every step records its return value, but the exporter only mapped
  input/output for LLM and tool steps; everything else dumped it into
  `metadata.result`, so memory/session/ui observations rendered blank.
- No parent was ever set, so observations were flat siblings of the trace.
- Steps carried only `ts`, which is second-resolution, so a whole turn
  collapsed onto one instant and could not even be ordered reliably.
- The trace output read `run["result"]`, which the engine never populates —
  the handler's return value lives on `run["output"]`.

Changes:

- Recognise `llm.chat` as an LLM call in both the exporter and usage
  metering, and journal the messages array + model parameters so the
  generation shows the real prompt it was sent.
- Record a true wall-clock span (`startedAt`/`endedAt`, ms) around every
  journaled step. `store.now_iso()` is deliberately untouched: it has 63
  call sites and its second-resolution format is compared when scheduling.
- Build a real tree — run root -> agent loop -> model step -> the tools that
  step called. ctx.llm.run stamps a `loopId` on its steps and on the tool
  calls they trigger; the loop span is synthesised at export time, so no
  extra journaled step is created and approval-pause/resume replay is
  unaffected.
- Give every observation input/output, and demote runtime bookkeeping
  (memory/session/file/connection/knowledge) to DEBUG under one collapsed
  `context` span, so the top level shows the agent's work rather than the
  runtime's. Durable operations are exported as SPANs because Langfuse only
  accepts an endTime on SPAN and above; instantaneous markers stay EVENTs.
  Failures carry level=ERROR plus a statusMessage.
- Populate trace-level `output` (from run["output"]), `sessionId` (recovered
  from the session step, which enables Langfuse's Sessions view for
  multi-turn agents), `userId` (new, set from the run's identity), and tags.
- Mirror the same hierarchy and real timings in the OTLP exporter so
  Phoenix/Tempo/Datadog show the identical shape.

Runs recorded before this change have no startedAt/loopId and still export,
falling back to the previous seq-derived ordering (covered by a test).

Verified against a live Langfuse instance: latency now reflects reality
(10.1s for a turn previously reported as 0.02s), no non-EVENT observation
has null input/output, and generations carry model parameters and usage.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…icy, workers

Adds the store-layer primitives PLATFORM_DESIGN needs above the existing
per-run journal: content-addressed version records, per-environment pointers
with promote/rollback history, an append-only policy log, worker registration,
and the queue/quota counters that back admission checks. Both the in-memory
and Postgres stores gain the same surface so tests and production stay
behavior-identical. Also declares the new platform error codes (journal
drift, version/bundle/environment not-found, promotion-blocked, quota-exceeded)
so they map to the right CLI exit codes instead of falling through to generic.
…otas, worker plane

Implements the core PLATFORM_DESIGN modules on top of the new store primitives:

- config.py: per-environment run config as data (D8) — env/secrets/model routes
  are resolved once from an explicit mapping, never from os.environ.
- bundles.py: content-hashed deployment artifacts (D12) — sha256 over sorted
  (path, content) pairs plus SDK version, packed as a byte-deterministic
  tar.gz; local and S3-backed storage arms.
- deployments.py: version/environment orchestration (D11/D12) — create,
  promote, rollback, retire, pinned-run tracking.
- gates.py: promotion admission checks (§9) — readiness and eval attestations
  gate staging->prod rather than being a client-side courtesy.
- quotas.py: per-workspace resource limits (D13, §11.12) — concurrent runs,
  runs/day, queue depth, tokens/cost per day, worker count.
- worker.py: the execution plane (§5.2/§6) — per (workspace, agent, version)
  process that loads a pinned bundle, advertises its handler set, registers,
  heartbeats, and scales to zero.
- cli/client.py: thin HTTP client the CLI and e2e script use to talk to a
  running platform server.

Each module ships with its own test module (bundles, bundles+S3, config,
deployments, promotion gates, quotas, worker, plus cross-cutting platform
governance tests).
…the platform

Connects the new store/platform modules to every surface that talks to them:

- api/app.py: routes for versions/deploy, promote/rollback, promotion gate
  policy and attestations, quota get/set, worker registration and heartbeat,
  workspace usage.
- cli/main.py + cli/__init__.py: `rya deploy`, `rya env promote/rollback`,
  `rya worker`, gate and quota subcommands, wired through the new
  cli/client.py HTTP client; scaffold no longer needs a tweak that platform
  deploy makes redundant.
- guard.py: policy enforcement extended to cover the new promotion/quota
  policy keys (D7 — a bundle cannot write privileged platform state).
- runtime/engine.py, queue.py: version-pinned execution and fair per-workspace
  claim ordering (the scheduling half of D13, quotas.py is the resource half).
- readiness.py: the local check is now also what the server-side promotion
  gate runs (§9).
- snapshot.py, tenancy.py, turns.py: multi-tenant/version-aware plumbing
  these needed to carry a version and workspace through a run.
- manifest/loader.py, manifest/schema.py: drop the manifest-level
  `environment` field now that an environment is a deploy-time pointer, not a
  build-time property of the artifact (D11).
- providers/*, sdk/context.py, sdk/__init__.py: consume config.py's explicit
  RunConfig instead of reading os.environ directly (D8).
- observability/usage.py: durable per-workspace token/cost meter that quotas
  and gates read from.
- console: surfaces versions, environments, gates and quotas in the UI.

Tests updated/added alongside: test_api, test_cli, test_console, test_evals,
test_guard, test_job_groups, test_llm_layer, conftest fixtures, and a new
test_sdk_surface covering the public SDK import surface.
scripts/e2e_platform.py drives a running server through the CLI client
(deploy, promote, run, gate, quota, worker) to catch wiring bugs that unit
tests miss because they exercise each module in isolation. AGENTS.md
documents the new scripts/ directory.
…latform)

PLATFORM_DESIGN D16: PyPI `rya` becomes the thin client SDK; the platform
ships separately as `rya-server`. Both are built from this same tree via
packaging/sdk and packaging/server, each with its own pyproject.toml,
LICENSE and README; packaging/surface.py checks that the SDK package only
exposes the intended public surface (type stubs included). The root
pyproject.toml stays the whole-tree dev install (`pip install -e .`) — see
docs/PACKAGING.md for why the two published wheels are alternatives rather
than co-installable halves. uv.lock is now committed so the dev environment
resolves reproducibly.
Breaks the monolithic src/index.ts into focused modules (http.ts, sse.ts,
queue.ts, turns.ts, errors.ts, types.ts, client.ts) so the published `rya`
package and this client track the same PLATFORM_DESIGN D16 SDK/platform
boundary as the Python side. Adds a real unit test suite (test/*.test.mjs)
covering request handling, SSE, queue polling and error mapping, a
tsconfig.check.json for standalone type-checking, and checks/usage.ts as a
compile-time usage sample. docs/typescript-sdk.md documents the client for
external consumers.
…ployment

Reflects the deploy/worker/environment split in the AWS CloudFormation
template and local docker-compose stack: separate worker service(s), the
Postgres-backed store, and the environment variables the new config.py
and multi-tenant worker plane expect.
Follows the manifest schema change (D11 drops the build-time `environment`
field) and config.py's explicit RunConfig, plus a README note for
csa-counsellor.
lokesh-danu and others added 12 commits July 30, 2026 06:44
…nding docs

PLATFORM_DESIGN.md moves from "proposal / RFC" to a record of what shipped,
trimmed down to the settled decisions and how they map to the modules that
now implement them (store, bundles, config, deployments, gates, quotas,
worker, packaging, the TS client split). The original RFC text is preserved
verbatim in PLATFORM_DESIGN.superseded.md for history.

README, DEEP_DIVE, KNOWLEDGE, VISION_GAP, architecture and mcp docs are
updated to describe the platform primitives (versions/environments,
promotion gates, quotas, workers, the rya/rya-server package split) instead
of the pre-platform single-process model.
Four conflicts, all where both branches extended the same seam:

deploy/aws/template.yaml — main moved the template into the package
(src/rya/deploy_assets/template.yaml) and left a symlink behind; this branch
edited it in place. Took main's structure and replayed this branch's edits
(worker service, RyaEnvironment/WorkerCount, fail-closed mutator) onto the
packaged asset. The api container keeps both new env vars. The worker task
also gets the Langfuse trio: §11.7 moved handler execution off the api
process, so the worker is where spans are now produced — without it
`rya deploy aws --langfuse` would stand Langfuse up with nothing writing
to it.

src/rya/cli/main.py — both sides added a deploy mode. Signature and body are
the union: `rya deploy aws|status|destroy`, `--env <name>`, and the original
`--target`. Combining an action with --env now fails E_VALIDATION instead of
silently doing only the bundle promotion.

src/rya/console/index.html — union of the view lists, and the Govern nav no
longer duplicates id="nav3" (it is nav4, added to NAVBTNS). show() keeps this
branch's superset. Overview cards keep main's terse one-liners from a1e3071
rather than reinstating the long copy; the new Deployments card matches that
voice.

521 passed, 39 skipped.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`_CODE_EXIT` is the contract a coding agent branches on, so a code missing
from it is not a cosmetic gap — it exits 1, which reads as "the runtime
crashed" rather than "your input was refused".

- E_EGRESS_BLOCKED exited 1 while its sibling E_GROUNDING_BLOCKED exits 5, so
  an Action Guard refusal was indistinguishable from a crash. Now 5.
- E_NO_EVENT_HANDLER is a refusal to ship, not a crash. Now 7, matching
  E_NOT_PRODUCTION_READY.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The S3 arm was complete and tested but could not talk to MinIO, Ceph or R2.
Pointing boto3 at a custom endpoint is not enough: botocore gives
`s3.addressing_style` no environment variable at all, and the default
virtual-host form templates a custom endpoint as `{bucket}.{host}` — so
bucket `rya-bundles` at http://minio:9000 becomes http://rya-bundles.minio:9000
and fails to resolve on a container network. Declaring RYA_BUNDLES_S3_ENDPOINT
therefore also forces path-style addressing, which is why the knob must stay
empty on real AWS, where virtual-host addressing is wanted.

`files_s3.py` gets RYA_FILES_S3_ENDPOINT with the same three lines rather than
a shared helper: it is platform-only and `bundles.py` is in the SDK surface, so
a common module would drag one into the other's wheel.

`unpack()` gains an optional `max_total_bytes`, checked before a byte is
written. A local deploy unpacks an archive it just built and leaves it unset;
an endpoint accepting archives from the network cannot, because gzip's ratio is
an amplification factor and "the upload was under the limit" says nothing about
what it expands to.

`botocore` joins SDK_OPTIONAL_THIRD_PARTY — bundles.py is an SDK module, so its
deferred import of Config would otherwise fail the surface test.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Closes the gap the README named: `rya deploy --env` shipped only in
`rya-server` and wrote bundle bytes and version rows directly with local
credentials, so a client repo holding just the thin SDK had nowhere to ship to.

`POST /agents/{agent_id}/versions` takes the packed archive as a raw
`application/gzip` body with the scalars in the query string — the shape
`POST /files` already established, and one that keeps the client stdlib-only.
It rebuilds the content hash from the bytes it received rather than trusting the
sidecar, because D12's point is that the CONTENT proves the version. A mismatch
names SDK skew when that is the cause, since the digest folds in the SDK version
and identical bytes hash differently across SDKs — the generic "artifact was
modified" message would send an operator hunting tampering that never happened.

It never imports the bundle (D13), which has a consequence worth stating rather
than hiding: readiness cannot be evaluated and no attestation is filed, so the
response carries `"attested": false` and a readiness-gated environment refuses
the promotion. `rya deploy --env` remains the only attesting path.

Publishing is refused outright while the control plane is unauthenticated.
Every other route is open in that mode; this one would be anonymous code upload
to a box whose worker imports it.

`publish` and `check` are defined in the client CLI and re-registered in the
operator CLI. Both distributions own the `rya` console script, and the
recommended local loop is an editable install of the platform — a client-only
command would vanish exactly when a developer is dev-linked against a checkout.

`publish` deliberately does not check that declared tools have implementations:
a tool may be served by a built-in from `tools/registry.py`, which the SDK does
not ship, so the client cannot tell a built-in from a hole and would reject the
scaffold's own template. The worker's preflight owns that check.

`RemoteClient` now preserves the server's error code instead of collapsing every
HTTP failure to E_REMOTE, so the stable-code contract survives the network
boundary. `_load_agent` also raises the registered E_AGENT_NOT_DEFINED rather
than an undeclared E_NO_AGENT — one condition must not report two codes
depending on which side of the split noticed it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…exist

Both strings reach a user directly, and `rya publish` makes the first far more
likely to be seen.

`gates.py` told an operator whose promotion was blocked to run
`rya attest readiness --version <id>`. No such command exists in either CLI, and
the HTTP publish path cannot produce that evidence at all, so the one hint shown
on the most likely failure was unactionable. It now names only what exists.

The MCP tool's docstring and payload said hosted deploy was "a later milestone".
Shipping exists. An MCP client renders the docstring as the tool description, so
that claim was being served to coding agents as current fact. Publishing stays
absent from MCP on purpose — a version promotion should need a human running the
command — and the strings now say that instead of implying it is unbuilt.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The api and the workers are separate containers, so a container-local
`.rya/bundles` leaves a worker resolving a version whose artifact it cannot see.
MinIO is therefore a service, not an option, and `s3` (boto3) stops being an
optional extra in the runtime image — without it the first publish fails
E_BUNDLE_STORE. The bucket is created by `mkdir`, since on MinIO's filesystem
backend a top-level directory in the data dir IS a bucket; that keeps it one
service instead of two.

`worker-pinned` sits behind the `pinned` profile because it is a different
operating model rather than a better one: it serves ONE content-hashed bundle,
ignores the mounted tree, and needs a restart to pick up a re-publish. It also
crash-loops until the first promotion, since resolving an environment with no
current version is an error rather than a wait — `restart: unless-stopped` turns
that into "comes up when you publish".

The project mount becomes `${RYA_PROJECT:-…}` on all three services. One
variable, because pointing only one service somewhere new would leave the api and
the worker serving different code. `/project/.rya` becomes a named volume: the
containers run as root, so without it the worker leaves root-owned files inside
the developer's working copy that they cannot delete.

Both deploy/aws images gain `s3` too. `template.yaml` sets RYA_FILES_S3_BUCKET on
both tasks and `files_s3` needs boto3 to honour it, so Dockerfile.baked would
have failed on its first offload; Dockerfile.project only worked because the
`[bedrock]` layer happened to drag boto3 in, which would have broken silently the
day Bedrock was dropped.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`phase_publish` is the only place in the repo where a venv already proven unable
to import `rya.worker` ships code to a real platform, which is the whole claim of
the HTTP path: a client repo needs a URL and a key, not database or bucket
credentials.

The load-bearing assertion is idempotency against the operator path. Publishing
the same tree `rya deploy --env` already deployed must return the SAME version id
— the two paths agree on the content address, or the hash is a label rather than
an address.

Also asserts the honest limit: a readiness-gated environment refuses an
unattested HTTP publish while still recording the version. That needs NEW
content, because the version the CLI pipeline created carries a readiness
attestation from `rya deploy` and gating on it would pass for a legitimate reason
and prove nothing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
D11 removed the field: one content-hashed bundle is promoted between
environments, so anything that differs per environment is platform config. The
loader already stripped the key with a warning, which is why this went unnoticed
— but `readiness.py` raises it as a BLOCK, so `crizac` and `csa-counsellor` could
not be deployed at all. Removing it is behaviour-preserving.

All four examples now report `"ready": true` from `rya check`. AGENTS.md gains the
two examples it never listed, the rule that no manifest may carry the key, and a
pointer to the separate rya-examples repo for the external-author experience.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…copies

The ops skill claimed "deploy" in its description and had no shipping section at
all. It now covers both paths, the pinned-worker restart, and the
one-agent-per-deployment limit — the three things an agent gets wrong first.

Editing it exposed a trap: `skills/<name>/SKILL.md` is duplicated by hand into
`src/rya/skills/skill_data.py`, and that module is the one that SHIPS (it is in
the SDK wheel and `rya skills install` writes from it). They were in sync, so
editing only the repo copy would have changed the documentation while every
installed agent kept the old text. `tests/test_skills.py` now fails if they
diverge, and also checks the frontmatter each skill is routed by and that the
bodies survive the triple-quoted literal they live in.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The limit lands in architecture.md, which owns topology and never stated it:
`build_app` resolves a single rya.agent.yaml at startup, so `{agent_id}` in every
route is decorative and `POST /agents/{id}/versions` refuses a bundle declaring a
different name. Serving a second agent means a second deployment, sharing one
Postgres and one bundle store. Its "Execution model (today: single worker)"
section had to go anyway — "one worker process runs all agents" was doubly wrong.

Also corrected there: the plane boundary is about IMPORTING code, not touching
state. Publish writes artifacts and flips pointers from the api process without
importing a handler, and that distinction is what makes the unattested-readiness
consequence follow rather than look like an oversight.

Reference tables brought back in step with the code across DEEP_DIVE, KNOWLEDGE,
devex and primitives: the new route and its contract, the CLI surface, the
error-code/exit-code and HTTP-status mappings (every row verified against
`_CODE_EXIT`), and the object-store and publish environment variables.

Pre-existing drift fixed while in there, most of it actively misleading:
- DEEP_DIVE's showcase manifest declared `environment: local`, i.e. it was
  un-shippable; `export: langfuse` is not a manifest field; the provider list and
  the 13-name `ctx` list were both short; RYA_LLM_PROVIDER does not exist.
- devex's exit-code table contradicted itself — E_ENTRYPOINT_NOT_FOUND was listed
  as 3 and also matched a literal `E_*_NOT_FOUND` wildcard mapping to 4.
- KNOWLEDGE claimed "no worker on the deploy" as an open gap; the AWS template has
  had a WorkerService and WorkerCount for some time.
- PLATFORM_DESIGN §7 still described `Engine._execute_action` as an ungoverned
  second dispatch path and named DEEP_DIVE as overclaiming on that basis. The code
  was fixed: it delegates to `ctx.tools.call_approved`, `prepare()` refuses an
  approval_required tool unless approved=True, and `ctx.tools.call` never passes
  it. DEEP_DIVE was right and this document was stale; §11.1 is marked done.
- VISION_GAP's control-plane section header said ❌ while its own body now says the
  plane split is deployed.
- Test counts everywhere: 63 files, 562 test functions.

deploy/aws documents that `rya publish` MISFIRES SILENTLY against that stack —
no bundle bucket, a task role scoped to FilesBucket, and a worker running bare
`rya worker` — so a version records, the call returns ok, and a pinned worker
later fails E_BUNDLE_NOT_FOUND. The three-part fix is written down; it is not
built. Also that the endpoint overrides must stay empty on real AWS.

Not done, listed so the next person does not assume otherwise: KNOWLEDGE's
endpoint list and DEEP_DIVE's console-nav list are still incomplete, VISION_GAP
has four other self-contradictions its own preamble claims were fixed,
PLATFORM_DESIGN's LOC count and template.yaml citations are stale, and §2/§11
still call the client package `rya-sdk` where D16 says `rya`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@lokesh-danu
lokesh-danu merged commit 1aa65f6 into main Jul 31, 2026
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