From afb93aec26f696707c13a06ad7c4cc322849d3f1 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Thu, 15 Oct 2026 05:37:40 +0800 Subject: [PATCH 01/16] feat(dsh): add opt-in native context observation tool Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- packages/dsh-loopx-plugin/README.md | 46 +++++++ packages/dsh-loopx-plugin/package.json | 4 + .../smoke/dsh-client-artifact-smoke.mjs | 15 ++- .../smoke/dsh-native-context-sdk-smoke.py | 61 +++++++++ .../src/native-operation-context.ts | 122 ++++++++++++++++++ .../tests/native-operation-context.test.ts | 77 +++++++++++ packages/dsh-loopx-plugin/tsdown.config.ts | 1 + 7 files changed, 325 insertions(+), 1 deletion(-) create mode 100644 packages/dsh-loopx-plugin/smoke/dsh-native-context-sdk-smoke.py create mode 100644 packages/dsh-loopx-plugin/src/native-operation-context.ts create mode 100644 packages/dsh-loopx-plugin/tests/native-operation-context.test.ts diff --git a/packages/dsh-loopx-plugin/README.md b/packages/dsh-loopx-plugin/README.md index 24af7b16f2..0fb026e08c 100644 --- a/packages/dsh-loopx-plugin/README.md +++ b/packages/dsh-loopx-plugin/README.md @@ -275,6 +275,52 @@ This is an experimental adapter implementing the RFC's P0 prototype components. It does not establish the RFC's P0 exit: C0 fidelity, a qualifying C1 run, and measured observer overhead remain separate evidence gates. +## Optional native operation context (SDK prerequisite) + +The independently loaded `dsh-loopx-plugin/native-operation-context` subpath +registers one read-only model tool, `loopx_operation_context`. It is absent from +the default plugin tree and `cordis.patch.yml`; installing the package does not +activate it. An explicit SDK Loader patch can load the built module: + +```yaml +- insert: + - id: loopx-native-operation-context + name: file:///absolute/path/to/dsh-loopx-plugin/lib/native-operation-context.js +``` + +The tool accepts `{}` only. It derives the calling live session and open Turn +from DSH's agent and session-projection services; caller-supplied identities, +closed Turns, cancelled calls and disposed or shadowed tool registrations fail. +Only provider, model and reasoning-effort options are returned. Other agent +options, including credentials, are excluded. Profile options are observations, +not HTTP wire attestation; `wire_profile_verified` and `execution_allowed` are +always `false`. This module creates no Core actor, grant, binding, approval or +account access, and consumes no approval. + +The initial runtime qualification uses the Python SDK and bundled DSH +`0.1.5rc1`: a real native tool call returned the matching SDK session and open +Turn. This does not qualify other DSH versions or the complete managed operation +journey. From a source checkout, build and verify registration using an +interpreter with the selected `deepseek-harness` SDK installed: + +```bash +pnpm --dir packages/dsh-loopx-plugin build +"$SDK_PYTHON" packages/dsh-loopx-plugin/smoke/dsh-native-context-sdk-smoke.py \ + --module packages/dsh-loopx-plugin/lib/native-operation-context.js \ + --provider deepseek-official --model deepseek-flash +``` + +This smoke initializes the real SDK with an isolated temporary home and dummy +credentials, checks the runtime's schema registration, and makes zero model +requests. It does not execute a Turn or test financial operations. Remove the +explicit Loader row to disable the module, then restart the owning SDK runtime; +the default plugin tree remains unchanged. Existing active sessions must be +closed by their owner rather than treated as revoked by editing a patch file. + +Core parent admission, verified dispatch/profile identity, revocation and first +approval consumption remain separate integration work. No frontend or Lark +operation path is delivered by this prerequisite. + ## GoalBar authority and privacy boundary The GoalBar carrier uses Connection's authenticated `/api/loopx.goalbar` diff --git a/packages/dsh-loopx-plugin/package.json b/packages/dsh-loopx-plugin/package.json index 4792bd49e9..4c784b9c10 100644 --- a/packages/dsh-loopx-plugin/package.json +++ b/packages/dsh-loopx-plugin/package.json @@ -22,6 +22,10 @@ "types": "./lib/types/observer.d.ts", "default": "./lib/observer.js" }, + "./native-operation-context": { + "types": "./lib/types/native-operation-context.d.ts", + "default": "./lib/native-operation-context.js" + }, "./client": { "types": "./lib/types/client/index.d.ts", "default": "./lib/client.js" diff --git a/packages/dsh-loopx-plugin/smoke/dsh-client-artifact-smoke.mjs b/packages/dsh-loopx-plugin/smoke/dsh-client-artifact-smoke.mjs index 6d70629dd2..14aa46184b 100644 --- a/packages/dsh-loopx-plugin/smoke/dsh-client-artifact-smoke.mjs +++ b/packages/dsh-loopx-plugin/smoke/dsh-client-artifact-smoke.mjs @@ -43,6 +43,7 @@ const packedStaticEntries = new Set([ 'package/lib/driver.js', 'package/lib/index.js', 'package/lib/init-command.js', + 'package/lib/native-operation-context.js', 'package/lib/observer.js', 'package/lib/types/cli.d.ts', 'package/lib/types/client/LoopXGoalBar.d.ts', @@ -60,6 +61,7 @@ const packedStaticEntries = new Set([ 'package/lib/types/index.d.ts', 'package/lib/types/init-command.d.ts', 'package/lib/types/managed-runtime.d.ts', + 'package/lib/types/native-operation-context.d.ts', 'package/lib/types/observer.d.ts', 'package/package.json', ]) @@ -143,6 +145,10 @@ async function assertManifest(root) { types: './lib/types/observer.d.ts', default: './lib/observer.js', }) + assert.deepEqual(manifest.exports?.['./native-operation-context'], { + types: './lib/types/native-operation-context.d.ts', + default: './lib/native-operation-context.js', + }) assert.deepEqual(manifest.dsh?.client, { inject: clientInject, platform: 'web', @@ -458,14 +464,16 @@ async function assertHostExports(root) { init: requireFromPlugin.resolve(`${packageId}/init-command`), driver: requireFromPlugin.resolve(`${packageId}/driver`), observer: requireFromPlugin.resolve(`${packageId}/observer`), + nativeContext: requireFromPlugin.resolve(`${packageId}/native-operation-context`), client: requireFromPlugin.resolve(`${packageId}/client`), } assert.equal(resolutions.client, join(root, 'lib', 'client.js')) - const [host, init, driver, observer] = await Promise.all([ + const [host, init, driver, observer, nativeContext] = await Promise.all([ import(pathToFileURL(resolutions.root).href), import(pathToFileURL(resolutions.init).href), import(pathToFileURL(resolutions.driver).href), import(pathToFileURL(resolutions.observer).href), + import(pathToFileURL(resolutions.nativeContext).href), ]) assert.equal(typeof host.apply, 'function') assert.equal(host.name, packageId) @@ -522,6 +530,10 @@ async function assertHostExports(root) { assert.deepEqual(observer.inject, []) assert.equal(typeof observer.apply, 'function') assert.equal(host.ShadowObserver, undefined) + assert.equal(nativeContext.name, 'loopx-native-operation-context') + assert.deepEqual(nativeContext.inject, ['agents', 'tools', 'sessionProjections']) + assert.equal(typeof nativeContext.apply, 'function') + assert.equal(host.createNativeContextTool, undefined) const [rootSource, driverSource, observerSource] = await Promise.all([ readFile(resolutions.root, 'utf8'), @@ -531,6 +543,7 @@ async function assertHostExports(root) { for (const source of [rootSource, driverSource]) { assert(!source.includes('LOOPX_DSH_SHADOW_OBSERVER_GOAL_ID')) assert(!source.includes('reliability_observer_envelope_v0')) + assert(!source.includes('loopx_operation_context')) } assert(observerSource.includes('LOOPX_DSH_SHADOW_OBSERVER_GOAL_ID')) assert(!observerSource.includes('.send(')) diff --git a/packages/dsh-loopx-plugin/smoke/dsh-native-context-sdk-smoke.py b/packages/dsh-loopx-plugin/smoke/dsh-native-context-sdk-smoke.py new file mode 100644 index 0000000000..4e9facb75b --- /dev/null +++ b/packages/dsh-loopx-plugin/smoke/dsh-native-context-sdk-smoke.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 +"""Load the optional native context module in the real Python SDK, without a Turn. + +Run with an interpreter containing deepseek-harness. This checks the runtime's +actual tool/schema registration, which a structural service fake cannot check. +No model request, Core binding, approval or account operation is performed. +""" + +import argparse +import json +import tempfile +from pathlib import Path + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--module", type=Path, required=True) + parser.add_argument("--provider", required=True) + parser.add_argument("--model", required=True) + args = parser.parse_args() + module = args.module.resolve(strict=True) + from deepseek_harness import DeepSeekHarness, DeepSeekHarnessConfig + + with tempfile.TemporaryDirectory(prefix="dsh-native-context-smoke-") as directory: + root = Path(directory) + # Keep SDK stdout ownership intact. The wrapper forbids model requests + # even if SDK initialization behavior changes in a later release. + wrapper = root / "smoke.mjs" + wrapper.write_text( + "import {apply as register} from " + json.dumps(module.as_uri()) + ";\n" + "export const name='native-context-startup-smoke';\n" + "export const inject=['agents','tools','sessionProjections'];\n" + "export function apply(ctx) { register(ctx);\n" + "ctx.on('agent/request', () => { throw new Error('model requests forbidden in startup smoke') });\n" + "}\n", + encoding="utf-8", + ) + patch = root / "smoke.patch.yml" + patch.write_text( + "- insert:\n - id: native-context-startup-smoke\n name: " + + json.dumps(wrapper.as_uri()) + "\n", + encoding="utf-8", + ) + config = DeepSeekHarnessConfig( + provider=args.provider, + model=args.model, + cwd=str(root), + dsh_home=str(root / "sdk-home"), + patches=(str(patch),), + # Never expose a caller's genuine provider credential to this smoke. + api_key="startup-smoke-placeholder", + base_url="http://127.0.0.1:9", + initialize_timeout_seconds=30, + request_timeout_seconds=5, + ) + with DeepSeekHarness(config): + print(json.dumps({"initialized": True, "model_request_count": 0})) + + +if __name__ == "__main__": + main() diff --git a/packages/dsh-loopx-plugin/src/native-operation-context.ts b/packages/dsh-loopx-plugin/src/native-operation-context.ts new file mode 100644 index 0000000000..a07d25e7b0 --- /dev/null +++ b/packages/dsh-loopx-plugin/src/native-operation-context.ts @@ -0,0 +1,122 @@ +/** Optional read-only native DSH metadata producer; not a Core actor issuer. */ + +export const name = 'loopx-native-operation-context' +export const inject = ['agents', 'tools', 'sessionProjections'] +export const NATIVE_CONTEXT_TOOL = 'loopx_operation_context' + +interface NativeAgent { + id: string + session: { id: string } + status: string + options: { provider?: unknown, model?: unknown, reasoningEffort?: unknown } +} + +interface Execution { + agent?: NativeAgent + signal: AbortSignal +} + +interface NativeContext { + schema_version: 'loopx_dsh_native_context_v0' + session_id: string + native_turn_number: number + turn_start_seq: number + profile_source: 'agent_options' + profile: { provider: string | null, model: string | null, reasoning_effort: string | null } + wire_profile_verified: false + execution_allowed: false +} + +export interface NativeContextTool { + name: string + description: string + parameters: object + output: { schema: object, render(args: unknown, value: unknown): { type: 'text', text: string }[] } + execute(args: unknown, exec: Execution): Promise +} + +/** Structural ports of DSH's existing services; this module owns no registry. */ +export interface NativeContextHost { + agents: { get(id: string): object | undefined } + tools: { + register(tool: NativeContextTool): unknown + get(name: string, scope: object): object | undefined + } + sessionProjections: { + stateOf(session: object, key: 'turnBoundary'): { + lastTurn: number + openTurnStartSeq: number | null + } | undefined + } +} + +const nullableString = { oneOf: [{ type: 'string' }, { type: 'null' }] } + +function observedString(value: unknown): string | null { + return typeof value === 'string' && value.length > 0 ? value : null +} + +export function createNativeContextTool(host: NativeContextHost): NativeContextTool { + const tool: NativeContextTool = { + name: NATIVE_CONTEXT_TOOL, + description: 'Read this calling DSH session, open native Turn and agent profile options. Observation only: no approval, execution grant or account access. Takes no arguments.', + parameters: { type: 'object', properties: {}, additionalProperties: false }, + output: { + schema: { + type: 'object', additionalProperties: false, + required: ['schema_version', 'session_id', 'native_turn_number', 'turn_start_seq', 'profile_source', 'profile', 'wire_profile_verified', 'execution_allowed'], + properties: { + schema_version: { type: 'string', const: 'loopx_dsh_native_context_v0' }, + // DSH's supported schema subset omits range/length keywords; + // execute enforces nonempty identity and safe native boundaries. + session_id: { type: 'string' }, + native_turn_number: { type: 'integer' }, + turn_start_seq: { type: 'integer' }, + profile_source: { type: 'string', const: 'agent_options' }, + profile: { + type: 'object', additionalProperties: false, + required: ['provider', 'model', 'reasoning_effort'], + properties: { provider: nullableString, model: nullableString, reasoning_effort: nullableString }, + }, + wire_profile_verified: { type: 'boolean', const: false }, + execution_allowed: { type: 'boolean', const: false }, + }, + }, + render: (_args, value) => [{ type: 'text', text: JSON.stringify(value) }], + }, + async execute(args, exec) { + if (args === null || typeof args !== 'object' || Array.isArray(args) || Object.keys(args).length !== 0) { + throw new Error('native context takes no caller identity or arguments') + } + if (exec.signal.aborted) throw new Error('native context call aborted') + const agent = exec.agent + if (!agent || typeof agent.id !== 'string' || !agent.id + || agent.session?.id !== agent.id || agent.status !== 'running' + || host.agents.get(agent.id) !== agent + || host.tools.get(NATIVE_CONTEXT_TOOL, agent) !== tool) { + throw new Error('native context requires the current live calling agent and tool') + } + const boundary = host.sessionProjections.stateOf(agent.session, 'turnBoundary') + if (!boundary || !Number.isSafeInteger(boundary.lastTurn) || boundary.lastTurn < 1 + || !Number.isSafeInteger(boundary.openTurnStartSeq) || (boundary.openTurnStartSeq ?? -1) < 0) { + throw new Error('native context requires an open native Turn') + } + return { + schema_version: 'loopx_dsh_native_context_v0', session_id: agent.id, + native_turn_number: boundary.lastTurn, turn_start_seq: boundary.openTurnStartSeq as number, + profile_source: 'agent_options', + profile: { + provider: observedString(agent.options?.provider), model: observedString(agent.options?.model), + reasoning_effort: observedString(agent.options?.reasoningEffort), + }, + wire_profile_verified: false, execution_allowed: false, + } + }, + } + return tool +} + +/** Explicit Loader opt-in only; the default plugin tree does not load this. */ +export function apply(host: NativeContextHost): void { + host.tools.register(createNativeContextTool(host)) +} diff --git a/packages/dsh-loopx-plugin/tests/native-operation-context.test.ts b/packages/dsh-loopx-plugin/tests/native-operation-context.test.ts new file mode 100644 index 0000000000..093e2973f5 --- /dev/null +++ b/packages/dsh-loopx-plugin/tests/native-operation-context.test.ts @@ -0,0 +1,77 @@ +import assert from 'node:assert/strict' +import { test } from 'vitest' +import { createNativeContextTool, NATIVE_CONTEXT_TOOL } from '../src/native-operation-context.ts' + +function fixture() { + const agent = { id: 'sdk-session', session: { id: 'sdk-session' }, status: 'running', + options: { provider: 'fixture-provider', model: 'fixture-model', reasoningEffort: 'high', apiKey: 'private-fixture' } } + let live: object | undefined = agent + let current: object | undefined + const boundary = { lastTurn: 1, openTurnStartSeq: 0 as number | null } + const host = { + agents: { get: (_id: string) => live }, + tools: { register: () => {}, get: (_name: string, _scope: object) => current }, + sessionProjections: { stateOf: (_session: object, _key: 'turnBoundary') => boundary }, + } + const tool = createNativeContextTool(host) + current = tool + const exec = { agent, signal: new AbortController().signal } + return { agent, boundary, tool, exec, revokeAgent: () => { live = undefined }, revokeTool: () => { current = undefined } } +} + +test('native context uses the live agent and open Turn; copies no credential fields', async () => { + const f = fixture() + const r = await f.tool.execute({}, f.exec) + assert.equal(f.tool.name, NATIVE_CONTEXT_TOOL) + assert.equal(r.session_id, 'sdk-session') + assert.equal(r.native_turn_number, 1) + assert.equal(r.turn_start_seq, 0) + assert.equal(r.profile_source, 'agent_options') + assert.deepEqual(r.profile, { provider: 'fixture-provider', model: 'fixture-model', reasoning_effort: 'high' }) + assert.equal(r.wire_profile_verified, false) + assert.equal(r.execution_allowed, false) + assert.ok(!JSON.stringify(r).includes('private-fixture')) + f.boundary.lastTurn = 2 + f.agent.options.model = 'replacement-model' + const next = await f.tool.execute({}, f.exec) + assert.equal(next.native_turn_number, 2) + assert.equal(next.profile.model, 'replacement-model') +}) + +test('caller-supplied identity and malformed arguments never select a session', async () => { + const f = fixture() + for (const args of [null, [], true, 1, { session_id: 'forged' }, { action: 'consume' }]) { + await assert.rejects(f.tool.execute(args, f.exec), /no caller identity/) + } +}) + +test('missing, idle, mismatched and replaced native callers are refused', async () => { + for (const change of [ + (f: ReturnType) => { f.exec.agent = undefined as never }, + (f: ReturnType) => { f.agent.status = 'idle' }, + (f: ReturnType) => { f.agent.session.id = 'different-session' }, + (f: ReturnType) => { f.revokeAgent() }, + (f: ReturnType) => { f.revokeTool() }, + ]) { + const f = fixture() + change(f) + await assert.rejects(f.tool.execute({}, f.exec), /current live/) + } +}) + +test('closed and invalid native Turns cannot produce a context', async () => { + const invalid: [number, number | null][] = [[0, 0], [1, null], [1, -1], [1.5, 0], [1, 0.5], [Number.MAX_SAFE_INTEGER + 1, 0]] + for (const [turn, seq] of invalid) { + const f = fixture() + f.boundary.lastTurn = turn as number + f.boundary.openTurnStartSeq = seq + await assert.rejects(f.tool.execute({}, f.exec), /open native Turn/) + } +}) + +test('cancelled calls stop before context readback', async () => { + const f = fixture() + const controller = new AbortController() + controller.abort() + await assert.rejects(f.tool.execute({}, { ...f.exec, signal: controller.signal }), /aborted/) +}) diff --git a/packages/dsh-loopx-plugin/tsdown.config.ts b/packages/dsh-loopx-plugin/tsdown.config.ts index 6f69af31c8..079f9623ba 100644 --- a/packages/dsh-loopx-plugin/tsdown.config.ts +++ b/packages/dsh-loopx-plugin/tsdown.config.ts @@ -7,6 +7,7 @@ export default defineConfig({ 'init-command': 'build-temp/host/init-command.js', driver: 'build-temp/host/driver.js', observer: 'build-temp/host/observer.js', + 'native-operation-context': 'build-temp/host/native-operation-context.js', }, outDir: 'lib', format: ['esm'], From 904f963c6efba63a7e6b30eb87fe5e5626911a18 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Thu, 15 Oct 2026 05:38:58 +0800 Subject: [PATCH 02/16] docs(dsh): record context prerequisite qualification boundary Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../rfcs/human-confirmed-domain-operations-v0.md | 8 ++++++++ .../rfcs/human-confirmed-domain-operations-v0.zh-CN.md | 6 ++++++ 2 files changed, 14 insertions(+) diff --git a/docs/architecture/rfcs/human-confirmed-domain-operations-v0.md b/docs/architecture/rfcs/human-confirmed-domain-operations-v0.md index 95cb10a56b..97eda171c2 100644 --- a/docs/architecture/rfcs/human-confirmed-domain-operations-v0.md +++ b/docs/architecture/rfcs/human-confirmed-domain-operations-v0.md @@ -445,6 +445,14 @@ and packaged presentation. The live context probe creates a new managed GPT session in its owning home; it performs no proposals, group clicks or financial effects. Synthetic approval fixtures are not genuine user approval. +The optional [DSH native context module](../../../packages/dsh-loopx-plugin/README.md#optional-native-operation-context-sdk-prerequisite) +adds a separately loaded, read-only SDK prerequisite. A real SDK/bundled runtime +`0.1.5rc1` tool call returned its calling session, open native Turn and whitelisted +agent options. This observation is neither a Core actor nor effective wire-profile +attestation. Managed DSH admission, revocation, first approval consumption and +original-channel outcome return remain unqualified; the module always reports +`execution_allowed: false`. Other DSH runtime versions remain unqualified. + Public `goal-channel inspect-operation/consume-operation/report-operation` remain fail-closed before private reads or writes, even with matching `CODEX_THREAD_ID`, route flags, self-signed proof or an older runtime's actor diff --git a/docs/architecture/rfcs/human-confirmed-domain-operations-v0.zh-CN.md b/docs/architecture/rfcs/human-confirmed-domain-operations-v0.zh-CN.md index a0b40f72da..053ca2d974 100644 --- a/docs/architecture/rfcs/human-confirmed-domain-operations-v0.zh-CN.md +++ b/docs/architecture/rfcs/human-confirmed-domain-operations-v0.zh-CN.md @@ -348,6 +348,12 @@ context 调用,结果均由既有 typed result validator 接受)、规范 真实 context 探针只在当前拥有的 home 创建新的受管 GPT session,不建提案、 不造群确认、不执行金融副作用;合成批准夹具不是真实用户批准。 +可选的 [DSH 原生上下文模块](../../../packages/dsh-loopx-plugin/README.md#optional-native-operation-context-sdk-prerequisite) +交付独立加载的只读 SDK 前置组件。真实 SDK/内置 runtime `0.1.5rc1` 的工具调用 +已返回当前调用 session、开放的原生 Turn 和白名单 agent options;该观察不是 Core +actor,也不证明 HTTP wire 实际配置。受管 DSH 准入、撤销、首次批准消费及原渠道 +结果回传仍未验;模块始终返回 `execution_allowed: false`,其它 DSH 版本仍未验。 + 公开 `goal-channel inspect-operation/consume-operation/report-operation` 仍在 私有读写前拒绝,即使 `CODEX_THREAD_ID`、路由、自签 proof 完全匹配或旧运行时 意外返回 actor 成功,也没有 proof-import 捷径。 From 9767d3034c149d2c09fb672d28c1cc7603035552 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Thu, 15 Oct 2026 03:58:14 +0800 Subject: [PATCH 03/16] fix(operations): report transport-specific recovery requirements Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../human-confirmed-domain-operations-v0.md | 8 ++ ...an-confirmed-domain-operations-v0.zh-CN.md | 8 +- .../control_plane/turn_driver/host_binding.py | 10 ++- .../work_items/operation_agent_handoff.ts | 12 ++- .../test_operation_transport_readback.py | 87 +++++++++++++++++++ tests/test_turn_managed_executor_binding.py | 42 +++++++++ 6 files changed, 161 insertions(+), 6 deletions(-) create mode 100644 tests/control_plane/test_operation_transport_readback.py diff --git a/docs/architecture/rfcs/human-confirmed-domain-operations-v0.md b/docs/architecture/rfcs/human-confirmed-domain-operations-v0.md index 97eda171c2..4e514bc099 100644 --- a/docs/architecture/rfcs/human-confirmed-domain-operations-v0.md +++ b/docs/architecture/rfcs/human-confirmed-domain-operations-v0.md @@ -347,6 +347,14 @@ validation and acceptance are reused. The profile is read back by the existing host owner; no new frontend configuration store or hidden default is added. Preflight reports an unpinned/unsupported configuration as unavailable. A valid argv is still runtime-unverified, not proof that a host or an operation ran. +The typed operation transport readback lists `supported_hosts` and +`supported_sandboxes`. An unsupported host returns +`select_supported_operation_host`; an unsupported sandbox returns +`select_supported_operation_sandbox`; missing model/effort returns +`correct_execution_profile`. Transport refusals do not suggest model-credential +environment variables. These are explicit recovery choices, not automatic host +selection or an execution grant. Ordinary host availability remains independent: +a launchable DSH model does not qualify the Codex-only operation connection. Use `workspace-write` only when the existing work grant requires it; `danger-full-access` is not accepted by this transport. diff --git a/docs/architecture/rfcs/human-confirmed-domain-operations-v0.zh-CN.md b/docs/architecture/rfcs/human-confirmed-domain-operations-v0.zh-CN.md index 053ca2d974..1a599d61d3 100644 --- a/docs/architecture/rfcs/human-confirmed-domain-operations-v0.zh-CN.md +++ b/docs/architecture/rfcs/human-confirmed-domain-operations-v0.zh-CN.md @@ -277,7 +277,13 @@ Goal instance,不改变 provider 权威,也不把旧操作移植到另一运 相同选项可用于 `turn run-once`。复用既有 delegation inspect/preflight、start、 准入、租约、session 续接、结果验证和验收。原 host owner 读回 profile; 不新增前端配置存储或隐藏默认。不固定或不受支持的配置在预检中标为 unavailable; -有效 argv 仍是 runtime-unverified,不能证明宿主或操作已运行。仅在现有工作授权 +有效 argv 仍是 runtime-unverified,不能证明宿主或操作已运行。 +typed operation transport 读回列出 `supported_hosts` 和 `supported_sandboxes`。 +不支持的宿主返回 `select_supported_operation_host`,不支持的 sandbox 返回 +`select_supported_operation_sandbox`,缺失模型/effort 返回 +`correct_execution_profile`。传输拒绝不再建议配置模型凭据环境变量。 +这些是显式恢复选项,不自动选择宿主或授予执行权限。普通宿主可用性独立保持: +DSH 模型可启动不能证明它拥有当前仅支持 Codex 的操作连接。仅在现有工作授权 确需时使用 `workspace-write`;此传输不接受 `danger-full-access`。 已准入宿主复用 `CodexChatAgentSession` app-server adapter,将不透明线程保存于 diff --git a/loopx/control_plane/turn_driver/host_binding.py b/loopx/control_plane/turn_driver/host_binding.py index 6a0be83f0a..a34f956c26 100644 --- a/loopx/control_plane/turn_driver/host_binding.py +++ b/loopx/control_plane/turn_driver/host_binding.py @@ -198,7 +198,7 @@ def _with_operation_transport( if observed["reason"]: binding["available"] = False binding["unavailable_reason"] = observed["reason"] - binding["unavailable_remediation"] = [REMEDY_CORRECT_EXECUTION_PROFILE] + binding["unavailable_remediation"] = observed["remediation"] return binding @@ -422,7 +422,13 @@ def managed_executor_unavailable_payload( if str(code) ], "remediation_host": INDIVIDUAL_TURN_HOST, - "remediation_env_vars": list(OPERATOR_CREDENTIAL_ENV_VARS), + # An operation transport refusal is not an instruction to configure + # model credentials. The existing typed transport owner supplies its + # remedies and supported hosts/sandboxes independently of launchability. + "remediation_env_vars": ( + [] if "operation_transport" in binding + else list(OPERATOR_CREDENTIAL_ENV_VARS) + ), } diff --git a/loopx/control_plane/work_items/operation_agent_handoff.ts b/loopx/control_plane/work_items/operation_agent_handoff.ts index aae367e96e..c88e4da693 100644 --- a/loopx/control_plane/work_items/operation_agent_handoff.ts +++ b/loopx/control_plane/work_items/operation_agent_handoff.ts @@ -67,14 +67,20 @@ export function resolveOperationSourceRoute(input: JsonObject): JsonObject { * Accept the CLI's provider-neutral effort vocabulary; actual model support * remains a host qualification, not something this preflight can establish. */ export function projectManagedOperationTransport(input: JsonObject): JsonObject { - const reason = input.host !== "codex-cli" ? "operation_transport_host_unsupported" - : !["read-only", "workspace-write"].includes(String(input.sandbox)) ? "operation_transport_sandbox_unsupported" + const supportedHosts = ["codex-cli"]; + const supportedSandboxes = ["read-only", "workspace-write"]; + const reason = typeof input.host !== "string" || !supportedHosts.includes(input.host) ? "operation_transport_host_unsupported" + : !supportedSandboxes.includes(String(input.sandbox)) ? "operation_transport_sandbox_unsupported" : typeof input.model !== "string" || !ID.test(input.model) || typeof input.reasoning_effort !== "string" || !["none", "minimal", "low", "medium", "high", "xhigh", "max", "ultra"].includes(input.reasoning_effort) ? "operation_transport_profile_required" : null; - return {reason, transport: {schema_version: "loopx_operation_transport_v0", + const remediation = reason === "operation_transport_host_unsupported" ? ["select_supported_operation_host"] + : reason === "operation_transport_sandbox_unsupported" ? ["select_supported_operation_sandbox"] + : reason === "operation_transport_profile_required" ? ["correct_execution_profile"] : []; + return {reason, remediation, transport: {schema_version: "loopx_operation_transport_v0", kind: "owned_app_server", revision: "app-server-operation-tools-v0", + supported_hosts: supportedHosts, supported_sandboxes: supportedSandboxes, configuration_valid: reason === null, runtime_qualified: false, identity_source: "native_thread_turn_metadata", human_confirmation_required: true, first_consumption_required: true, source_conversation_is_executor: false}}; diff --git a/tests/control_plane/test_operation_transport_readback.py b/tests/control_plane/test_operation_transport_readback.py new file mode 100644 index 0000000000..e5b3888271 --- /dev/null +++ b/tests/control_plane/test_operation_transport_readback.py @@ -0,0 +1,87 @@ +"""The original delegation CLI distinguishes transport from model readiness.""" + +import json + +import pytest + +from test_delegation_cli import cli +from test_local_delegation import service as delegation_service + +service = delegation_service + + +@pytest.mark.parametrize("host", [["codex-cli"], {"host": "codex-cli"}, None, True, 1]) +def test_supported_host_readback_never_coerces_an_invalid_host(host): + from loopx.control_plane.effect_runtime import effect_runtime_result + + result = effect_runtime_result("operation.managed_transport.project", { + "host": host, "sandbox": "read-only", "model": "test-model", + "reasoning_effort": "high", + }) + assert result["reason"] == "operation_transport_host_unsupported" + assert result["remediation"] == ["select_supported_operation_host"] + assert result["transport"]["configuration_valid"] is False + + +@pytest.mark.parametrize( + ("options", "reason", "remedy"), + [ + ( + ["--host", "codex-cli", "--codex-model", "test-model", + "--codex-reasoning-effort", "high", "--codex-sandbox", "danger-full-access"], + "operation_transport_sandbox_unsupported", "select_supported_operation_sandbox", + ), + ( + ["--host", "codex-cli"], + "operation_transport_profile_required", "correct_execution_profile", + ), + ], +) +def test_original_cli_inspection_returns_typed_transport_recovery_without_effects( + service, options, reason, remedy, +): + root, runner = service + config = json.loads(runner.config.read_text()) + config["bindings"][0]["host_args"] = [*options, "--codex-operation-tools"] + runner.config.write_text(json.dumps(config)) + before = runner.config.read_bytes(), runner.registry.read_bytes() + + status, result = cli(runner, "inspect", "--binding-id", "analysis") + + assert status == 0, result + assert result["executor"]["host"] == options[1] + assert result["executor"]["available"] is False + assert result["executor"]["reason"] == reason + assert result["executor"]["unavailable_remediation"] == [remedy] + transport = result["executor"]["operation_transport"] + assert transport["supported_hosts"] == ["codex-cli"] + assert transport["supported_sandboxes"] == ["read-only", "workspace-write"] + assert transport["configuration_valid"] is False + assert transport["runtime_qualified"] is False + assert transport["human_confirmation_required"] is True + assert transport["first_consumption_required"] is True + assert not any(result["effects"].values()) + assert not (root / "host-started").exists() + assert before == (runner.config.read_bytes(), runner.registry.read_bytes()) + + +def test_cli_does_not_repurpose_codex_operation_flag_for_a_dsh_executor(service): + root, runner = service + config = json.loads(runner.config.read_text()) + config["bindings"][0]["host_args"] = [ + "--host", "dsh", "--dsh-runner", "fixture-runner.py", + "--dsh-provider", "deepseek-official", "--dsh-model", "deepseek-flash", + "--dsh-reasoning-effort", "high", "--codex-operation-tools", + ] + runner.config.write_text(json.dumps(config)) + before = runner.config.read_bytes(), runner.registry.read_bytes() + + status, result = cli(runner, "inspect", "--binding-id", "analysis") + + # The original argv owner rejects this before executor projection. Keep + # that boundary; direct binding readback does not qualify a DSH CLI route. + assert status == 1 + assert result["ok"] is False + assert "--codex-operation-tools requires the codex-cli host" in result["error"] + assert not (root / "host-started").exists() + assert before == (runner.config.read_bytes(), runner.registry.read_bytes()) diff --git a/tests/test_turn_managed_executor_binding.py b/tests/test_turn_managed_executor_binding.py index 475bb45cf8..ab9d336735 100644 --- a/tests/test_turn_managed_executor_binding.py +++ b/tests/test_turn_managed_executor_binding.py @@ -116,6 +116,7 @@ def test_owned_operation_transport_is_opt_in_and_read_back_by_the_existing_host_ blocked = managed_executor_binding_from_host_args(options, environ={}) assert blocked["available"] is False assert blocked["unavailable_reason"] == "operation_transport_profile_required" + assert blocked["unavailable_remediation"] == ["correct_execution_profile"] pinned = managed_executor_binding_from_host_args( [*options, "--codex-model", "test-model", "--codex-reasoning-effort", effort], environ={}, @@ -136,10 +137,51 @@ def test_operation_transport_never_disappears_or_falls_back_on_another_host(host ) assert observed["available"] is False assert observed["unavailable_reason"] == "operation_transport_host_unsupported" + assert observed["executor"] == host + assert observed["unavailable_remediation"] == ["select_supported_operation_host"] + assert observed["operation_transport"]["supported_hosts"] == ["codex-cli"] + assert observed["operation_transport"]["supported_sandboxes"] == ["read-only", "workspace-write"] assert observed["operation_transport"]["configuration_valid"] is False assert observed["operation_transport"]["runtime_qualified"] is False +def test_operation_transport_sandbox_refusal_is_not_a_model_or_credential_remedy(): + binding = managed_executor_binding( + "codex-cli", environ={}, codex_operation_tools=True, + codex_sandbox="danger-full-access", model="test-model", reasoning_effort="high", + ) + assert binding["available"] is False + assert binding["unavailable_reason"] == "operation_transport_sandbox_unsupported" + assert binding["unavailable_remediation"] == ["select_supported_operation_sandbox"] + payload = managed_executor_unavailable_payload( + {"managed_executor": binding}, execute=True, host_projection={"kind": "codex-cli"}, + ) + assert payload["remediation"] == binding["unavailable_remediation"] + assert payload["remediation_env_vars"] == [] + + +def test_configured_dsh_model_does_not_qualify_operation_transport_or_change_host(): + kwargs = dict( + environ={"DEEPSEEK_API_KEY": "fixture-token"}, module_probe=_RUNTIME, + provider="deepseek-official", model="deepseek-flash", reasoning_effort="high", + ) + ordinary = managed_executor_binding("dsh", **kwargs) + assert ordinary["available"] is True + assert "operation_transport" not in ordinary + blocked = managed_executor_binding("dsh", codex_operation_tools=True, **kwargs) + assert blocked["executor"] == ordinary["executor"] == "dsh" + assert blocked["execution_profile"] == ordinary["execution_profile"] + assert blocked["runtime_probe"] == ordinary["runtime_probe"] + assert blocked["unavailable_remediation"] == ["select_supported_operation_host"] + payload = managed_executor_unavailable_payload( + {"managed_executor": blocked}, execute=True, host_projection={"kind": "dsh"}, + ) + assert payload["status"] == "unavailable" + assert payload["host"] == {"kind": "dsh"} + assert payload["remediation_env_vars"] == [] + assert "fixture-token" not in json.dumps([ordinary, blocked, payload]) + + def test_managed_executor_reports_the_operator_credential_and_endpoint(): binding = managed_executor_binding( "dsh", From e34dc3e8a1efd977996f269bf713548a2c764e36 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Thu, 15 Oct 2026 05:28:59 +0800 Subject: [PATCH 04/16] fix(operations): require a string sandbox in transport preflight Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../work_items/operation_agent_handoff.ts | 2 +- .../test_operation_transport_readback.py | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/loopx/control_plane/work_items/operation_agent_handoff.ts b/loopx/control_plane/work_items/operation_agent_handoff.ts index c88e4da693..89d6c1c74a 100644 --- a/loopx/control_plane/work_items/operation_agent_handoff.ts +++ b/loopx/control_plane/work_items/operation_agent_handoff.ts @@ -70,7 +70,7 @@ export function projectManagedOperationTransport(input: JsonObject): JsonObject const supportedHosts = ["codex-cli"]; const supportedSandboxes = ["read-only", "workspace-write"]; const reason = typeof input.host !== "string" || !supportedHosts.includes(input.host) ? "operation_transport_host_unsupported" - : !supportedSandboxes.includes(String(input.sandbox)) ? "operation_transport_sandbox_unsupported" + : typeof input.sandbox !== "string" || !supportedSandboxes.includes(input.sandbox) ? "operation_transport_sandbox_unsupported" : typeof input.model !== "string" || !ID.test(input.model) || typeof input.reasoning_effort !== "string" || !["none", "minimal", "low", "medium", "high", "xhigh", "max", "ultra"].includes(input.reasoning_effort) diff --git a/tests/control_plane/test_operation_transport_readback.py b/tests/control_plane/test_operation_transport_readback.py index e5b3888271..f81685f04b 100644 --- a/tests/control_plane/test_operation_transport_readback.py +++ b/tests/control_plane/test_operation_transport_readback.py @@ -23,6 +23,19 @@ def test_supported_host_readback_never_coerces_an_invalid_host(host): assert result["transport"]["configuration_valid"] is False +@pytest.mark.parametrize("sandbox", [["read-only"], ["workspace-write"], {"sandbox": "read-only"}, None, True, 1]) +def test_supported_sandbox_readback_never_coerces_an_invalid_sandbox(sandbox): + from loopx.control_plane.effect_runtime import effect_runtime_result + + result = effect_runtime_result("operation.managed_transport.project", { + "host": "codex-cli", "sandbox": sandbox, "model": "test-model", + "reasoning_effort": "high", + }) + assert result["reason"] == "operation_transport_sandbox_unsupported" + assert result["remediation"] == ["select_supported_operation_sandbox"] + assert result["transport"]["configuration_valid"] is False + + @pytest.mark.parametrize( ("options", "reason", "remedy"), [ From d2d963b30f9a1eba5e6dc6ad75d9147f9d6aef62 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Thu, 15 Oct 2026 08:42:51 +0800 Subject: [PATCH 05/16] feat(dsh): connect owned native operation tools to canonical consumption Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../human-confirmed-domain-operations-v0.md | 25 +- ...an-confirmed-domain-operations-v0.zh-CN.md | 18 +- .../collaboration/operation_handoff.py | 10 +- .../turn_driver/codex_operation_host.py | 15 +- .../turn_driver/codex_sessions.py | 37 +- .../turn_driver/dsh_operation_connection.py | 250 ++++++++++ .../turn_driver/dsh_operation_host.py | 452 ++++++++++++++++++ .../work_items/operation_agent_handoff.ts | 29 +- packages/dsh-loopx-plugin/README.md | 38 +- packages/dsh-loopx-plugin/package.json | 4 + .../smoke/dsh-client-artifact-smoke.mjs | 14 +- .../src/native-operation-context.ts | 57 ++- .../dsh-loopx-plugin/src/native-operation.ts | 88 ++++ .../tests/native-operation.test.ts | 64 +++ packages/dsh-loopx-plugin/tsdown.config.ts | 1 + .../test_dsh_operation_connection.py | 308 ++++++++++++ tests/control_plane/test_dsh_operation_sdk.py | 324 +++++++++++++ 17 files changed, 1679 insertions(+), 55 deletions(-) create mode 100644 loopx/control_plane/turn_driver/dsh_operation_connection.py create mode 100644 loopx/control_plane/turn_driver/dsh_operation_host.py create mode 100644 packages/dsh-loopx-plugin/src/native-operation.ts create mode 100644 packages/dsh-loopx-plugin/tests/native-operation.test.ts create mode 100644 tests/control_plane/test_dsh_operation_connection.py create mode 100644 tests/control_plane/test_dsh_operation_sdk.py diff --git a/docs/architecture/rfcs/human-confirmed-domain-operations-v0.md b/docs/architecture/rfcs/human-confirmed-domain-operations-v0.md index 4e514bc099..0e83eae1f4 100644 --- a/docs/architecture/rfcs/human-confirmed-domain-operations-v0.md +++ b/docs/architecture/rfcs/human-confirmed-domain-operations-v0.md @@ -457,9 +457,28 @@ The optional [DSH native context module](../../../packages/dsh-loopx-plugin/READ adds a separately loaded, read-only SDK prerequisite. A real SDK/bundled runtime `0.1.5rc1` tool call returned its calling session, open native Turn and whitelisted agent options. This observation is neither a Core actor nor effective wire-profile -attestation. Managed DSH admission, revocation, first approval consumption and -original-channel outcome return remain unqualified; the module always reports -`execution_allowed: false`. Other DSH runtime versions remain unqualified. +attestation; the read-only module always reports `execution_allowed: false`. + +The separate optional `native-operation` module and internal +`OwnedDshOperationHost` now connect the real SDK/bundled runtime `0.1.5rc1` +to the existing Core handler. Two separately admitted Turns on one retained +SDK connection qualify native session/Turn/call pairing, observed profile, +canonical preparation, refusal before confirmation, fixture confirmation, +one-shot consumption, rejected repetition and canonical `not_executed` outcome. +File/SQLite Goal authority consumers retain the original action-store owner. +Kernel peer PID and exact SDK-native tool arguments must match the private +bounded Unix connection; there is no bearer, caller actor, second approval store +or effect retry. Legacy Codex executor encoding and default/off behavior remain. + +This stage is a backend integration with a scripted loopback provider, not +genuine user approval, original-card delivery or financial utility. The SDK +creates sessions instead of rehydrating persisted ones after process exit; +the observed `session already exists` failure is retained. A new owned host +refuses such resume before runtime launch. The embedding driver must retain +the context or qualify upstream SDK resume; no silent replacement is allowed. +Public CLI/delegation activation stays Codex-only, and DSH cross-process resume, +frontend/Lark return, other runtime versions and live financial effects remain +unqualified. Do not enable the public DSH preflight from these backend results. Public `goal-channel inspect-operation/consume-operation/report-operation` remain fail-closed before private reads or writes, even with matching diff --git a/docs/architecture/rfcs/human-confirmed-domain-operations-v0.zh-CN.md b/docs/architecture/rfcs/human-confirmed-domain-operations-v0.zh-CN.md index 1a599d61d3..f14a3805c2 100644 --- a/docs/architecture/rfcs/human-confirmed-domain-operations-v0.zh-CN.md +++ b/docs/architecture/rfcs/human-confirmed-domain-operations-v0.zh-CN.md @@ -357,8 +357,22 @@ context 调用,结果均由既有 typed result validator 接受)、规范 可选的 [DSH 原生上下文模块](../../../packages/dsh-loopx-plugin/README.md#optional-native-operation-context-sdk-prerequisite) 交付独立加载的只读 SDK 前置组件。真实 SDK/内置 runtime `0.1.5rc1` 的工具调用 已返回当前调用 session、开放的原生 Turn 和白名单 agent options;该观察不是 Core -actor,也不证明 HTTP wire 实际配置。受管 DSH 准入、撤销、首次批准消费及原渠道 -结果回传仍未验;模块始终返回 `execution_allowed: false`,其它 DSH 版本仍未验。 +actor,也不证明 HTTP wire 实际配置;该只读模块始终返回 `execution_allowed: false`。 + +独立可选的 `native-operation` 模块和内部 `OwnedDshOperationHost` 现将真实 +SDK/内置 runtime `0.1.5rc1` 接到既有 Core handler。同一活 SDK 连接的两个独立 +已准入 Turn 覆盖原生 session/Turn/call 配对、实际观察的 profile、规范准备、 +确认前拒绝、夹具确认、首次消费、重复拒绝与规范 `not_executed` 结果。 +File/SQLite Goal authority 消费者继续使用原 action-store owner。内核 peer PID +及精确原生工具参数必须匹配私有有界 Unix 连接;不新增 bearer、调用者 actor、 +批准账本或副作用重试,旧 Codex executor 编码和默认关闭行为保持兼容。 + +该阶段是使用本地合成响应源的后端集成,不是真实用户批准、原卡送达或金融效用。 +SDK 在进程退出后仍尝试创建已有 session,实际出现的 `session already exists` +失败予以保留;新宿主会在启动 runtime 前拒绝这种恢复。嵌入 driver 须保留原连接, +或先资格化上游 SDK 的持久会话恢复,不能静默替换获批身份。公开 CLI/delegation +入口仍仅支持 Codex;DSH 跨进程恢复、前端/Lark 回传、其它 runtime 和真实金融 +效果仍未验,不能因后端结果开启公开 DSH preflight。 公开 `goal-channel inspect-operation/consume-operation/report-operation` 仍在 私有读写前拒绝,即使 `CODEX_THREAD_ID`、路由、自签 proof 完全匹配或旧运行时 diff --git a/loopx/control_plane/collaboration/operation_handoff.py b/loopx/control_plane/collaboration/operation_handoff.py index 0b20532e06..4c96c77f1a 100644 --- a/loopx/control_plane/collaboration/operation_handoff.py +++ b/loopx/control_plane/collaboration/operation_handoff.py @@ -33,7 +33,7 @@ def managed_operation_binding_current( ) -> bool: """Read the original Turn session owner; the shared TS owner judges it.""" from ..effect_runtime import effect_runtime_result - from ..turn_driver.codex_cli import load_codex_cli_session + from ..turn_driver.codex_sessions import load_codex_cli_session executor = parameters["executor"] lineage = { @@ -46,7 +46,8 @@ def managed_operation_binding_current( "operation.managed_binding.current", { "parameters": dict(parameters), - "session": load_codex_cli_session(runtime_root, lineage=lineage), + "session": load_codex_cli_session(runtime_root, lineage=lineage, + host=executor.get("host", "codex-cli")), }, )["current"] is True @@ -234,7 +235,7 @@ def agent_operation_action( }, ) ) - if actor.get("host_surface") == "loopx-managed-codex": + if actor.get("host_surface") in {"loopx-managed-codex", "loopx-managed-dsh"}: session_paths.add( _session_path( runtime_root, @@ -288,8 +289,9 @@ def _commit_agent_operation( "revision": "managed-turn-handoff-v0", "model": actor.get("model"), "reasoning_effort": actor.get("reasoning_effort"), + **({"host": "dsh"} if actor.get("host_surface") == "loopx-managed-dsh" else {}), } - if actor.get("host_surface") == "loopx-managed-codex" + if actor.get("host_surface") in {"loopx-managed-codex", "loopx-managed-dsh"} else actor ) actor_current = ( diff --git a/loopx/control_plane/turn_driver/codex_operation_host.py b/loopx/control_plane/turn_driver/codex_operation_host.py index a5ec78fbcc..fb490f6d91 100644 --- a/loopx/control_plane/turn_driver/codex_operation_host.py +++ b/loopx/control_plane/turn_driver/codex_operation_host.py @@ -83,12 +83,16 @@ def operation_tool_handler( reasoning_effort: str, source_route: Mapping[str, str] | None = None, goal_admission: FirstPartyHostGoalAdmission | None = None, + host: str = "codex-cli", ): """Private closure installed only on a process owned by the Turn driver. The caller must already have checked native thread/Turn metadata. This is not an MCP endpoint or a public actor/proof deserializer. """ + if host not in {"codex-cli", "dsh"}: + raise ValueError("unsupported owned operation host") + surface = "loopx-managed-dsh" if host == "dsh" else "loopx-managed-codex" executor = { "kind": "managed_turn", "todo_id": lineage["todo_id"], @@ -97,10 +101,11 @@ def operation_tool_handler( "revision": REVISION, "model": model, "reasoning_effort": reasoning_effort, + **({"host": "dsh"} if host == "dsh" else {}), } executor_route = { **lineage, - "host_surface": "loopx-managed-codex", + "host_surface": surface, "thread_id": session_id, "profile_digest": profile_digest, } @@ -194,7 +199,7 @@ def handle(tool: str, arguments: Any, native: dict[str, Any]) -> dict[str, Any]: } actor = { **lineage, - "host_surface": "loopx-managed-codex", + "host_surface": surface, "thread_id": session_id, "profile_digest": profile_digest, "model": model, @@ -217,7 +222,11 @@ def handle(tool: str, arguments: Any, native: dict[str, Any]) -> dict[str, Any]: if exc.diagnostic_code == "operation_source_route_ambiguous": return {"ok": False, "error": "operation_source_route_ambiguous", "execution_allowed": False, - "next_action": "Select a registered return audience with --codex-operation-source-route-json in the host invocation; source routing is not executor authority."} + "next_action": ( + "Select a registered return audience in the owned DSH host source_route; source routing is not executor authority." + if host == "dsh" else + "Select a registered return audience with --codex-operation-source-route-json in the host invocation; source routing is not executor authority." + )} return {"ok": False, "error": "operation_admission_rejected", "execution_allowed": False} except (ValueError, KeyError, TypeError, RuntimeError): # Private payloads, paths and adapter error text never enter the model tool error. diff --git a/loopx/control_plane/turn_driver/codex_sessions.py b/loopx/control_plane/turn_driver/codex_sessions.py index cf7ef5aa12..ca2b93b8c8 100644 --- a/loopx/control_plane/turn_driver/codex_sessions.py +++ b/loopx/control_plane/turn_driver/codex_sessions.py @@ -22,9 +22,18 @@ from .driver import selected_turn_todo, session_identity_fields CODEX_CLI_SESSION_SCHEMA_VERSION = "loopx_codex_cli_session_v1" +MANAGED_CLI_SESSION_SCHEMA_VERSION = "loopx_managed_cli_session_v1" SESSION_ID_MAX_CHARS = 256 +def _host_session_schema(host: str) -> str: + if host == "codex-cli": + return CODEX_CLI_SESSION_SCHEMA_VERSION + if host == "dsh": + return MANAGED_CLI_SESSION_SCHEMA_VERSION + raise ValueError("unsupported managed session host") + + def _lineage(request: Mapping[str, Any]) -> dict[str, str]: envelope = request.get("turn_envelope") or {} todo = selected_turn_todo(envelope) @@ -79,7 +88,9 @@ def load_codex_cli_session( *, lineage: Mapping[str, str], session_scope: str = "todo", + host: str = "codex-cli", ) -> dict[str, Any] | None: + schema = _host_session_schema(host) path = _session_path(runtime_root, lineage, session_scope) if not path.exists(): return None @@ -89,7 +100,7 @@ def load_codex_cli_session( return None if not isinstance(value, dict): return None - if value.get("schema_version") != CODEX_CLI_SESSION_SCHEMA_VERSION: + if value.get("schema_version") != schema or value.get("host", "codex-cli") != host: return None if value.get("session_scope", "todo") != session_scope: return None @@ -109,9 +120,11 @@ def _codex_session_goal_ref( *, lineage: Mapping[str, str], session_scope: str = "todo", + host: str = "codex-cli", ) -> object: if ( - value.get("schema_version") != CODEX_CLI_SESSION_SCHEMA_VERSION + value.get("schema_version") != _host_session_schema(host) + or value.get("host", "codex-cli") != host or value.get("session_scope", "todo") != session_scope or any( value.get(field) != lineage[field] @@ -169,6 +182,7 @@ def _store_codex_cli_session( *, lineage: Mapping[str, str], session_scope: str = "todo", + host: str = "codex-cli", session_id: str, goal_ref: Mapping[str, Any] | None = None, session_profile_digest: str | None = None, @@ -191,7 +205,7 @@ def _store_codex_cli_session( raise ValueError("session binding already exists; read back the original admission") # Provenance survives ordinary observations of this exact conversation, # but cannot migrate to a replacement session, profile or Goal lifetime. - prior = load_codex_cli_session(runtime_root, lineage=lineage, session_scope=session_scope) + prior = load_codex_cli_session(runtime_root, lineage=lineage, session_scope=session_scope, host=host) retained_source = None retained_root = None if prior and (prior.get("session_id"), prior.get("goal_ref"), prior.get("session_profile_digest")) == ( @@ -213,6 +227,7 @@ def _store_codex_cli_session( runtime_root, lineage=lineage, session_scope=session_scope, + host=host, session_profile_digest=session_profile_digest, session_id=session_id, goal_ref=goal_ref, @@ -229,6 +244,7 @@ def _write_codex_cli_session( *, lineage: Mapping[str, str], session_scope: str = "todo", + host: str = "codex-cli", session_id: str, goal_ref: Mapping[str, Any] | None = None, session_profile_digest: str | None = None, @@ -238,6 +254,7 @@ def _write_codex_cli_session( checkpoint_artifact_sha256: str | None = None, checkpoint_artifact_root: str | None = None, ) -> None: + schema = _host_session_schema(host) normalized_session_id = _valid_session_id(session_id) if not normalized_session_id: raise ValueError("Codex CLI returned an invalid session id") @@ -254,9 +271,9 @@ def _write_codex_cli_session( descriptor = -1 with handle: payload: dict[str, Any] = { - "schema_version": CODEX_CLI_SESSION_SCHEMA_VERSION, + "schema_version": schema, **lineage, - "host": "codex-cli", + "host": host, "session_id": normalized_session_id, } if session_scope != "todo": @@ -270,7 +287,10 @@ def _write_codex_cli_session( if checkpoint_artifact_root is not None: payload["checkpoint_artifact_root"] = checkpoint_artifact_root if operation_profile_digest is not None: - payload["operation_transport"] = "app-server-operation-tools-v0" + payload["operation_transport"] = ( + "dsh-native-operation-tools-v0" if host == "dsh" + else "app-server-operation-tools-v0" + ) payload["operation_profile_digest"] = operation_profile_digest payload["operation_model"] = operation_model payload["operation_reasoning_effort"] = operation_reasoning_effort @@ -309,6 +329,7 @@ def select_codex_cli_session( *, lineage: Mapping[str, str], session_scope: str = "todo", + host: str = "codex-cli", goal_admission: FirstPartyHostGoalAdmission | None = None, ) -> dict[str, Any] | None: """Select through the existing Goal lifetime fence, then validate identity. @@ -317,6 +338,7 @@ def select_codex_cli_session( silently create a replacement conversation. """ session_identity_fields(session_scope) + _host_session_schema(host) def read() -> dict[str, Any] | None: return _read_codex_cli_session_document( @@ -332,6 +354,7 @@ def read() -> dict[str, Any] | None: item, lineage=lineage, session_scope=session_scope, + host=host, ), ) if goal_admission is not None @@ -339,7 +362,7 @@ def read() -> dict[str, Any] | None: ) if value is None: return None - if _codex_session_goal_ref(value, lineage=lineage, session_scope=session_scope) == { + if _codex_session_goal_ref(value, lineage=lineage, session_scope=session_scope, host=host) == { "malformed": True }: if session_scope == "agent": diff --git a/loopx/control_plane/turn_driver/dsh_operation_connection.py b/loopx/control_plane/turn_driver/dsh_operation_connection.py new file mode 100644 index 0000000000..1aab279734 --- /dev/null +++ b/loopx/control_plane/turn_driver/dsh_operation_connection.py @@ -0,0 +1,250 @@ +"""Bounded local IO for one SDK-owned DSH process; never an actor/proof API. + +Kernel peer PID and SDK native event/call pairing must both match. Canonical +approval, consumption, revocation and outcome decisions stay in the TS owner. +No retry, journal, domain effect or alternate session store lives here. +""" + +from __future__ import annotations + +import json +import os +import socket +import struct +import sys +import tempfile +import threading +import time +from pathlib import Path +from typing import Any, Callable, Mapping + +FRAME_LIMIT = 262144 + + +def _peer_pid(connection: socket.socket) -> int: + if sys.platform == "darwin": + # Apple's sys/un.h: SOL_LOCAL=0, LOCAL_PEERPID=0x002. + return struct.unpack("i", connection.getsockopt(0, 2, 4))[0] + if sys.platform.startswith("linux") and hasattr(socket, "SO_PEERCRED"): + return struct.unpack( + "3i", connection.getsockopt(socket.SOL_SOCKET, socket.SO_PEERCRED, 12) + )[0] + raise ValueError("native operation peer credentials unavailable on this host") + + +class DshOperationConnection: + """Private lifetime-scoped endpoint; constructing it grants no execution.""" + + def __init__( + self, + session_id: str, + profile: Mapping[str, str], + *, + on_start: Callable[[str], None] | None = None, + ) -> None: + self.session_id = session_id + self.profile = dict(profile) + self.on_start = on_start + # Short AF_UNIX path on macOS; no workspace/private payload in address. + self._temporary = tempfile.TemporaryDirectory(prefix="lx-dsh-", dir="/tmp") + os.chmod(self._temporary.name, 0o700) + self.path = Path(self._temporary.name) / "op.sock" + self._condition = threading.Condition() + self._closed = False + self._pid: int | None = None + self._handler: Callable | None = None + self._turn: tuple[int, int] | None = None + self._header_current = False + self._calls: dict[str, dict[str, Any]] = {} + self._used: set[str] = set() + self._listener = socket.socket(socket.AF_UNIX) + self._listener.bind(str(self.path)) + os.chmod(self.path, 0o600) + self._listener.listen(4) + self._listener.settimeout(0.2) + self._thread = threading.Thread( + target=self._serve, name="dsh-operation-io", daemon=True + ) + self._thread.start() + + def bind(self, pid: int, handler: Callable) -> None: + with self._condition: + if ( + self._closed + or self._pid is not None + or type(pid) is not int + or pid <= 0 + ): + raise ValueError("owned SDK process cannot be rebound") + self._pid, self._handler = pid, handler + + def observe(self, notification: Any) -> None: + """Only the owned SDK subscription supplies these notifications.""" + if ( + notification.method != "session.event" + or notification.payload.get("sessionId") != self.session_id + ): + return + event = notification.payload.get("event") or {} + data = event.get("data") or {} + with self._condition: + if self._closed: + return + if event.get("type") == "turn/start": + turn, seq = data.get("turn"), event.get("seq") + if type(turn) is not int or turn < 1 or type(seq) is not int or seq < 0: + raise ValueError("invalid native Turn boundary") + self._turn = (turn, seq) + # DSH logs request/header only when the effective call config + # changes. Retain its observed value on this exact live SDK + # connection; never seed it from a persisted session document. + self._calls.clear() + self._used.clear() + if self.on_start is not None: + self.on_start(f"dsh-{turn}-{seq}") + elif event.get("type") == "request/header": + config = (data.get("header") or {}).get("config") or {} + observed = { + "provider": config.get("provider"), + "model": config.get("model"), + "reasoning_effort": config.get("reasoningEffort"), + } + self._header_current = observed == self.profile + elif ( + event.get("type") == "tool/call" + and data.get("name") == "loopx_operation" + ): + if self._turn is not None and data.get("turn") == self._turn[0]: + call_id = data.get("callId") + arguments = data.get("arguments") + if isinstance(arguments, str): + arguments = json.loads(arguments) + if ( + isinstance(call_id, str) + and 0 < len(call_id) <= 256 + and isinstance(arguments, dict) + ): + if len(self._calls) >= 128: + raise ValueError("native operation call budget exceeded") + self._calls[call_id] = arguments + elif event.get("type") == "turn/end": + self._turn = None + self._calls.clear() + self._condition.notify_all() + + def _dispatch(self, envelope: Any) -> dict[str, Any]: + if not isinstance(envelope, dict) or set(envelope) != { + "schema_version", + "native", + "call_id", + "arguments", + }: + raise ValueError("invalid frame") + if envelope["schema_version"] != "loopx_dsh_operation_call_v0": + raise ValueError("invalid protocol") + call_id = envelope["call_id"] + if not isinstance(call_id, str) or not 0 < len(call_id) <= 256: + raise ValueError("invalid native call") + with self._condition: + deadline = time.monotonic() + 5 + # Native event delivery and the Unix socket are independent IO. + while ( + not self._closed + and call_id not in self._calls + and time.monotonic() < deadline + ): + self._condition.wait(deadline - time.monotonic()) + if ( + self._closed + or self._turn is None + or not self._header_current + or self._handler is None + ): + raise ValueError("inactive native binding") + expected = { + "schema_version": "loopx_dsh_native_context_v0", + "session_id": self.session_id, + "native_turn_number": self._turn[0], + "turn_start_seq": self._turn[1], + "profile_source": "agent_options", + "profile": self.profile, + "wire_profile_verified": False, + "execution_allowed": False, + } + + # JSON encoding also distinguishes true from 1 at identity fields. + def canonical(value: Any) -> str: + return json.dumps(value, sort_keys=True, separators=(",", ":")) + + if ( + canonical(envelope["native"]) != canonical(expected) + or call_id in self._used + or call_id not in self._calls + or canonical(envelope["arguments"]) != canonical(self._calls[call_id]) + ): + raise ValueError("native call mismatch") + self._used.add(call_id) + return self._handler( + "loopx_operation", + envelope["arguments"], + { + "thread_id": self.session_id, + "host_turn_id": f"dsh-{self._turn[0]}-{self._turn[1]}", + }, + ) + + def _serve(self) -> None: + while not self._closed: + try: + connection, _ = self._listener.accept() + except socket.timeout: + continue + except OSError: + break + with connection: + connection.settimeout(10) + try: + if self._pid is None or _peer_pid(connection) != self._pid: + raise ValueError("peer is not the owned SDK runtime") + data = bytearray() + while b"\n" not in data and len(data) <= FRAME_LIMIT: + part = connection.recv(min(8192, FRAME_LIMIT + 1 - len(data))) + if not part: + raise ValueError("incomplete frame") + data.extend(part) + if ( + len(data) > FRAME_LIMIT + or data.count(b"\n") != 1 + or not data.endswith(b"\n") + ): + raise ValueError("invalid frame boundary") + result = self._dispatch(json.loads(data)) + except (ValueError, TypeError, KeyError, RuntimeError, OSError): + result = { + "ok": False, + "error": "operation_native_binding_rejected", + "execution_allowed": False, + } + try: + frame = json.dumps(result, separators=(",", ":")).encode() + b"\n" + if len(frame) > FRAME_LIMIT: + frame = b'{"ok":false,"error":"operation_response_too_large","execution_allowed":false}\n' + connection.sendall(frame) + except OSError: + pass # Response loss grants no retry or second consumption. + + def close(self) -> None: + with self._condition: + self._closed = True + self._turn = None + self._handler = None + self._condition.notify_all() + self._listener.close() + self._thread.join(timeout=11) + self._temporary.cleanup() + + def __enter__(self) -> DshOperationConnection: + return self + + def __exit__(self, *_args) -> None: + self.close() diff --git a/loopx/control_plane/turn_driver/dsh_operation_host.py b/loopx/control_plane/turn_driver/dsh_operation_host.py new file mode 100644 index 0000000000..3a6175ce24 --- /dev/null +++ b/loopx/control_plane/turn_driver/dsh_operation_host.py @@ -0,0 +1,452 @@ +"""Opt-in SDK-owned operation host; not wired to the public Turn CLI yet. + +The ordinary DSH adapter/default plugin remain unchanged. This internal host +requires an admitted Todo request and explicit compiled module. It preserves +SDK stdio ownership and delegates all operation decisions to the existing owner. +""" + +from __future__ import annotations + +import hashlib +from contextlib import ExitStack +import importlib.metadata +import json +import threading +import uuid +from pathlib import Path +from typing import Any, Mapping + +from ...chat_action_store import ChatActionStore +from ...file_lock import exclusive_file_lock +from ...dsh_goal_mode.turn_host_adapter import ( + build_result, + build_sdk_config, + normalize_runner_outcome, + parse_model_json, + render_prompt, + extract_turn_authority, + terminal_error_reason, + terminal_output_budget_state, +) +from ..collaboration.goal_instance_scope import collaboration_goal_scope +from ..collaboration.operation_handoff import ( + agent_operation_action, + managed_operation_binding_current, + pending_operation_handoffs, +) +from ..goals.first_party_host_admission import FirstPartyHostGoalAdmission +from .codex_operation_host import operation_tool_handler +from .codex_sessions import ( + _lineage, + _read_codex_cli_session_document, + _session_path, + _write_codex_cli_session, + select_codex_cli_session, +) +from .dsh_operation_connection import DshOperationConnection +from .executor import LOOPX_TURN_HOST_REQUEST_SCHEMA_VERSION +from .host_failure import BuiltInHostError + +TRANSPORT = "dsh-native-operation-tools-v0" + + +def _sdk_runtime_pin() -> dict[str, Any]: + # The private process handle is fail-closed and qualified against this SDK. + # A new SDK/runtime version needs its own real-path qualification first. + from deepseek_harness_runtime import resolve_bundled_launch_args + + versions = { + name: importlib.metadata.version(name) + for name in ("deepseek-harness-sdk", "deepseek-harness-runtime-bin") + } + if set(versions.values()) != {"0.1.5rc1"}: + raise ValueError( + "DSH operation process adapter requires a qualified SDK/runtime version" + ) + launch = resolve_bundled_launch_args() + files = [Path(item).resolve() for item in launch if Path(item).is_file()] + if not files: + raise ValueError("DSH runtime executable pin is unavailable") + return { + "versions": versions, + "launch_files": {str(path): _digest(path) for path in files}, + } + + +def _digest(path: Path) -> str: + with path.open("rb") as stream: + return hashlib.file_digest(stream, "sha256").hexdigest() + + +def _run_dsh_operation_host( + request: Mapping[str, Any], + *, + runtime_root: Path, + registry_path: Path, + project: Path, + dsh_home: Path, + operation_module: Path, + provider: str, + model: str, + reasoning_effort: str, + max_tokens: int, + timeout_seconds: float = 120, + cordis: Path | None = None, + source_route: Mapping[str, str] | None = None, + goal_admission: FirstPartyHostGoalAdmission | None = None, + confirmed_operation_id: str | None = None, + on_notification: Any = None, + owner: OwnedDshOperationHost, +) -> dict[str, Any]: + """One owned SDK run; final prose cannot create an operation outcome.""" + if request.get("schema_version") != LOOPX_TURN_HOST_REQUEST_SCHEMA_VERSION: + raise ValueError("unsupported LoopX Turn host request") + authority = extract_turn_authority(request) + if not all( + isinstance(value, str) and value + for value in (provider, model, reasoning_effort) + ): + raise ValueError("an explicit DSH operation profile is required") + if ( + type(max_tokens) is not int + or not 1 <= max_tokens <= 32768 + or not 0 < timeout_seconds <= 600 + ): + raise ValueError("DSH operation run requires bounded token and time budgets") + from deepseek_harness import DeepSeekHarness, DeepSeekHarnessConfig + + lineage = _lineage(request) + module = operation_module.resolve(strict=True) + patch = cordis.resolve(strict=True) if cordis else None + profile = { + "transport": TRANSPORT, + "provider": provider, + "model": model, + "reasoning_effort": reasoning_effort, + "max_tokens": max_tokens, + "workspace": str(project.resolve()), + "dsh_home": str(dsh_home.resolve()), + "module": { + "path": str(module), + "sha256": _digest(module), + "package_js": { + path.name: _digest(path) + for path in sorted(module.parent.glob("*.js")) + if path.is_file() + }, + }, + "patch": {"path": str(patch), "sha256": _digest(patch)} if patch else None, + "runtime": _sdk_runtime_pin(), + } + profile_digest = hashlib.sha256( + json.dumps(profile, sort_keys=True, separators=(",", ":")).encode() + ).hexdigest() + original = _read_codex_cli_session_document(runtime_root, lineage=lineage) + binding = select_codex_cli_session( + runtime_root, lineage=lineage, host="dsh", goal_admission=goal_admission + ) + session_plan = request.get("session") or {} + fresh = (session_plan.get("context_policy") or {}).get("mode") == "fresh" + if fresh: + binding = None + elif original is not None and binding is None: + raise ValueError( + "existing session is not a qualified DSH operation binding; select fresh explicitly" + ) + action = session_plan.get("action") + if action not in {"start_new", "resume"} or (action == "resume") != ( + binding is not None + ): + raise ValueError("managed DSH session changed after Turn planning") + if binding and ( + binding.get("operation_transport") != TRANSPORT + or binding.get("operation_profile_digest") != profile_digest + ): + raise ValueError( + "DSH operation profile changed; select fresh and obtain fresh approval" + ) + if binding and owner.session_id is None: + raise ValueError( + "DSH SDK cannot resume a persisted session in a new process; retain the owned connection or qualify upstream resume" + ) + session_id = binding["session_id"] if binding else "loopx-dsh-" + uuid.uuid4().hex + if owner.session_id is not None and owner.session_id != session_id: + raise ValueError("an owned DSH connection cannot switch session identity") + route = { + **lineage, + "host_surface": "loopx-managed-dsh", + "thread_id": session_id, + "profile_digest": profile_digest, + } + store = ChatActionStore(runtime_root / "chat" / "actions") + if confirmed_operation_id is not None: + from ..collaboration.operation_wake import require_current_operation_scope + + proposal = store.load(confirmed_operation_id) + if proposal is None or binding is None or action != "resume": + raise ValueError("confirmed operation requires the original DSH session") + parameters = proposal["normalized_parameters"] + require_current_operation_scope(registry_path, parameters) + store._agent_operation_plan( + proposal, + action="wake", + binding_current=managed_operation_binding_current(runtime_root, parameters), + launch_context={ + "host": "dsh", + "operation_tools": True, + "iteration_context": (session_plan.get("context_policy") or {}).get( + "mode" + ), + }, + executor_route={ + **route, + "model": model, + "reasoning_effort": reasoning_effort, + }, + ) + continuations: list[dict[str, Any]] = [] + + def native_start(host_turn_id: str) -> None: + for locator in continuations: + agent_operation_action( + runtime_root, + registry_path, + proposal_id=locator["operation_id"], + actor={ + **route, + "model": model, + "reasoning_effort": reasoning_effort, + "host_turn_id": host_turn_id, + }, + action="observe_host_start", + turn_key=request["turn_key"], + ) + + native_profile = { + "provider": provider, + "model": model, + "reasoning_effort": reasoning_effort, + } + owner.on_start = native_start + if owner.connection is None: + connection = owner.resources.enter_context( + DshOperationConnection( + session_id, + native_profile, + on_start=lambda turn: owner.native_start(turn), + ) + ) + owner.connection = connection + owner.session_id = session_id + wrapper = connection.path.parent / "operation.mjs" + wrapper.write_text( + "import {apply} from " + json.dumps(module.as_uri()) + ";\n" + "export const name='loopx-owned-operation';\n" + "export const inject=['agents','tools','sessionProjections'];\n" + "export function applyOwned(ctx) { apply(ctx,{socketPath:" + + json.dumps(str(connection.path)) + + "}); }\n" + "export {applyOwned as apply};\n", + encoding="utf-8", + ) + owned_patch = connection.path.parent / "operation.patch.yml" + owned_patch.write_text( + "- insert:\n - id: loopx-owned-operation\n name: " + + json.dumps(wrapper.as_uri()) + + "\n", + encoding="utf-8", + ) + config = build_sdk_config( + provider=provider, + model=model, + reasoning_effort=reasoning_effort, + workspace=project.resolve(), + dsh_home=dsh_home.resolve(), + max_tokens=max_tokens, + cordis=patch, + runtime_bin=None, + request_timeout_seconds=timeout_seconds, + ) + config["patches"] = (*config.get("patches", ()), str(owned_patch)) + harness = owner.resources.enter_context( + DeepSeekHarness(DeepSeekHarnessConfig(**config)) + ) + owner.harness = harness + process = getattr(harness.client, "_proc", None) + if process is None or process.poll() is not None: + raise ValueError("SDK owned process handle is unavailable") + connection.bind(process.pid, owner.handle) + connection, harness = owner.connection, owner.harness + if harness is None or connection is None: + raise ValueError("owned DSH connection is unavailable") + process = getattr(harness.client, "_proc", None) + if process is None or process.poll() is not None: + raise ValueError("SDK owned process handle is unavailable") + handler = operation_tool_handler( + runtime_root=runtime_root, + registry_path=registry_path, + lineage=lineage, + session_id=session_id, + profile_digest=profile_digest, + model=model, + reasoning_effort=reasoning_effort, + source_route=source_route, + goal_admission=goal_admission, + host="dsh", + ) + + def store_binding() -> None: + with exclusive_file_lock(_session_path(runtime_root, lineage)): + if ( + _read_codex_cli_session_document(runtime_root, lineage=lineage) + != original + ): + raise ValueError("DSH binding changed during SDK initialization") + _write_codex_cli_session( + runtime_root, + lineage=lineage, + host="dsh", + session_id=session_id, + goal_ref=request.get("goal_ref"), + operation_profile_digest=profile_digest, + operation_model=model, + operation_reasoning_effort=reasoning_effort, + ) + + if goal_admission is None: + store_binding() + else: + goal_admission.accept_result(store_binding) + owner.handler = handler + with collaboration_goal_scope( + registry_path, + goal_id=lineage["goal_id"], + agents=(lineage["agent_id"],), + caller_goal_ref=request.get("goal_ref"), + require_active=True, + ) as scope: + pending = pending_operation_handoffs( + runtime_root, + lineage["goal_id"], + lineage["agent_id"], + registry_path=registry_path, + scope=scope, + executor_route=route, + cursor_scope=hashlib.sha256( + json.dumps(route, sort_keys=True).encode() + ).hexdigest(), + ) + continuations.extend( + { + key: item[key] + for key in ( + "operation_id", + "payload_digest", + "confirmation_digest", + "claim_id", + ) + } + for item in pending["items"] + if item["status"] == "authorized_pending" + ) + + def observe(notification: Any) -> None: + connection.observe(notification) + if on_notification is not None: + on_notification(notification) + + prompt = render_prompt(authority) + ( + "\nUse loopx_operation for context/pending/prepare/inspect/consume/report. " + "Prepare creates only a canonical proposal and may run before human confirmation. " + "Only first successful consume with execution_allowed=true permits the exact domain effect. " + "Consumed or unknown submissions require original evidence reconciliation, never retry. " + "Final prose is not an operation outcome. Do not invent missing terms.\n" + "Canonical locators for this binding: " + + json.dumps(continuations, sort_keys=True) + ) + timer = threading.Timer(timeout_seconds, owner.close) + timer.daemon = True + timer.start() + try: + outcome = normalize_runner_outcome( + harness.run(prompt, session_id=session_id, on_notification=observe) + ) + except BaseException: + # A callback/transport timeout can return while the native agent is + # still running. Quiesce the owned SDK instead of reusing that Turn. + owner.close() + raise + finally: + timer.cancel() + if ( + terminal_error_reason(outcome) is not None + or terminal_output_budget_state(outcome) is not None + ): + raise BuiltInHostError( + "dsh_operation_native_turn_failed", + failure_kind="unknown", + recovery_kind="resume_session", + ) + return build_result(request, parse_model_json(outcome["final_response"])) + + +class OwnedDshOperationHost: + """Own one SDK connection across separately admitted Turns. + + No background scheduler or approval store. SDK 0.1.5rc1 cannot rehydrate a + persisted session after process exit, so another instance refuses resume. + The embedding driver must retain this context or wait for upstream support. + """ + + def __init__(self) -> None: + self.resources = ExitStack() + self.connection: DshOperationConnection | None = None + self.harness: Any = None + self.session_id: str | None = None + self.handler: Any = None + self.on_start: Any = None + self._entered = False + self._lock = threading.Lock() + + def __enter__(self) -> OwnedDshOperationHost: + if self._entered or self.session_id is not None: + raise ValueError("owned DSH host cannot be reopened") + self._entered = True + return self + + def run(self, request: Mapping[str, Any], **options: Any) -> dict[str, Any]: + if not self._entered or not self._lock.acquire(blocking=False): + raise ValueError("owned DSH host is closed or already running") + try: + return _run_dsh_operation_host(request, owner=self, **options) + finally: + self.handler = None + self.on_start = None + self._lock.release() + + def handle(self, *args: Any) -> dict[str, Any]: + if not self._entered or self.handler is None: + raise ValueError("no active admitted DSH Turn") + return self.handler(*args) + + def native_start(self, turn: str) -> None: + if not self._entered or self.on_start is None: + raise ValueError("no active admitted DSH Turn") + self.on_start(turn) + + def close(self) -> None: + self._entered = False + self.handler = None + self.on_start = None + self.resources.close() + + def __exit__(self, *_args: Any) -> None: + self.close() + + +def run_dsh_operation_host( + request: Mapping[str, Any], **options: Any +) -> dict[str, Any]: + """Convenience for one fresh run. Resume requires a retained owned host.""" + with OwnedDshOperationHost() as host: + return host.run(request, **options) diff --git a/loopx/control_plane/work_items/operation_agent_handoff.ts b/loopx/control_plane/work_items/operation_agent_handoff.ts index 89d6c1c74a..5183149caf 100644 --- a/loopx/control_plane/work_items/operation_agent_handoff.ts +++ b/loopx/control_plane/work_items/operation_agent_handoff.ts @@ -89,16 +89,20 @@ export function projectManagedOperationTransport(input: JsonObject): JsonObject export function normalizeAgentOperationExecutor(input: JsonObject): JsonObject { const executor = requireJsonObject(input.executor, "agent executor"); if (executor.kind === "managed_turn") { - const keys = ["kind", "todo_id", "session_id", "profile_digest", "model", "reasoning_effort", "revision"]; + const keys = ["kind", "todo_id", "session_id", "profile_digest", "model", "reasoning_effort", "revision", + ...("host" in executor ? ["host"] : [])]; if (Object.keys(executor).length !== keys.length || keys.some(key => !(key in executor)) || executor.revision !== MANAGED_OPERATION_REVISION || typeof executor.profile_digest !== "string" || !BARE_SHA256_PATTERN.test(executor.profile_digest)) { throw new EffectRuntimeRequestError("managed operation executor binding is invalid"); } + if ("host" in executor && executor.host !== "dsh") { + throw new EffectRuntimeRequestError("unsupported managed operation host"); + } return {kind: "managed_turn", todo_id: id(executor.todo_id, "todo_id"), session_id: id(executor.session_id, "session_id"), profile_digest: executor.profile_digest, model: id(executor.model, "model"), reasoning_effort: id(executor.reasoning_effort, "reasoning_effort"), - revision: MANAGED_OPERATION_REVISION}; + revision: MANAGED_OPERATION_REVISION, ...("host" in executor ? {host: executor.host} : {})}; } const keys = ["kind", "host_surface", "thread_id", "revision"]; if (Object.keys(executor).length !== keys.length || keys.some(key => !(key in executor)) @@ -109,6 +113,15 @@ export function normalizeAgentOperationExecutor(input: JsonObject): JsonObject { thread_id: id(executor.thread_id, "thread_id"), revision: AGENT_OPERATION_REVISION}; } +/** Legacy tickets retain their exact Codex encoding; new hosts are explicit + * immutable executor operands, never inferred from the calling transport. */ +function managedHost(executor: JsonObject): string { + return executor.host === "dsh" ? "dsh" : "codex-cli"; +} +function managedSurface(executor: JsonObject): string { + return managedHost(executor) === "dsh" ? "loopx-managed-dsh" : "loopx-managed-codex"; +} + /** Filesystem/session observations are supplied by the existing Turn session * owner. A registered source conversation is context/return routing, not the * identity of a managed executor. No transport proof is accepted by this RPC. */ @@ -124,10 +137,12 @@ export function managedOperationBindingCurrent(input: JsonObject): JsonObject { : observedRef !== null && expectedRef.goal_id === observedRef.goal_id && expectedRef.goal_instance_id === observedRef.goal_instance_id; return {current: executor.kind === "managed_turn" && session !== null - && session.schema_version === "loopx_codex_cli_session_v1" + && (managedHost(executor) === "dsh" + ? session.schema_version === "loopx_managed_cli_session_v1" && session.host === "dsh" + : session.schema_version === "loopx_codex_cli_session_v1" && (session.host == null || session.host === "codex-cli")) && session.goal_id === parameters.goal_id && session.agent_id === parameters.agent_id && session.todo_id === executor.todo_id && session.session_id === executor.session_id - && session.operation_transport === "app-server-operation-tools-v0" + && session.operation_transport === (managedHost(executor) === "dsh" ? "dsh-native-operation-tools-v0" : "app-server-operation-tools-v0") && session.operation_profile_digest === executor.profile_digest && session.operation_model === executor.model && session.operation_reasoning_effort === executor.reasoning_effort && sameGoalRef}; @@ -158,7 +173,7 @@ function historicalAccess(input: JsonObject, route: JsonObject, consumed: boolea return {mode: original ? "original_session" : "replacement_reconciliation", owner, original_route: route, permission: "historical_evidence_only", execution_allowed: false, authority_source: original ? "original_operation_route" - : actor.host_surface === "loopx-managed-codex" ? "current_turn_session_binding" : "current_registry_binding"}; + : actor.host_surface === "loopx-managed-codex" || actor.host_surface === "loopx-managed-dsh" ? "current_turn_session_binding" : "current_registry_binding"}; } export function planAgentOperationHandoff(input: JsonObject): JsonObject { @@ -186,7 +201,7 @@ export function planAgentOperationHandoff(input: JsonObject): JsonObject { const expires = timestamp(operation.expires_at); const managed = executor.kind === "managed_turn"; const route: JsonObject = {goal_id: parameters.goal_id, agent_id: parameters.agent_id, - ...(managed ? {host_surface: "loopx-managed-codex", thread_id: executor.session_id, + ...(managed ? {host_surface: managedSurface(executor), thread_id: executor.session_id, todo_id: executor.todo_id, profile_digest: executor.profile_digest} : {host_surface: executor.host_surface, thread_id: executor.thread_id})}; const base: JsonObject = {schema_version: "loopx_operation_agent_handoff_v0", operation_id: operation.operation_id, @@ -221,7 +236,7 @@ export function planAgentOperationHandoff(input: JsonObject): JsonObject { const launch = requireJsonObject(input.launch_context, "operation wake launch context"); const selected = requireJsonObject(input.executor_route, "operation wake executor route"); requireThat(managed && input.binding_current === true - && launch.host === "codex-cli" && launch.operation_tools === true + && launch.host === managedHost(executor) && launch.operation_tools === true && launch.iteration_context !== "fresh" && Object.entries(route).every(([key, value]) => selected[key] === value) && selected.model === executor.model && selected.reasoning_effort === executor.reasoning_effort, diff --git a/packages/dsh-loopx-plugin/README.md b/packages/dsh-loopx-plugin/README.md index 0fb026e08c..a2f765fbb7 100644 --- a/packages/dsh-loopx-plugin/README.md +++ b/packages/dsh-loopx-plugin/README.md @@ -317,9 +317,41 @@ explicit Loader row to disable the module, then restart the owning SDK runtime; the default plugin tree remains unchanged. Existing active sessions must be closed by their owner rather than treated as revoked by editing a patch file. -Core parent admission, verified dispatch/profile identity, revocation and first -approval consumption remain separate integration work. No frontend or Lark -operation path is delivered by this prerequisite. +The read-only module alone delivers no frontend or Lark operation path. + +## Optional owned SDK operation connection + +`dsh-loopx-plugin/native-operation` is a separate, explicit Loader module for +the internal `OwnedDshOperationHost`. It registers `loopx_operation` with an +absolute, parent-owned Unix socket supplied by the host; it is absent from the +default exports and patch. Installing the package grants no execution access. + +The host retains the original SDK stdio connection across separately admitted +Turns. Kernel peer PID, native session/Turn/call ID, exact native arguments and +the observed provider/model/effort must match before dispatch to the existing +Core operation handler. The existing Turn session file and typed operation +store own profile binding, revocation, approval, first consumption and outcome; +there is no second approval store or public actor/proof/consume endpoint. +Changing the module/package JS, model profile or session requires fresh +admission and approval. Transport loss never permits an automatic effect retry. + +The real SDK/bundled runtime `0.1.5rc1` qualifies two Turns on one retained +connection with a scripted loopback provider: prepare, reject before approval, +fixture confirmation, inspect, one permitted consumption, rejected repeat and +canonical `not_executed` outcome. Synthetic confirmation is not human approval; +no venue/account effect or paid model call is involved. The read-only real-model +probe above and this integrated synthetic-provider test prove different stages. + +The SDK's `session/prompt` creates sessions and cannot reload a persisted +session after process exit in this version (`session already exists`). A new +host therefore refuses resume before starting a runtime; it never replaces the +approved session silently. Retaining an owned context is not a new daemon or +scheduler. Cross-process resume, public CLI/delegation activation, frontend/Lark +original-card return, other runtime versions and actual financial execution +remain unqualified. The public managed-operation preflight remains Codex-only +until those entry points are integrated. Disable by omitting this explicit +module and closing its owned host; do not interpret patch removal as revoking +an already running process. ## GoalBar authority and privacy boundary diff --git a/packages/dsh-loopx-plugin/package.json b/packages/dsh-loopx-plugin/package.json index 4c784b9c10..2ed079a3ca 100644 --- a/packages/dsh-loopx-plugin/package.json +++ b/packages/dsh-loopx-plugin/package.json @@ -26,6 +26,10 @@ "types": "./lib/types/native-operation-context.d.ts", "default": "./lib/native-operation-context.js" }, + "./native-operation": { + "types": "./lib/types/native-operation.d.ts", + "default": "./lib/native-operation.js" + }, "./client": { "types": "./lib/types/client/index.d.ts", "default": "./lib/client.js" diff --git a/packages/dsh-loopx-plugin/smoke/dsh-client-artifact-smoke.mjs b/packages/dsh-loopx-plugin/smoke/dsh-client-artifact-smoke.mjs index 14aa46184b..baea03402b 100644 --- a/packages/dsh-loopx-plugin/smoke/dsh-client-artifact-smoke.mjs +++ b/packages/dsh-loopx-plugin/smoke/dsh-client-artifact-smoke.mjs @@ -44,6 +44,7 @@ const packedStaticEntries = new Set([ 'package/lib/index.js', 'package/lib/init-command.js', 'package/lib/native-operation-context.js', + 'package/lib/native-operation.js', 'package/lib/observer.js', 'package/lib/types/cli.d.ts', 'package/lib/types/client/LoopXGoalBar.d.ts', @@ -62,6 +63,7 @@ const packedStaticEntries = new Set([ 'package/lib/types/init-command.d.ts', 'package/lib/types/managed-runtime.d.ts', 'package/lib/types/native-operation-context.d.ts', + 'package/lib/types/native-operation.d.ts', 'package/lib/types/observer.d.ts', 'package/package.json', ]) @@ -149,6 +151,10 @@ async function assertManifest(root) { types: './lib/types/native-operation-context.d.ts', default: './lib/native-operation-context.js', }) + assert.deepEqual(manifest.exports?.['./native-operation'], { + types: './lib/types/native-operation.d.ts', + default: './lib/native-operation.js', + }) assert.deepEqual(manifest.dsh?.client, { inject: clientInject, platform: 'web', @@ -465,15 +471,17 @@ async function assertHostExports(root) { driver: requireFromPlugin.resolve(`${packageId}/driver`), observer: requireFromPlugin.resolve(`${packageId}/observer`), nativeContext: requireFromPlugin.resolve(`${packageId}/native-operation-context`), + nativeOperation: requireFromPlugin.resolve(`${packageId}/native-operation`), client: requireFromPlugin.resolve(`${packageId}/client`), } assert.equal(resolutions.client, join(root, 'lib', 'client.js')) - const [host, init, driver, observer, nativeContext] = await Promise.all([ + const [host, init, driver, observer, nativeContext, nativeOperation] = await Promise.all([ import(pathToFileURL(resolutions.root).href), import(pathToFileURL(resolutions.init).href), import(pathToFileURL(resolutions.driver).href), import(pathToFileURL(resolutions.observer).href), import(pathToFileURL(resolutions.nativeContext).href), + import(pathToFileURL(resolutions.nativeOperation).href), ]) assert.equal(typeof host.apply, 'function') assert.equal(host.name, packageId) @@ -534,6 +542,10 @@ async function assertHostExports(root) { assert.deepEqual(nativeContext.inject, ['agents', 'tools', 'sessionProjections']) assert.equal(typeof nativeContext.apply, 'function') assert.equal(host.createNativeContextTool, undefined) + assert.equal(nativeOperation.name, 'loopx-native-operation') + assert.deepEqual(nativeOperation.inject, ['agents', 'tools', 'sessionProjections']) + assert.equal(typeof nativeOperation.createNativeOperationTool, 'function') + assert.equal(host.createNativeOperationTool, undefined) const [rootSource, driverSource, observerSource] = await Promise.all([ readFile(resolutions.root, 'utf8'), diff --git a/packages/dsh-loopx-plugin/src/native-operation-context.ts b/packages/dsh-loopx-plugin/src/native-operation-context.ts index a07d25e7b0..8f732fc28a 100644 --- a/packages/dsh-loopx-plugin/src/native-operation-context.ts +++ b/packages/dsh-loopx-plugin/src/native-operation-context.ts @@ -11,9 +11,10 @@ interface NativeAgent { options: { provider?: unknown, model?: unknown, reasoningEffort?: unknown } } -interface Execution { +export interface Execution { agent?: NativeAgent signal: AbortSignal + callId?: string } interface NativeContext { @@ -39,7 +40,7 @@ export interface NativeContextTool { export interface NativeContextHost { agents: { get(id: string): object | undefined } tools: { - register(tool: NativeContextTool): unknown + register(tool: object): unknown get(name: string, scope: object): object | undefined } sessionProjections: { @@ -56,6 +57,34 @@ function observedString(value: unknown): string | null { return typeof value === 'string' && value.length > 0 ? value : null } +/** Reads DSH-owned services, never caller-supplied identity. Used by both the + * read-only observation tool and the explicit managed-operation transport. */ +export function readNativeContext(host: NativeContextHost, tool: { name: string }, exec: Execution): NativeContext { + if (exec.signal.aborted) throw new Error('native context call aborted') + const agent = exec.agent + if (!agent || typeof agent.id !== 'string' || !agent.id + || agent.session?.id !== agent.id || agent.status !== 'running' + || host.agents.get(agent.id) !== agent + || host.tools.get(tool.name, agent) !== tool) { + throw new Error('native context requires the current live calling agent and tool') + } + const boundary = host.sessionProjections.stateOf(agent.session, 'turnBoundary') + if (!boundary || !Number.isSafeInteger(boundary.lastTurn) || boundary.lastTurn < 1 + || !Number.isSafeInteger(boundary.openTurnStartSeq) || (boundary.openTurnStartSeq ?? -1) < 0) { + throw new Error('native context requires an open native Turn') + } + return { + schema_version: 'loopx_dsh_native_context_v0', session_id: agent.id, + native_turn_number: boundary.lastTurn, turn_start_seq: boundary.openTurnStartSeq as number, + profile_source: 'agent_options', + profile: { + provider: observedString(agent.options?.provider), model: observedString(agent.options?.model), + reasoning_effort: observedString(agent.options?.reasoningEffort), + }, + wire_profile_verified: false, execution_allowed: false, + } +} + export function createNativeContextTool(host: NativeContextHost): NativeContextTool { const tool: NativeContextTool = { name: NATIVE_CONTEXT_TOOL, @@ -88,29 +117,7 @@ export function createNativeContextTool(host: NativeContextHost): NativeContextT if (args === null || typeof args !== 'object' || Array.isArray(args) || Object.keys(args).length !== 0) { throw new Error('native context takes no caller identity or arguments') } - if (exec.signal.aborted) throw new Error('native context call aborted') - const agent = exec.agent - if (!agent || typeof agent.id !== 'string' || !agent.id - || agent.session?.id !== agent.id || agent.status !== 'running' - || host.agents.get(agent.id) !== agent - || host.tools.get(NATIVE_CONTEXT_TOOL, agent) !== tool) { - throw new Error('native context requires the current live calling agent and tool') - } - const boundary = host.sessionProjections.stateOf(agent.session, 'turnBoundary') - if (!boundary || !Number.isSafeInteger(boundary.lastTurn) || boundary.lastTurn < 1 - || !Number.isSafeInteger(boundary.openTurnStartSeq) || (boundary.openTurnStartSeq ?? -1) < 0) { - throw new Error('native context requires an open native Turn') - } - return { - schema_version: 'loopx_dsh_native_context_v0', session_id: agent.id, - native_turn_number: boundary.lastTurn, turn_start_seq: boundary.openTurnStartSeq as number, - profile_source: 'agent_options', - profile: { - provider: observedString(agent.options?.provider), model: observedString(agent.options?.model), - reasoning_effort: observedString(agent.options?.reasoningEffort), - }, - wire_profile_verified: false, execution_allowed: false, - } + return readNativeContext(host, tool, exec) }, } return tool diff --git a/packages/dsh-loopx-plugin/src/native-operation.ts b/packages/dsh-loopx-plugin/src/native-operation.ts new file mode 100644 index 0000000000..a18fed7c76 --- /dev/null +++ b/packages/dsh-loopx-plugin/src/native-operation.ts @@ -0,0 +1,88 @@ +/** Explicit SDK-owned transport. No default registration, bearer or actor API. */ +import { connect } from 'node:net' +import { isAbsolute } from 'node:path' +import { readNativeContext, type Execution, type NativeContextHost } from './native-operation-context.js' + +export const name = 'loopx-native-operation' +export const inject = ['agents', 'tools', 'sessionProjections'] +export const OPERATION_TOOL = 'loopx_operation' +const LIMIT = 262144 + +export interface OperationConnectionConfig { + socketPath: string +} + +function exchange(path: string, envelope: object, signal: AbortSignal): Promise { + const frame = Buffer.from(JSON.stringify(envelope) + '\n') + if (frame.length > LIMIT) return Promise.reject(new Error('operation frame exceeds limit')) + return new Promise((resolve, reject) => { + const socket = connect({ path }) + let data = Buffer.alloc(0) + let done = false + const finish = (error?: Error, value?: object) => { + if (done) return + done = true + signal.removeEventListener('abort', aborted) + socket.destroy() + if (error) reject(error) + else resolve(value!) + } + const aborted = () => finish(new Error('operation connection aborted; reconcile consumed results')) + signal.addEventListener('abort', aborted, { once: true }) + if (signal.aborted) return aborted() + socket.setTimeout(20000, () => finish(new Error('operation connection timed out; do not retry an effect'))) + socket.on('connect', () => socket.write(frame)) + socket.on('error', () => finish(new Error('operation connection unavailable; reconcile before retry'))) + socket.on('end', () => { if (!done) finish(new Error('operation connection closed without receipt')) }) + socket.on('data', chunk => { + data = Buffer.concat([data, chunk]) + if (data.length > LIMIT) return finish(new Error('operation response exceeds limit')) + const end = data.indexOf(10) + if (end < 0) return + try { + if (end !== data.length - 1) throw new Error('extra frame') + const value: unknown = JSON.parse(data.subarray(0, end).toString('utf8')) + if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('invalid receipt') + finish(undefined, value) + } catch { finish(new Error('invalid operation response; reconcile before retry')) } + }) + }) +} + +export function createNativeOperationTool(host: NativeContextHost, config: OperationConnectionConfig) { + if (typeof config.socketPath !== 'string' || !isAbsolute(config.socketPath)) { + throw new Error('an explicit parent-owned absolute operation socket is required') + } + const tool = { + name: OPERATION_TOOL, + description: 'Read context/pending, prepare an exact proposal, inspect, consume human approval once, or report original evidence. Identity comes from the owned native connection. A repeated consume never grants execution. Do not retry unknown effects.', + parameters: { + type: 'object', additionalProperties: false, required: ['action'], + properties: { + action: { type: 'string', enum: ['context', 'pending', 'prepare', 'inspect', 'consume', 'report'] }, + proposal_id: { type: 'string' }, consumption_id: { type: 'string' }, cursor: { type: 'string' }, + request: { type: 'object' }, outcome: { type: 'object' }, + }, + }, + output: { + schema: { type: 'object' }, + render: (_args: unknown, value: unknown) => [{ type: 'text' as const, text: JSON.stringify(value) }], + }, + async execute(args: unknown, exec: Execution) { + const native = readNativeContext(host, tool, exec) + if (!args || typeof args !== 'object' || Array.isArray(args) + || typeof exec.callId !== 'string' || !exec.callId || exec.callId.length > 256) { + throw new Error('operation requires native call identity and object arguments') + } + // No retries: a lost reply may follow a committed canonical consumption. + return exchange(config.socketPath, { + schema_version: 'loopx_dsh_operation_call_v0', native, call_id: exec.callId, arguments: args, + }, exec.signal) + }, + } + return tool +} + +export function apply(host: NativeContextHost, config: OperationConnectionConfig): void { + host.tools.register(createNativeOperationTool(host, config)) +} diff --git a/packages/dsh-loopx-plugin/tests/native-operation.test.ts b/packages/dsh-loopx-plugin/tests/native-operation.test.ts new file mode 100644 index 0000000000..4870ac7ce7 --- /dev/null +++ b/packages/dsh-loopx-plugin/tests/native-operation.test.ts @@ -0,0 +1,64 @@ +import assert from 'node:assert/strict' +import { mkdtemp, rm } from 'node:fs/promises' +import { createServer } from 'node:net' +import { join } from 'node:path' +import { test } from 'vitest' +import { createNativeOperationTool } from '../src/native-operation.ts' + +function fixture(socketPath: string) { + const agent = { id: 'owned-sdk', session: { id: 'owned-sdk' }, status: 'running', + options: { provider: 'fixture', model: 'fixture', reasoningEffort: 'high' } } + let current: object + const host = { agents: { get: () => agent }, tools: { register: () => {}, get: () => current }, + sessionProjections: { stateOf: () => ({ lastTurn: 1, openTurnStartSeq: 4 }) } } + const tool = createNativeOperationTool(host, { socketPath }) + current = tool + return { tool, agent, exec: { agent, signal: new AbortController().signal, callId: 'sdk-call' }, + replace: () => { current = {} } } +} + +test('native tool sends actual caller/call metadata and returns the canonical receipt on the same connection', async () => { + const directory = await mkdtemp('/tmp/lx-dsh-test-') + const path = join(directory, 'io.sock') + let frame: unknown + let connections = 0 + const server = createServer(socket => { + connections++ + socket.once('data', data => { + frame = JSON.parse(data.toString()) + socket.end(JSON.stringify({ ok: true, execution_allowed: false, status: 'already_consumed' }) + '\n') + }) + }) + await new Promise(resolve => server.listen(path, resolve)) + try { + const f = fixture(path) + const result = await f.tool.execute({ action: 'context' }, f.exec) + assert.deepEqual(result, { ok: true, execution_allowed: false, status: 'already_consumed' }) + assert.equal(connections, 1) + assert.deepEqual(frame, { + schema_version: 'loopx_dsh_operation_call_v0', call_id: 'sdk-call', arguments: { action: 'context' }, + native: { schema_version: 'loopx_dsh_native_context_v0', session_id: 'owned-sdk', native_turn_number: 1, + turn_start_seq: 4, profile_source: 'agent_options', profile: { provider: 'fixture', model: 'fixture', reasoning_effort: 'high' }, + wire_profile_verified: false, execution_allowed: false }, + }) + } finally { + await new Promise(resolve => server.close(() => resolve())) + await rm(directory, { recursive: true, force: true }) + } +}) + +test('missing native call ID, cancelled and replaced tools refuse before connecting', async () => { + const f = fixture('/tmp/does-not-exist-operation.sock') + await assert.rejects(f.tool.execute({}, { agent: f.agent, signal: f.exec.signal }), /native call identity/) + const controller = new AbortController() + controller.abort() + await assert.rejects(f.tool.execute({}, { ...f.exec, signal: controller.signal }), /aborted/) + f.replace() + await assert.rejects(f.tool.execute({}, f.exec), /current live/) +}) + +test('disconnected transport fails without an automatic retry or execute receipt', async () => { + const f = fixture('/tmp/does-not-exist-operation.sock') + await assert.rejects(f.tool.execute({ action: 'consume' }, f.exec), /unavailable/) + assert.throws(() => fixture('relative.sock'), /absolute operation socket/) +}) diff --git a/packages/dsh-loopx-plugin/tsdown.config.ts b/packages/dsh-loopx-plugin/tsdown.config.ts index 079f9623ba..5071fa6ae2 100644 --- a/packages/dsh-loopx-plugin/tsdown.config.ts +++ b/packages/dsh-loopx-plugin/tsdown.config.ts @@ -8,6 +8,7 @@ export default defineConfig({ driver: 'build-temp/host/driver.js', observer: 'build-temp/host/observer.js', 'native-operation-context': 'build-temp/host/native-operation-context.js', + 'native-operation': 'build-temp/host/native-operation.js', }, outDir: 'lib', format: ['esm'], diff --git a/tests/control_plane/test_dsh_operation_connection.py b/tests/control_plane/test_dsh_operation_connection.py new file mode 100644 index 0000000000..de8c949b65 --- /dev/null +++ b/tests/control_plane/test_dsh_operation_connection.py @@ -0,0 +1,308 @@ +"""Actual local socket, native-event pairing and canonical one-shot consumer.""" + +import json +import os +import socket +from types import SimpleNamespace + +import pytest + +from examples import operation_action_fixtures as fixtures +from loopx.control_plane.turn_driver.codex_operation_host import operation_tool_handler +from loopx.control_plane.turn_driver.codex_sessions import ( + _store_codex_cli_session, + load_codex_cli_session, +) +from loopx.control_plane.turn_driver.dsh_operation_connection import ( + DshOperationConnection, +) + +from loopx.control_plane.coordination.runtime_shadow import ( + build_todo_runtime_shadow_projection, +) +from tests.control_plane.canonical_authority_fixture import ( + initialize_canonical_authority, + isolate_sqlite_runtime, +) + + +PROFILE = { + "provider": "deepseek-official", + "model": "fixture-model", + "reasoning_effort": "high", +} + + +def event(connection, kind, data, seq=4, session="owned-dsh"): + connection.observe( + SimpleNamespace( + method="session.event", + payload={ + "sessionId": session, + "event": {"type": kind, "seq": seq, "data": data}, + }, + ) + ) + + +def envelope(arguments, call_id="native-call"): + return { + "schema_version": "loopx_dsh_operation_call_v0", + "call_id": call_id, + "arguments": arguments, + "native": { + "schema_version": "loopx_dsh_native_context_v0", + "session_id": "owned-dsh", + "native_turn_number": 1, + "turn_start_seq": 4, + "profile_source": "agent_options", + "profile": PROFILE, + "wire_profile_verified": False, + "execution_allowed": False, + }, + } + + +def exchange(connection, row): + with socket.socket(socket.AF_UNIX) as client: + client.settimeout(10) + client.connect(str(connection.path)) + client.sendall(json.dumps(row).encode() + b"\n") + data = bytearray() + while b"\n" not in data: + data.extend(client.recv(8192)) + return json.loads(data) + + +def opened(connection): + event(connection, "turn/start", {"turn": 1}) + event( + connection, + "request/header", + { + "header": { + "config": { + "provider": PROFILE["provider"], + "model": PROFILE["model"], + "reasoningEffort": "high", + } + } + }, + ) + + +def call(connection, args, call_id): + event( + connection, + "tool/call", + { + "turn": 1, + "callId": call_id, + "name": "loopx_operation", + "arguments": json.dumps(args), + }, + ) + return exchange(connection, envelope(args, call_id)) + + +@pytest.mark.skipif(not hasattr(socket, "AF_UNIX"), reason="native Unix transport") +@pytest.mark.parametrize("authority_provider", ["file", "sqlite"]) +def test_native_socket_prepares_consumes_once_and_reports_to_original_route( + tmp_path, monkeypatch, authority_provider +): + service, store = fixtures.service(tmp_path) + runtime = store.root.parent.parent + isolate_sqlite_runtime(tmp_path, monkeypatch) + projection = build_todo_runtime_shadow_projection( + goal_id=fixtures.GOAL_ID, todos=[], handoff_mode="soft_claim" + ) + initialize_canonical_authority( + runtime, + fixtures.GOAL_ID, + projection, + state_path=service.registry_path.parent.parent / "ACTIVE_GOAL_STATE.md", + provider=authority_provider, + ) + lineage = { + "goal_id": fixtures.GOAL_ID, + "agent_id": "finance-fixture-agent", + "todo_id": "todo-managed", + } + options = { + "lineage": lineage, + "session_id": "owned-dsh", + "host": "dsh", + "operation_profile_digest": "d" * 64, + "operation_model": "fixture-model", + "operation_reasoning_effort": "high", + } + _store_codex_cli_session(runtime, **options) + assert ( + load_codex_cli_session(runtime, lineage=lineage) is None + ) # No Codex cross-host resume. + handler = operation_tool_handler( + runtime_root=runtime, + registry_path=service.registry_path, + lineage=lineage, + session_id="owned-dsh", + profile_digest="d" * 64, + model="fixture-model", + reasoning_effort="high", + host="dsh", + ) + with DshOperationConnection("owned-dsh", PROFILE) as connection: + connection.bind(os.getpid(), handler) + opened(connection) + request = fixtures.request( + payload={"schema_version": "qualification_v0", "marker": "synthetic"} + ) + terms = request["normalized_parameters"] + terms.pop("executor") + terms.update( + domain="qualification", + operation_kind="qualification.observe", + operation_schema="qualification_v0", + ) + terms["projection"]["simulated"] = False + prepared = call( + connection, {"action": "prepare", "request": request}, "prepare" + ) + assert prepared["ok"], prepared + proposal = prepared["proposal"] + assert proposal["normalized_parameters"]["executor"]["host"] == "dsh" + consume = { + "action": "consume", + "proposal_id": proposal["proposal_id"], + "consumption_id": "synthetic-consume", + } + assert call(connection, consume, "unapproved")["execution_allowed"] is False + delivered = store.record_operation_delivery( + proposal["proposal_id"], delivery=fixtures.delivery(proposal) + ) + confirmed = store.decide_operation( + proposal["proposal_id"], + decision="confirm", + confirmation=fixtures.confirmation(delivered), + ) + assert call(connection, consume, "first")["execution_allowed"] is True + assert call(connection, consume, "retry")["execution_allowed"] is False + outcome = fixtures.agent_result( + confirmed, "synthetic-consume", result="not_executed" + ) + reported = call( + connection, + { + "action": "report", + "proposal_id": proposal["proposal_id"], + "outcome": outcome, + }, + "report", + ) + assert reported["ok"], reported + stored = store.load(proposal["proposal_id"]) + assert stored["operation"]["outcome"]["outcome"] == "not_executed" + assert ( + stored["operation"]["outcome_report"]["original_route"]["host_surface"] + == "loopx-managed-dsh" + ) + # Replacement revokes the original native connection without a new approval store. + _store_codex_cli_session( + runtime, **{**options, "session_id": "replacement-dsh"} + ) + assert call(connection, consume, "revoked")["execution_allowed"] is False + + +@pytest.mark.parametrize( + "mutation", ["identity", "profile", "turn", "arguments", "replay", "closed", "peer"] +) +def test_native_connection_rejects_forgery_replay_and_closed_turn(mutation): + calls = [] + with DshOperationConnection("owned-dsh", PROFILE) as connection: + connection.bind( + os.getpid() + (1 if mutation == "peer" else 0), + lambda *args: calls.append(args) or {"ok": True}, + ) + opened(connection) + arguments = {"action": "context"} + event( + connection, + "tool/call", + { + "turn": 1, + "callId": "native-call", + "name": "loopx_operation", + "arguments": '{"action":"context"}', + }, + ) + row = envelope(arguments) + if mutation == "identity": + row["native"]["session_id"] = "foreign" + if mutation == "profile": + row["native"]["profile"] = {**PROFILE, "model": "drift"} + if mutation == "turn": + row["native"]["native_turn_number"] = True + if mutation == "arguments": + row["arguments"] = {"action": "consume"} + if mutation == "closed": + event(connection, "turn/end", {"turn": 1}) + if mutation == "replay": + assert exchange(connection, row)["ok"] + result = exchange(connection, row) + assert result == { + "ok": False, + "error": "operation_native_binding_rejected", + "execution_allowed": False, + } + assert len(calls) == (1 if mutation == "replay" else 0) + + +def test_observed_header_survives_only_this_connection_and_profile_drift_revokes_it(): + with DshOperationConnection("owned-dsh", PROFILE) as connection: + connection.bind(os.getpid(), lambda *_args: {"ok": True}) + opened(connection) + assert call(connection, {"action": "context"}, "first")["ok"] + event(connection, "turn/end", {"turn": 1}) + event(connection, "turn/start", {"turn": 2}, seq=32) + args = {"action": "context"} + event( + connection, + "tool/call", + { + "turn": 2, + "callId": "second", + "name": "loopx_operation", + "arguments": json.dumps(args), + }, + ) + row = envelope(args, "second") + row["native"].update(native_turn_number=2, turn_start_seq=32) + assert exchange(connection, row)["ok"] + event( + connection, + "request/header", + { + "header": { + "config": { + "provider": "foreign", + "model": "fixture-model", + "reasoningEffort": "high", + } + } + }, + ) + event( + connection, + "tool/call", + { + "turn": 2, + "callId": "drift", + "name": "loopx_operation", + "arguments": json.dumps(args), + }, + ) + row["call_id"] = "drift" + assert exchange(connection, row)["execution_allowed"] is False + with DshOperationConnection("owned-dsh", PROFILE) as replacement: + replacement.bind(os.getpid(), lambda *_args: {"ok": True}) + event(replacement, "turn/start", {"turn": 1}) + assert call(replacement, args, "unobserved")["execution_allowed"] is False diff --git a/tests/control_plane/test_dsh_operation_sdk.py b/tests/control_plane/test_dsh_operation_sdk.py new file mode 100644 index 0000000000..10d19b9315 --- /dev/null +++ b/tests/control_plane/test_dsh_operation_sdk.py @@ -0,0 +1,324 @@ +"""Real pinned SDK/runtime + native tools + Core; loopback scripted provider. + +Opt in with DSH_NATIVE_OPERATION_MODULE=/absolute/built/native-operation.js. +No model charge, genuine credential, human approval or venue effect is involved. +Confirmation is an authenticated *fixture*, never live transaction authority. +""" + +import json +import os +import threading +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer +from pathlib import Path + +import pytest + +from examples import operation_action_fixtures as fixtures +from loopx.control_plane.quota.turn_envelope import ( + turn_envelope_action_signature_document, +) +from loopx.control_plane.turn_driver.codex_sessions import ( + _lineage, + load_codex_cli_session, +) +from loopx.control_plane.turn_driver.dsh_operation_host import ( + OwnedDshOperationHost, + run_dsh_operation_host, +) +from loopx.control_plane.turn_driver.host_candidate import _canonical_hash +from loopx.control_plane.turn_driver.executor import validate_loopx_turn_host_result +from loopx.control_plane.coordination.runtime_shadow import ( + build_todo_runtime_shadow_projection, +) +from tests.control_plane.canonical_authority_fixture import ( + initialize_canonical_authority, + isolate_sqlite_runtime, +) + + +def request(): + envelope = { + "schema_version": "loopx_turn_envelope_v0", + "goal_id": fixtures.GOAL_ID, + "agent_id": "finance-fixture-agent", + "action": { + "primary_action": "Synthetic no-effect operation qualification", + "selected_todo": { + "todo_id": "todo-managed", + "text": "Qualify owned DSH transport", + }, + }, + "boundary": {"write_scope": [], "workspace_guard": {}}, + "required_reads": [], + } + digest = _canonical_hash(turn_envelope_action_signature_document(envelope)) + envelope["action_signature"] = { + "source_hash": digest, + "envelope_hash": digest, + "matches": True, + } + return { + "schema_version": "loopx_turn_host_request_v0", + "turn_key": "sha256:" + "a" * 64, + "turn_envelope": envelope, + "session": {"action": "start_new"}, + "result_contract": {"completed_phases": ["host_execute", "typed_result"]}, + } + + +def tool_results(notifications): + results = [] + for notification in notifications: + event = notification.payload.get("event") or {} + if event.get("type") == "tool/result": + for block in event.get("data", {}).get("message", {}).get("content") or []: + for part in block.get("content", []): + if part.get("type") == "text": + try: + results.append(json.loads(part["text"])) + except (KeyError, ValueError): + pass + return results + + +@pytest.mark.parametrize("authority_provider", ["file", "sqlite"]) +def test_real_sdk_native_prepare_confirmation_consume_report_and_profile_revocation( + tmp_path, monkeypatch, authority_provider +): + pytest.importorskip("deepseek_harness") + module = os.environ.get("DSH_NATIVE_OPERATION_MODULE") + if not module: + pytest.skip("explicit built native-operation module required") + service, store = fixtures.service(tmp_path) + isolate_sqlite_runtime(tmp_path, monkeypatch) + projection = build_todo_runtime_shadow_projection( + goal_id=fixtures.GOAL_ID, todos=[], handoff_mode="soft_claim" + ) + initialize_canonical_authority( + store.root.parent.parent, + fixtures.GOAL_ID, + projection, + state_path=service.registry_path.parent.parent / "ACTIVE_GOAL_STATE.md", + provider=authority_provider, + ) + operation_request = fixtures.request( + payload={"schema_version": "qualification_v0", "marker": "no-external-effect"} + ) + terms = operation_request["normalized_parameters"] + terms.pop("executor") + terms.update( + domain="qualification", + operation_kind="qualification.observe", + operation_schema="qualification_v0", + ) + terms["projection"]["simulated"] = False + steps = [] + errors = [] + + def operation(): + rows = store.list(goal_id=fixtures.GOAL_ID) + assert len(rows) == 1 + return rows[0] + + def arguments(index): + if index == 0: + return {"action": "context"} + if index == 1: + return {"action": "prepare", "request": operation_request} + proposal = operation() + if index == 4: + return {"action": "inspect", "proposal_id": proposal["proposal_id"]} + if index == 7: + return { + "action": "report", + "proposal_id": proposal["proposal_id"], + "outcome": fixtures.agent_result( + proposal, "native-sdk-consumption", result="not_executed" + ), + } + return { + "action": "consume", + "proposal_id": proposal["proposal_id"], + "consumption_id": "native-sdk-consumption", + } + + class Provider(BaseHTTPRequestHandler): + def log_message(self, *_args): + pass + + def do_POST(self): + try: + body = json.loads(self.rfile.read(int(self.headers["Content-Length"]))) + assert body["model"] == "fixture-model" + index = len(steps) + assert index < 9, "bounded scripted request budget exceeded" + steps.append(index) + if index in {3, 8}: + delta = { + "role": "assistant", + "content": json.dumps( + { + "result_kind": "wait", + "classification": "synthetic_native_operation", + "summary": "Original no-effect result retained", + "recommended_action": "Continue separate live qualification", + } + ), + } + finish = "stop" + else: + delta = { + "role": "assistant", + "reasoning_content": "", + "tool_calls": [ + { + "index": 0, + "id": f"sdk-native-call-{index}", + "type": "function", + "function": { + "name": "loopx_operation", + "arguments": json.dumps(arguments(index)), + }, + } + ], + } + finish = "tool_calls" + chunks = [ + { + "id": f"fixture-{index}", + "object": "chat.completion.chunk", + "created": 1, + "model": "fixture-model", + "choices": [ + {"index": 0, "delta": delta, "finish_reason": None} + ], + }, + { + "id": f"fixture-{index}", + "object": "chat.completion.chunk", + "created": 1, + "model": "fixture-model", + "choices": [{"index": 0, "delta": {}, "finish_reason": finish}], + "usage": { + "prompt_tokens": 1, + "completion_tokens": 1, + "total_tokens": 2, + }, + }, + ] + data = ( + "".join("data: " + json.dumps(chunk) + "\n\n" for chunk in chunks) + + "data: [DONE]\n\n" + ).encode() + self.send_response(200) + self.send_header("Content-Type", "text/event-stream") + self.send_header("Content-Length", str(len(data))) + self.end_headers() + self.wfile.write(data) + except Exception as exc: + errors.append(repr(exc)) + self.send_error(500) + + server = ThreadingHTTPServer(("127.0.0.1", 0), Provider) + thread = threading.Thread(target=server.serve_forever, daemon=True) + thread.start() + monkeypatch.setenv("DEEPSEEK_API_KEY", "loopback-fixture-placeholder") + monkeypatch.setenv("DEEPSEEK_BASE_URL", f"http://127.0.0.1:{server.server_port}") + # No built-in account/shell tools exposed in this synthetic qualification. + patch_module = tmp_path / "restrict.mjs" + patch_module.write_text( + "export const name='fixture-operation-only';\nexport function apply(ctx){" + "ctx.on('agent/created',({agent})=>{agent.ctx.tools.presentAs('native');" + "agent.ctx.tools.restrict({allow:['loopx_operation']})})}\n" + ) + patch = tmp_path / "restrict.patch.yml" + patch.write_text( + "- insert:\n - id: fixture-operation-only\n name: " + + json.dumps(patch_module.as_uri()) + + "\n" + ) + options = { + "runtime_root": store.root.parent.parent, + "registry_path": service.registry_path, + "project": service.registry_path.parent.parent, + "dsh_home": tmp_path / "sdk-home", + "operation_module": Path(module), + "provider": "deepseek-official", + "model": "fixture-model", + "reasoning_effort": "high", + "max_tokens": 1024, + "timeout_seconds": 45, + "cordis": patch, + } + notifications = [] + turn = request() + try: + with OwnedDshOperationHost() as host: + first = host.run(turn, **options, on_notification=notifications.append) + assert first["result_kind"] == "wait", (first, errors) + assert len(steps) == 4, (steps, errors) + proposal = operation() + assert proposal["operation"].get("agent_handoff") is None + delivered = store.record_operation_delivery( + proposal["proposal_id"], delivery=fixtures.delivery(proposal) + ) + store.decide_operation( + proposal["proposal_id"], + decision="confirm", + confirmation=fixtures.confirmation(delivered), + ) + turn["session"]["action"] = "resume" + turn["turn_key"] = "sha256:" + "b" * 64 + second = host.run( + turn, + **options, + confirmed_operation_id=proposal["proposal_id"], + on_notification=notifications.append, + ) + assert second["result_kind"] == "wait", (second, errors) + validation = validate_loopx_turn_host_result( + {"transaction": {"turn_key": turn["turn_key"]}}, second + ) + assert validation["ok"], validation["errors"] + assert len(steps) == 9 and not errors, (steps, errors) + stored = operation() + (tmp_path / "native-events.json").write_text( + json.dumps( + [ + {"method": row.method, "payload": row.payload} + for row in notifications + ] + ) + ) + assert stored["operation"]["outcome"] is not None, json.dumps( + tool_results(notifications) + ) + assert stored["operation"]["outcome"]["outcome"] == "not_executed" + assert stored["operation"]["host_start"]["turn_key"] == turn["turn_key"] + assert stored["operation"]["agent_handoff"]["host_turn_id"].startswith( + "dsh-2-" + ) + results = tool_results(notifications) + assert sum(row.get("execution_allowed") is True for row in results) == 1, ( + results + ) + assert any(row.get("status") == "already_consumed" for row in results), ( + results + ) + binding = load_codex_cli_session( + options["runtime_root"], lineage=_lineage(turn), host="dsh" + ) + assert ( + binding["session_id"] + == stored["normalized_parameters"]["executor"]["session_id"] + ) + with pytest.raises(ValueError, match="profile changed"): + host.run(turn, **{**options, "model": "replacement-model"}) + assert len(steps) == 9 # Profile drift refused before a provider request. + with pytest.raises(ValueError, match="cannot resume a persisted session"): + run_dsh_operation_host(turn, **options) + assert len(steps) == 9 + finally: + server.shutdown() + server.server_close() + thread.join(timeout=2) From 04007fd88949f104b62b1043d9bb90711e2e7bea Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Thu, 15 Oct 2026 08:44:16 +0800 Subject: [PATCH 06/16] test(dsh): verify callback failures quiesce the owned runtime Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- tests/control_plane/test_dsh_operation_sdk.py | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/tests/control_plane/test_dsh_operation_sdk.py b/tests/control_plane/test_dsh_operation_sdk.py index 10d19b9315..3fdf0c57c8 100644 --- a/tests/control_plane/test_dsh_operation_sdk.py +++ b/tests/control_plane/test_dsh_operation_sdk.py @@ -322,3 +322,41 @@ def do_POST(self): server.shutdown() server.server_close() thread.join(timeout=2) + + +def test_callback_failure_quiesces_owned_sdk_before_host_can_be_reused( + tmp_path, monkeypatch +): + pytest.importorskip("deepseek_harness") + module = os.environ.get("DSH_NATIVE_OPERATION_MODULE") + if not module: + pytest.skip("explicit built native-operation module required") + service, store = fixtures.service(tmp_path) + monkeypatch.setenv("DEEPSEEK_API_KEY", "loopback-fixture-placeholder") + monkeypatch.setenv("DEEPSEEK_BASE_URL", "http://127.0.0.1:9") + processes = [] + with OwnedDshOperationHost() as host: + + def fail(notification): + if notification.payload.get("event", {}).get("type") == "turn/start": + processes.append(host.harness.client._proc) + raise RuntimeError("fixture callback aborted") + + options = { + "runtime_root": store.root.parent.parent, + "registry_path": service.registry_path, + "project": service.registry_path.parent.parent, + "dsh_home": tmp_path / "sdk-home", + "operation_module": Path(module), + "provider": "deepseek-official", + "model": "fixture-model", + "reasoning_effort": "high", + "max_tokens": 128, + "timeout_seconds": 15, + } + with pytest.raises(RuntimeError, match="fixture callback aborted"): + host.run(request(), **options, on_notification=fail) + assert len(processes) == 1 and processes[0].poll() is not None + assert not host.connection.path.exists() + with pytest.raises(ValueError, match="closed"): + host.run(request(), **options) From 5085182f33c361e8b583d85d738ea2e60b4dbffe Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Thu, 15 Oct 2026 10:13:12 +0800 Subject: [PATCH 07/16] feat(dsh): restore owned operation sessions through the official SDK Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../human-confirmed-domain-operations-v0.md | 20 +-- ...an-confirmed-domain-operations-v0.zh-CN.md | 13 +- .../turn_driver/dsh_operation_host.py | 80 +++++++++--- packages/dsh-loopx-plugin/README.md | 24 ++-- tests/control_plane/test_dsh_operation_sdk.py | 115 +++++++++++++++++- 5 files changed, 210 insertions(+), 42 deletions(-) diff --git a/docs/architecture/rfcs/human-confirmed-domain-operations-v0.md b/docs/architecture/rfcs/human-confirmed-domain-operations-v0.md index 0e83eae1f4..fd23f9ec48 100644 --- a/docs/architecture/rfcs/human-confirmed-domain-operations-v0.md +++ b/docs/architecture/rfcs/human-confirmed-domain-operations-v0.md @@ -471,14 +471,18 @@ bounded Unix connection; there is no bearer, caller actor, second approval store or effect retry. Legacy Codex executor encoding and default/off behavior remain. This stage is a backend integration with a scripted loopback provider, not -genuine user approval, original-card delivery or financial utility. The SDK -creates sessions instead of rehydrating persisted ones after process exit; -the observed `session already exists` failure is retained. A new owned host -refuses such resume before runtime launch. The embedding driver must retain -the context or qualify upstream SDK resume; no silent replacement is allowed. -Public CLI/delegation activation stays Codex-only, and DSH cross-process resume, -frontend/Lark return, other runtime versions and live financial effects remain -unqualified. Do not enable the public DSH preflight from these backend results. +genuine user approval, original-card delivery or financial utility. SDK +`0.1.5rc1` still refuses cross-process resume before launch; its observed +`session already exists` failure is retained. An isolated Python SDK `0.2.1a3` +wheel built from upstream `2fbf4359`, with explicit npm runtime +`@deepseek-ai/dsh@0.2.1-alpha.3`, uses public `resume_session` instead of +replacing identity. SDK files, Node/entry bytes and the entire npm closure join +the frozen profile. File/SQLite restart cases recover the original operation; +missing/unreadable history refuses without provider input or a reusable failed +client. This is candidate-source qualification, not a published Python runtime +or whole-plugin compatibility claim. Public CLI/delegation stays Codex-only; +frontend/Lark return, other runtimes and live financial effects remain +unqualified. Do not enable public DSH preflight from these backend results. Public `goal-channel inspect-operation/consume-operation/report-operation` remain fail-closed before private reads or writes, even with matching diff --git a/docs/architecture/rfcs/human-confirmed-domain-operations-v0.zh-CN.md b/docs/architecture/rfcs/human-confirmed-domain-operations-v0.zh-CN.md index f14a3805c2..04bb5f2330 100644 --- a/docs/architecture/rfcs/human-confirmed-domain-operations-v0.zh-CN.md +++ b/docs/architecture/rfcs/human-confirmed-domain-operations-v0.zh-CN.md @@ -368,11 +368,14 @@ File/SQLite Goal authority 消费者继续使用原 action-store owner。内核 批准账本或副作用重试,旧 Codex executor 编码和默认关闭行为保持兼容。 该阶段是使用本地合成响应源的后端集成,不是真实用户批准、原卡送达或金融效用。 -SDK 在进程退出后仍尝试创建已有 session,实际出现的 `session already exists` -失败予以保留;新宿主会在启动 runtime 前拒绝这种恢复。嵌入 driver 须保留原连接, -或先资格化上游 SDK 的持久会话恢复,不能静默替换获批身份。公开 CLI/delegation -入口仍仅支持 Codex;DSH 跨进程恢复、前端/Lark 回传、其它 runtime 和真实金融 -效果仍未验,不能因后端结果开启公开 DSH preflight。 +SDK `0.1.5rc1` 的跨进程恢复仍在启动前拒绝,原 `session already exists` 失败保留。 +隔离候选使用上游 `2fbf4359` 官方脚本构建的 Python SDK `0.2.1a3` wheel,配合 +显式 npm runtime `@deepseek-ai/dsh@0.2.1-alpha.3`,通过公开 `resume_session` +恢复原身份。SDK 文件、Node/入口原字节及完整 npm closure 纳入冻结 profile。 +File/SQLite 重启用例恢复同一操作;历史丢失/不可读时拒绝,不向模型发送输入, +不替换绑定,失败 client 不可复用。这只是候选源码资格,不能当作已发布 Python +runtime 或整个插件的兼容性证明。公开 CLI/delegation 仍仅支持 Codex,前端/Lark +回传、其它 runtime 和真实金融效果仍未验,不能据此开启公开 DSH preflight。 公开 `goal-channel inspect-operation/consume-operation/report-operation` 仍在 私有读写前拒绝,即使 `CODEX_THREAD_ID`、路由、自签 proof 完全匹配或旧运行时 diff --git a/loopx/control_plane/turn_driver/dsh_operation_host.py b/loopx/control_plane/turn_driver/dsh_operation_host.py index 3a6175ce24..a1a65675ff 100644 --- a/loopx/control_plane/turn_driver/dsh_operation_host.py +++ b/loopx/control_plane/turn_driver/dsh_operation_host.py @@ -11,6 +11,7 @@ from contextlib import ExitStack import importlib.metadata import json +import shutil import threading import uuid from pathlib import Path @@ -47,12 +48,54 @@ from .executor import LOOPX_TURN_HOST_REQUEST_SCHEMA_VERSION from .host_failure import BuiltInHostError -TRANSPORT = "dsh-native-operation-tools-v0" +DSH_OPERATION_TRANSPORT = "dsh-native-operation-tools-v0" -def _sdk_runtime_pin() -> dict[str, Any]: +def _sdk_runtime_pin(runtime_bin: Path | None = None) -> dict[str, Any]: # The private process handle is fail-closed and qualified against this SDK. # A new SDK/runtime version needs its own real-path qualification first. + sdk = importlib.metadata.distribution("deepseek-harness-sdk") + if runtime_bin is not None: + entry = runtime_bin.resolve(strict=True) + package = entry.parent.parent + manifest = json.loads((package / "package.json").read_text()) + modules = package.parent.parent + if ( + sdk.version != "0.2.1a3" + or manifest.get("name") != "@deepseek-ai/dsh" + or manifest.get("version") != "0.2.1-alpha.3" + or entry != package / "lib" / "bin.js" + or modules.name != "node_modules" + ): + raise ValueError("explicit DSH operation runtime is not qualified") + node = shutil.which("node") + if node is None: + raise ValueError("explicit DSH operation runtime requires Node") + closure = hashlib.sha256() + for path in sorted(modules.rglob("*")): + if not path.is_file(): + continue + if not path.resolve().is_relative_to(modules): + raise ValueError("DSH runtime closure contains an external file") + closure.update(str(path.relative_to(modules)).encode() + b"\0") + closure.update(bytes.fromhex(_digest(path))) + return { + "versions": { + "deepseek-harness-sdk": sdk.version, + "@deepseek-ai/dsh": manifest["version"], + }, + "sdk_files": { + str(path): _digest(Path(sdk.locate_file(path))) + for path in sdk.files or () + if str(path).startswith("deepseek_harness/") + and str(path).endswith(".py") + }, + "launch_files": { + str(entry): _digest(entry), + str(Path(node).resolve()): _digest(Path(node)), + }, + "npm_closure_sha256": closure.hexdigest(), + } from deepseek_harness_runtime import resolve_bundled_launch_args versions = { @@ -96,6 +139,7 @@ def _run_dsh_operation_host( goal_admission: FirstPartyHostGoalAdmission | None = None, confirmed_operation_id: str | None = None, on_notification: Any = None, + runtime_bin: Path | None = None, owner: OwnedDshOperationHost, ) -> dict[str, Any]: """One owned SDK run; final prose cannot create an operation outcome.""" @@ -119,7 +163,7 @@ def _run_dsh_operation_host( module = operation_module.resolve(strict=True) patch = cordis.resolve(strict=True) if cordis else None profile = { - "transport": TRANSPORT, + "transport": DSH_OPERATION_TRANSPORT, "provider": provider, "model": model, "reasoning_effort": reasoning_effort, @@ -136,7 +180,7 @@ def _run_dsh_operation_host( }, }, "patch": {"path": str(patch), "sha256": _digest(patch)} if patch else None, - "runtime": _sdk_runtime_pin(), + "runtime": _sdk_runtime_pin(runtime_bin), } profile_digest = hashlib.sha256( json.dumps(profile, sort_keys=True, separators=(",", ":")).encode() @@ -159,13 +203,14 @@ def _run_dsh_operation_host( ): raise ValueError("managed DSH session changed after Turn planning") if binding and ( - binding.get("operation_transport") != TRANSPORT + binding.get("operation_transport") != DSH_OPERATION_TRANSPORT or binding.get("operation_profile_digest") != profile_digest ): raise ValueError( "DSH operation profile changed; select fresh and obtain fresh approval" ) - if binding and owner.session_id is None: + can_restore = profile["runtime"]["versions"]["deepseek-harness-sdk"] == "0.2.1a3" + if binding and owner.session_id is None and not can_restore: raise ValueError( "DSH SDK cannot resume a persisted session in a new process; retain the owned connection or qualify upstream resume" ) @@ -264,7 +309,7 @@ def native_start(host_turn_id: str) -> None: dsh_home=dsh_home.resolve(), max_tokens=max_tokens, cordis=patch, - runtime_bin=None, + runtime_bin=str(runtime_bin.resolve()) if runtime_bin else None, request_timeout_seconds=timeout_seconds, ) config["patches"] = (*config.get("patches", ()), str(owned_patch)) @@ -276,6 +321,10 @@ def native_start(host_turn_id: str) -> None: if process is None or process.poll() is not None: raise ValueError("SDK owned process handle is unavailable") connection.bind(process.pid, owner.handle) + if binding: + # Official restore rejects absent/unreadable/occupied sessions. It + # sends no prompt and never creates a replacement identity. + harness.resume_session(session_id) connection, harness = owner.connection, owner.harness if harness is None or connection is None: raise ValueError("owned DSH connection is unavailable") @@ -371,11 +420,6 @@ def observe(notification: Any) -> None: outcome = normalize_runner_outcome( harness.run(prompt, session_id=session_id, on_notification=observe) ) - except BaseException: - # A callback/transport timeout can return while the native agent is - # still running. Quiesce the owned SDK instead of reusing that Turn. - owner.close() - raise finally: timer.cancel() if ( @@ -393,9 +437,8 @@ def observe(notification: Any) -> None: class OwnedDshOperationHost: """Own one SDK connection across separately admitted Turns. - No background scheduler or approval store. SDK 0.1.5rc1 cannot rehydrate a - persisted session after process exit, so another instance refuses resume. - The embedding driver must retain this context or wait for upstream support. + No background scheduler or approval store. SDK 0.1.5rc1 requires a retained + connection; the qualified explicit runtime uses the official restore API. """ def __init__(self) -> None: @@ -419,6 +462,11 @@ def run(self, request: Mapping[str, Any], **options: Any) -> dict[str, Any]: raise ValueError("owned DSH host is closed or already running") try: return _run_dsh_operation_host(request, owner=self, **options) + except BaseException: + # Failed initialization/restore must not leave a reusable client + # whose next prompt could create an un-restored session instead. + self.close() + raise finally: self.handler = None self.on_start = None @@ -447,6 +495,6 @@ def __exit__(self, *_args: Any) -> None: def run_dsh_operation_host( request: Mapping[str, Any], **options: Any ) -> dict[str, Any]: - """Convenience for one fresh run. Resume requires a retained owned host.""" + """Own one run; restore only through a qualified SDK/runtime's public API.""" with OwnedDshOperationHost() as host: return host.run(request, **options) diff --git a/packages/dsh-loopx-plugin/README.md b/packages/dsh-loopx-plugin/README.md index a2f765fbb7..3e6e76370d 100644 --- a/packages/dsh-loopx-plugin/README.md +++ b/packages/dsh-loopx-plugin/README.md @@ -342,14 +342,22 @@ canonical `not_executed` outcome. Synthetic confirmation is not human approval; no venue/account effect or paid model call is involved. The read-only real-model probe above and this integrated synthetic-provider test prove different stages. -The SDK's `session/prompt` creates sessions and cannot reload a persisted -session after process exit in this version (`session already exists`). A new -host therefore refuses resume before starting a runtime; it never replaces the -approved session silently. Retaining an owned context is not a new daemon or -scheduler. Cross-process resume, public CLI/delegation activation, frontend/Lark -original-card return, other runtime versions and actual financial execution -remain unqualified. The public managed-operation preflight remains Codex-only -until those entry points are integrated. Disable by omitting this explicit +SDK `0.1.5rc1` cannot reload persisted sessions (`session already exists`); +a new host still refuses that version's resume before runtime launch. +An isolated candidate uses Python SDK `0.2.1a3`, built with the official wheel +script from [upstream source `2fbf4359`](https://github.com/deepseek-ai/deepseek-harness/tree/2fbf4359b82fb94c03dcce0b831660d87f12ea5c), +and an explicit `runtime_bin` pointing to npm `@deepseek-ai/dsh@0.2.1-alpha.3`. +The SDK's public `resume_session` restores the same persisted identity without +sending input. The host pins SDK files, the executable/Node and complete npm +closure into the immutable profile; changes require fresh admission/approval. +File/SQLite tests close the first process, restore the original session and +repeat the operation sequence. Missing or unreadable history refuses without +a provider call, replacement binding or reusable failed client. This candidate +does not qualify the default plugin's peer range or a published Python release. + +Public CLI/delegation activation, frontend/Lark original-card return and actual +financial execution remain unqualified. The public managed-operation preflight +remains Codex-only until those entry points are integrated. Disable by omitting this explicit module and closing its owned host; do not interpret patch removal as revoking an already running process. diff --git a/tests/control_plane/test_dsh_operation_sdk.py b/tests/control_plane/test_dsh_operation_sdk.py index 3fdf0c57c8..13f7e4fb4a 100644 --- a/tests/control_plane/test_dsh_operation_sdk.py +++ b/tests/control_plane/test_dsh_operation_sdk.py @@ -8,6 +8,7 @@ import json import os import threading +from contextlib import ExitStack from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from pathlib import Path @@ -72,7 +73,7 @@ def tool_results(notifications): event = notification.payload.get("event") or {} if event.get("type") == "tool/result": for block in event.get("data", {}).get("message", {}).get("content") or []: - for part in block.get("content", []): + for part in block.get("content") or [block]: if part.get("type") == "text": try: results.append(json.loads(part["text"])) @@ -82,13 +83,19 @@ def tool_results(notifications): @pytest.mark.parametrize("authority_provider", ["file", "sqlite"]) +@pytest.mark.parametrize( + "connection_lifetime", ["retained", "restarted", "missing", "unreadable"] +) def test_real_sdk_native_prepare_confirmation_consume_report_and_profile_revocation( - tmp_path, monkeypatch, authority_provider + tmp_path, monkeypatch, authority_provider, connection_lifetime ): pytest.importorskip("deepseek_harness") module = os.environ.get("DSH_NATIVE_OPERATION_MODULE") if not module: pytest.skip("explicit built native-operation module required") + runtime_bin = os.environ.get("DSH_NATIVE_OPERATION_RUNTIME") + if connection_lifetime != "retained" and not runtime_bin: + pytest.skip("explicit resume-capable runtime required") service, store = fixtures.service(tmp_path) isolate_sqlite_runtime(tmp_path, monkeypatch) projection = build_todo_runtime_shadow_projection( @@ -210,6 +217,49 @@ def do_POST(self): "".join("data: " + json.dumps(chunk) + "\n\n" for chunk in chunks) + "data: [DONE]\n\n" ).encode() + if self.path.endswith("/messages"): + # 0.2.1's official adapter uses Messages SSE; 0.1.5 uses + # Chat Completions. Both are actual loopback wire paths. + block = ( + {"type": "text", "text": delta["content"]} + if finish == "stop" + else { + "type": "tool_use", + "id": f"sdk-native-call-{index}", + "name": "loopx_operation", + "input": arguments(index), + } + ) + events = [ + { + "type": "message_start", + "message": { + "id": f"fixture-{index}", + "model": "fixture-model", + "usage": {"input_tokens": 1, "output_tokens": 0}, + }, + }, + { + "type": "content_block_start", + "index": 0, + "content_block": block, + }, + {"type": "content_block_stop", "index": 0}, + { + "type": "message_delta", + "delta": { + "stop_reason": "end_turn" + if finish == "stop" + else "tool_use" + }, + "usage": {"output_tokens": 1}, + }, + {"type": "message_stop"}, + ] + data = "".join( + f"event: {event['type']}\ndata: {json.dumps(event)}\n\n" + for event in events + ).encode() self.send_response(200) self.send_header("Content-Type", "text/event-stream") self.send_header("Content-Length", str(len(data))) @@ -250,10 +300,13 @@ def do_POST(self): "timeout_seconds": 45, "cordis": patch, } + if runtime_bin: + options["runtime_bin"] = Path(runtime_bin) notifications = [] turn = request() try: - with OwnedDshOperationHost() as host: + with ExitStack() as hosts: + host = hosts.enter_context(OwnedDshOperationHost()) first = host.run(turn, **options, on_notification=notifications.append) assert first["result_kind"] == "wait", (first, errors) assert len(steps) == 4, (steps, errors) @@ -267,8 +320,44 @@ def do_POST(self): decision="confirm", confirmation=fixtures.confirmation(delivered), ) + if connection_lifetime != "retained": + first_process = host.harness.client._proc + host.close() + assert first_process.poll() is not None + host = hosts.enter_context(OwnedDshOperationHost()) turn["session"]["action"] = "resume" turn["turn_key"] = "sha256:" + "b" * 64 + if connection_lifetime in {"missing", "unreadable"}: + original_binding = load_codex_cli_session( + options["runtime_root"], lineage=_lineage(turn), host="dsh" + ) + sessions = options["dsh_home"] / "sessions" + if connection_lifetime == "missing": + sessions.rename(options["dsh_home"] / "saved-sessions") + else: + persisted = next(sessions.rglob("session.*.jsonl*")) + persisted.rename(persisted.with_suffix(persisted.suffix + ".saved")) + persisted.write_bytes(b"not a session stream") + from deepseek_harness.errors import JsonRpcError + + with pytest.raises(JsonRpcError): + host.run( + turn, **options, confirmed_operation_id=proposal["proposal_id"] + ) + assert not host.connection.path.exists() + with pytest.raises(ValueError, match="closed"): + host.run(turn, **options) + assert ( + load_codex_cli_session( + options["runtime_root"], lineage=_lineage(turn), host="dsh" + ) + == original_binding + ) + assert len(steps) == 4 and not errors + assert operation()["operation"].get("agent_handoff") is None + if connection_lifetime == "missing": + assert not list(sessions.rglob("session.*.jsonl*")) + return second = host.run( turn, **options, @@ -315,10 +404,24 @@ def do_POST(self): with pytest.raises(ValueError, match="profile changed"): host.run(turn, **{**options, "model": "replacement-model"}) assert len(steps) == 9 # Profile drift refused before a provider request. - with pytest.raises(ValueError, match="cannot resume a persisted session"): - run_dsh_operation_host(turn, **options) + if not runtime_bin: + with pytest.raises( + ValueError, match="cannot resume a persisted session" + ): + run_dsh_operation_host(turn, **options) assert len(steps) == 9 finally: + (tmp_path / "native-events.json").write_text( + json.dumps( + [ + {"method": row.method, "payload": row.payload} + for row in notifications + ] + ) + ) + (tmp_path / "provider-steps.json").write_text( + json.dumps({"steps": steps, "errors": errors}) + ) server.shutdown() server.server_close() thread.join(timeout=2) @@ -354,6 +457,8 @@ def fail(notification): "max_tokens": 128, "timeout_seconds": 15, } + if os.environ.get("DSH_NATIVE_OPERATION_RUNTIME"): + options["runtime_bin"] = Path(os.environ["DSH_NATIVE_OPERATION_RUNTIME"]) with pytest.raises(RuntimeError, match="fixture callback aborted"): host.run(request(), **options, on_notification=fail) assert len(processes) == 1 and processes[0].poll() is not None From 07361462056c81e30f39a6cd5e34311931223b45 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Thu, 15 Oct 2026 11:45:23 +0800 Subject: [PATCH 08/16] feat(dsh): route managed operations through the public Turn entry Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- loopx/cli_commands/turn.py | 19 +- loopx/cli_commands/turn_dsh_host.py | 47 +++++ loopx/cli_commands/turn_registration.py | 8 + loopx/cli_commands/turn_run_once.py | 24 ++- loopx/cli_commands/turn_selection.py | 1 + .../collaboration/operation_wake.py | 27 ++- .../turn_driver/codex_sessions.py | 2 + .../turn_driver/dsh_operation_host.py | 10 +- .../control_plane/turn_driver/host_binding.py | 12 +- .../work_items/operation_agent_handoff.ts | 10 +- .../control_plane/test_dsh_operation_entry.py | 196 ++++++++++++++++++ .../test_operation_transport_readback.py | 13 ++ tests/test_turn_managed_executor_binding.py | 20 ++ 13 files changed, 362 insertions(+), 27 deletions(-) create mode 100644 tests/control_plane/test_dsh_operation_entry.py diff --git a/loopx/cli_commands/turn.py b/loopx/cli_commands/turn.py index 5a477a57f5..5656129fc3 100644 --- a/loopx/cli_commands/turn.py +++ b/loopx/cli_commands/turn.py @@ -89,18 +89,24 @@ def handle_turn_command( strict_goal_admission = goal_admission if goal_admission.enabled else None if getattr(args, "codex_operation_tools", False) and args.host != "codex-cli": raise ValueError("--codex-operation-tools requires the codex-cli host") - if getattr(args, "codex_confirmed_operation_id", None) and not getattr( - args, "codex_operation_tools", False + dsh_operation_module = getattr(args, "dsh_operation_module", None) + if dsh_operation_module and ( + args.host != "dsh" or getattr(args, "dsh_runner", None) ): + raise ValueError("--dsh-operation-module requires the dsh SDK host without --dsh-runner") + if dsh_operation_module and not Path(dsh_operation_module).is_absolute(): + raise ValueError("--dsh-operation-module must be an absolute compiled module path") + operation_tools = bool(getattr(args, "codex_operation_tools", False) or dsh_operation_module) + if getattr(args, "codex_confirmed_operation_id", None) and not operation_tools: raise ValueError( "confirmed operation continuation requires the owned operation transport" ) if ( getattr(args, "codex_operation_source_route_json", None) is not None - and not getattr(args, "codex_operation_tools", False) + and not operation_tools ): raise ValueError( - "--codex-operation-source-route-json requires --codex-operation-tools" + "--operation-source-route-json requires --codex-operation-tools for codex-cli or --dsh-operation-module for dsh" ) # Planning and dry-run execution inspect existing admitted intents. # Only an executing wake may sync inboxes or reserve a calendar window. @@ -164,7 +170,7 @@ def handle_turn_command( and not getattr(args, "codex_operation_tools", False) else "todo") if ( args.turn_command == "run-once" - and args.host == "codex-cli" + and (args.host == "codex-cli" or dsh_operation_module) and not resume_requested and not args.resume_turn_key and args.iteration_context != "fresh" @@ -178,9 +184,10 @@ def handle_turn_command( turn_envelope, goal_admission=strict_goal_admission, session_scope=session_scope, + host=args.host, ) if strict_goal_admission is not None - else codex_cli_session_binding(runtime_root, turn_envelope, session_scope=session_scope) + else codex_cli_session_binding(runtime_root, turn_envelope, session_scope=session_scope, host=args.host) ) payload = build_loopx_turn_plan( turn_envelope, diff --git a/loopx/cli_commands/turn_dsh_host.py b/loopx/cli_commands/turn_dsh_host.py index 522a5b37da..2629e50213 100644 --- a/loopx/cli_commands/turn_dsh_host.py +++ b/loopx/cli_commands/turn_dsh_host.py @@ -6,6 +6,10 @@ from typing import Any from ..dsh_goal_mode.turn_host_adapter import DshHostConfig, run_dsh_host +from ..control_plane.goals.first_party_host_admission import FirstPartyHostGoalAdmission +from ..control_plane.turn_driver.execution_profile import managed_execution_profile +from ..control_plane.turn_driver.host_binding import dsh_output_token_budget +from ..dsh_goal_mode.turn_host_adapter import resolve_dsh_home from ..control_plane.operator_credential import ( OPERATOR_CREDENTIAL_ENV_VARS, OPERATOR_ENDPOINT_ENV_VAR, @@ -54,3 +58,46 @@ def run(request: Mapping[str, Any]) -> dict[str, Any]: return run_dsh_host(request, config=config) return run + + +def build_dsh_operation_host_runner( + args: argparse.Namespace, + *, + runtime_root: Path, + registry_path: Path, + workspace: Path, + environ: Mapping[str, str], + goal_admission: FirstPartyHostGoalAdmission | None, +) -> DshHostRunner: + """Bind explicit native transport options; no runner or ambient actor hook.""" + from ..control_plane.turn_driver.dsh_operation_host import run_dsh_operation_host + + profile = managed_execution_profile( + environ, provider=args.dsh_provider, model=args.dsh_model, + reasoning_effort=args.dsh_reasoning_effort, + ) + credential = { + name: str(environ[name]) + for name in (*OPERATOR_CREDENTIAL_ENV_VARS, OPERATOR_ENDPOINT_ENV_VAR) + if environ.get(name) + } + options = { + "runtime_root": runtime_root, "registry_path": registry_path, + "project": workspace, + "dsh_home": resolve_dsh_home(workspace, Path(args.dsh_home) if args.dsh_home else None), + "operation_module": Path(args.dsh_operation_module), + "provider": profile["provider"], "model": profile["model"], + "reasoning_effort": profile["reasoning_effort"], + "max_tokens": dsh_output_token_budget(args.dsh_max_tokens)["max_tokens"], + "timeout_seconds": 120 if args.timeout_seconds is None else max(1.0, args.timeout_seconds - 5.0), + "runtime_bin": Path(args.dsh_runtime_bin) if args.dsh_runtime_bin else None, + "cordis": Path(args.dsh_cordis) if args.dsh_cordis else None, + "source_route": args.codex_operation_source_route_json, + "confirmed_operation_id": args.codex_confirmed_operation_id, + "goal_admission": goal_admission, "env": credential, + } + + def run(request: Mapping[str, Any]) -> dict[str, Any]: + return run_dsh_operation_host(request, **options) + + return run diff --git a/loopx/cli_commands/turn_registration.py b/loopx/cli_commands/turn_registration.py index a3147bbb4b..52c2c9d8e7 100644 --- a/loopx/cli_commands/turn_registration.py +++ b/loopx/cli_commands/turn_registration.py @@ -241,11 +241,15 @@ def register_turn_commands( help="Opt in to the owned app-server operation transport for this admitted codex-cli Turn. Reuses the original Todo/session; does not authenticate an attached Desktop or grant domain effects.", ) run_once.add_argument( + "--confirmed-operation-id", "--codex-confirmed-operation-id", + dest="codex_confirmed_operation_id", help="Internal exact-operation resume fence for an operator-granted callback continuation. Requires operation tools; does not authenticate a caller or permit a domain effect.", ) run_once.add_argument( + "--operation-source-route-json", "--codex-operation-source-route-json", + dest="codex_operation_source_route_json", type=json.loads, help="Registered return audience {host_surface,thread_id} for operation proposals. Required when this Agent has several source routes; not executor authentication or execution permission.", ) @@ -318,6 +322,10 @@ def register_turn_commands( help="cordis.yml used by the built-in dsh host runtime.", ) run_once.add_argument("--dsh-runtime-bin") + run_once.add_argument( + "--dsh-operation-module", + help="Opt in to the owned DSH operation transport with this absolute compiled native-operation module. Requires the real SDK, not --dsh-runner; installation grants no execution permission.", + ) run_once.add_argument( "--dsh-runner", help=( diff --git a/loopx/cli_commands/turn_run_once.py b/loopx/cli_commands/turn_run_once.py index dc0d71fe38..3f7c32a57b 100644 --- a/loopx/cli_commands/turn_run_once.py +++ b/loopx/cli_commands/turn_run_once.py @@ -51,7 +51,7 @@ from ..control_plane.todos.path_resolution import resolve_todo_state_path from .turn_cadence import managed_cadence_start from .turn_decision import collect_turn_status_payload -from .turn_dsh_host import build_dsh_host_runner +from .turn_dsh_host import build_dsh_host_runner, build_dsh_operation_host_runner from .turn_rendering import build_turn_error_payload from .turn_post_writeback import drain_committed_turn_post_writeback_hooks from .turn_todo_writeback import ( @@ -824,11 +824,23 @@ def resolve_built_in_session_binding( session_binding_resolver = resolve_built_in_session_binding elif args.host == "dsh": - host_runner = build_dsh_host_runner( - args, - workspace=project, - environ=operator_environ, - ) + if getattr(args, "dsh_operation_module", None): + host_runner = build_dsh_operation_host_runner( + args, runtime_root=runtime_root, registry_path=registry_path, + workspace=project, environ=operator_environ, + goal_admission=goal_admission, + ) + + def resolve_dsh_operation_session(envelope: Mapping[str, Any]) -> dict[str, str] | None: + return codex_cli_session_binding( + runtime_root, envelope, goal_admission=goal_admission, host="dsh" + ) + + session_binding_resolver = resolve_dsh_operation_session + else: + host_runner = build_dsh_host_runner( + args, workspace=project, environ=operator_environ, + ) def post_settlement_reward_memory( plan: Mapping[str, Any], diff --git a/loopx/cli_commands/turn_selection.py b/loopx/cli_commands/turn_selection.py index 8253444c38..6dd1b47d4b 100644 --- a/loopx/cli_commands/turn_selection.py +++ b/loopx/cli_commands/turn_selection.py @@ -126,5 +126,6 @@ def managed_executor_cli_binding( else None ), codex_operation_tools=bool(getattr(args, "codex_operation_tools", False)), + dsh_operation_module_configured=bool(getattr(args, "dsh_operation_module", None)), codex_sandbox=getattr(args, "codex_sandbox", "read-only"), ) diff --git a/loopx/control_plane/collaboration/operation_wake.py b/loopx/control_plane/collaboration/operation_wake.py index 26d7fa9137..184c8e02bf 100644 --- a/loopx/control_plane/collaboration/operation_wake.py +++ b/loopx/control_plane/collaboration/operation_wake.py @@ -13,6 +13,8 @@ from ...chat_action_store import ChatActionStore from ..turn_driver.codex_cli import load_codex_cli_session from ..turn_driver.host_binding import turn_host_arg_option +from ..turn_driver.execution_profile import managed_execution_profile +from ..operator_provider import operator_provider_environ from .operation_handoff import managed_operation_binding_current from .goal_instance_scope import collaboration_goal_scope, decide_collaboration_lifecycle @@ -75,23 +77,34 @@ def dispatch_confirmed_operation_wake( projection = ChatActionStore._agent_operation_plan(proposal, action="project") if projection["status"] != "authorized_pending" or projection["host_start"] is not None: return {**base, "state": "no_new_launch"} + argv = binding["host_args"] + host = turn_host_arg_option(argv, "--host") + dsh = host == "dsh" session = load_codex_cli_session(runtime_root, lineage={ "goal_id": service.goal_id, "agent_id": binding["agent_id"], "todo_id": binding["todo_id"], - }) or {} - argv = binding["host_args"] + }, host=host) or {} + profile = managed_execution_profile( + operator_provider_environ(), + provider=turn_host_arg_option(argv, "--dsh-provider"), + model=turn_host_arg_option(argv, "--dsh-model"), + reasoning_effort=turn_host_arg_option(argv, "--dsh-reasoning-effort"), + ) if dsh else { + "model": turn_host_arg_option(argv, "--codex-model"), + "reasoning_effort": turn_host_arg_option(argv, "--codex-reasoning-effort"), + } ChatActionStore._agent_operation_plan( proposal, action="wake", binding_current=managed_operation_binding_current(runtime_root, parameters), - launch_context={"host": turn_host_arg_option(argv, "--host"), - "operation_tools": "--codex-operation-tools" in argv, + launch_context={"host": host, + "operation_tools": bool(turn_host_arg_option(argv, "--dsh-operation-module")) if dsh else "--codex-operation-tools" in argv, "iteration_context": turn_host_arg_option(argv, "--iteration-context")}, executor_route={"goal_id": service.goal_id, "agent_id": binding["agent_id"], - "todo_id": binding["todo_id"], "host_surface": "loopx-managed-codex", + "todo_id": binding["todo_id"], "host_surface": "loopx-managed-dsh" if dsh else "loopx-managed-codex", "thread_id": session.get("session_id"), "profile_digest": session.get("operation_profile_digest"), - "model": turn_host_arg_option(argv, "--codex-model"), - "reasoning_effort": turn_host_arg_option(argv, "--codex-reasoning-effort")}, + "model": profile["model"], + "reasoning_effort": profile["reasoning_effort"]}, ) brief = { "schema_version": "collaboration_brief_v0", diff --git a/loopx/control_plane/turn_driver/codex_sessions.py b/loopx/control_plane/turn_driver/codex_sessions.py index ca2b93b8c8..322335c8ea 100644 --- a/loopx/control_plane/turn_driver/codex_sessions.py +++ b/loopx/control_plane/turn_driver/codex_sessions.py @@ -161,6 +161,7 @@ def codex_cli_session_binding( *, goal_admission: FirstPartyHostGoalAdmission | None = None, session_scope: str = "todo", + host: str = "codex-cli", ) -> dict[str, str] | None: lineage = _lineage({"turn_envelope": dict(turn_envelope)}) session = select_codex_cli_session( @@ -168,6 +169,7 @@ def codex_cli_session_binding( lineage=lineage, session_scope=session_scope, goal_admission=goal_admission, + host=host, ) if session is None: return None diff --git a/loopx/control_plane/turn_driver/dsh_operation_host.py b/loopx/control_plane/turn_driver/dsh_operation_host.py index a1a65675ff..5f3c3b0d03 100644 --- a/loopx/control_plane/turn_driver/dsh_operation_host.py +++ b/loopx/control_plane/turn_driver/dsh_operation_host.py @@ -1,4 +1,4 @@ -"""Opt-in SDK-owned operation host; not wired to the public Turn CLI yet. +"""Explicitly opted-in SDK-owned operation host for managed Turns. The ordinary DSH adapter/default plugin remain unchanged. This internal host requires an admitted Todo request and explicit compiled module. It preserves @@ -140,6 +140,7 @@ def _run_dsh_operation_host( confirmed_operation_id: str | None = None, on_notification: Any = None, runtime_bin: Path | None = None, + env: Mapping[str, str] | None = None, owner: OwnedDshOperationHost, ) -> dict[str, Any]: """One owned SDK run; final prose cannot create an operation outcome.""" @@ -182,6 +183,12 @@ def _run_dsh_operation_host( "patch": {"path": str(patch), "sha256": _digest(patch)} if patch else None, "runtime": _sdk_runtime_pin(runtime_bin), } + if env is not None: + # CLI-owned credentials stay out of state. Bind an explicitly resolved + # endpoint without serializing its potentially private URL. + profile["provider_endpoint_sha256"] = hashlib.sha256( + env.get("DEEPSEEK_BASE_URL", "").encode() + ).hexdigest() profile_digest = hashlib.sha256( json.dumps(profile, sort_keys=True, separators=(",", ":")).encode() ).hexdigest() @@ -311,6 +318,7 @@ def native_start(host_turn_id: str) -> None: cordis=patch, runtime_bin=str(runtime_bin.resolve()) if runtime_bin else None, request_timeout_seconds=timeout_seconds, + env=env, ) config["patches"] = (*config.get("patches", ()), str(owned_patch)) harness = owner.resources.enter_context( diff --git a/loopx/control_plane/turn_driver/host_binding.py b/loopx/control_plane/turn_driver/host_binding.py index a34f956c26..c34d833dd6 100644 --- a/loopx/control_plane/turn_driver/host_binding.py +++ b/loopx/control_plane/turn_driver/host_binding.py @@ -185,6 +185,7 @@ def _with_operation_transport( sandbox: str, model: str | None, reasoning_effort: str | None, + dsh_operation_module_configured: bool = False, ) -> dict[str, Any]: if enabled: from ..effect_runtime import effect_runtime_result @@ -192,6 +193,7 @@ def _with_operation_transport( observed = effect_runtime_result( "operation.managed_transport.project", {"host": host, "sandbox": sandbox, + "dsh_operation_module_configured": dsh_operation_module_configured, "model": model, "reasoning_effort": reasoning_effort}, ) binding["operation_transport"] = observed["transport"] @@ -213,6 +215,7 @@ def managed_executor_binding( reasoning_effort: str | None = None, max_tokens: int | None = None, codex_operation_tools: bool = False, + dsh_operation_module_configured: bool = False, codex_sandbox: str = "read-only", ) -> dict[str, Any]: """Project the executor one planned Turn would run on. @@ -280,8 +283,9 @@ def managed_executor_binding( }, } return _with_operation_transport( - binding, enabled=codex_operation_tools, host=host, - sandbox=codex_sandbox, model=model, reasoning_effort=reasoning_effort, + binding, enabled=codex_operation_tools or dsh_operation_module_configured, host=host, + sandbox=codex_sandbox, model=profile["model"], reasoning_effort=profile["reasoning_effort"], + dsh_operation_module_configured=dsh_operation_module_configured, ) individual_profile: str | None = None if host == INDIVIDUAL_TURN_HOST and (model or reasoning_effort): @@ -308,8 +312,9 @@ def managed_executor_binding( "runtime_probe": None, } return _with_operation_transport( - binding, enabled=codex_operation_tools, host=host, + binding, enabled=codex_operation_tools or dsh_operation_module_configured, host=host, sandbox=codex_sandbox, model=model, reasoning_effort=reasoning_effort, + dsh_operation_module_configured=dsh_operation_module_configured, ) @@ -374,6 +379,7 @@ def managed_executor_binding_from_host_args( else None ), codex_operation_tools="--codex-operation-tools" in host_args, + dsh_operation_module_configured=bool(turn_host_arg_option(host_args, "--dsh-operation-module")), codex_sandbox=turn_host_arg_option(host_args, "--codex-sandbox") or "read-only", ) diff --git a/loopx/control_plane/work_items/operation_agent_handoff.ts b/loopx/control_plane/work_items/operation_agent_handoff.ts index 5183149caf..f4406ede4f 100644 --- a/loopx/control_plane/work_items/operation_agent_handoff.ts +++ b/loopx/control_plane/work_items/operation_agent_handoff.ts @@ -67,10 +67,11 @@ export function resolveOperationSourceRoute(input: JsonObject): JsonObject { * Accept the CLI's provider-neutral effort vocabulary; actual model support * remains a host qualification, not something this preflight can establish. */ export function projectManagedOperationTransport(input: JsonObject): JsonObject { - const supportedHosts = ["codex-cli"]; - const supportedSandboxes = ["read-only", "workspace-write"]; + const dshTransport = input.host === "dsh" && input.dsh_operation_module_configured === true; + const supportedHosts = input.dsh_operation_module_configured === true ? ["codex-cli", "dsh"] : ["codex-cli"]; + const supportedSandboxes = dshTransport ? [] : ["read-only", "workspace-write"]; const reason = typeof input.host !== "string" || !supportedHosts.includes(input.host) ? "operation_transport_host_unsupported" - : typeof input.sandbox !== "string" || !supportedSandboxes.includes(input.sandbox) ? "operation_transport_sandbox_unsupported" + : !dshTransport && (typeof input.sandbox !== "string" || !supportedSandboxes.includes(input.sandbox)) ? "operation_transport_sandbox_unsupported" : typeof input.model !== "string" || !ID.test(input.model) || typeof input.reasoning_effort !== "string" || !["none", "minimal", "low", "medium", "high", "xhigh", "max", "ultra"].includes(input.reasoning_effort) @@ -79,7 +80,8 @@ export function projectManagedOperationTransport(input: JsonObject): JsonObject : reason === "operation_transport_sandbox_unsupported" ? ["select_supported_operation_sandbox"] : reason === "operation_transport_profile_required" ? ["correct_execution_profile"] : []; return {reason, remediation, transport: {schema_version: "loopx_operation_transport_v0", - kind: "owned_app_server", revision: "app-server-operation-tools-v0", + kind: dshTransport ? "owned_native_sdk" : "owned_app_server", + revision: dshTransport ? "dsh-native-operation-tools-v0" : "app-server-operation-tools-v0", supported_hosts: supportedHosts, supported_sandboxes: supportedSandboxes, configuration_valid: reason === null, runtime_qualified: false, identity_source: "native_thread_turn_metadata", human_confirmation_required: true, diff --git a/tests/control_plane/test_dsh_operation_entry.py b/tests/control_plane/test_dsh_operation_entry.py new file mode 100644 index 0000000000..ce5ea20ddc --- /dev/null +++ b/tests/control_plane/test_dsh_operation_entry.py @@ -0,0 +1,196 @@ +"""Public CLI and detached wake with the actual opt-in SDK/runtime. + +The only provider is loopback Messages SSE. Fixture confirmation authenticates +a no-effect qualification operation; it is never live financial authority. +""" + +import json +import os +import subprocess +import sys +import threading +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer + +import pytest + +from examples import operation_action_fixtures as fixtures +from loopx.chat_action_store import ChatActionStore +from loopx.control_plane.collaboration.operation_wake import ( + dispatch_confirmed_operation_wake, +) +from loopx.control_plane.turn_driver.codex_sessions import load_codex_cli_session +from test_local_delegation import service as delegation_service, wait + +service = delegation_service + + +def test_public_dsh_operation_entry_and_confirmed_callback(service, monkeypatch): + pytest.importorskip("deepseek_harness") + module = os.environ.get("DSH_NATIVE_OPERATION_MODULE") + runtime = os.environ.get("DSH_NATIVE_OPERATION_RUNTIME") + if not module or not runtime: + pytest.skip("explicit built operation module and resume-capable runtime required") + root, runner = service + store = ChatActionStore(runner.root / "chat" / "actions") + intent = fixtures.request( + goal_id=runner.goal_id, + payload={"schema_version": "qualification_v0", "marker": "no-external-effect"}, + ) + terms = intent["normalized_parameters"] + terms.pop("executor") + terms.update(agent_id="analyst", domain="qualification", + operation_kind="qualification.observe", operation_schema="qualification_v0") + terms["projection"]["simulated"] = False + steps, errors, responses = [], [], [] + + def operation(): + rows = store.list(goal_id=runner.goal_id) + assert len(rows) == 1 + return rows[0] + + def arguments(index): + if index == 0: + return {"action": "context"} + if index == 1: + return {"action": "prepare", "request": intent} + proposal = operation() + if index == 4: + return {"action": "inspect", "proposal_id": proposal["proposal_id"]} + if index == 7: + return {"action": "report", "proposal_id": proposal["proposal_id"], + "outcome": fixtures.agent_result(proposal, "entry-consumption", result="not_executed")} + return {"action": "consume", "proposal_id": proposal["proposal_id"], + "consumption_id": "entry-consumption"} + + class Provider(BaseHTTPRequestHandler): + def log_message(self, *_args): + pass + + def do_POST(self): + try: + body = json.loads(self.rfile.read(int(self.headers["Content-Length"]))) + assert self.path.endswith("/messages") and body["model"] == "fixture-model" + index = len(steps) + assert index < 9, "bounded scripted request budget exceeded" + steps.append(index) + responses.append(body.get("messages", [])) + terminal = index in {3, 8} + block = ({"type": "text", "text": json.dumps({ + "result_kind": "wait", "classification": "synthetic_native_operation", + "summary": "Original no-effect result retained", + "recommended_action": "Continue separate live qualification", + })} if terminal else {"type": "tool_use", "id": f"entry-{index}", + "name": "loopx_operation", "input": arguments(index)}) + events = [ + {"type": "message_start", "message": {"id": f"fixture-{index}", + "model": "fixture-model", "usage": {"input_tokens": 1, "output_tokens": 0}}}, + {"type": "content_block_start", "index": 0, "content_block": block}, + {"type": "content_block_stop", "index": 0}, + {"type": "message_delta", "delta": {"stop_reason": "end_turn" if terminal else "tool_use"}, + "usage": {"output_tokens": 1}}, + {"type": "message_stop"}, + ] + data = "".join(f"event: {event['type']}\ndata: {json.dumps(event)}\n\n" for event in events).encode() + self.send_response(200) + self.send_header("Content-Type", "text/event-stream") + self.send_header("Content-Length", str(len(data))) + self.end_headers() + self.wfile.write(data) + except Exception as exc: + errors.append(repr(exc)) + self.send_error(500) + + server = ThreadingHTTPServer(("127.0.0.1", 0), Provider) + thread = threading.Thread(target=server.serve_forever, daemon=True) + thread.start() + monkeypatch.setenv("DEEPSEEK_API_KEY", "loopback-fixture-placeholder") + monkeypatch.setenv("DEEPSEEK_BASE_URL", f"http://127.0.0.1:{server.server_port}") + patch_module = root / "restrict.mjs" + patch_module.write_text( + "export const name='fixture-operation-only';\nexport function apply(ctx){" + "ctx.on('agent/created',({agent})=>{agent.ctx.tools.presentAs('native');" + "agent.ctx.tools.restrict({allow:['loopx_operation']})})}\n" + ) + patch = root / "restrict.patch.yml" + patch.write_text("- insert:\n - id: fixture-operation-only\n name: " + json.dumps(patch_module.as_uri()) + "\n") + config = json.loads(runner.config.read_text()) + binding = config["bindings"][0] + binding["timeout_seconds"] = 120 + binding["host_args"] = ["--host", "dsh", "--dsh-operation-module", module, + "--dsh-runtime-bin", runtime, "--dsh-home", str(root / "sdk-home"), + "--dsh-cordis", str(patch), "--dsh-provider", "deepseek-official", + "--dsh-model", "fixture-model", "--dsh-reasoning-effort", "high", "--dsh-max-tokens", "1024"] + runner.config.write_text(json.dumps(config)) + lineage = {"goal_id": runner.goal_id, "agent_id": binding["agent_id"], "todo_id": binding["todo_id"]} + command = [sys.executable, "-m", "loopx.cli", "--registry", str(runner.registry), + "--runtime-root", str(runner.root), "--format", "json", "turn", "run-once", + "--goal-id", runner.goal_id, "--agent-id", binding["agent_id"], "--todo-id", binding["todo_id"], + "--project", binding["workspace"], "--scan-root", binding["workspace"], "--no-global-sync", + "--timeout-seconds", "120", "--execute", *binding["host_args"]] + try: + for option, replacement, expected in [ + ("--host", "generic-cli", "requires the dsh SDK host"), + ("--dsh-operation-module", "relative.js", "must be an absolute"), + ]: + invalid = command.copy() + invalid[invalid.index(option) + 1] = replacement + refused = subprocess.run(invalid, capture_output=True, text=True, timeout=20) + assert expected in refused.stdout + refused.stderr, refused + assert not steps # Invalid transport never reaches the provider. + first = subprocess.run(command, capture_output=True, text=True, timeout=150) + (root / "preparation-cli.json").write_text(first.stdout) + (root / "preparation-cli.stderr").write_text(first.stderr) + result = json.loads(first.stdout) + assert first.returncode == 0 and result["ok"] and result["status"] == "stopped", (result, errors) + assert len(steps) == 4 and not errors, (steps, errors) + session = load_codex_cli_session(runner.root, lineage=lineage, host="dsh") + proposal = operation() + assert proposal["operation"].get("agent_handoff") is None + delivered = store.record_operation_delivery(proposal["proposal_id"], delivery=fixtures.delivery(proposal)) + confirmed = store.decide_operation(proposal["proposal_id"], decision="confirm", + confirmation=fixtures.confirmation(delivered)) + wake_config = {"registry_path": str(runner.registry), "goal_id": runner.goal_id, + "requester_agent_id": runner.agent_id, "project": str(root), + "execution_config": "delegations.json", "binding_id": binding["id"]} + original_args = binding["host_args"].copy() + binding["host_args"][binding["host_args"].index("--dsh-model") + 1] = "replacement-model" + runner.config.write_text(json.dumps(config)) + refused = dispatch_confirmed_operation_wake(confirmed, runtime_root=runner.root, configuration=wake_config) + assert refused["state"] == "blocked", refused + assert len(steps) == 4 + binding["host_args"] = original_args + runner.config.write_text(json.dumps(config)) + receipt = dispatch_confirmed_operation_wake(confirmed, runtime_root=runner.root, configuration=wake_config) + assert receipt["state"] == "delegation_requested", receipt + completion = wait(runner, receipt["operation_id"]) + stored = store.load(proposal["proposal_id"]) + resumed = load_codex_cli_session(runner.root, lineage=lineage, host="dsh") + assert resumed["session_id"] == session["session_id"] + assert resumed["operation_profile_digest"] == session["operation_profile_digest"] + assert len(steps) == 9 and not errors, (completion, steps, errors) + assert stored["operation"]["outcome"]["outcome"] == "not_executed", stored + journal = json.loads(runner.path(receipt["operation_id"]).read_text()) + assert stored["operation"]["host_start"]["turn_key"] == journal["turn_key"] + assert stored["operation"]["host_start"]["route"]["thread_id"] == session["session_id"] + # Waiting is deliberately not validated Todo completion or a domain effect. + assert completion["status"] == "rejected", completion + tool_results = [block for message in responses[-1] + for block in message.get("content", []) if isinstance(block, dict) and block.get("type") == "tool_result"] + results = [] + for block in tool_results: + content = block.get("content", []) + for part in content if isinstance(content, list) else [{"text": content}]: + try: + results.append(json.loads(part["text"])) + except (KeyError, ValueError): + pass + assert any(row.get("status") == "already_consumed" for row in results), results + assert sum(row.get("execution_allowed") is True for row in results) == 1, results + assert dispatch_confirmed_operation_wake(stored, runtime_root=runner.root, + configuration=wake_config)["state"] == "existing_delegation" + assert len(steps) == 9 + finally: + (root / "provider-steps.json").write_text(json.dumps({"steps": steps, "errors": errors})) + server.shutdown() + server.server_close() + thread.join(timeout=2) diff --git a/tests/control_plane/test_operation_transport_readback.py b/tests/control_plane/test_operation_transport_readback.py index f81685f04b..5d1a5b862e 100644 --- a/tests/control_plane/test_operation_transport_readback.py +++ b/tests/control_plane/test_operation_transport_readback.py @@ -10,6 +10,19 @@ service = delegation_service +@pytest.mark.parametrize("configured", [False, None, "true", 1, [True]]) +def test_dsh_transport_requires_explicit_native_module_observation(configured): + from loopx.control_plane.effect_runtime import effect_runtime_result + + result = effect_runtime_result("operation.managed_transport.project", { + "host": "dsh", "dsh_operation_module_configured": configured, + "model": "fixture-model", "reasoning_effort": "high", + }) + assert result["reason"] == "operation_transport_host_unsupported" + assert result["transport"]["configuration_valid"] is False + assert result["transport"]["runtime_qualified"] is False + + @pytest.mark.parametrize("host", [["codex-cli"], {"host": "codex-cli"}, None, True, 1]) def test_supported_host_readback_never_coerces_an_invalid_host(host): from loopx.control_plane.effect_runtime import effect_runtime_result diff --git a/tests/test_turn_managed_executor_binding.py b/tests/test_turn_managed_executor_binding.py index ab9d336735..6e9e643934 100644 --- a/tests/test_turn_managed_executor_binding.py +++ b/tests/test_turn_managed_executor_binding.py @@ -182,6 +182,26 @@ def test_configured_dsh_model_does_not_qualify_operation_transport_or_change_hos assert "fixture-token" not in json.dumps([ordinary, blocked, payload]) +def test_explicit_dsh_native_transport_uses_resolved_profile_without_claiming_qualification(): + kwargs = dict( + environ={"DEEPSEEK_API_KEY": "fixture-token"}, module_probe=_RUNTIME, + provider="deepseek-official", model="fixture-model", reasoning_effort="high", + ) + ordinary = managed_executor_binding("dsh", **kwargs) + observed = managed_executor_binding("dsh", dsh_operation_module_configured=True, **kwargs) + assert "operation_transport" not in ordinary + assert observed["available"] is True + assert observed["execution_profile"] == ordinary["execution_profile"] + transport = observed["operation_transport"] + assert transport["kind"] == "owned_native_sdk" + assert transport["revision"] == "dsh-native-operation-tools-v0" + assert transport["configuration_valid"] is True + assert transport["runtime_qualified"] is False + assert transport["human_confirmation_required"] is True + assert transport["first_consumption_required"] is True + assert transport["source_conversation_is_executor"] is False + + def test_managed_executor_reports_the_operator_credential_and_endpoint(): binding = managed_executor_binding( "dsh", From 06f5131d7456d57200fed4995e177ff711ac9d86 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Thu, 15 Oct 2026 11:45:23 +0800 Subject: [PATCH 09/16] docs(dsh): describe explicit operation entry and callback recovery Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../human-confirmed-domain-operations-v0.md | 18 +++++++++-- ...an-confirmed-domain-operations-v0.zh-CN.md | 14 ++++++-- docs/reference/local-delegation.md | 32 ++++++++++++++++++- 3 files changed, 58 insertions(+), 6 deletions(-) diff --git a/docs/architecture/rfcs/human-confirmed-domain-operations-v0.md b/docs/architecture/rfcs/human-confirmed-domain-operations-v0.md index fd23f9ec48..db84d16708 100644 --- a/docs/architecture/rfcs/human-confirmed-domain-operations-v0.md +++ b/docs/architecture/rfcs/human-confirmed-domain-operations-v0.md @@ -480,9 +480,21 @@ replacing identity. SDK files, Node/entry bytes and the entire npm closure join the frozen profile. File/SQLite restart cases recover the original operation; missing/unreadable history refuses without provider input or a reusable failed client. This is candidate-source qualification, not a published Python runtime -or whole-plugin compatibility claim. Public CLI/delegation stays Codex-only; -frontend/Lark return, other runtimes and live financial effects remain -unqualified. Do not enable public DSH preflight from these backend results. +or whole-plugin compatibility claim. + +The explicit `turn run-once --host dsh --dsh-operation-module /absolute/native-operation.js` +entry now shares this owner, the existing Todo session planner and the +operator-granted delegation callback. The module flag is default-off and +cannot be combined with `--dsh-runner` or another host. Configuration readback +identifies `owned_native_sdk` but retains `runtime_qualified: false`. +File/SQLite qualification uses the actual CLI, SDK and detached callback to +resume the original persisted session/profile, consume once, reject repetition +and record `not_executed`; the typed wait remains a rejected delegation rather +than validated domain completion. Provider endpoint bytes join the immutable +profile without serializing credentials. The generic `--confirmed-operation-id` +and `--operation-source-route-json` flags retain their Codex aliases. +Frontend/Lark return, other runtimes and live financial effects remain +unqualified. The CLI fixture does not certify original-card delivery or approval. Public `goal-channel inspect-operation/consume-operation/report-operation` remain fail-closed before private reads or writes, even with matching diff --git a/docs/architecture/rfcs/human-confirmed-domain-operations-v0.zh-CN.md b/docs/architecture/rfcs/human-confirmed-domain-operations-v0.zh-CN.md index 04bb5f2330..d7d3b2fb94 100644 --- a/docs/architecture/rfcs/human-confirmed-domain-operations-v0.zh-CN.md +++ b/docs/architecture/rfcs/human-confirmed-domain-operations-v0.zh-CN.md @@ -374,8 +374,18 @@ SDK `0.1.5rc1` 的跨进程恢复仍在启动前拒绝,原 `session already ex 恢复原身份。SDK 文件、Node/入口原字节及完整 npm closure 纳入冻结 profile。 File/SQLite 重启用例恢复同一操作;历史丢失/不可读时拒绝,不向模型发送输入, 不替换绑定,失败 client 不可复用。这只是候选源码资格,不能当作已发布 Python -runtime 或整个插件的兼容性证明。公开 CLI/delegation 仍仅支持 Codex,前端/Lark -回传、其它 runtime 和真实金融效果仍未验,不能据此开启公开 DSH preflight。 +runtime 或整个插件的兼容性证明。 + +显式 `turn run-once --host dsh --dsh-operation-module /absolute/native-operation.js` +入口现复用该 owner、原 Todo session planner 和 operator 授权的 delegation +callback。模块开关默认关闭,不能与 `--dsh-runner` 或其它 host 混用。配置读回 +标明 `owned_native_sdk`,仍保持 `runtime_qualified: false`。File/SQLite 使用 +真实 CLI、SDK 和独立 callback 进程恢复原持久 session/profile,首次消费、重复 +拒绝并记录 `not_executed`;typed wait 仍是 rejected delegation,不是业务完成。 +endpoint 原字节的哈希纳入不可变 profile,不序列化凭据。通用 +`--confirmed-operation-id`、`--operation-source-route-json` 保留旧 Codex 别名。 +前端/Lark 回传、其它 runtime 和真实金融效果仍未验;CLI 夹具不证明原卡送达 +或真实用户批准。 公开 `goal-channel inspect-operation/consume-operation/report-operation` 仍在 私有读写前拒绝,即使 `CODEX_THREAD_ID`、路由、自签 proof 完全匹配或旧运行时 diff --git a/docs/reference/local-delegation.md b/docs/reference/local-delegation.md index 087607148c..532990052f 100644 --- a/docs/reference/local-delegation.md +++ b/docs/reference/local-delegation.md @@ -1258,11 +1258,41 @@ This is a fragment, not a complete collector configuration. `execution_config` is project-relative, without traversal or symlinks, and remains outside the member workspace. The selected binding must grant that registered requester the operation's exact Agent/Todo. Use `codex-cli`, `--codex-operation-tools` -and the original pinned model/effort. Do not select `fresh`, change the owning +and the original pinned model/effort, or the explicit DSH entry described below. +Do not select `fresh`, change the owning home, binary, sandbox or effective MCP configuration, or retarget the Todo. Prepare the operation in this same managed session/profile. Existing delegation inspection and ordinary Turn admission still determine whether it can run. +For the candidate DSH transport, prepare through the normal CLI with +`--host dsh --dsh-operation-module /absolute/native-operation.js` and retain +those exact options in the original delegation binding. Also pin the original +`--dsh-runtime-bin`, `--dsh-home`, `--dsh-cordis`, provider/model/effort and token +budget. The supported restart candidate is Python SDK `0.2.1a3` built from +official source `2fbf4359` plus explicit npm runtime +`@deepseek-ai/dsh@0.2.1-alpha.3`; it is not a published Python runtime release. +SDK/bundled runtime `0.1.5rc1` cannot resume across processes. Do not substitute +`--dsh-runner`, a replacement home/session, or a changed provider endpoint. +The native module, SDK/runtime closure, effective profile, patch and endpoint +hash are compared again before model input. Missing/corrupt history refuses. + +`--operation-source-route-json '{"host_surface":"registered-surface","thread_id":"registered-thread"}'` +selects an already registered return audience when required. The original +source is not executor authentication. `--confirmed-operation-id` is the +callback-owned exact locator; both flags retain their old Codex aliases. +Do not supply a locator as proof of confirmation. Removing +`--dsh-operation-module` disables this transport for subsequent Turns; it does +not cancel an existing operation or reconcile an uncertain result. Keep the +old pinned SDK/runtime/profile available for rollback and original recovery. +Ordinary DSH and legacy Codex paths retain their existing behavior. + +中文:DSH 候选通过正常 CLI 显式加载绝对路径原生模块,并把相同参数保留在原 +delegation binding。保持原 runtime/home/patch、provider、模型/深度、token +预算及 endpoint;不能换 session 绕过恢复失败。缺失/损坏历史在模型输入前拒绝。 +配置可用不等于运行资格;夹具确认、派发、规范结果、原卡回传和真实交易分别验收。 +移除模块开关只关闭后续 Turn 的该 transport,不取消旧操作或解除未知结果;保留 +旧 pin 供回滚与原操作恢复,不改全局安装。 + If the original standalone operation Session was prepared **without** a delegation MCP server, its profile includes `mcp_server: null`. Preserve that exact profile using the existing operator-owned binding option: From 828d633df4c9835da91ded28403e55e4c6543531 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Thu, 15 Oct 2026 12:22:25 +0800 Subject: [PATCH 10/16] fix(extensions): reuse legacy status proof ordering Reuse the verified status-readback correction from PR #6463 (00fcd1a99ea9dfe25166866d49271ca4cda4407b). Missing legacy revision metadata remains readable and unverified; activation still refuses it. Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- loopx/extensions/runtime.py | 8 ++++---- tests/test_local_state_migration.py | 17 +++++++++++++++-- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/loopx/extensions/runtime.py b/loopx/extensions/runtime.py index 9c52db6413..112e671b81 100644 --- a/loopx/extensions/runtime.py +++ b/loopx/extensions/runtime.py @@ -478,11 +478,11 @@ def extension_status( "enabled": bool(entry.get("enabled")), "active_revision": entry.get("active_revision"), "rollback_available": bool(entry.get("rollback_revision")), - # Readiness is the same predicate the catalog and dispatch use: - # a stale proof under an incompatible API is not "verified". + # Legacy records stay readable but unverified. Check the proof + # before parsing their possibly absent revision metadata. "doctor_verified": bool(entry.get("enabled")) - and _active_manifest_incompatibility(entry) is None - and _verified_entrypoint(entry) is not None, + and _verified_entrypoint(entry) is not None + and _active_manifest_incompatibility(entry) is None, "revision_count": len(entry.get("revisions") or []), } for entry in visible diff --git a/tests/test_local_state_migration.py b/tests/test_local_state_migration.py index fb5a7e2ce1..7dd1136f20 100644 --- a/tests/test_local_state_migration.py +++ b/tests/test_local_state_migration.py @@ -614,9 +614,22 @@ def test_extension_cli_keeps_legacy_machine_state_without_a_global_registry(tmp_ "schema_version": "loopx_extension_state_v0", "extensions": {"example": {"id": "example", "enabled": True}}, }) - assert _list_extensions(source.parents[1])[0]["id"] == "example" + state_file = source / "extensions" / "state.json" + original = state_file.read_bytes() + [status] = _list_extensions(source.parents[1]) + assert status["id"] == "example" + assert status["enabled"] is True + assert status["doctor_verified"] is False + assert state_file.read_bytes() == original + refused = _extension_cli_result(source.parents[1], "enable", "example", "--execute") + assert refused.returncode == 2 + assert "extension revision history is invalid" in refused.stdout + assert state_file.read_bytes() == original assert _extension_cli(source.parents[1], "disable", "example", "--execute")["changed"] - assert _read(source / "extensions" / "state.json")["extensions"]["example"]["enabled"] is False + [disabled] = _list_extensions(source.parents[1]) + assert disabled["enabled"] is False + assert disabled["doctor_verified"] is False + assert _read(state_file)["extensions"]["example"] == {"id": "example", "enabled": False} assert not target.exists() From 81ce66f954104b67f72d389f58951b965c946a79 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Thu, 15 Oct 2026 12:22:26 +0800 Subject: [PATCH 11/16] test(dsh): claim and select work before exact receipt closeout Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../tests/admission-closeout.integration.spec.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/packages/dsh-loopx-plugin/tests/admission-closeout.integration.spec.ts b/packages/dsh-loopx-plugin/tests/admission-closeout.integration.spec.ts index d5818b460a..a7b979def4 100644 --- a/packages/dsh-loopx-plugin/tests/admission-closeout.integration.spec.ts +++ b/packages/dsh-loopx-plugin/tests/admission-closeout.integration.spec.ts @@ -289,14 +289,24 @@ describe('DSH admission-to-closeout integration', () => { ) expect(admission.kind).toBe('enter') + // The queued conversation lets the agent choose work; it does not claim + // the fixture's Todo or bind the Turn on the agent's behalf. + const claimed = await runCli(testFixture, [ + 'todo', 'claim', '--goal-id', goalId, '--todo-id', todoId, + '--agent-id', agentId, '--claimed-by', agentId, + ]) + expect(claimed.exitCode, JSON.stringify(claimed.payload)).toBe(0) + expect(claimed.payload.ok).toBe(true) const guard = await runCli(testFixture, [ 'quota', 'should-run', '--goal-id', goalId, '--agent-id', agentId, + '--todo-id', todoId, '--runtime-profile', 'generic_cli', '--turn-instance-id', turnInstanceId, ]) expect(guard.exitCode).toBe(0) + expect(guard.payload.ok).toBe(true) const heartbeatReceipt = record(guard.payload.heartbeat_receipt) expect(heartbeatReceipt.turn_instance_id).toBe(turnInstanceId) const cliChannel = record(record(guard.payload.interaction_contract).cli_channel) @@ -365,7 +375,11 @@ describe('DSH admission-to-closeout integration', () => { && event.agent_id === agentId && event.run_id === turnInstanceId )) - expect(admissionReceipts).toHaveLength(1) + // Explicit selection upgrades the same receipt, producing another audit + // observation. The invariant is one Turn/effect and one spend, not one read. + expect(admissionReceipts.length).toBeGreaterThan(0) + expect(new Set(admissionReceipts.map(event => event.run_id))) + .toEqual(new Set([turnInstanceId])) expect(events.filter(event => ( event.event_kind === 'quota_should_run' && event.run_id !== turnInstanceId From ce1b6ee7911867436163e1056e16ed851979cb51 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Thu, 15 Oct 2026 12:46:08 +0800 Subject: [PATCH 12/16] test(dsh): qualify explicit native operation package entries Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- packages/dsh-loopx-plugin/smoke/dsh-goalbar-runtime-smoke.mjs | 4 ++++ packages/dsh-loopx-plugin/smoke/dsh-profile-smoke.mjs | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/packages/dsh-loopx-plugin/smoke/dsh-goalbar-runtime-smoke.mjs b/packages/dsh-loopx-plugin/smoke/dsh-goalbar-runtime-smoke.mjs index 8e8b4558e9..92adb57365 100644 --- a/packages/dsh-loopx-plugin/smoke/dsh-goalbar-runtime-smoke.mjs +++ b/packages/dsh-loopx-plugin/smoke/dsh-goalbar-runtime-smoke.mjs @@ -46,6 +46,8 @@ const packedStaticEntries = new Set([ 'package/lib/driver.js', 'package/lib/index.js', 'package/lib/init-command.js', + 'package/lib/native-operation-context.js', + 'package/lib/native-operation.js', 'package/lib/observer.js', 'package/lib/types/cli.d.ts', 'package/lib/types/client/LoopXGoalBar.d.ts', @@ -63,6 +65,8 @@ const packedStaticEntries = new Set([ 'package/lib/types/index.d.ts', 'package/lib/types/init-command.d.ts', 'package/lib/types/managed-runtime.d.ts', + 'package/lib/types/native-operation-context.d.ts', + 'package/lib/types/native-operation.d.ts', 'package/lib/types/observer.d.ts', 'package/package.json', ]) diff --git a/packages/dsh-loopx-plugin/smoke/dsh-profile-smoke.mjs b/packages/dsh-loopx-plugin/smoke/dsh-profile-smoke.mjs index 54e720f873..9bf545eea6 100755 --- a/packages/dsh-loopx-plugin/smoke/dsh-profile-smoke.mjs +++ b/packages/dsh-loopx-plugin/smoke/dsh-profile-smoke.mjs @@ -26,6 +26,8 @@ const packedStaticEntries = new Set([ 'package/lib/driver.js', 'package/lib/index.js', 'package/lib/init-command.js', + 'package/lib/native-operation-context.js', + 'package/lib/native-operation.js', 'package/lib/observer.js', 'package/lib/types/cli.d.ts', 'package/lib/types/client/LoopXGoalBar.d.ts', @@ -43,6 +45,8 @@ const packedStaticEntries = new Set([ 'package/lib/types/index.d.ts', 'package/lib/types/init-command.d.ts', 'package/lib/types/managed-runtime.d.ts', + 'package/lib/types/native-operation-context.d.ts', + 'package/lib/types/native-operation.d.ts', 'package/lib/types/observer.d.ts', 'package/package.json', ]) From e9161b8313fd3a3e0d76d1b3dbadc6d889b99cbc Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Thu, 15 Oct 2026 13:01:32 +0800 Subject: [PATCH 13/16] fix(lark): verify rendered confirmation text on original cards Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../lark/goal_channel_message_delivery.py | 27 ++++- ...test_lark_goal_channel_message_delivery.py | 109 ++++++++++++++++++ 2 files changed, 133 insertions(+), 3 deletions(-) create mode 100644 tests/extensions/test_lark_goal_channel_message_delivery.py diff --git a/loopx/extensions/lark/goal_channel_message_delivery.py b/loopx/extensions/lark/goal_channel_message_delivery.py index c4c54a4b4f..7eb48aefbe 100644 --- a/loopx/extensions/lark/goal_channel_message_delivery.py +++ b/loopx/extensions/lark/goal_channel_message_delivery.py @@ -1,6 +1,7 @@ from __future__ import annotations import hashlib +import html import json from collections.abc import Callable, Mapping from contextlib import ExitStack @@ -242,13 +243,29 @@ def _card_v2_element_lines(value: object) -> list[str] | None: tag = value.get("tag") if tag == "markdown": content = value.get("content") - return [content] if isinstance(content, str) and content else None + # The provider's userDSL describes rendered Markdown, including entity + # decoding. Plain-text elements below retain their literal content. + return ( + [html.unescape(content)] if isinstance(content, str) and content else None + ) if tag in {"plain_text", "text"}: content = value.get("content") or value.get("text") return [content] if isinstance(content, str) and content else None if tag == "button": label = _text_content(value.get("text")) - return [f"[{label}]"] if label else None + if not label: + return None + text = f"[{label}]" + if "confirm" in value: + confirm = value["confirm"] + if not isinstance(confirm, Mapping): + return None + title = _text_content(confirm.get("title")) + body = _text_content(confirm.get("text")) + if not title or not body: + return None + text += f"(confirm:{json.dumps(f'{title}: {body}', ensure_ascii=False)})" + return [text] children: object = None if tag == "column_set": children = value.get("columns") @@ -265,7 +282,11 @@ def _card_v2_element_lines(value: object) -> list[str] | None: if ( isinstance(child, Mapping) and child.get("tag") == "column" - and all(line.startswith("[") and line.endswith("]") for line in child_lines) + and isinstance(child.get("elements"), list) + and all( + isinstance(item, Mapping) and item.get("tag") == "button" + for item in child["elements"] + ) ): button_labels.extend(child_lines) else: diff --git a/tests/extensions/test_lark_goal_channel_message_delivery.py b/tests/extensions/test_lark_goal_channel_message_delivery.py new file mode 100644 index 0000000000..dba7454ecb --- /dev/null +++ b/tests/extensions/test_lark_goal_channel_message_delivery.py @@ -0,0 +1,109 @@ +"""Provider-rendered confirmation text must preserve its visible terms.""" + +from copy import deepcopy + +from loopx.extensions.lark.card_callback import callback_card_content_matches +from loopx.extensions.lark.goal_channel_message_delivery import ( + message_card_matches, + normalized_card_text, +) + + +def confirmation_card(): + return { + "schema": "2.0", + "header": {"title": {"tag": "plain_text", "content": "Review request"}}, + "body": { + "elements": [ + { + "tag": "markdown", + "content": "Result: not_executed & retained", + }, + { + "tag": "column_set", + "columns": [ + { + "tag": "column", + "elements": [ + { + "tag": "button", + "text": { + "tag": "plain_text", + "content": "Continue", + }, + "confirm": { + "title": { + "tag": "plain_text", + "content": "Confirm request?", + }, + "text": { + "tag": "plain_text", + "content": "Terms cannot change.", + }, + }, + "behaviors": [ + { + "type": "callback", + "value": {"decision": "confirm"}, + } + ], + } + ], + }, + { + "tag": "column", + "elements": [ + { + "tag": "button", + "text": {"tag": "plain_text", "content": "Reject"}, + } + ], + }, + ], + }, + ], + }, + } + + +PROVIDER_CONTENT = ( + '\n' + "Result: not_executed & retained\n" + '[Continue](confirm:"Confirm request?: Terms cannot change.") [Reject]\n' + "" +) + + +def test_native_user_dsl_preserves_confirmation_and_rendered_entities(): + card = confirmation_card() + assert normalized_card_text(card) == PROVIDER_CONTENT + assert message_card_matches({"content": PROVIDER_CONTENT}, card) + assert callback_card_content_matches(PROVIDER_CONTENT, card) + # Visible equality still cannot authenticate hidden callback terms in history. + assert not message_card_matches( + {"content": PROVIDER_CONTENT}, card, allow_normalized=False + ) + + +def test_changed_visible_terms_or_confirmation_are_rejected(): + for changed in ( + PROVIDER_CONTENT.replace("not_executed", "filled"), + PROVIDER_CONTENT.replace("Terms cannot change.", "Terms may change."), + PROVIDER_CONTENT.replace( + '(confirm:"Confirm request?: Terms cannot change.")', "" + ), + ): + assert not message_card_matches({"content": changed}, confirmation_card()) + assert not callback_card_content_matches(changed, confirmation_card()) + + +def test_unsupported_confirmation_and_plain_text_are_not_silently_normalized(): + card = deepcopy(confirmation_card()) + card["body"]["elements"][1]["columns"][0]["elements"][0]["confirm"] = {} + assert normalized_card_text(card) is None + plain = { + "schema": "2.0", + "header": {"title": {"content": "Title"}}, + "body": {"elements": [{"tag": "plain_text", "content": "a & b"}]}, + } + assert normalized_card_text(plain) == '\na & b\n' From 5d37ec76dd16b595b763977fcf886c0b296e17dd Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Thu, 15 Oct 2026 13:01:32 +0800 Subject: [PATCH 14/16] test(lark): exercise the shared atomic writer after kernel migration Follow the existing atomic-write owner from PR #6413 so a failed rename still proves the private binding is preserved. Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- tests/extensions/test_lark_goal_channel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/extensions/test_lark_goal_channel.py b/tests/extensions/test_lark_goal_channel.py index 9fabb1efd4..18b926402a 100644 --- a/tests/extensions/test_lark_goal_channel.py +++ b/tests/extensions/test_lark_goal_channel.py @@ -439,7 +439,7 @@ def fail_replace(source: object, destination: object) -> None: raise OSError("fixture replace failure") monkeypatch.setattr( - "loopx.extensions.lark.private_json.os.replace", + "loopx.file_lock.os.replace", fail_replace, ) From 5a0abe440e03cc1eef8996e73ed6b4b468614680 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Thu, 15 Oct 2026 13:23:30 +0800 Subject: [PATCH 15/16] docs(dsh): record live original-card return and remaining wakeup gap Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../rfcs/human-confirmed-domain-operations-v0.md | 16 ++++++++++++++-- ...human-confirmed-domain-operations-v0.zh-CN.md | 11 +++++++++-- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/docs/architecture/rfcs/human-confirmed-domain-operations-v0.md b/docs/architecture/rfcs/human-confirmed-domain-operations-v0.md index a9493a3d1c..defa39ecb2 100644 --- a/docs/architecture/rfcs/human-confirmed-domain-operations-v0.md +++ b/docs/architecture/rfcs/human-confirmed-domain-operations-v0.md @@ -493,8 +493,20 @@ and record `not_executed`; the typed wait remains a rejected delegation rather than validated domain completion. Provider endpoint bytes join the immutable profile without serializing credentials. The generic `--confirmed-operation-id` and `--operation-source-route-json` flags retain their Codex aliases. -Frontend/Lark return, other runtimes and live financial effects remain -unqualified. The CLI fixture does not certify original-card delivery or approval. +The CLI fixture does not certify original-card delivery or genuine approval. + +A subsequent bounded live qualification used the real model, an actual Lark +card and authenticated human confirmation. Normal native CLI continuation +restored the original DSH session/profile, consumed once, recorded canonical +`not_executed` and patched the same original card; independent provider +readback verified the result. This involved zero financial action. The +continuation was manually invoked after the callback: unattended wakeup was +not configured, and the approximately 246-second confirmation-to-host-start +observation does not meet the 60-second healthy automatic-path target below. +Other runtimes, global installation, account effects and financial utility +remain unqualified. An older globally installed Core rejects the new DSH +executor during inbox projection; the candidate-source entry reads it, so +rollback must retain those pins rather than assume old-reader compatibility. Public `goal-channel inspect-operation/consume-operation/report-operation` remain fail-closed before private reads or writes, even with matching diff --git a/docs/architecture/rfcs/human-confirmed-domain-operations-v0.zh-CN.md b/docs/architecture/rfcs/human-confirmed-domain-operations-v0.zh-CN.md index d91e066777..7db86af484 100644 --- a/docs/architecture/rfcs/human-confirmed-domain-operations-v0.zh-CN.md +++ b/docs/architecture/rfcs/human-confirmed-domain-operations-v0.zh-CN.md @@ -384,8 +384,15 @@ callback。模块开关默认关闭,不能与 `--dsh-runner` 或其它 host 拒绝并记录 `not_executed`;typed wait 仍是 rejected delegation,不是业务完成。 endpoint 原字节的哈希纳入不可变 profile,不序列化凭据。通用 `--confirmed-operation-id`、`--operation-source-route-json` 保留旧 Codex 别名。 -前端/Lark 回传、其它 runtime 和真实金融效果仍未验;CLI 夹具不证明原卡送达 -或真实用户批准。 +CLI 夹具不证明原卡送达或真实用户批准。 + +后续有界真实验收使用实际模型、真实 Lark 卡片及经过认证的人类确认。正常原生 +CLI 续接恢复原 DSH session/profile,首次消费一次,登记规范 `not_executed`, +并更新同一原卡;独立 provider 读回验证结果,金融动作是零。此续接由角色在 +callback 后手动调用:无人值守唤醒尚未配置,实际确认至宿主启动约 246 秒, +不满足下文健康自动路径 60 秒目标。其它 runtime、全局安装、账户副作用和金融 +效用仍未验。本机较旧 Core 在 inbox 投影时拒绝新的 DSH executor,候选源码 +入口可读;回滚须保留这些 pin,不能假定旧 reader 兼容。 公开 `goal-channel inspect-operation/consume-operation/report-operation` 仍在 私有读写前拒绝,即使 `CODEX_THREAD_ID`、路由、自签 proof 完全匹配或旧运行时 From 56deaac013a858856ac9e7c31b50bc394ee3a10f Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Thu, 15 Oct 2026 15:41:21 +0800 Subject: [PATCH 16/16] test(workspace): restore the last task on browser Back Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../personal-workspace-browser/goal-workspace-m2.mjs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/examples/personal-workspace-browser/goal-workspace-m2.mjs b/examples/personal-workspace-browser/goal-workspace-m2.mjs index f7c782231b..74098aaae6 100644 --- a/examples/personal-workspace-browser/goal-workspace-m2.mjs +++ b/examples/personal-workspace-browser/goal-workspace-m2.mjs @@ -76,11 +76,19 @@ export const goalWorkspaceM2Scenario = { await now.locator('.work-map').waitFor(); await now.getByRole('button',{name:'按负责人',exact:true}).click(); await now.locator('[data-todo-id="showcase-reconcile"]').click(); + await page.waitForURL(value => value.searchParams.get("todo") === "showcase-reconcile"); await page.locator('.personal-drawer-close').click(); + await page.waitForURL(value => !value.searchParams.has("todo")); + await page.locator('.personal-context-drawer').waitFor({state:"hidden"}); await page.goBack({waitUntil:"domcontentloaded"}); + // Back restores the task just closed, not a selection from an earlier view. + await page.waitForURL(value => value.searchParams.get("todo") === "showcase-reconcile"); await page.locator('.personal-context-drawer').waitFor(); - assert.equal(new URL(page.url()).searchParams.get("todo"), "showcase-map-columns"); - await page.locator('.personal-drawer-close').click(); + await now.locator('[data-todo-id="showcase-reconcile"][aria-pressed="true"]').waitFor(); + assert.equal(await now.locator('[data-todo-id="showcase-reconcile"]').getAttribute('aria-pressed'),'true'); + await page.goForward({waitUntil:"domcontentloaded"}); + await page.waitForURL(value => !value.searchParams.has("todo")); + await page.locator('.personal-context-drawer').waitFor({state:"hidden"}); assert.equal(writes.length,0,`Read-only navigation made an effect request: ${writes}`); await page.screenshot({path:resolve(outputDir,'goal-workspace-m2-desktop.png'),animations:'disabled'}); await page.setViewportSize({width:390,height:844});