* fix(external-context): read the response body with a reader, not for-await Async-iterating a ReadableStream needs [Symbol.asyncIterator] on the TYPE, and whether it is there depends on which lib set the program resolves — @types/node's stream has it, the DOM lib's needs lib.dom.asynciterable. That resolution flipped underneath this file on 2026-08-08: #8693 installed @types/jsdom at the root, vitest's types pull the jsdom types in wherever they exist, and jsdom's carry /// <reference lib="dom" />. #8693 shipped the tsconfig `types` guard in the same commit, so main stayed green — but the guard travels with the BRANCH while node_modules travel with the TRUSTED BASE in the autofix verification build, so every managed branch behind #8693 failed that build with TS2504 on this line. Two legs measured on run 31276008548: 63 minutes of accepted agent work discarded per round, 18 more minutes burned by a repair step that cannot fix a failure outside the PR's diff (#8614 reached attempt 13 that way; #8616 died identically). Reproduced locally in both directions before changing anything: @types/jsdom installed + guard removed = the gate's exact error, character for character; with the reader loop the same poisoned setup builds clean. The guard stays — belt and suspenders — but the build no longer depends on it, or on which lib set any future environment resolves. Behavior is unchanged and now pinned by tests the file never had: multi-chunk assembly, the exact MAX_RESPONSE_BYTES boundary (bound is strictly-greater), invalid-UTF-8 rejection, and the easy one to drop in this rewrite — cancelling the stream on early exit, which `for await` did implicitly via iterator return(). Mutation-tested: removing the cancel fails exactly that test against an endless producer. The package's other for-awaits iterate process.stdin (a Node stream, async-iterable in every lib set) and are untouched. * fix(external-context): await stream cancellation before rejecting the request On early exit from the reader loop (the oversize throw) cancellation was started fire-and-forget, so postJson() rejected while the stream's teardown was still settling — `for await` had awaited its implicit iterator return() before propagating. An immediate retry could overlap the previous response transport's unfinished cancellation. Await reader.cancel() before releaseLock(), and pin the sequencing with a deferred-cancel regression test that fails against the fire-and-forget form. Also cover read() rejecting after a partial chunk was received: the error maps to the request-did-not-complete transport error rather than EOF-then-parse of the partial JSON, and the reader lock is still released. * fix(external-context): drop the types guard the reader rewrite made obsolete The `"types": ["node"]` override existed solely to keep @types/jsdom's lib.dom out of this program while http-client.ts read the response body with `for await` — the DOM lib's ReadableStream is not async-iterable, and the flip broke the build with TS2504 (#8693). The reader loop that replaced the `for await` types identically in every lib set, so the guard is no longer load-bearing: with it removed, lib.dom re-enters the program and the package still builds cleanly. Drop it with its stale comment instead of leaving maintainers two contradicting stories about whether it is needed. Also export MAX_RESPONSE_BYTES and import it in the boundary tests instead of re-declaring it locally, so the tests pin the real constant rather than a copy that can silently drift. * test(external-context): make the invalid-UTF-8 test pin fatal decoding --------- Co-authored-by: verify <verify@local> Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com> |
||
|---|---|---|
| .. | ||
| examples | ||
| src | ||
| package.json | ||
| qwen-extension.json | ||
| README.md | ||
| tsconfig.json | ||
| vitest.config.ts | ||
External Context extension
This private Qwen Code integration connects one interactive CLI process to one administrator-bound external context corpus without changing Qwen Core. It has three managed deployment variants:
- Read-only on-demand: version 1 configuration and the MCP
context_search({ query })tool. - Mem0 write: version 1 Mem0 configuration, the on-demand search tool, and
the optional
context_remember({ content })tool with a separate content-visible confirmation Hook. - Auto-recall: version 2 configuration and an administrator-installed
UserPromptSubmitHook, with no external-context MCP server.
The built-in adapters support Mem0 Platform V3 search and a small Generic HTTP Search V1 contract for existing knowledge or RAG services. Only Mem0 has an optional write path. There is no generic ingestion protocol, personal memory, trusted user identity, per-document ACL, or tamper-resistant audit.
Use the governed Gateway/Orchestrator Profile described in #7449 when those controls are required.
Trust boundary
In the read-only on-demand profile, the model can provide only the search
query. In the Mem0 write variant, it can additionally provide only the exact
content to store. In the auto-recall profile, the query is derived only from
Qwen's optional submitted_prompt provenance, captured before model-bound
expansions.
Provider type, endpoint, credential, Mem0 app_id, and all other corpus
selectors are fixed before either process starts.
The actual corpus-isolation boundary is the provider-side credential, project,
index, or corpus. A Mem0 app_id or any other client-supplied filter is
classification, not authorization. Read-only deployments should use a
read-only credential where the provider supports one. The write variant
requires a credential that can add memories and therefore grants same-UID code
access to that capability outside Qwen as well.
For on-demand retrieval, the extension manifest alone is not a managed binding.
Qwen merges MCP servers by name, and a settings, project, or command-line
server named external-context can replace the manifest contribution while
retaining the same permission-rule name. A managed deployment must therefore
start Qwen with an administrator-owned --mcp-config based on
examples/managed-mcp.json. The Phase 1 launcher must construct the complete
Qwen argument vector itself and must not pass through arbitrary caller
arguments. This command-line tier overrides user, project, workspace, and
system MCP settings. The documented permission rule is safe to deploy only
inside that pinned process.
The launcher must also construct an administrator-approved environment rather
than inherit caller-controlled values. Qwen can subsequently load values from
the repository's .env and .qwen/.env files, so the Direct Profile requires
the repository, those files, and same-UID code to be trusted. The source pin
prevents same-name MCP configuration collisions; it is not a process sandbox.
Use the governed profile when those inputs may be hostile or when credentials
and process execution must be isolated.
The search tool omits MCP readOnlyHint because a provider search may record
access metadata or otherwise have provider-side read effects. It exposes no
explicit mutation in the default extension manifest. The managed Mem0 write
variant exposes an explicit add operation. Both variants pass content to an
external service, so this integration is not a DLP boundary. Credentials
inherited by Qwen may also be visible to same-UID processes and tools. Use the
governed profile when the credential, write authorization, or outbound-content
policy must be isolated from the CLI user.
Retrieved context remains untrusted reference data if the model proposes storing it back into the shared corpus. A user approval does not upgrade that trust label. Review the complete write carefully because persisting retrieved or injected text can expose it to later users and model turns.
The managed-settings example allows Qwen to invoke search without per-call
confirmation. It is on-demand rather than prompt-triggered, but it is not
necessarily initiated manually by the user. In interactive non-YOLO mode,
placing the tool under permissions.ask requests confirmation. YOLO mode
auto-approves ordinary tools despite ask, and users can change approval mode
during a session. Phase 1 does not provide non-bypassable per-call
confirmation; use the governed profile when that is required.
Configure
On-demand profile
-
Give the repository its own provider-side project, index, or corpus and a credential restricted to it. Verify that the credential cannot access or select another corpus.
-
Copy the applicable provider configuration from
examples/to an administrator-owned location outside the repository that the CLI user cannot modify. ConfigureapiKeyEnvortokenEnvif needed, then set the referenced environment variable to the credential.timeoutMsdefaults to 5000 and may be between 1 and 30000 milliseconds. -
Have the managed launcher set
QWEN_EXTERNAL_CONTEXT_CONFIGto the absolute configuration path. -
From the Qwen Code checkout, install dependencies and build this workspace:
npm install npm run build --workspace @qwen-code/external-contextPhase 1 is a private monorepo workspace. Copying the directory or its npm tarball without packaging its runtime dependencies is not a supported deployment.
-
Copy
examples/managed-mcp.jsonto an administrator-owned location and replace every placeholder with an absolute path. Thecommand,args, andcwdmust identify an administrator-controlled Node executable, reviewed checkout, and dependency tree that the CLI user cannot modify. The managed launcher must accept no arbitrary Qwen arguments. It must construct a clean, administrator-approved environment, inject the provider configuration and credential, change to the intended repository, and invoke:qwen --mcp-config /administrator/path/external-context-mcp.jsonThe MCP subprocess honors
HTTP_PROXY,HTTPS_PROXY, andNO_PROXYthrough an environment-aware dispatcher. If the provider requires an egress proxy, the launcher must include the administrator-approved proxy variables in that clean environment. Includelocalhost,127.0.0.1, and[::1]inNO_PROXYwhen using the loopback Generic HTTP provider. -
Point
QWEN_CODE_SYSTEM_SETTINGS_PATHat an administrator-controlled copy ofexamples/managed-settings.jsononly inside this managed launcher; do not install its automatic allow rule for unrelated Qwen sessions. It disables/cdto reduce accidental workspace/corpus mismatch and allows the pinned search tool. Neither setting is an authorization boundary: the provider credential is still the corpus boundary, and a new Qwen process is required to switch repositories.
For a local trusted trial, the built directory may instead be linked with
qwen extensions link. The extension manifest contribution and
workspace-scoped enablement are convenience mechanisms only; they do not
provide the managed MCP source binding described above. Before starting Qwen,
the trial environment must set QWEN_EXTERNAL_CONTEXT_CONFIG to the absolute
configuration path and set the credential environment variable referenced by
that file.
Each MCP subprocess reads configuration and credentials once when it starts.
Qwen may restart that subprocess, so the configuration path, file contents,
and credential-to-corpus binding must remain immutable for the whole Qwen
session. Do not overwrite or reuse a configuration path for another corpus.
Changing the working directory does not change the configured corpus. The
managed settings disable Qwen's /cd command as an accidental-misuse guard,
but cannot prevent every same-UID action. To switch corpora, terminate the old
Qwen session and start a new one with a new managed configuration path.
Managed Mem0 write variant
This variant is an explicit opt-in for trusted collaborators who need to save
repository-shared notes. It uses Mem0 Direct Import with infer: false, so the
validated input is sent unchanged as one user message. Direct Import skips
Mem0 inference and duplicate detection. Repeating an approved write may create
another copy of the same content.
- Provision a dedicated Mem0 Project for the repository. Do not share its key
with another security domain. Mem0
app_idremains classification within the Project, not authorization. - Copy
examples/mem0-write.jsonto an administrator-owned path and set the referencedMEM0_API_KEY.writeis accepted only for version 1 Mem0 configurations. Generic HTTP and version 2 configurations reject it. The top-leveltimeoutMsapplies to both search and write; choose a write budget appropriate for the deployment because a client timeout produces an ambiguousunknownresult. - Build the workspace so both
dist/main.jsanddist/write-confirmation.jsexist. - Start Qwen with an administrator-owned copy of
examples/managed-mem0-write-mcp.json. The default extension manifest stays search-only and does not enablecontext_remember. Register this managed MCP server under exactlyexternal-context; the Hook matcher usesmcp__external-context__context_remember, and the command ignores other tools if a matcher is accidentally broadened. Another server name would therefore prevent the content confirmation from running. - Point
QWEN_CODE_SYSTEM_SETTINGS_PATHat an administrator-controlled copy ofexamples/managed-mem0-write-system-settings.json. The provided system settings deliberately omit the Hook. Put the applicable POSIX or Windows managed write user-settings example in a dedicated administrator-controlledQWEN_HOME/settings.json. User-scope Hooks take precedence over legacy merged Hooks, but a system-scope Hook can still run when user Hooks are absent, so audit both scopes and avoid additional matchers for this command. - Use the same fixed-path, no-user-arguments, environment-allowlist launcher
requirements as the read-only profile. In addition, pin the Hook and
dedicated
QWEN_HOME; refuse headless, ACP,serve, resume/continue, and startup YOLO modes; and setQWEN_CODE_MEMORY_TEAM=0,QWEN_CODE_MEMORY_TEAM_SYNC=0,QWEN_TELEMETRY_ENABLED=0,QWEN_TELEMETRY_LOG_PROMPTS=0,QWEN_TELEMETRY_INCLUDE_SENSITIVE_SPAN_ATTRIBUTES=0, andQWEN_USAGE_STATISTICS_ENABLED=0. On Windows, the approvedPATHmust resolvepowershellto the system executable, and PowerShell profiles must be absent or administrator-controlled.
In regular interactive mode, the permissions.ask rule presents Qwen's normal
server/tool confirmation and the PreToolUse Hook then presents a second
confirmation containing a reversible, safely escaped representation of the
complete text. The Hook confirmation is rendered literally rather than as
inline Markdown, so formatting markers and link targets remain visible. If the
text exceeds the constrained terminal area, Qwen reports the hidden line count
and Ctrl-S reveals the remaining confirmation content. YOLO
bypasses the ordinary permission rule, but a working Hook still asks once.
Plan mode, unknown permission modes, malformed Hook input, and environments
unable to confirm are denied. Extra tool arguments are ignored consistently by
the Hook and MCP schema and never reach Mem0. Hook transport failure follows Qwen's existing
fail-open semantics, so this is a user-experience safeguard, not an enforceable
authorization boundary. Strict approval requires the governed profile.
The confirmation Hook code does not read the Provider configuration or credential, but Qwen command Hooks inherit ordinary third-party credentials from the parent environment. The Hook process may therefore receive the Mem0 key in its environment. The Direct Profile does not provide credential isolation from administrator-pinned child processes or same-UID code.
context_remember accepts at most 4000 Unicode code points, preserves leading
and trailing whitespace, and rejects empty/invisible-only text and malformed
UTF-16. It never accepts a model-selected tenant, Project, app_id, filter, or
metadata. Each approval performs exactly one request; the integration does not
pre-search, retry, poll, cache, or deduplicate.
Mem0 V3 Add is asynchronous in normal operation. PENDING with a valid
event_id is therefore the expected successful response and returns
accepted, not stored. A valid synchronous SUCCEEDED response returns
stored as a defensive compatibility path. Explicit FAILED and HTTP 400,
401, 403, or 404 responses return a stable failed result. A timeout,
cancellation, redirect, other HTTP status, broken response, unexpected status,
or invalid operation identifier returns unknown and an MCP error warning the
model not to retry automatically, because Mem0 may already have accepted the
write. Cancellation therefore does not prove that no memory was created.
Auto-recall profile
Auto-recall sends a sanitized best-effort query to the external provider for
each eligible ordinary interactive prompt. It requires a non-empty
submitted_prompt captured by the supported interactive TUI before reminders,
file and resource expansion, extension output, and vision expansion. It never
falls back to the legacy model-bound prompt. Missing or invalid provenance
fails closed before configuration or credentials are read. Common credential
shapes are removed from the submitted text, but this is not DLP.
- Copy
examples/auto-recall-mem0.jsonorexamples/auto-recall-generic-http.jsonto an administrator-owned location. SetrepositoryRootto the one absolute repository bound to the Provider credential. The directory must exist and must not be a filesystem root. - Build this workspace so
dist/auto-recall.jsexists. - Put the applicable
examples/managed-auto-recall-user-settings-posix.jsonorexamples/managed-auto-recall-user-settings-windows.jsoncontent in thesettings.jsonof a dedicated administrator-controlledQWEN_HOME. Replace all placeholders with fixed absolute Node and Hook paths. - Point
QWEN_CODE_SYSTEM_SETTINGS_PATHat an administrator-controlled copy ofexamples/managed-auto-recall-system-settings.json. Its system-leveldisableAllHooks: falseprevents lower-precedence workspace settings from suppressing the required Hook. - Use a launcher that accepts no arguments, verifies stdin and stdout are
TTYs, starts in
repositoryRoot, supplies only an administrator-approved environment, and fixes all Qwen, Node, Hook, configuration, settings, and credential paths. It must setQWEN_CODE_MEMORY_TEAM=0,QWEN_CODE_MEMORY_TEAM_SYNC=0,QWEN_TELEMETRY_ENABLED=0,QWEN_TELEMETRY_LOG_PROMPTS=0,QWEN_TELEMETRY_INCLUDE_SENSITIVE_SPAN_ATTRIBUTES=0, andQWEN_USAGE_STATISTICS_ENABLED=0. On Windows, the approvedPATHmust resolvepowershellto the system executable, and PowerShell profiles must be absent or administrator-controlled. A user-controlled shell shim or profile is outside the Direct Profile trust model.
Do not link or enable the external-context Extension Manifest and do not
install examples/managed-mcp.json in the auto-recall process; either action
would add the on-demand MCP surface and permit duplicate retrieval. The
shared configuration loader accepts v1 and v2, but the MCP process entry point
requires v1 and the Hook requires v2. Supplying the same v2 configuration to
the MCP therefore fails startup. The managed Auto Profile must still omit the
extension and MCP configuration because a separately configured v1 MCP process
would permit duplicate retrieval. In v2, autoRecall.timeoutMs is the only
request timeout the Hook reads; the top-level timeoutMs remains for schema
compatibility and has no current runtime consumer. The profile supports only a
fresh interactive TTY session. The managed system settings explicitly disable
speculative execution because accepted speculation can bypass the normal
UserPromptSubmit path. It does not support
-p, stream-json, ACP, serve, YOLO, --continue, --resume, arbitrary
launcher arguments, or switching repositories in one process. Mid-turn
steering messages do not fire UserPromptSubmit and therefore do not trigger
recall; only ordinary submitted turns are eligible.
The Hook reads at most 1 MiB from stdin, emits at most 4000 code units of
structured untrusted_external_context, performs no retries or caching, and
fails open as {} after the Node entry point starts. Failure to spawn the
pinned Node process and a Qwen outer command timeout retain Qwen's blocking
command-Hook semantics. The Provider timeout defaults to 1500ms and is capped
at 5000ms; the internal Hook wall-clock budget is 6500ms and the managed Qwen
command timeout is 8000ms. Each Hook invocation destroys its own proxy
dispatcher after the attempted retrieval so stalled proxy connections cannot
retain the child process; the long-running MCP process keeps its dispatcher.
Retrieved results are sent to the model provider as user-layer additional context. The managed profile disables Qwen chat recording, native memory, usage statistics, and telemetry by default. Provider-side access logs remain outside this integration's control.
For Mem0 auto-recall, verify that Memory Decay is disabled for the bound Project. If that cannot be verified, use the on-demand profile.
The integration emits no local per-request audit record. It does not write
queries, results, credentials, provider errors, or operation metadata to
stderr. The on-demand MCP entry point may write a sanitized startup
configuration error once before it exits; unexpected startup failures remain
opaque. The auto-recall Hook emits only {} on failure.
Operators who need access records may use provider-side logs, but those are
outside this integration and are not a tamper-resistant compliance audit.
Generic HTTP Search V1
The configured baseUrl must be an origin with no path, query, credentials, or
fragment. That origin receives a request at the fixed path
/v1/context/search:
POST /v1/context/search
Authorization: Bearer <credential>
Accept: application/json
Content-Type: application/json
{"query":"normalized query","limit":5}
The response is:
{
"items": [
{
"id": "opaque-id",
"content": "retrieved text",
"title": "optional title",
"uri": "optional provenance URI",
"score": 0.82,
"updated_at": "2026-07-23T00:00:00Z"
}
]
}
The fixed endpoint and the credential's effective capabilities must together restrict access to one corpus. A bearer credential that can access another corpus through another endpoint or selector does not meet the Direct Profile boundary. The request contains no client-selected tenant, repository, namespace, or filter. HTTPS is required except for explicit loopback HTTP used in local development.
Mem0 Platform V3
The adapter calls POST /v3/memories/search/ with the configured app_id,
top_k: 5, threshold: 0.1, and rerank: false. The API key's effective
Mem0 Project must already be restricted to the intended corpus; a different
app_id in the same broadly accessible Project does not establish isolation.
The default extension manifest and read-only examples do not enable writes.
The managed write variant additionally calls POST /v3/memories/add/ with one
exact user message, the configured app_id, and infer: false. It exposes no
get-all, update, delete, delete-all, entity, event, or Project-management tool.
Mem0 Direct Import does not perform duplicate detection, and the asynchronous
Add API can leave the client unable to determine whether a write was accepted.
The integration therefore never retries an ambiguous write.
Mem0 Memory Decay is opt-in and off by default. If enabled, search reinforces returned memories, updates access history, and can affect later ranking. Keep it disabled when search must have no semantic provider-side state change. Provider audit or access logs may still be retained. See Mem0 Memory Decay.
Rollout and rollback
Start with the pinned read-only on-demand MCP for one workspace and validate search quality and provenance. For writes, first run the repository's interactive fake-Mem0 test harness, then progress through an isolated temporary Mem0 Project, one trusted repository, and a small team. The shipped Mem0 configuration always targets Mem0 Platform; only the test harness injects a local endpoint. Enable auto-recall only after the administrator accepts automatic query forwarding. Do not run auto-recall and an on-demand MCP in one process.
Removing the pinned MCP or auto-recall Hook from the managed launcher rolls back the Qwen integration; local on-demand trials can instead disable or remove the extension. Restore a preserved v1 configuration before rolling back to a binary that does not understand v2. To roll back the write variant, remove its MCP configuration, Hook, and credential, restore the read-only v1 configuration, and restart Qwen. Rollback does not delete memories already accepted by Mem0 or remove provider-side search logs and access metadata; administrators must handle existing records at the Provider.