* refactor(qa): route live channel selectors through suite host
* fix(qa): repair live selector CI
* fix(qa): remove retired runner exports
* fix(qa): remove obsolete live evidence
* chore(plugins): gate deprecation hygiene in CI and purge internal deprecated usage
* fix(agents): migrate auth-profiles oauth import to OAuthProviderId
* fix(whatsapp): use durability param in delivery-recovery test
* chore(scripts): declare shouldRunDeprecationHygieneChecks in check-changed types
* test(plugins): update runtime-api guard for ssrf policy rename
* fix(codex): use structured turn status for aborts, repair tool schemas, relay native results to middleware
- turn aborts are detected from the correlated turn/completed status instead
of byte-matching private codex-rs prose; the <turn_aborted> marker remains a
non-terminal user-interrupt hint (tags only, wording-independent) because the
app-server collapses all abort reasons into "interrupted" (#99268)
- dynamic-tool schemas are repaired before projection via the shared OpenAI
normalizer: annotation nulls stripped, type:null inferred from object/array
shape hints; unrepairable null constraints stay quarantined instead of
widening the schema (#106277, #97913)
- Codex-native tool results now flow through agentToolResultMiddleware at the
post_tool_use relay boundary (observe-only: the Codex PostToolUse contract
cannot replace a native tool response) (#95597)
* test(codex): regenerate prompt snapshots for normalized dynamic-tool schemas
The shared OpenAI strict-compat normalization now runs on Codex dynamic tools,
so empty object schemas gain additionalProperties:false and required:[] in the
snapshot payloads, matching what the OpenAI provider family sends.
* fix(github-copilot): reject unsupported OAuth enterprise domain before refresh and model routing
Legacy github-copilot OAuth credentials can carry a non-github.com
enterpriseUrl. The token-refresh path templated it into the endpoint and
sent the bearer refresh token there with no allowlist, and the model
routing path derived a base URL from the same credential, so an unexpired
credential kept routing its access token to that origin without ever
reaching a refresh.
A persisted credential origin that is not on the Copilot host allowlist
is now rejected before any request is issued and before any model URL is
produced. Because an off-allowlist origin means the stored access token
may itself have been minted by that origin, the credential is refused as
a whole rather than coerced to github.com: modifyModels withholds the
github-copilot models so the token has no route, and its proxy-ep is
never trusted. Login rejects an unsupported host up front, so every path
that turns a domain into a bearer-bearing URL validates first.
formatAuthDoctorHint guides affected users to re-authenticate, mirroring
the qwen-portal hint. Supported hosts are unchanged: github.com and
*.ghe.com data-residency tenants still route and refresh as before.
The allowlist plus a new isSupportedGithubCopilotDomain predicate move to
a dependency-free plugin-sdk leaf so the core OAuth runtime can share them
without closing a module cycle; plugin-sdk/provider-auth re-exports
normalizeGithubCopilotDomain, so the plugin-owned GitHub Copilot provider
keeps its existing import.
Fixes#103078.
* fix(github-copilot): resolve provider auth base conflict
* fix(github-copilot): resolve provider auth base conflict
* fix(github-copilot): validate token proxy endpoint
* fix(github-copilot): remove stale helper import
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Co-authored-by: Peter Steinberger <peter@steipete.me>
* refactor(plugin-sdk)!: delete the zero-consumer channel-ingress facade, AccessFacts projections, and dead dispatch aliases
* chore(plugin-sdk): refresh API baseline hash for the facade deletions
* fix(channels): drop dead AccessFacts field writes stranded by the facade deletion
* chore(plugin-sdk): refresh API baseline hash after rebase
* refactor(channels): converge internal group-policy drift onto the scope tree
* chore(plugin-sdk): refresh API baseline hash for scope-key exports
* fix(config): avoid shadowing the scopeKey export in the case-insensitive lookup
Mechanical: manager, combined facade, install path, lifecycle, config
loading, and resolver types move to focused modules; the runtime facade
re-exports its prior surface so importers are unchanged. Deletes dead
tools-barrel re-exports flagged by check-dependencies. No behavior
change.
Requester-scoped servers never enter harness-native MCP config: Codex
thread and CLI user-MCP projections partition them out (fail-closed,
static-partition fingerprints stay sender-independent so shared threads
never rotate). Codex delivers them as per-attempt dynamic tools through
a scoped-only runtime path on the same core manager, with a
session-stable advertised catalog so authed and unauthed senders share
one spec surface; unauthed calls get a clean not-connected tool error.
New SDK seam materializeRequesterScopedMcpToolsForHarnessRun lets any
future harness consume the same contract.
Plugins can register api.registerMcpServerConnectionResolver({serverName,
resolve}) to bind an MCP server's transport (url/headers) to the trusted
sender of each message. Server identity stays static; only the connection
is per-requester. Runtimes are cached per (sessionId, channel, accountId,
senderId); runs without a trusted requester fail closed (cron, subagent,
heartbeat). Resolved credentials never enter fingerprints or logs. Static
servers keep the existing session-scoped lifecycle byte-for-byte.
Enables per-user hosted MCP sessions (e.g. per-user OAuth email/calendar)
in multi-sender channels like Slack.
Refs #106229
Catalog session rows (sidebar context menu + click), the built-in viewer
header, and a new "Open Codex/Claude sessions in" preference can launch the
native CLI (codex resume / claude --resume) in the operator terminal on the
machine that owns the session.
- Gateway-local sessions spawn through the existing terminal launch policy
(sandbox/enabled gates preserved) with the resume command in the session cwd.
- Paired-node sessions run through a new seq-ordered node PTY relay: a
duplex node-host command streams PTY output via node.invoke.progress and
receives keystrokes/resize via a new node.invoke.input event, behind the
unchanged terminal.* client protocol (TerminalSessionManager gains a backend
abstraction; node relay reuses the streaming-invoke controller).
- Owner boundary: each plugin owns its resume command and builds argv from a
validated thread id; the gateway routes node opens through the node command
allowlist and plugin invoke policy (no advertisement-only trust), and nodes
re-verify session eligibility before spawning.
- UI setting catalogOpenTarget + canOpenTerminal capability gate every entry
point; capability requires the owning host to actually have the CLI.
Node PATH is normalized before command-availability probes, Windows .cmd/.bat
shims spawn via ComSpec, and catalog terminal opens reattach persisted tabs
before opening the new tab.
* fix(channels): time-gate progress drafts and defer narration until visible
Progress drafts started immediately on the second work event, so the 5s
initial delay never filtered small fast tasks and Discord showed narrated
status for turns that finish in seconds. The gate is now purely time-based
(explicit startNow paths like approvals/patches are unchanged), and the
progress narrator no longer spends utility-model completions before the
channel draft is visible - notes buffer so the first visible narration has
full context.
Surface: src/channels/streaming.ts gate, src/auto-reply/reply/progress-narrator.ts,
Discord draft-preview wiring; test updates across discord/matrix/msteams/slack.
Refs #105872
* feat(channels): model-preamble status headline for progress drafts
In progress mode the draft's status line is now the model's own typed pre-tool
preamble whenever it is fresh (always on, no new config); utility-model
narration fills the slot only when the model has been quiet for ~20s or emits
no preamble. Preambles never start the draft early - the time gate still owns
the small-vs-big decision - and narration generation is suppressed while a
fresh preamble exists, saving utility calls. Discord and Telegram render the
headline; the opt-in streaming.progress.commentary lane and its receipt
counters are unchanged.
Refs #105872
* refactor(channels): split status-text and telegram preview helpers to honor the LOC ratchet
The LOC ratchet forbids raising baseline entries, so the headline feature's
growth is paid for with clean splits: reasoning/commentary text normalization
moves to src/channels/progress-draft-status-text.ts, Telegram progress preview
rendering to extensions/telegram/src/progress-draft-preview.ts, and small
ownership moves (formatCompactionModelRef, resolveFollowupAbortSignal, shared
CLI preamble gate, Discord preamble item-event handling) shrink the runners
back under their pins. Baseline entries lowered to exact new counts; the
compositor entry is removed (424 lines, under the 500 ceiling).
Refs #105872
* test(auto-reply): add resolveFollowupAbortSignal to the queue mock factory
The explicit ./queue.js vi.mock factory must export every binding prod touches;
the relocated resolveFollowupAbortSignal was missing and failed the whole
followup-runner suite.
Refs #105872
* fix(channels): sanitize preamble headlines and retry skipped narration
Review findings: preamble headlines now strip inline delivery directives and
reject silent-control tokens (NO_REPLY) before rendering, sharing the
commentary sanitizer; rejected input keeps the previous valid headline. The
narrator schedules cancellation-aware retries when a run is skipped for draft
invisibility or preamble freshness, so single long tool calls still narrate
and the ~20s utility filler fires without needing another event. Silent
preambles no longer stamp freshness or add narration notes.
Refs #105872
* fix(channels): bound narrator visibility polling and gate Telegram headline mode on accepted preambles
Visibility retries stop after 30 one-second attempts (turn completion has no
narrator disposal signal) and re-arm on new activity; Telegram derives its
headline-rendering state from the compositor's sanitized headline instead of
raw payload text, so directive-only or NO_REPLY preambles keep structured
tool-line rendering. Docs scope the preamble headline to Discord and Telegram.
Refs #105872
* fix(telegram): read headline state from the compositor at render time
The progressHeadlineActive mirror flag was set after pushPreambleHeadline had
already rendered, so the first valid preamble arriving after the draft opened
rendered through the structured-line path and the headline never appeared
until the next event. The renderer and formatLine now read the compositor's
sanitized hasStatusHeadline directly and the mirror flag is deleted; adds an
ordering regression test. LOC baseline lowered to the exact new count.
Refs #105872
* fix(channels): let the opt-in commentary lane own preambles and scope narration docs to Discord
With streaming.progress.commentary enabled, the status headline would replace
the documented interleaved 💬 lines for any turn with a preamble (the exact
event the lane consumes). The compositor headline now declines when the
commentary lane is enabled, preserving that lane's shipped shape, and docs
state the utility-model filler is Discord-only today (Telegram's headline is
preamble-only; it has no narrator wiring).
Refs #105872
* fix(msteams): cancel the pending progress gate at finalize
With the purely time-based gate, every fast Teams progress turn leaves a
pending 5s start timer; the SDK resets its stream id on close, so a late
timer fired renderInformativeUpdate against the closed stream and posted a
stale working card below the final answer. finalize() now cancels the gate
and the informative renderer refuses to run once final text is queued.
Refs #105872
* docs(channels): record the headline-vs-commentary ownership decision inline
Refs #105872
* style: oxfmt the late review-fix rounds
Four files from the final review-fix commits were never oxfmt-formatted and
failed the shared CI formatting step; baseline lowered for the one-line
telegram shrink.
Refs #105872
* fix(channels): harden progress draft lifecycle
* fix(channels): refresh repeated preamble items
* fix(plugin-sdk): expose progress lifecycle options
* style(auto-reply): simplify narrator reset
* docs(channels): refresh progress draft docs map
* fix(auto-reply): clear queued narrator request context
* fix(discord): cancel delayed progress gate at final
* docs: refresh plugin sdk api baseline
* feat(config): add canonical group-policy scope-tree resolver
* chore(plugin-sdk): refresh API baseline hash for scope-tree exports
* refactor(channels): migrate googlechat, imessage, and whatsapp group policy onto the scope tree
* chore(plugin-sdk): refresh API baseline hash for groups scope-tree builder
* refactor(channels): migrate line, qqbot, and mattermost group policy onto the scope tree
* chore(plugin-sdk): refresh API baseline hash for case-insensitive scope key helper
* chore(plugin-sdk): refresh API baseline hash after rebase onto current main
* chore(plugin-sdk): refresh API baseline hash after rebase
* chore(plugin-sdk): refresh API baseline hash after rebase
* chore(plugin-sdk): refresh API baseline hash after rebase
* fix(sdk): add prototype-pollution guard to migration config merge
mergeMigrationConfigValue and writeMigrationConfigPath had no
isBlockedObjectKey guard, allowing __proto__/constructor/prototype
keys from imported config files to trigger prototype pollution.
Add the same guard used by the sibling config-path writer
(setConfigValueAtPath/parseConfigPath) to reject blocked object
keys before they reach a bracket assignment.
Fixes#103059
* fix(sdk): harden migration config patches
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* refactor: delete dead infra and config exports
* refactor: preserve live infra and config contracts
* refactor(config): remove obsolete file-store lifecycle APIs
* refactor(infra): finish current-main dead export cleanup
* refactor(deadcode): trim auto-reply and CLI exports
* refactor(deadcode): trim cron and task exports
* refactor(deadcode): trim fleet and process exports
* test(deadcode): exercise live task and process seams
* test(fleet): cover stream redaction through owner module
* refactor(security): trim dead internal exports
* refactor(secrets): trim dead internal exports
* refactor(deadcode): trim remaining src exports
* refactor(deadcode): remove test-only runtime exports
* refactor(deadcode): trim pairing test exports
* refactor(deadcode): reconcile refreshed baseline
* test(auto-reply): deduplicate queue state imports