@opengeni/runtime 0.14.1 → 0.14.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/dist/{chunk-4YJ5Z52Z.js → chunk-7E2ZSCZK.js} +6 -6
  2. package/dist/{chunk-4YJ5Z52Z.js.map → chunk-7E2ZSCZK.js.map} +1 -1
  3. package/dist/{chunk-HHWIZHQM.js → chunk-GNEGANUF.js} +942 -300
  4. package/dist/chunk-GNEGANUF.js.map +1 -0
  5. package/dist/codex-tool-search.d.ts +100 -0
  6. package/dist/context-compaction.d.ts +236 -0
  7. package/dist/history-sanitizer.d.ts +320 -0
  8. package/dist/index.d.ts +186 -718
  9. package/dist/index.js +612 -126
  10. package/dist/index.js.map +1 -1
  11. package/dist/{localSources-T3HZ6KY4.js → localSources-BXHBKGF4.js} +2 -2
  12. package/dist/mcp-network.d.ts +32 -35
  13. package/dist/metrics.d.ts +33 -0
  14. package/dist/sandbox/capabilities.d.ts +11 -0
  15. package/dist/sandbox/channel-a.d.ts +226 -0
  16. package/dist/sandbox/display-stack.d.ts +99 -0
  17. package/dist/sandbox/errors.d.ts +16 -0
  18. package/dist/sandbox/exec-banner.d.ts +19 -0
  19. package/dist/sandbox/index.d.ts +190 -6
  20. package/dist/sandbox/index.js +20 -4
  21. package/dist/sandbox/op-correlation.d.ts +24 -0
  22. package/dist/sandbox/provider-errors.d.ts +32 -0
  23. package/dist/sandbox/provider-operation-gate.d.ts +12 -0
  24. package/dist/sandbox/providers/blaxel.d.ts +2 -0
  25. package/dist/sandbox/providers/cloudflare.d.ts +2 -0
  26. package/dist/sandbox/providers/daytona.d.ts +2 -0
  27. package/dist/sandbox/providers/docker.d.ts +2 -0
  28. package/dist/sandbox/providers/e2b.d.ts +2 -0
  29. package/dist/sandbox/providers/index.d.ts +11 -0
  30. package/dist/sandbox/providers/local.d.ts +2 -0
  31. package/dist/sandbox/providers/modal.d.ts +132 -0
  32. package/dist/sandbox/providers/none.d.ts +2 -0
  33. package/dist/sandbox/providers/runloop.d.ts +2 -0
  34. package/dist/sandbox/providers/selfhosted.d.ts +2 -0
  35. package/dist/sandbox/providers/types.d.ts +30 -0
  36. package/dist/sandbox/providers/vercel.d.ts +2 -0
  37. package/dist/sandbox/recording.d.ts +80 -0
  38. package/dist/sandbox/routing/backend-resolver.d.ts +173 -0
  39. package/dist/sandbox/routing/routing-session.d.ts +401 -0
  40. package/dist/sandbox/run-credentials.d.ts +65 -0
  41. package/dist/sandbox/select.d.ts +120 -0
  42. package/dist/sandbox/selfhosted/capabilities.d.ts +91 -0
  43. package/dist/sandbox/selfhosted/control-rpc.d.ts +175 -0
  44. package/dist/sandbox/selfhosted/fault-rendering.d.ts +40 -0
  45. package/dist/sandbox/selfhosted/op-observer.d.ts +40 -0
  46. package/dist/sandbox/selfhosted/op-stream.d.ts +104 -0
  47. package/dist/sandbox/selfhosted/op-testing.d.ts +108 -0
  48. package/dist/sandbox/selfhosted/op-transport.d.ts +60 -0
  49. package/dist/sandbox/selfhosted/retry-policy.d.ts +106 -0
  50. package/dist/sandbox/selfhosted/session.d.ts +552 -0
  51. package/dist/sandbox/selfhosted/testing.d.ts +48 -0
  52. package/dist/sandbox/stream-port.d.ts +80 -0
  53. package/dist/sandbox/stream-token.d.ts +44 -0
  54. package/dist/sandbox/terminal-server.d.ts +67 -0
  55. package/dist/sandbox/toolspace-token.d.ts +23 -0
  56. package/dist/sandbox/turn-tool-cancellation.d.ts +72 -0
  57. package/dist/sandbox/workspace-archive.d.ts +39 -0
  58. package/dist/sandbox-computer.d.ts +227 -0
  59. package/dist/screenshot-error-card.d.ts +1 -0
  60. package/dist/skill-library.d.ts +10 -12
  61. package/dist/usage-telemetry.d.ts +45 -0
  62. package/dist/workspace-skills.d.ts +31 -0
  63. package/package.json +7 -7
  64. package/src/context-compaction.ts +600 -6
  65. package/src/history-sanitizer.ts +10 -92
  66. package/src/index.ts +301 -62
  67. package/src/sandbox/channel-a.ts +267 -81
  68. package/src/sandbox/index.ts +56 -94
  69. package/src/sandbox/provider-operation-gate.ts +110 -0
  70. package/src/sandbox/providers/docker.ts +13 -1
  71. package/src/sandbox/providers/modal.ts +433 -3
  72. package/src/sandbox/routing/routing-session.ts +84 -8
  73. package/src/sandbox/selfhosted/session.ts +7 -6
  74. package/src/sandbox/turn-tool-cancellation.ts +47 -7
  75. package/src/sandbox/workspace-archive.ts +176 -73
  76. package/src/sandbox-computer.ts +25 -75
  77. package/dist/chunk-HHWIZHQM.js.map +0 -1
  78. package/dist/index-BraazrAO.d.ts +0 -2931
  79. /package/dist/{localSources-T3HZ6KY4.js.map → localSources-BXHBKGF4.js.map} +0 -0
@@ -0,0 +1,65 @@
1
+ import type { RunCredentialAuthNeeded, RunCredentialFile, RunCredentialRedaction, RunCredentialsResolution } from "@opengeni/contracts";
2
+ import type { ExecCommandArgs, SandboxClient, SandboxSessionLike } from "@openai/agents/sandbox";
3
+ export type RunCredentialExpectedScope = {
4
+ accountId: string;
5
+ workspaceId: string;
6
+ sessionId: string;
7
+ };
8
+ export type NormalizedRunCredentialMaterial = {
9
+ environment: Record<string, string>;
10
+ files: RunCredentialFile[];
11
+ fileEnvironment: Record<string, string>;
12
+ expiresAt: Date | null;
13
+ authNeeded: RunCredentialAuthNeeded[];
14
+ redactions: RunCredentialRedaction[];
15
+ };
16
+ export type RunCredentialCommandSession = Pick<SandboxSessionLike, "exec" | "execCommand">;
17
+ export type RunCredentialCommandRunner = (session: RunCredentialCommandSession, args: ExecCommandArgs) => Promise<unknown>;
18
+ export type RunCredentialSessionReady = (session: RunCredentialCommandSession) => Promise<void> | void;
19
+ export declare class RunCredentialValidationError extends Error {
20
+ constructor(message: string);
21
+ }
22
+ /**
23
+ * Validate the untrusted host-port response before any secret is written to a
24
+ * box. The host owns credential selection; OpenGeni owns scope checks, bounds,
25
+ * path safety, and the transport lifecycle.
26
+ */
27
+ export declare function normalizeRunCredentialsResolution(resolution: Extract<RunCredentialsResolution, {
28
+ status: "not_applicable";
29
+ }>, expected: RunCredentialExpectedScope, now?: Date): null;
30
+ export declare function normalizeRunCredentialsResolution(resolution: Exclude<RunCredentialsResolution, {
31
+ status: "not_applicable";
32
+ }>, expected: RunCredentialExpectedScope, now?: Date): NormalizedRunCredentialMaterial;
33
+ export declare function normalizeRunCredentialsResolution(resolution: RunCredentialsResolution, expected: RunCredentialExpectedScope, now?: Date): NormalizedRunCredentialMaterial | null;
34
+ export declare function runCredentialRoot(sessionId: string): string;
35
+ export declare function runCredentialPointerFile(sessionId: string): string;
36
+ export type MaterializeRunCredentialsOptions = {
37
+ sessionId: string;
38
+ attemptId: string;
39
+ executionGeneration: number;
40
+ /** Initial provision after recovery may prune generations from older attempts. */
41
+ pruneOtherAttempts?: boolean;
42
+ /** An all-auth-needed replacement removes every prior readable generation. */
43
+ prunePreviousGenerations?: boolean;
44
+ /** Renewal keeps the active and immediately prior same-attempt generations. */
45
+ pruneSupersededGenerations?: boolean;
46
+ commandRunner?: RunCredentialCommandRunner;
47
+ };
48
+ /** Materialize one complete immutable generation, then atomically flip current. */
49
+ export declare function materializeRunCredentials(session: RunCredentialCommandSession, material: NormalizedRunCredentialMaterial, options: MaterializeRunCredentialsOptions): Promise<void>;
50
+ export declare function clearRunCredentials(session: RunCredentialCommandSession, sessionId: string, commandRunner?: RunCredentialCommandRunner): Promise<void>;
51
+ /**
52
+ * Remove only generations owned by one attempt. A successor may already have
53
+ * atomically activated its own pointer after queue admission; this cleanup must
54
+ * never clear or overwrite that newer generation.
55
+ */
56
+ export declare function clearRunCredentialsForAttempt(session: RunCredentialCommandSession, options: Pick<MaterializeRunCredentialsOptions, "sessionId" | "attemptId" | "executionGeneration">, commandRunner?: RunCredentialCommandRunner): Promise<void>;
57
+ /** Prefix one sandbox command so every new process observes the active generation. */
58
+ export declare function withRunCredentialEnvironment(cmd: string, sessionId: string): string;
59
+ /**
60
+ * Preserve the provider session identity/capabilities while decorating only
61
+ * command creation. Non-command methods stay bound to the original instance.
62
+ */
63
+ export declare function withRunCredentialsSession<T extends object>(session: T, sessionId: string): T;
64
+ /** Decorate every client-created/resumed session after the host seeds it. */
65
+ export declare function withRunCredentialsClient(client: SandboxClient, sessionId: string, onSessionReady?: RunCredentialSessionReady): SandboxClient;
@@ -0,0 +1,120 @@
1
+ import { type CapabilityDescriptor, type SandboxBackend, type SandboxOs, type SessionCapabilities } from "@opengeni/contracts";
2
+ export interface NegotiationContext {
3
+ sessionId: string;
4
+ backend: SandboxBackend;
5
+ os: SandboxOs;
6
+ /** Current lease liveness; cold means nothing is provisioned yet. */
7
+ liveness: "cold" | "warming" | "warm" | "draining";
8
+ /** The lease epoch echoed on viewer heartbeats (the split-brain fence). */
9
+ leaseEpoch: number;
10
+ /** Numeric/boolean archive completeness truth. Non-persistable targets use
11
+ * null/null/false; archive locations and provider details are never exposed. */
12
+ workspaceGeneration?: number | null;
13
+ archiveGeneration?: number | null;
14
+ archiveComplete?: boolean;
15
+ /** The deployment desktop toggle (settings.sandboxDesktopEnabled). */
16
+ desktopEnabled: boolean;
17
+ /**
18
+ * The HUMAN take-control toggle (settings.sandboxDesktopInteractive). When true
19
+ * (default) and the desktop cell is available, the negotiated DesktopStream.mode
20
+ * is "interactive" — the noVNC viewer can drive mouse+keyboard into :0 (the box's
21
+ * x11vnc runs without -viewonly). When false the cell reports mode "read-only"
22
+ * and the client disables the "Take control" affordance (a genuinely read-only
23
+ * deployment). Independent of `computerUseReadOnly`, which gates the AGENT
24
+ * driver, not the human viewer plane. Defaults to true so a caller that never
25
+ * threads it (e.g. headless tests) still gets the interactive plane when the
26
+ * desktop is available.
27
+ */
28
+ desktopInteractive?: boolean;
29
+ /** The deployment computer-use toggle (settings.computerUseEnabled). The agent
30
+ * drives :0 via xdotool/scrot; availability tracks desktop. Defaults to true. */
31
+ computerUseEnabled?: boolean;
32
+ /** Whether the agent computer-use driver is gated to no-op input
33
+ * (settings.computerUseReadOnly). v1 default false (the agent clicks/types). */
34
+ computerUseReadOnly?: boolean;
35
+ /**
36
+ * Whether a scoped-stream-token secret is resolvable (stream-token availability contract). When desktop
37
+ * is enabled but this is false (no streamTokenSecret AND no delegationSecret),
38
+ * the desktop plane GRACEFULLY DEGRADES to transport:null — the deployment
39
+ * boots, but the pixel plane cannot mint scoped tokens. Defaults to true so a
40
+ * caller that never threads it (e.g. headless tests) is unaffected.
41
+ */
42
+ streamTokenSecretAvailable?: boolean;
43
+ /** Whether the calling principal has acknowledged the un-redacted desktop
44
+ * pixels (and, for a shared box, the shared-exposure disclosure). When the
45
+ * box is shared this must be the SHARED acknowledgment; a bare un-redacted ack
46
+ * does not satisfy a shared box. */
47
+ desktopAcknowledged?: boolean;
48
+ /** True when the box's group has >1 session: watching this desktop also shows
49
+ * the sibling sessions' agents on the one :0 framebuffer (addendum E.1). */
50
+ shared?: boolean;
51
+ /** The OTHER sessions whose agents may appear on the shared desktop — IDS
52
+ * ONLY, never their conversation/metadata (stress g). Empty for a solo box. */
53
+ sharedSessionIds?: string[];
54
+ /**
55
+ * The minted pixel-plane endpoint (P4.2): the direct-to-provider WS URL + the
56
+ * scoped stream token + its expiry + the framebuffer geometry. Threaded by the
57
+ * API-direct handshake AFTER it has resumed the box, ensured the display stack,
58
+ * and resolved the provider tunnel. When ABSENT (the negotiation-only read, a
59
+ * cold lease, or a degraded desktop) the DesktopStream cell reports url/token/
60
+ * expiresAt as null — the capability is advertised, the live address is not yet
61
+ * minted (the caller POSTs to /viewers to mint it). Presence does NOT override
62
+ * the gates: a degraded/cold/unacked desktop still reports transport:null and
63
+ * the minted endpoint is dropped.
64
+ */
65
+ desktopStream?: {
66
+ url: string;
67
+ token: string;
68
+ expiresAt: string;
69
+ resolution: [number, number];
70
+ };
71
+ /** The deployment terminal toggle (settings.sandboxTerminalEnabled). The REAL
72
+ * PTY (ttyd pty-ws) is gated on this + a real-PTY backend; when off the
73
+ * Terminal cell still advertises the read-only sse-events firehose. Defaults to
74
+ * true so a caller that never threads it is unaffected. */
75
+ terminalEnabled?: boolean;
76
+ /**
77
+ * The minted terminal-plane endpoint (P5.t): the direct-to-provider ttyd
78
+ * PTY-over-websocket URL + the scoped stream token + its expiry. Threaded by the
79
+ * API-direct handshake AFTER it has resumed the box, ensured the terminal
80
+ * server, and resolved the provider tunnel (mintTerminalStream) — SYMMETRIC with
81
+ * `desktopStream`. When ABSENT (the negotiation-only read, a cold lease, or a
82
+ * degraded terminal) the Terminal cell reports url/token/expiresAt as null and
83
+ * falls back to transport "sse-events" (the read-only firehose) — the caller
84
+ * POSTs to /viewers to mint the live pty-ws address.
85
+ */
86
+ terminalStream?: {
87
+ url: string;
88
+ token: string;
89
+ expiresAt: string;
90
+ };
91
+ /** Override the negotiation clock (tests). */
92
+ now?: Date;
93
+ }
94
+ /**
95
+ * Resolve the descriptor for a backend. Throws on an unknown backend rather than
96
+ * returning a half-formed default (the registry is the single source of truth).
97
+ */
98
+ export declare function selectBackend(backend: SandboxBackend): CapabilityDescriptor;
99
+ /** True iff the descriptor lists the requested OS as supported. */
100
+ export declare function backendSupportsOs(descriptor: CapabilityDescriptor, os: SandboxOs): boolean;
101
+ /**
102
+ * True iff the backend can serve the Channel-B desktop pixel plane at all — i.e.
103
+ * its static descriptor advertises DesktopStream as available. The gate the
104
+ * worker / API use before launching the display stack (so a headless-only
105
+ * backend like cloudflare/vercel/none never tries). This is the STATIC
106
+ * feasibility only; the runtime `sandboxDesktopEnabled` policy toggle and the
107
+ * stream-token-secret gate are layered on by the caller / negotiateCapabilities.
108
+ *
109
+ * Accepts EITHER the SandboxBackend enum value (e.g. "local") OR the SDK
110
+ * client backendId (e.g. "unix_local") — they diverge for the local backend —
111
+ * so a caller holding only `established.backendId` resolves correctly.
112
+ */
113
+ export declare function desktopCapableBackend(backend: SandboxBackend | string): boolean;
114
+ /**
115
+ * Negotiate a coherent SessionCapabilities document for (backend, os). Every
116
+ * capability is reported with availability + a reason-when-unavailable; nothing
117
+ * is ever absent. The reason precedence is: os_unsupported (the OS axis can't be
118
+ * served at all) > the per-capability static feasibility > policy/liveness gates.
119
+ */
120
+ export declare function negotiateCapabilities(ctx: NegotiationContext): SessionCapabilities;
@@ -0,0 +1,91 @@
1
+ import type { SandboxOs, SessionCapabilities } from "@opengeni/contracts";
2
+ import type { SelfhostedSession } from "./session";
3
+ /**
4
+ * The structural slice of the M2 `@opengeni/db` `EnrollmentRecord` the selfhosted
5
+ * negotiation reads. Defined STRUCTURALLY (not imported from `@opengeni/db`) so
6
+ * the agent-loop-free sandbox leaf does not couple to the DB package's graph —
7
+ * the API/worker pass an `EnrollmentRecord`, which satisfies this shape. The
8
+ * fields: `status` (active gates reachability), `exposure` +
9
+ * `allowScreenControl` (whole-machine + screen-control consent),`hasDisplay`
10
+ * (the display plane), `lastSeenAt` (the reconnecting-window disambiguator).
11
+ */
12
+ export interface SelfhostedEnrollment {
13
+ status: string;
14
+ exposure: string;
15
+ allowScreenControl: boolean;
16
+ hasDisplay: boolean;
17
+ lastSeenAt: string | null;
18
+ /** An un-cleared clean going-offline marker (the machine announced a typed
19
+ * GoingOffline). When set, the derivation reads the machine OFFLINE immediately,
20
+ * regardless of a still-fresh `lastSeenAt`. NULL ⇒ no pending goodbye. */
21
+ wentOfflineAt: string | null;
22
+ /** The typed reason string of the pending goodbye (rides alongside
23
+ * `wentOfflineAt`; NULL when there is no un-cleared marker). */
24
+ wentOfflineReason: string | null;
25
+ }
26
+ /** The derived liveness state of a selfhosted machine (the online/offline/
27
+ * reconnecting/consent/display matrix). */
28
+ export interface SelfhostedLivenessState {
29
+ /** The dominant machine state. */
30
+ state: "online" | "reconnecting" | "offline";
31
+ /** Whole-machine + screen-control consent acknowledged (gates desktop input). */
32
+ consented: boolean;
33
+ /** A display (real or Xvfb) is present (gates the desktop pixel plane). */
34
+ hasDisplay: boolean;
35
+ }
36
+ /**
37
+ * The window after `lastSeenAt` within which a missed liveness probe is read as a
38
+ * transient BLIP (`reconnecting`) rather than a hard `offline`. Mirrors the
39
+ * resiliency model (§10.6: reconnecting after 1 missed window, offline after
40
+ * ~30s). A probe miss with a lastSeenAt inside this window → reconnecting.
41
+ */
42
+ export declare const SELFHOSTED_RECONNECT_WINDOW_MS = 30000;
43
+ /**
44
+ * Derive the selfhosted liveness state from the enrollment row + a liveness probe
45
+ * outcome. The probe is the authoritative "is the agent answering NOW" signal;
46
+ * `lastSeenAt` disambiguates a probe-miss into reconnecting (recent) vs offline
47
+ * (stale / never seen).
48
+ *
49
+ * - no enrollment / revoked → offline (the machine isn't enrolled).
50
+ * - un-cleared clean goodbye → offline (an announced GoingOffline beats
51
+ * last_seen aging AND a lingering probe).
52
+ * - probe responded → online.
53
+ * - probe missed, lastSeenAt recent → reconnecting (a transient blip).
54
+ * - probe missed, lastSeenAt stale → offline.
55
+ */
56
+ export declare function selfhostedLiveness(input: {
57
+ enrollment: SelfhostedEnrollment | null;
58
+ /** The ControlRpc Ping outcome: true iff a responder answered. */
59
+ probeResponded: boolean;
60
+ /** Override the clock (tests). */
61
+ now?: Date;
62
+ }): SelfhostedLivenessState;
63
+ export interface SelfhostedNegotiationInput {
64
+ sessionId: string;
65
+ os?: SandboxOs;
66
+ leaseEpoch: number;
67
+ /** The M2 enrollment row for the machine (null → never enrolled → offline). */
68
+ enrollment: SelfhostedEnrollment | null;
69
+ /** A live liveness probe — typically `session.ping()`. When a session is
70
+ * provided this is called; otherwise pass `probeResponded` explicitly. */
71
+ session?: Pick<SelfhostedSession, "ping">;
72
+ /** Explicit probe outcome (when no session is given, e.g. a pure read). */
73
+ probeResponded?: boolean;
74
+ /** The deployment desktop/terminal/computer-use policy toggles (threaded
75
+ * through to the base negotiation). */
76
+ desktopEnabled?: boolean;
77
+ terminalEnabled?: boolean;
78
+ computerUseEnabled?: boolean;
79
+ /** Whether the calling principal acknowledged the un-redacted desktop. */
80
+ desktopAcknowledged?: boolean;
81
+ shared?: boolean;
82
+ sharedSessionIds?: string[];
83
+ /** Override the clock (tests). */
84
+ now?: Date;
85
+ }
86
+ /**
87
+ * Negotiate the full `SessionCapabilities` document for a selfhosted machine,
88
+ * with the online/offline/reconnecting/consent_required/display_unavailable cells
89
+ * correctly decided. Async because it issues the liveness probe.
90
+ */
91
+ export declare function negotiateSelfhostedCapabilities(input: SelfhostedNegotiationInput): Promise<SessionCapabilities>;
@@ -0,0 +1,175 @@
1
+ import { AgentError, ControlRequest, ControlResponse, ErrorCode } from "@opengeni/agent-proto";
2
+ import type { CapabilityUnavailableReason } from "@opengeni/contracts";
3
+ export type SelfhostedUnavailableReason = Extract<CapabilityUnavailableReason, "agent_offline" | "agent_reconnecting" | "consent_required" | "display_unavailable">;
4
+ /**
5
+ * The selfhosted control-plane transport seam. ONE method: `request` — send a
6
+ * `ControlRequest` to the agent addressed by subject and await its
7
+ * `ControlResponse`. The subject is `subjectFor(workspaceId, agentId)`.
8
+ *
9
+ * The CONTRACT every implementor MUST honour (the M3 ruling): a
10
+ * no-responder / request-timeout is NOT an exception that means "not found" — it
11
+ * is surfaced as a `ControlResponse` carrying an `AgentError` with code
12
+ * `AGENT_OFFLINE` (no responder at all) or, when the caller can distinguish a
13
+ * transient blip, `TIMEOUT` (→ `agent_reconnecting`). The session maps these to
14
+ * the runtime error taxonomy; it NEVER lets agent-offline look like a provider
15
+ * NotFound (which would cold-create a rival box for a user's real machine).
16
+ */
17
+ export interface ControlRpc {
18
+ request(subject: string, req: ControlRequest, opts: {
19
+ timeoutMs: number;
20
+ }): Promise<ControlResponse>;
21
+ }
22
+ /** The control-plane RPC subject for an enrolled agent — its subscription IS the
23
+ * registry (the binding two-plane decision). */
24
+ export declare function subjectFor(workspaceId: string, agentId: string): string;
25
+ /**
26
+ * The runtime-level error a `SelfhostedSession` op throws when the agent returns
27
+ * an `AgentError` (or no responder / timeout maps to one). It carries:
28
+ * - `code` — the wire `ErrorCode` (single-source-of-truth);
29
+ * - `reason` — the negotiated `CapabilityUnavailableReason` the capability /
30
+ * liveness surface uses (`agent_offline` / `agent_reconnecting`
31
+ * / `consent_required`), or null for op-level errors
32
+ * (OS/NOT_FOUND/UNSUPPORTED/STREAM/PROTOCOL) that are not a
33
+ * machine-liveness condition;
34
+ * - `retryable`— whether the caller should re-resolve + retry (DRAINING /
35
+ * FENCED / a reconnecting blip);
36
+ * - `notFound` — ALWAYS the provider-NotFound discriminator value: for
37
+ * selfhosted this is true ONLY for an OS-level NOT_FOUND of a
38
+ * path/ref (a real "the file does not exist"), and is FALSE for
39
+ * AGENT_OFFLINE (the machine isn't recreatable — never let the
40
+ * lease cold-create a rival). `isProviderSandboxNotFoundError`
41
+ * reads this.
42
+ */
43
+ export declare class SelfhostedControlError extends Error {
44
+ readonly name = "SelfhostedControlError";
45
+ readonly code: ErrorCode;
46
+ readonly reason: SelfhostedUnavailableReason | null;
47
+ readonly retryable: boolean;
48
+ readonly fenced: boolean;
49
+ readonly draining: boolean;
50
+ readonly agentOffline: boolean;
51
+ readonly osNotFound: boolean;
52
+ /** The reply exceeded the transport's max payload (a distinguishing flag so the
53
+ * fault renderer can name the size wall + recovery instead of a generic OS error). */
54
+ readonly payloadTooLarge: boolean;
55
+ /** The transport synthesized this fault PRE-SEND — it KNOWS the request never
56
+ * reached the machine (no connection, or no responder on the subject), so the op
57
+ * provably did not execute. Only such a fault is safe to re-issue for ANY op kind
58
+ * (a post-send timeout is ambiguous — at-least-once — and must not be re-sent
59
+ * unless the op is read-only). Never set for an agent-returned error. */
60
+ readonly neverSent: boolean;
61
+ readonly detail: Record<string, string>;
62
+ constructor(input: {
63
+ message: string;
64
+ code: ErrorCode;
65
+ reason: SelfhostedUnavailableReason | null;
66
+ retryable: boolean;
67
+ fenced?: boolean;
68
+ draining?: boolean;
69
+ agentOffline?: boolean;
70
+ osNotFound?: boolean;
71
+ payloadTooLarge?: boolean;
72
+ neverSent?: boolean;
73
+ detail?: Record<string, string>;
74
+ });
75
+ }
76
+ /**
77
+ * Map an `AgentError` (from a `ControlResponse`) to the runtime
78
+ * `SelfhostedControlError`. THE load-bearing mapping (the M3 ruling):
79
+ * - AGENT_OFFLINE → reason `agent_offline`, agentOffline=true,
80
+ * osNotFound=FALSE (NEVER a provider NotFound).
81
+ * - TIMEOUT (a transient missed-window / no-responder blip the caller marked
82
+ * retryable) → reason `agent_reconnecting`.
83
+ * - CONSENT_REQUIRED → reason `consent_required`.
84
+ * - DRAINING → no capability reason; retryable (turn pauses + retries).
85
+ * - FENCED → no capability reason; retryable (the existing
86
+ * epoch-fence retry; the caller re-resolves + retries).
87
+ * - NOT_FOUND → an OS-level path/ref NotFound — osNotFound=true (a
88
+ * real "file does not exist"), no machine-liveness
89
+ * reason. (This is the ONLY NotFound; it is NOT the
90
+ * box-gone NotFound that licenses a cold restore.)
91
+ * - PAYLOAD_TOO_LARGE → the reply exceeded the transport's max payload;
92
+ * non-retryable, with actionable copy (the sizes + a
93
+ * redirect-to-file workaround).
94
+ * - OS / UNSUPPORTED / STREAM / PROTOCOL / UNSPECIFIED → op-level error, no
95
+ * reason, non-retryable.
96
+ */
97
+ export declare function agentErrorToControlError(err: AgentError): SelfhostedControlError;
98
+ /**
99
+ * PAYLOAD_TOO_LARGE copy: state the actual sizes (from the agent's `detail` map —
100
+ * `encoded_bytes` / `max_payload`) and tell the caller how to get the data anyway:
101
+ * redirect the output to a file and read it back in ranges/chunks.
102
+ */
103
+ export declare function payloadTooLargeMessage(detail: Record<string, string>): string;
104
+ /**
105
+ * DRAINING copy: the machine is at its concurrent-work capacity. `retries` is the
106
+ * number of times `SelfhostedSession.call` already re-tried before giving up (0 at
107
+ * the mapping layer, the final count when it surfaces after exhausting retries).
108
+ */
109
+ export declare function drainingMessage(retries: number): string;
110
+ /** Rebuild a DRAINING error with the final retry count folded into its message —
111
+ * used by `SelfhostedSession.call` when the bounded DRAINING retries are exhausted. */
112
+ export declare function drainingExhaustedError(base: SelfhostedControlError, retries: number): SelfhostedControlError;
113
+ /**
114
+ * exec-deadline copy: the command was terminated at the exec time limit. Advise
115
+ * running long jobs in the background and polling their output rather than blocking
116
+ * the turn on one long command. Surfaced on a timed-out exec result's stderr.
117
+ */
118
+ export declare function execDeadlineHint(seconds: number): string;
119
+ /** The reserved `detail` key the transport sets to "1" when it KNOWS a synthesized
120
+ * AGENT_OFFLINE fault occurred pre-send (no connection / no responder) — so the op
121
+ * provably never reached the machine. `agentErrorToControlError` reads it into
122
+ * `SelfhostedControlError.neverSent`. Reserved (never sent by the agent). */
123
+ export declare const NEVER_SENT_DETAIL_KEY = "_never_sent";
124
+ /** Build a synthesized AGENT_OFFLINE `AgentError` — the control plane uses this
125
+ * when no agent responds on the subject at all. `neverSent` marks that the request
126
+ * provably never reached the machine (no connection / no responder), which makes it
127
+ * safe to re-issue for ANY op kind; leave it false for an AMBIGUOUS transport error
128
+ * (a connection torn mid-request may have delivered the op). */
129
+ export declare function offlineAgentError(message?: string, neverSent?: boolean): AgentError;
130
+ /** Build a synthesized TIMEOUT `AgentError` — the control plane uses this when a
131
+ * responder existed but the request timed out (a transient blip → reconnecting). */
132
+ export declare function timeoutAgentError(message?: string): AgentError;
133
+ /**
134
+ * The minimal NATS request/reply surface `NatsControlRpc` needs. It mirrors the
135
+ * `nats` `NatsConnection.request` signature WITHOUT importing `nats` into the
136
+ * agent-loop-free runtime leaf: the API/worker injects the live connection (the
137
+ * SAME `@opengeni/events` bus connection). A factory may return `null` when NATS
138
+ * is not configured (boot must not require a live NATS) — `NatsControlRpc` then
139
+ * surfaces `agent_offline` for every request rather than throwing.
140
+ */
141
+ export interface NatsRequestConnection {
142
+ request(subject: string, payload: Uint8Array, opts: {
143
+ timeout: number;
144
+ }): Promise<{
145
+ data: Uint8Array;
146
+ }>;
147
+ }
148
+ /**
149
+ * A thin `ControlRpc` over a NATS request/reply connection. Constructed with a
150
+ * LAZY factory: the connection is resolved on first `request` (so boot never
151
+ * requires a live NATS). A null factory result, a no-responder error, or a
152
+ * request timeout each yield a `ControlResponse` carrying a synthesized
153
+ * `AgentError` (AGENT_OFFLINE / TIMEOUT) — NEVER a thrown transport error and
154
+ * NEVER a NotFound.
155
+ *
156
+ * The factory is async and memoized; it may itself dial the bus. M4 replaces the
157
+ * factory's body with the Accounts-scoped, hardened connection — this class's
158
+ * shape does not change.
159
+ */
160
+ export declare class NatsControlRpc implements ControlRpc {
161
+ private readonly connect;
162
+ private connection;
163
+ private connecting;
164
+ constructor(connect: () => Promise<NatsRequestConnection | null>);
165
+ private resolveConnection;
166
+ request(subject: string, req: ControlRequest, opts: {
167
+ timeoutMs: number;
168
+ }): Promise<ControlResponse>;
169
+ }
170
+ /** A `ControlResponse` carrying a synthesized AGENT_OFFLINE error. `neverSent` marks
171
+ * a pre-send synthesis (no connection / no responder) — the op provably never
172
+ * reached the machine, so it is safe to re-issue for any op kind. */
173
+ export declare function offlineControlResponse(requestId: string, neverSent?: boolean): ControlResponse;
174
+ /** A `ControlResponse` carrying a synthesized TIMEOUT error (→ reconnecting). */
175
+ export declare function timeoutControlResponse(requestId: string): ControlResponse;
@@ -0,0 +1,40 @@
1
+ import { SelfhostedControlError } from "./control-rpc";
2
+ /**
3
+ * A stable, low-cardinality fault-class string for a `SelfhostedControlError` — the
4
+ * out-of-band twin of the in-band renderer's class taxonomy (single-sourced here).
5
+ * Used for the `machine.op.*` event class + the metrics label + the infra filter.
6
+ */
7
+ export declare function selfhostedFaultClass(error: SelfhostedControlError): string;
8
+ /**
9
+ * The fault classes that count as an INFRASTRUCTURE fault (a `machine.op.failed`
10
+ * event). A semantic miss the model asked about — a missing path (`not_found`), a
11
+ * consent gate (`consent`), a nonzero exit (never a fault at all) — is an OUTCOME,
12
+ * not an infra fault, and is DELIBERATELY excluded; `fenced` is a routing/session
13
+ * condition, not a machine fault. (Doctrine two-planes / the team ruling.)
14
+ */
15
+ export declare const SELFHOSTED_INFRASTRUCTURE_FAULT_CLASSES: ReadonlySet<string>;
16
+ /** The four mandatory field labels (exported so tests can assert their presence). */
17
+ export declare const FAULT_FIELD_WHAT_HAPPENED = "What happened:";
18
+ export declare const FAULT_FIELD_WHICH_LAYER = "Which layer:";
19
+ export declare const FAULT_FIELD_WHAT_PRESERVED = "What was preserved:";
20
+ export declare const FAULT_FIELD_WHAT_TO_TRY = "What to try:";
21
+ /**
22
+ * Render a `SelfhostedControlError` into the doctrine's four in-band fields with a
23
+ * correct retry verdict. The fault CLASS is read from the typed flags (never the
24
+ * message prose), so each class gets the right "which layer" and "what to try":
25
+ *
26
+ * - PAYLOAD_TOO_LARGE → the command ran but its reply was too big to return; the
27
+ * fix is to bound the output, not to retry.
28
+ * - DRAINING → pre-admission backpressure; nothing ran; safe to retry shortly.
29
+ * - CONSENT_REQUIRED → a machine-side consent gate; retrying cannot help.
30
+ * - AGENT_OFFLINE → the machine link is down. If the transport KNOWS the request
31
+ * never left (`neverSent`), nothing ran; otherwise the effect is ambiguous and
32
+ * the model must check before re-running. Either way "try again" is wrong until
33
+ * the machine reconnects.
34
+ * - agent_reconnecting (TIMEOUT) → a transient link blip AFTER send: ambiguous, so
35
+ * a state-changing command must be checked before re-running (at-least-once).
36
+ * - NOT_FOUND → a command/filesystem miss, not a link fault.
37
+ * - FENCED → the active machine session changed (a swap); re-run.
38
+ * - default (OS/UNSUPPORTED/STREAM/PROTOCOL) → the command/op failed on the machine.
39
+ */
40
+ export declare function renderSelfhostedFault(error: SelfhostedControlError): string;
@@ -0,0 +1,40 @@
1
+ import type { ErrorCode } from "@opengeni/agent-proto";
2
+ import type { SelfhostedUnavailableReason } from "./control-rpc";
3
+ /**
4
+ * One completed control-op observation. `outcome` is the terminal result AFTER any
5
+ * in-call retries: `ok` (the op succeeded) or `failed` (a terminal fault surfaced).
6
+ * `healed` marks a success that only landed after ≥1 retry — the doctrine's
7
+ * "healed and invisible in-band, always out-of-band" breadcrumb (drives
8
+ * `machine.op.recovered`). Every field is derivable at the `SelfhostedSession.call`
9
+ * exit points with no extra round-trip.
10
+ */
11
+ export interface SelfhostedOpObservation {
12
+ /** The control-op kind (`ControlRequest["op"].$case`): exec / fsRead / ping / … */
13
+ op: string;
14
+ outcome: "ok" | "failed";
15
+ /** True when `outcome==="ok"` only after ≥1 in-call retry (draining/timeout/never-sent). */
16
+ healed: boolean;
17
+ /** Total in-call retries before this outcome (summed across the retry classes). */
18
+ retries: number;
19
+ /** Wall time from the first attempt to the terminal outcome, in ms. */
20
+ durationMs: number;
21
+ /** The typed wire code — present on `failed`. */
22
+ code?: ErrorCode;
23
+ /** The negotiated capability reason (agent_offline / agent_reconnecting / …), if any. */
24
+ reason?: SelfhostedUnavailableReason | null;
25
+ /** Whether a failed fault provably never reached the machine (a pre-send synthesis). */
26
+ neverSent?: boolean;
27
+ /** The enrolled machine (agent) id the op addressed — for machine-scoped fan-out. */
28
+ machineId?: string;
29
+ /** A stable fault-class string (`selfhostedFaultClass`): the fault class on a
30
+ * `failed` op, or the class of retries that a `healed` op recovered from. Absent
31
+ * on a clean success. Drives the `machine.op.*` event mapping + its infra filter. */
32
+ faultClass?: string;
33
+ /** The reply size in bytes when known — set on a PAYLOAD_TOO_LARGE fault (from the
34
+ * agent's `encoded_bytes` detail). The op-stream client fills the success-reply
35
+ * size once it owns the framed transport; today's request/reply does not surface it. */
36
+ replyBytes?: number;
37
+ }
38
+ /** A fire-and-forget observer of completed control ops. It MUST NOT throw (the
39
+ * session guards the call) and MUST NOT block the op — it is a telemetry tap. */
40
+ export type SelfhostedOpObserver = (observation: SelfhostedOpObservation) => void;
@@ -0,0 +1,104 @@
1
+ import { type ExecRequest, type ExecResponse } from "@opengeni/agent-proto";
2
+ import { type ControlRpc } from "./control-rpc";
3
+ import { type SelfhostedRetryClock } from "./retry-policy";
4
+ import { type OpStreamTransport } from "./op-transport";
5
+ /** Initial send-credit window (bytes) when the caller does not size it. Matches
6
+ * the wire default OpStart advertises (~4 MiB). */
7
+ export declare const OP_STREAM_DEFAULT_WINDOW_BYTES: number;
8
+ /** Cumulative-ack repetition cadence while an op is live (ruling M1: acks are
9
+ * best-effort and healed by repetition). */
10
+ export declare const OP_STREAM_ACK_INTERVAL_MS = 5000;
11
+ /** Frame-silence threshold before the client starts probing with OpQuery. The
12
+ * runner emits Progress every 5s while an op is alive and quiet, so 30s of
13
+ * TOTAL silence means frames are not reaching us (or the op is gone). */
14
+ export declare const OP_STREAM_SILENCE_TIMEOUT_MS = 30000;
15
+ /** How long a silent op is held open across a machine reconnect window before
16
+ * the client fails it typed (PROTOCOL liveness rule: hold ≤120s, then only a
17
+ * definitive answer fails the op). */
18
+ export declare const OP_STREAM_RECONNECT_HOLD_MS = 120000;
19
+ /**
20
+ * The durable-resume journal the worker adapts onto Temporal (design delta Δ1:
21
+ * the attach generation is `Info.currentAttemptScheduledTimestampMs` — strictly
22
+ * monotonic across worker-death redispatches, which `Info.attempt` is NOT under
23
+ * `maximumAttempts: 1`). `persistSettled` folds the op's settled frontier into
24
+ * the activity heartbeat details and MUST complete before the wire final ack
25
+ * (the durable-before-wire-ack gate); the default no-journal client (tests,
26
+ * non-activity callers) uses generation "1" and skips persistence.
27
+ */
28
+ export interface OpStreamJournal {
29
+ /** The consumer's attach generation (u64 decimal string). */
30
+ attachGeneration(): string;
31
+ /** Durably record `opId` as settled at `exitSeq` BEFORE its final ack. */
32
+ persistSettled(opId: string, exitSeq: string): void | Promise<void>;
33
+ }
34
+ export interface OpStreamExecClientDeps {
35
+ workspaceId: string;
36
+ agentId: string;
37
+ /** The lease/active epoch every ControlRequest is fenced under. */
38
+ epoch: number;
39
+ /** The rpc request/reply seam (OpStart/OpQuery/OpAttach ride here). */
40
+ controlRpc: ControlRpc;
41
+ /** The rpc subject (`agent.<ws>.<id>.rpc`). */
42
+ rpcSubject: string;
43
+ /** The frame/ack seam (subscribe + publish). */
44
+ transport: OpStreamTransport;
45
+ journal?: OpStreamJournal;
46
+ windowBytes?: number;
47
+ /** Control-op round-trip timeout for OpStart/OpQuery/OpAttach (the SHORT
48
+ * control budget, not the exec deadline). */
49
+ controlTimeoutMs: number;
50
+ /** The retry clock (sleep + jitter), injected for deterministic tests. */
51
+ retryClock: SelfhostedRetryClock;
52
+ ackIntervalMs?: number;
53
+ silenceTimeoutMs?: number;
54
+ reconnectHoldMs?: number;
55
+ }
56
+ /** A completed op-stream exec: the legacy-shaped response plus the healing
57
+ * telemetry the session folds into its op observation. */
58
+ export interface OpStreamExecOutcome {
59
+ response: ExecResponse;
60
+ /** Attach/query heals that occurred after streaming began (gap replays,
61
+ * silence probes that re-attached). >0 marks the op `healed`. */
62
+ heals: number;
63
+ /** OpStart-level retries (draining/blip) before the op was accepted. */
64
+ startRetries: number;
65
+ /** Total reassembled Data payload bytes (the observer's replyBytes). */
66
+ replyBytes: number;
67
+ }
68
+ /**
69
+ * The per-session op-stream exec client. One instance per `SelfhostedSession`;
70
+ * `exec()` runs one op end-to-end; `finalizeSettledOps()` is the turn-end hook
71
+ * that advances the acked frontier durably (journal) and then on the wire.
72
+ */
73
+ export declare class OpStreamExecClient {
74
+ private readonly deps;
75
+ private readonly settled;
76
+ private readonly inFlight;
77
+ constructor(deps: OpStreamExecClientDeps);
78
+ private generation;
79
+ /**
80
+ * Cancel an exec by its durable op id. OpCancel is idempotent and also acts
81
+ * as a cancel-before-start tombstone, so calling this while OpStart is still
82
+ * racing is safe: the runner either kills the accepted process tree or
83
+ * refuses the later start without spawning it.
84
+ */
85
+ cancel(opId: string): Promise<void>;
86
+ /**
87
+ * Run one exec over the op stream. `opId` is the durable id (B1) — a re-issue
88
+ * with the same id attaches instead of re-running. `deadlineMs` is the exec
89
+ * process budget (relative); the runner's enforcement is authoritative.
90
+ * `wallMs` is the client-side give-up wall (deadline + reply grace).
91
+ */
92
+ exec(opId: string, exec: ExecRequest, deadlineMs: number, wallMs: number): Promise<OpStreamExecOutcome>;
93
+ /**
94
+ * The turn-end durability hook (the hard gate's ordering): for every op whose
95
+ * result the turn has durably consumed — (1) already true when this runs —
96
+ * (2) persist the settled frontier to the journal, then (3) publish the wire
97
+ * final ack (`acked_seq = exit_seq, final = true`), which licenses the runner
98
+ * to GC the op. A kill between (2) and (3) is safe: the re-dispatched turn
99
+ * does not re-execute a durably-recorded call, and the runner's retention TTL
100
+ * reaps the never-final-acked op — no loss, bounded residue. Publish failures
101
+ * are swallowed for the same reason.
102
+ */
103
+ finalizeSettledOps(): Promise<void>;
104
+ }