Files
deepseek-harness/packages/subprocess/subprocess-local/src/linux-scope-selector.ts
T
imccyu da5e23e8e8 refactor(subprocess-local): decide Linux containment in a testable selector
The provider's Linux decisions now live in LinuxScopeSelector, a state machine
over injected observations: the deep probe, the user-manager listener, the
manager confirmation, and the systemd-run binary identity. Each decision
carries its reason and probe trigger, the runtime logs it at debug level, and
table-driven scenarios cover every transition.

- A positive verdict probes again when the systemd-run found through PATH
  changes device, inode, size, or modification time, so an in-place upgrade
  past v254 cannot reuse options that no longer keep argv literal.
- The listener is read before the probe, so a manager that starts during a
  failed probe counts as a new listener.
- The kernel socket table is decoded as UTF-8, matching runtime directories
  outside ASCII.
- LinuxScopeNotStartedError drops its unused code field.
2026-10-15 00:51:05 +08:00

102 lines
4.9 KiB
TypeScript

/** Linux native containment decisions over injected host observations. */
import type { LiteralArgvOptions } from './linux-scope.ts'
/** Host observations behind each decision; the runtime supplies the real checks and tests script them. */
export interface LinuxScopeObservations {
/** Run the deep transient-scope probe: literal-argv options when a scope can start, otherwise undefined. */
probe(): LiteralArgvOptions | undefined
/** Identify the process listening on the user manager's private socket, or undefined when none listens. */
managerListener(): string | undefined
/** Confirm the user manager before a launch under a positive verdict. */
confirmManager(): boolean
/** Identify the systemd-run binary scope launches resolve, or undefined when none resolves. */
launcherIdentity(): string | undefined
}
/** Event that made the selector probe: the first selection or one that invalidated the previous verdict. */
export type LinuxProbeTrigger = 'first' | 'scope-not-started' | 'new-listener' | 'launcher-changed'
/** One containment decision and the branch that produced it. */
export type LinuxScopeDecision =
| {
readonly kind: 'scope'
/** A probe found scope support, or a positive verdict confirmed the user manager. */
readonly reason: 'probe-available' | 'manager-confirmed'
readonly literalArgv: LiteralArgvOptions
/** Probe count when the verdict was made; pass it to scopeNotStarted for a failed launch. */
readonly generation: number
readonly probe: LinuxProbeTrigger | undefined
}
| {
readonly kind: 'fallback'
/**
* A probe found no scope support; a positive verdict failed to confirm the manager for this launch;
* or a negative verdict saw no listener or the listener present at the failed probe.
*/
readonly reason: 'probe-unavailable' | 'manager-unconfirmed' | 'no-listener' | 'same-listener'
readonly probe: LinuxProbeTrigger | undefined
}
type LinuxScopeState =
| { readonly kind: 'unprobed'; readonly trigger: LinuxProbeTrigger }
| { readonly kind: 'available'; readonly literalArgv: LiteralArgvOptions; readonly launcher: string | undefined }
| { readonly kind: 'unavailable'; readonly listener: string | undefined }
/**
* Provider-lifetime Linux scope selection. A probe runs on the first selection and again only after
* a scope launch never started its target, a different user-manager listener appears under a
* negative verdict, or the systemd-run binary changes under a positive one. A positive verdict
* confirms the user manager on every selection. Selection is synchronous, so a decision stays
* valid for the launch that follows it.
*/
export class LinuxScopeSelector {
private state: LinuxScopeState = { kind: 'unprobed', trigger: 'first' }
private generation = 0
/** @param observe - host checks consulted by each selection. */
constructor(private readonly observe: LinuxScopeObservations) {}
/**
* Choose containment for one eligible launch.
* @returns the decision and the branch that produced it.
*/
select(): LinuxScopeDecision {
const state = this.state
if (state.kind === 'unprobed') return this.probe(state.trigger)
if (state.kind === 'available') {
if (this.observe.launcherIdentity() !== state.launcher) return this.probe('launcher-changed')
return this.observe.confirmManager()
? { kind: 'scope', reason: 'manager-confirmed', literalArgv: state.literalArgv, generation: this.generation, probe: undefined }
: { kind: 'fallback', reason: 'manager-unconfirmed', probe: undefined }
}
const listener = this.observe.managerListener()
if (listener === undefined) return { kind: 'fallback', reason: 'no-listener', probe: undefined }
if (listener === state.listener) return { kind: 'fallback', reason: 'same-listener', probe: undefined }
return this.probe('new-listener')
}
/**
* Record that a scope launched under generation exited before its bootstrap started the target,
* so the next selection probes again. Launches from a superseded probe are ignored.
* @param generation - generation of the scope decision that launched it.
*/
scopeNotStarted(generation: number): void {
if (generation === this.generation) this.state = { kind: 'unprobed', trigger: 'scope-not-started' }
}
private probe(trigger: LinuxProbeTrigger): LinuxScopeDecision {
this.generation += 1
// Read before probing: a manager that starts during a failed probe then counts as a new listener.
const listener = this.observe.managerListener()
const launcher = this.observe.launcherIdentity()
const literalArgv = this.observe.probe()
if (literalArgv === undefined) {
this.state = { kind: 'unavailable', listener }
return { kind: 'fallback', reason: 'probe-unavailable', probe: trigger }
}
this.state = { kind: 'available', literalArgv, launcher }
return { kind: 'scope', reason: 'probe-available', literalArgv, generation: this.generation, probe: trigger }
}
}