Skip to content

DO NOT MERGE — merge train: 3413,3415,3416,3419,3417,3418,3420,3414,3421,3422 - #3424

Closed
vybe wants to merge 31 commits into
devfrom
train/20261009-0735
Closed

vybe wants to merge 31 commits into
devfrom
train/20261009-0735

Conversation

@vybe

@vybe vybe commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

Integration surface for #3413, #3415, #3416, #3419, #3417, #3418, #3420, #3414, #3421, #3422. Never merged; members merge individually once green.

Trinity Agent (trinity) and others added 30 commits October 8, 2026 18:14
…3312)

sanitize_dict / sanitize_list returned the raw subtree once depth exceeded
max_depth, so a secret nested 12+ containers deep (a stream-json tool input
with ~6 levels of its own nesting) was persisted unredacted. Past the cap the
subtree is now replaced with REDACTION_PLACEHOLDER; recursion stays bounded.

sanitize_json_string (both copies) and the agent's sanitize_subprocess_line
also catch RecursionError from json.loads on pathologically deep input and
fall back to the linear sanitize_text pass instead of raising.

Tests: strict-xfail removed from test_secret_below_max_depth_is_still_redacted;
new 2000-level dict/list cases and a 200k-deep JSON string case (backend
edges + agent copy, incl. sanitize_subprocess_line); both
test_max_depth_protection now assert the secret is absent; the property test
drops its depth<=11 assume. Mutation: with the fix reverted, 6 backend tests
(both S12 params, both deep-object params, deep-JSON, backend
max_depth_protection) and 5 agent tests (max_depth_protection + the 4
TestDepthFailsClosed cases) went red.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…dded_credentials (#3323)

`//tok@github.com/o/r` was prefixed with the assumed scheme, giving
`https:////tok@…` whose netloc parses empty, so the token passed the
check and validate_skills_library_url's shorthand branch persisted it
verbatim to skill_sources.url and the audit row.

reject_embedded_credentials now strips its input and applies the same
`had_authority` rule as strip_url_credentials (`://` or a leading `//`).
A ValueError from urlparse is caught ONLY for `//` inputs and falls back
to the shared _AUTHORITY_USERINFO_RE, so `//[oops/x` does not become a
new 500; every other input raises exactly as before (#3322 / D4 stays a
strict xfail). Empty userinfo (`//@host`) is still not refused.

Tests: D3 strict-xfail marker removed (+4 refuse cases incl. the regex
fallback lane); new must-not-raise cases for `//host`, `//[oops/x`,
`//@host` and the `///tok@` scope boundary. Mutation: with the fix
reverted the 7 D3 refuse cases go red; with only the `//` try/except
removed, D3-protocol-relative-unparseable-fallback and
D3-protocol-relative-unparseable-no-userinfo go red.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d as a rewrite (#3314)

The #2915 sync fingerprint must reproduce what ingest stored. Two seams
diverged, so an untouched agent entry read SYNC_CHANGED and every operator
answer was refused 409 item_diverged:

- options: the store keeps a falsy value ([], "", {}, 0, False) as NULL,
  but `_canonical_options` serialised it ("[]"). It now canonicalises any
  falsy value like None, mirroring the store's truthiness.
- title / question: a JSON number or bool passed the clamp untouched and
  was stored verbatim (read back backend-dependently: SQLite True -> "1",
  PG "true"), while `_entry_content` fingerprinted it as the default. A new
  `_scalar_text` stores it as its text ("123", "True") in the clamp and
  mirrors that in `_entry_content`, so both sides match on every backend.

Not covered: no number conversion before the #3243 title cap check (a
numeric title is still never cap-held; scope kept narrow). `type`
handling untouched (#3385).

Stale rows: asks stored before this fix with a boolean title hold "1" on
SQLite and stay flagged as diverged; integer-titled rows recover on the
next sync. No migration.

Tests: the strict xfails r88-r91 in test_ec_operator_queue_edges.py are
removed and r90b (boolean title) added. With the fix absent all five go
red: r88/r89 `['options'] == []`, r90/r90b `['title'] == []`,
r91 `['question'] == []`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…sy options and non-string titles (#3314)

The Tier-2 real-SQLite property (entry -> clamp -> store -> sync read ->
changed_fields == []) excluded the #3314 inputs while they were strict
xfails. It now generates falsy options ([], "", {}, 0, False) and
int/bool/float(nan, inf)/[]/{} titles and questions, with explicit
examples for `options: []` and `title: 123`. This is the parity guard for
the fingerprint mirroring the store's options truthiness. Only the null
`type` exclusion (#3315/#3385) remains.

With the #3314 service fix reverted the property goes red
(`['title', 'question'] == []` on the explicit examples).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…#3320)

idempotency_service.begin() reports an in-flight claim as replay=True,
in_flight=True, so post_message's replay return ran first and its 409
branch was unreachable: a same-key duplicate got 200 {"replayed": True}
with no message. Check in_flight first, and use /chat's detail shape
(error=request_in_progress, message, execution_id - None for rooms).

TestRoomsInFlightBoundary: strict xfail removed; _drive takes `posted`
so the test asserts nothing was dispatched and the detail shape.
Mutation: with the router reverted, test_in_flight_duplicate_is_409_
not_a_silent_success fails (DID NOT RAISE HTTPException).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
)

load_hardened_yaml let two failure classes escape its HardenedYamlError
contract, so deploy_system (which catches only ManifestError/ValueError)
answered an unnamed 500:

- Control characters: PyYAML's Reader checks printability in __init__,
  and `loader(text)` sat above the try that maps YAMLError ->
  `{kind}_yaml_invalid`. Construction now sits inside the try, and the
  finally only disposes an instance that was actually built.
- Deep nesting: no depth bound, so ~330 flow levels (6 KB) or 500 block
  levels (~125 KB), both under the byte cap, exhausted the stack. A
  compose_node depth gate (DEFAULT_MAX_DEPTH = 64, per-call `max_depth`)
  now refuses `{kind}_too_deep` before recursing. An `except
  RecursionError` backstop maps recursion the gate cannot see (a `<<`
  merge chain walked by flatten_mapping) to the same code.

The agent-server copy is byte-identical (Invariant #5).

Tests: removed the two #3324 strict-xfail markers in TestDocumentShape
(the deep test now pins `k_too_deep`); new ent314 tests cover the 64/65
boundary, block form, per-call max_depth, the manifest codes through
parse_manifest, and the merge-chain budget + backstop. Before the fix
they failed on RecursionError / ReaderError (and on the missing API),
and they pass after it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ot_number (#3319)

slot_number is ZCARD+1 at acquire and repeats after out-of-order releases,
so get_status's slots[0] (sorted by slot_number) could surface a newer slot
as current_execution. SlotInfo now carries the ZSET score as a required
start_score field, and get_status takes min((start_score, execution_id)).
get_slot_state's order, the /capacity response, and acquisition/release/
renewal/cleanup/admission are unchanged.

Tests: the strict xfail on test_r56 is removed; the new test_r56b covers
sub-second ordering where every duration_seconds truncates to 0. Both went
red on the unfixed code (picked 'D' instead of 'C') and are green with the fix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ope shape (#3325)

CredentialEncryptionService.decrypt documents ValueError for a bad format,
but a non-object JSON document ("[]", "null", "1", '"x"') hit data.get() and
raised AttributeError, and a non-string nonce/ciphertext made b64decode raise
TypeError outside the (KeyError, ValueError) arm. .credentials.enc is
agent-writable, so POST /api/agents/{n}/credentials/import answered 500 with a
stack trace where its ValueError arm promises 400.

Reject a non-object envelope up front and widen the base64 arm to TypeError.
Any input that decrypts today decrypts identically.

Removes the two strict xfails (M50a-f, M50g); they went red with the
markers removed and the fix absent (7 failed), green with it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… "add the key" (#3325)

encrypt_secret_setting wrapped the whole encrypt() call in `except
ValueError` -> MissingEncryptionKeyError. A lone UTF-16 surrogate in the value
makes the UTF-8 encode raise UnicodeEncodeError (a ValueError), so a configured
install was told to add CREDENTIAL_ENCRYPTION_KEY and the settings routes
answered 500.

- secret_settings: probe the key on its own (the only key-error source), then
  encrypt; an UnicodeEncodeError becomes the new SecretSettingValueError
  (ValueError subclass, .key). It is raised outside the except block and
  `from None`, so neither __cause__ nor __context__ holds the
  UnicodeEncodeError, whose .object is the whole secret. The message names
  the key, never the value.
- error_handlers + main.py: one app-level handler maps it to 422 {"detail"}.
- routers/settings/credentials.py: pass-through arms in the three routes whose
  `except Exception` would turn it into a 500 (Anthropic key, GitHub PAT,
  PUT /slack). The routes without a try reach the handler directly.

Missing/invalid key is unchanged: still MissingEncryptionKeyError, and now
reported before any value check.

Tests: removes the M58 strict xfail; new tests/unit/test_3325_credential_error_types.py
(value absent from message, args, chain and rendered traceback; key-first
order; anthropic / slack / slack-connect -> value-free 422 with no log line
carrying it; main.py registers the handler). Mutation: with the anthropic
pass-through arm and the main.py registration reverted,
test_route_answers_a_value_free_422[anthropic] and
test_main_registers_the_handler went red; restored byte-identical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e DB sink (#3385)

`_clamp_ingested_item` bounded title, question, context and options but
copied an agent's free-text `type` straight through. A queue-file entry
could therefore store, and broadcast, a multi-MB type.

- `_bounded_type`: truncate-with-marker at OPERATOR_QUEUE_TYPE_MAX = 64,
  strings only. A fixed constant: every platform type literal (the
  longest is 24 chars) and every ASK_TYPES value passes unchanged.
- The clamp applies it. `_comparable_type` applies the same bound before
  the empty-value default, so the #2915 fingerprint does not report a
  clamped row as `changed` against its own raw entry. Without that,
  responses would be refused with 409 `item_diverged`.
- DB belt: `_insert_values` raises ValueError for a type over 1 KiB
  (`_DB_BELT_TYPE_MAX_BYTES`), like the id/title/question/context belts,
  so a caller that skips the clamp is quarantined (#1525).
- Reworded the `ingested` audit comment that said the clamp does not
  bound `type`.

Stacked on feature/3314-queue-fingerprint-falsy (923b1ed).

Mutation record (fix reverted on a scratch copy, then restored
byte-identical):
- clamp line removed: TestClampType::test_oversize_type_shortened_with_marker
  and ::test_one_over_cap_gets_marker go red
- `_comparable_type` bound removed: test_3385_clamped_type_is_not_a_rewrite
  [clamped-row-vs-raw-entry] goes red
- belt removed: TestDbBelt::test_belt_rejects_oversize_type goes red

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…3321)

_build_claim_response composed the platform system prompt with the row's
recorded timeout_seconds, and claim_next_task only clamped it to the agent
timeout (#2846) afterwards. A row enqueued at 1800s under an agent now set
to 600s told the model "Timeout: 1800s" and was killed at 600s.

_build_claim_response now takes an optional cap and clamps with the existing
_turn_limit before composing; the #2846 warning moves with it (same text).
claim_next_task passes cap= and drops its after-the-fact clamp. A row with no
recorded timeout now states the agent timeout in its prompt, as push does.
_turn_limit, _shortened_note, the lease length and the row's recorded
timeout_seconds are unchanged.

Tests: T14's strict-xfail marker removed; T15 (absent -> agent cap) and T16
(under cap -> unchanged) added. T14 and T15 were red before the fix (prompt
said 1800s; compose saw None), green after.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The platform contract parser read only `user_invocable`, while Claude Code,
the skill libraries and the agent server use `user-invocable`. A library
skill marked `user-invocable: false` was therefore reported invocable by
GET /api/skills/library and MCP list_skills.

Read both spellings per source through the existing `_first_present`
helper (hyphen first), so a `trinity:` block still wins whichever spelling
either level uses and a null value masks nothing. Non-bool values still
read as invocable. Wire name stays `user_invocable`.

Mutation check (scratch copy): the issue's literal merged-scope form turns
[trinity-underscore-beats-flat-hyphen] and [null-hyphen-does-not-mask] red;
the underscore-only original turns [hyphen],
[trinity-hyphen-beats-flat-underscore], [flat-hyphen-beats-underscore] and
[hyphen-wins-in-trinity] red.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… leaf (#3324)

Review I1: the compose_node counter incremented for every node, so
DEFAULT_MAX_DEPTH = 64 admitted only 63 collection levels for any
document ending in a scalar. Count only mapping/sequence starts; add a
leaf-bearing 64/65 boundary test for flow and block style. Both copies
byte-identical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… the measured figure (#3324)

Review I2: measured on PyYAML 6.0.3 with the compose_node override,
4 frames per level, ~265 at depth 64, ~725 of 1000 left for the caller
(a 64-level doc parses with the caller 725 frames deep). Comment only;
both copies byte-identical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ict-xfail (#3323)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lative rule (#3323)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…3385)

The three docs that enumerate the _clamp_ingested_item truncate-with-marker
fields and the create_item DB belt now name type: 64 characters at ingest
(OPERATOR_QUEUE_TYPE_MAX, with the truncation marker) and 1 KiB at the belt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…3314)

merge-train: mechanical, per the merge-train note on the PR. `_scalar_text(0)`
returned "0", but `create_item` stores the `or`-default ("Agent request") for a
falsy title/question, and every row ingested before this fix carries that
default — so the entry fingerprint "0" diverged from the row and flipped such
rows to 409 `item_diverged` on the next sync. Only truthy scalars are
stringified now; a 0/False title keeps displaying the default as before.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…#3321)

merge-train: mechanical, per the merge-train note on the PR.

Co-Authored-By: Claude Fable 5.1 <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