@executablemd/runtime 0.8.1 → 0.9.1

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.
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Exclusive ownership of one logical agent session
3
+ * (specs/native-agent-session-launch-spec.md §Ownership and concurrency).
4
+ *
5
+ * An advertised provider-returned session can be handed to a native UI, and a
6
+ * native UI is a live owner nothing on the XMD side can observe. So every
7
+ * operation that could act on such a session — establishing it, running a turn,
8
+ * launching into it — enters here first, and a host that cannot answer the
9
+ * ownership question refuses rather than guessing.
10
+ *
11
+ * This is a plain capability, injected directly by the host into the provider
12
+ * that needs it. It is deliberately not a contextual Api: ownership is a
13
+ * security decision, and a decision document middleware can replace is not one.
14
+ *
15
+ * Acquisition never waits. A native UI may stay open for hours, and a caller
16
+ * that queued would hold the reader's terminal while offering no way to reach
17
+ * the owner it waits for.
18
+ */
19
+ import type { Operation, Result } from "effection";
20
+ /** Which logical session is being owned. Never an authored alias. */
21
+ export interface AgentSessionKey {
22
+ provider: string;
23
+ agent: string;
24
+ sessionKey: string;
25
+ }
26
+ /** What is asking. Recorded so a tombstone says what failed to finish. */
27
+ export type AgentSessionOwnerKind = "session" | "prompt" | "native-launch";
28
+ export interface AgentSessionOwner {
29
+ kind: AgentSessionOwnerKind;
30
+ /** A fresh opaque id per live attempt. Never a launch or native identity. */
31
+ operationId: string;
32
+ }
33
+ /**
34
+ * The acknowledgement a body gives when its session can no longer be acted on.
35
+ *
36
+ * One use, and only from inside the body. Returning without it — by success,
37
+ * failure or cancellation — leaves the durable marker active, because what it
38
+ * acknowledges is not "I finished" but "nothing I started can still touch this
39
+ * session".
40
+ */
41
+ export interface AgentSessionOwnership {
42
+ quiesced(): void;
43
+ }
44
+ /** Another live owner holds this session right now. */
45
+ export declare class AgentSessionBusy extends Error {
46
+ name: string;
47
+ }
48
+ /**
49
+ * The last owner never proved it stopped.
50
+ *
51
+ * A crash releases the kernel lock but not this: nothing observable afterwards
52
+ * distinguishes a session whose owner died mid-turn from one it left cleanly,
53
+ * so the conservative answer stands until someone recovers it deliberately.
54
+ */
55
+ export declare class AgentSessionRecoveryRequired extends Error {
56
+ name: string;
57
+ }
58
+ export interface AgentSessionCoordinator {
59
+ /**
60
+ * Run `body` while holding exclusive ownership of `key`.
61
+ *
62
+ * The body runs only once ownership is established. Busy and
63
+ * recovery-required arrive on the failure channel of the `Result`; an
64
+ * unexpected filesystem or protocol failure raises, having preserved the
65
+ * conservative marker.
66
+ */
67
+ coordinate<T>(key: AgentSessionKey, owner: AgentSessionOwner, body: (ownership: AgentSessionOwnership) => Operation<T>): Operation<Result<T>>;
68
+ }
69
+ /**
70
+ * The digest that names one session's sidecar and ownership record.
71
+ *
72
+ * Canonical, so every process derives the same name, and a digest, so the
73
+ * coordination namespace holds no agent name, session name, path or authored
74
+ * value.
75
+ */
76
+ export declare function agentSessionKeyDigest(key: AgentSessionKey): string;
77
+ /** The exact durable record one owner writes. No other member is accepted. */
78
+ export interface AgentSessionOwnershipRecordV1 {
79
+ schema: "agent-session-ownership.v1";
80
+ keyDigest: string;
81
+ state: "active" | "idle";
82
+ ownerKind: AgentSessionOwnerKind;
83
+ operationId: string;
84
+ }
85
+ /**
86
+ * Read an ownership record strictly.
87
+ *
88
+ * An unknown schema, a missing field, or a member this build cannot account for
89
+ * describes state it must not act on — so it is refused rather than read
90
+ * partially, and never repaired.
91
+ */
92
+ export declare function parseAgentSessionOwnership(value: unknown): AgentSessionOwnershipRecordV1 | undefined;
93
+ export declare function serializeAgentSessionOwnership(record: AgentSessionOwnershipRecordV1): string;
package/types/apis.d.ts CHANGED
@@ -85,6 +85,25 @@ export interface StatResult {
85
85
  isFile: boolean;
86
86
  isDirectory: boolean;
87
87
  }
88
+ /**
89
+ * Result of an `lstat` call: what the entry itself is, rather than what it
90
+ * leads to.
91
+ *
92
+ * A final symbolic link is the answer here instead of being followed, which is
93
+ * the whole reason this exists beside `stat`. `isSymbolicLink` is therefore its
94
+ * own member rather than an absence to infer: a link reports `isFile: false`
95
+ * and `isDirectory: false` whatever it points at, and "neither" is also what a
96
+ * socket or a device reports.
97
+ *
98
+ * Missing answers `{ exists: false, … }` rather than throwing, for the same
99
+ * reason `stat` does.
100
+ */
101
+ export interface LinkStatResult {
102
+ exists: boolean;
103
+ isFile: boolean;
104
+ isDirectory: boolean;
105
+ isSymbolicLink: boolean;
106
+ }
88
107
  /**
89
108
  * Minimal response headers interface.
90
109
  *
@@ -93,6 +112,34 @@ export interface StatResult {
93
112
  */
94
113
  export interface ResponseHeaders {
95
114
  get(key: string): string | null;
115
+ /**
116
+ * Every header the response carries, as name/value pairs in the order the
117
+ * provider reports them.
118
+ *
119
+ * Optional, because a provider written against `get()` alone still satisfies
120
+ * this interface. A caller that must retain the whole set — `<Fetch>` — fails
121
+ * when it is absent rather than recording the part of a response `get()`
122
+ * happens to be asked for.
123
+ */
124
+ entries?(): Iterable<readonly [string, string]>;
125
+ }
126
+ /** What a request may say about itself, beyond where it is going. */
127
+ export interface FetchInit {
128
+ method?: string;
129
+ headers?: Record<string, string>;
130
+ body?: string;
131
+ timeout?: number;
132
+ /**
133
+ * Fail the request on any status outside 2xx.
134
+ *
135
+ * Part of the request rather than something a caller applies to the answer,
136
+ * because middleware wrapping this operation is entitled to know it: a
137
+ * provider that authenticates, routes or refuses a request reads what the
138
+ * caller asked for, and "this caller treats a non-2xx as a failure" is part of
139
+ * that. The default adapter hands it to the transport, which raises its own
140
+ * `HttpError`.
141
+ */
142
+ expect?: boolean;
96
143
  }
97
144
  /**
98
145
  * Response shape returned by the fetch context API.
@@ -138,6 +185,14 @@ interface ProcessHandler {
138
185
  interface FsHandler {
139
186
  readTextFile(path: string): Operation<string>;
140
187
  stat(path: string): Operation<StatResult>;
188
+ /**
189
+ * What the entry at `path` is, without following a final symbolic link.
190
+ *
191
+ * A caller that has to decide whether to *remove* a path asks this rather
192
+ * than `stat`: `stat` answers about the file a link names, and removing the
193
+ * link is not removing that file. Missing is an answer here too.
194
+ */
195
+ lstat(path: string): Operation<LinkStatResult>;
141
196
  /**
142
197
  * Files and symbolic links beneath `root` whose path relative to it matches
143
198
  * `patterns` and matches none of `exclude`. Paths come back relative and
@@ -178,12 +233,7 @@ interface FsHandler {
178
233
  realpath(path: string): Operation<string | undefined>;
179
234
  }
180
235
  interface FetchHandler {
181
- fetch(input: string, init?: {
182
- method?: string;
183
- headers?: Record<string, string>;
184
- body?: string;
185
- timeout?: number;
186
- }): Operation<RuntimeFetchResponse>;
236
+ fetch(input: string, init?: FetchInit): Operation<RuntimeFetchResponse>;
187
237
  }
188
238
  /**
189
239
  * A compiled eval block accepts the document binding environment and returns
@@ -237,13 +287,32 @@ export declare function exec(options: ProcessExecOptions & {
237
287
  }>;
238
288
  export declare const readTextFile: typeof API.Fs.operations.readTextFile;
239
289
  export declare const stat: typeof API.Fs.operations.stat;
290
+ export declare const lstat: typeof API.Fs.operations.lstat;
240
291
  export declare const glob: typeof API.Fs.operations.glob;
241
292
  export declare const writeTextFile: typeof API.Fs.operations.writeTextFile;
242
293
  export declare const ensureDir: typeof API.Fs.operations.ensureDir;
243
294
  export declare const rename: typeof API.Fs.operations.rename;
244
295
  export declare const remove: typeof API.Fs.operations.remove;
245
296
  export declare const realpath: typeof API.Fs.operations.realpath;
246
- export declare const fetch: typeof API.Fetch.operations.fetch;
297
+ /**
298
+ * One HTTP request, ready to be yielded or chained.
299
+ *
300
+ * Yielding it gives the settled response, which is what every caller inside the
301
+ * engine wants. The chain is for authored eval code, whose calling shape
302
+ * predates this operation: `fetch(url).expect()` and
303
+ * `fetch(url, init).expect().json()` are what documents in this repository are
304
+ * written with, and they mean the same thing here — with the request now
305
+ * crossing `API.Fetch`, so a host that narrows destinations narrows theirs too.
306
+ */
307
+ export interface FetchOperation extends Operation<RuntimeFetchResponse> {
308
+ /** The response body as text. */
309
+ text(): Operation<string>;
310
+ /** The response body parsed as JSON. */
311
+ json(): Operation<unknown>;
312
+ /** The same request, asked for with `expect` — failing outside 2xx. */
313
+ expect(): FetchOperation;
314
+ }
315
+ export declare function fetch(input: string, init?: FetchInit): FetchOperation;
247
316
  export declare const env: typeof API.Env.operations.env;
248
317
  export declare const cwd: typeof API.Env.operations.cwd;
249
318
  export declare const platform: typeof API.Env.operations.platform;
@@ -0,0 +1,11 @@
1
+ import type { AgentSessionCoordinator } from "./agent-session-coordinator.js";
2
+ /** Whether this host can coordinate agent sessions at all. */
3
+ export declare function hasDenoAgentSessionCoordinator(): boolean;
4
+ /**
5
+ * Build the coordinator rooted at `root`.
6
+ *
7
+ * Returns nothing on a host with no such filesystem: a host that cannot answer
8
+ * the ownership question installs no coordinator, and every advertised
9
+ * provider-returned operation refuses.
10
+ */
11
+ export declare function createDenoAgentSessionCoordinator(root: string): AgentSessionCoordinator | undefined;
@@ -0,0 +1,14 @@
1
+ import type { ExecutableObserver } from "./executable-observer.js";
2
+ /** Whether this host can observe an executable at all. */
3
+ export declare function hasDenoExecutableObserver(): boolean;
4
+ /**
5
+ * Build an observer rooted in this process's real environment.
6
+ *
7
+ * `overrides` exist for the focused proof only: a test that wants to watch
8
+ * PATH search happen supplies its own search path and working directory rather
9
+ * than moving the ones every other thing in the process is using.
10
+ */
11
+ export declare function createDenoExecutableObserver(overrides?: {
12
+ path?: string;
13
+ cwd?: string;
14
+ }): ExecutableObserver | undefined;
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Observing which executable build a command actually runs
3
+ * (specs/native-agent-session-launch-spec.md §Executable binding).
4
+ *
5
+ * A provider session whose identity XMD chose itself only means something
6
+ * while the build that established it can be reproduced. Two builds of one
7
+ * provider accept the same session identity and disagree silently about what
8
+ * it names — that is how issue #519's first gate produced a healthy-looking
9
+ * session with no history in it. So before a session crosses an ownership
10
+ * boundary, the exact file about to run is observed, and enough is retained to
11
+ * recognize it later.
12
+ *
13
+ * What is observed is a canonical path, the SHA-256 of that file's bytes, and
14
+ * whatever the file says when asked its version. Only the last two ever become
15
+ * durable: a path stops being true when a build moves, and names host layout
16
+ * besides.
17
+ *
18
+ * This is a plain capability the trusted host builds and hands directly to the
19
+ * provider that needs it. It is deliberately not a contextual Api. Executable
20
+ * validation decides which retained history is accepted, and a decision
21
+ * document middleware could replace is not one — a replaceable resolver could
22
+ * point the observation at a different binary than the one that runs.
23
+ *
24
+ * Provider-specific meaning is not this module's business: which command to
25
+ * run, what a version string looks like, and what to do about a mismatch
26
+ * belong to the adapter that knows the provider.
27
+ */
28
+ import type { Operation } from "effection";
29
+ /** Why an executable could not be observed, in terms a caller can act on. */
30
+ export type ExecutableRefusal = "not-found" | "not-a-file" | "not-executable" | "unreadable" | "version-unavailable";
31
+ /**
32
+ * An observation failure that names its reason.
33
+ *
34
+ * The reason is the actionable part and the message is diagnostic. Neither is
35
+ * retained: a caller turns this into its own refusal, and the paths involved
36
+ * stay on this side of that boundary.
37
+ */
38
+ export declare class ExecutableObservationError extends Error {
39
+ name: string;
40
+ refusal: ExecutableRefusal;
41
+ constructor(message: string, options: {
42
+ refusal: ExecutableRefusal;
43
+ cause?: unknown;
44
+ });
45
+ }
46
+ /**
47
+ * One executable, as it exists during this invocation.
48
+ *
49
+ * `path` is canonical and live: it is what a caller spawns and what it asked
50
+ * for a version, and it is absent from everything durable. `versionOutput` is
51
+ * raw — the adapter that knows the provider parses it, and neither this string
52
+ * nor the path may reach a record, a diagnostic, or the environment of
53
+ * anything but the matching child.
54
+ */
55
+ export interface ObservedExecutable {
56
+ path: string;
57
+ digest: {
58
+ algorithm: "sha256";
59
+ value: string;
60
+ };
61
+ versionOutput: string;
62
+ }
63
+ export interface ExecutableObserver {
64
+ /**
65
+ * Resolve `command`, canonicalize it, require an executable regular file,
66
+ * hash its bytes, and ask that exact path for its version.
67
+ *
68
+ * Asking the same path that was hashed is the point: a version read from a
69
+ * differently-resolved file describes a build this observation did not make.
70
+ */
71
+ observe(command: string, options?: {
72
+ versionArgs?: readonly string[];
73
+ }): Operation<ObservedExecutable>;
74
+ }
package/types/files.d.ts CHANGED
@@ -13,8 +13,10 @@
13
13
  * replacement — admission, resolution, target classification, parent creation,
14
14
  * and commit — rather than a sequence a caller assembles, because assembling it
15
15
  * from outside is what would let a path admitted by one provider be used by
16
- * another. `API.Fs` remains the low-level host surface a host adapter is built
17
- * on; it is not this boundary.
16
+ * another. `deleteFile` is the same kind of whole act, and every operation here
17
+ * is mandatory: a provider with nothing to offer for one does not omit it and
18
+ * leave a document reaching the host instead. `API.Fs` remains the low-level
19
+ * host surface a host adapter is built on; it is not this boundary.
18
20
  *
19
21
  * `checkFilePath` is the one exception, and it is deliberately weak: pure path
20
22
  * arithmetic, no filesystem access, and nothing usable comes back — no path, no
@@ -47,6 +49,7 @@
47
49
  * and `instanceof` answers false across them, which would turn a provider
48
50
  * failure into an unrecognized throw exactly when it matters most.
49
51
  */
52
+ import "./_dnt.polyfills.js";
50
53
  import { type Api } from "@effectionx/context-api";
51
54
  import type { Operation, Result } from "effection";
52
55
  /** The stable discriminant on ordinary filesystem failure data. */
@@ -65,7 +68,7 @@ export declare const FILES_WRITE_SUCCESS = "executablemd.runtime.files-write-suc
65
68
  */
66
69
  export type FilesReason = "empty-path" | "absolute-path" | "lexical-escape" | "resolved-escape" | "missing" | "directory" | "special-file" | "not-directory" | "permission-denied" | "read-only" | "too-many-symlinks" | "path-too-long" | "no-space" | "quota-exhausted" | "cross-device" | "busy" | "too-many-open-files" | "directory-not-empty" | "invalid-pattern" | "operation-failed";
67
70
  /** The operations whose failure carries no commit outcome. */
68
- export type FilesOperation = "check-file-path" | "read" | "glob" | "temporary-directory";
71
+ export type FilesOperation = "check-file-path" | "read" | "delete" | "glob" | "temporary-directory";
69
72
  /** Where a non-write operation stopped. */
70
73
  export type FilesPhase = "lexical" | "resolution" | "target" | "access" | "pattern" | "traversal" | "acquire";
71
74
  /** Where a write stopped, which is what decides what may be said about the target. */
@@ -131,6 +134,17 @@ export interface FilesHandler {
131
134
  checkFilePath(input: FilePathInput): Operation<Result<void>>;
132
135
  readTextFile(input: FilePathInput): Operation<Result<string>>;
133
136
  writeTextFile(input: FileWriteInput): Operation<Result<FileWriteSuccess>>;
137
+ /**
138
+ * Remove one entry this path names, and answer with nothing.
139
+ *
140
+ * Mandatory, like every other operation here: a provider that has no
141
+ * deletion to offer does not omit it and fall through to a host. Success
142
+ * carries no value at all — there is no receipt, no path, and no record of
143
+ * whether anything was there — because a document that asked for a file to be
144
+ * gone has been answered by its absence. A path that already names nothing is
145
+ * that same success.
146
+ */
147
+ deleteFile(input: FilePathInput): Operation<Result<void>>;
134
148
  /** Sorted, deduplicated, POSIX-separated paths of the regular files that match. */
135
149
  globFiles(input: GlobInput): Operation<Result<string[]>>;
136
150
  /**
@@ -35,6 +35,25 @@
35
35
  * has nothing to resolve, and `rename` replaces the link rather than following
36
36
  * it wherever it points.
37
37
  *
38
+ * ## Deletions
39
+ *
40
+ * A deletion is the mirror image of a write in the one place that matters: the
41
+ * final path segment is deliberately *not* resolved. A write follows an
42
+ * internal link to the file it names, because replacing the link would be the
43
+ * surprising outcome; a deletion removes the link itself, because following it
44
+ * would remove something the document never named — possibly outside the
45
+ * working directory entirely. So resolution stops at the parent prefix, which
46
+ * still catches a directory link leading out, and the authored last segment is
47
+ * put back onto it unresolved.
48
+ *
49
+ * What is then removed is decided by an explicit `lstat` rather than by the
50
+ * platform. A directory is refused whether or not it is empty, and every
51
+ * runtime this ships to reports a nonrecursive removal of one differently.
52
+ * Absence is success on both sides of that classification: a path that already
53
+ * names nothing was already what the document asked for. The removal is the
54
+ * single commit point, and nothing is acquired around it, so cancellation
55
+ * before it changes nothing and there is no cleanup to fail.
56
+ *
38
57
  * ## What crosses the boundary
39
58
  *
40
59
  * Nothing from a caught platform error. An errno code *selects* a
@@ -55,13 +74,21 @@ import type { FilesHandler } from "./files.js";
55
74
  * observable rather than merely stated.
56
75
  */
57
76
  export interface HostFilesEvent {
58
- readonly operation: "read" | "write" | "glob";
77
+ readonly operation: "read" | "write" | "delete" | "glob";
59
78
  readonly phase: "target" | "access" | "parents" | "temporary" | "commit" | "cleanup" | "read-dir";
60
79
  }
61
80
  /** Synchronous, so nothing can run between the observation and the call it precedes. */
62
81
  export type HostFilesObserver = (event: HostFilesEvent) => void;
63
82
  export interface HostFilesOptions {
64
83
  readonly observe?: HostFilesObserver;
84
+ /**
85
+ * The directory temporary directories are minted under, defaulting to the
86
+ * host's temporary root. The host's root is shared by every process on the
87
+ * machine, so a caller that censuses the minted namespace — a lifetime test
88
+ * proving nothing survives a cancellation — points this at a directory it
89
+ * owns, where the only entries are the ones this provider created.
90
+ */
91
+ readonly temporaryRoot?: string;
65
92
  }
66
93
  /**
67
94
  * Build a host provider.
@@ -0,0 +1,105 @@
1
+ /**
2
+ * The native launcher — how a host hands one child process the terminal.
3
+ *
4
+ * This is not `exec`. An ordinary command is a captured child: its stdout and
5
+ * stderr are piped so a document can display, capture and journal them, and
6
+ * its exit status is a value the document reads. A native coding-agent UI is
7
+ * the opposite of that. It draws on the terminal, reads the person's
8
+ * keystrokes, and owns the conversation it has with them. None of that may
9
+ * become an XMD process result or a journaled transcript, and a piped child
10
+ * cannot be interactive at all.
11
+ *
12
+ * So a launch asks for three things in order, and each is refusable on its
13
+ * own:
14
+ *
15
+ * 1. `reserve()` takes the one foreground-terminal lease for the run. A host
16
+ * with no terminal refuses here, which is before any session ownership has
17
+ * moved. Two launches cannot hold it at once even when they name different
18
+ * sessions, so native UIs are sequential by construction.
19
+ * 2. `flush()` gives the reader everything the document has produced so far,
20
+ * so the native UI does not open on top of half-written output.
21
+ * 3. `launch()` spawns the child with the terminal inherited, waits for it,
22
+ * and reports its terminal status and nothing else.
23
+ *
24
+ * There is no host default. `xmd run` installs the foreground launcher;
25
+ * a test or embedding host installs a controlled one that needs no terminal.
26
+ * Until one is installed every operation refuses, which is what keeps
27
+ * document help and inspection free of any of this.
28
+ */
29
+ import { type Api } from "@effectionx/context-api";
30
+ import type { Operation } from "effection";
31
+ /**
32
+ * What a provider asks the host to run.
33
+ *
34
+ * `command` is the complete argv, built by the provider's adapter from the
35
+ * provider-native session identity. Raw prepared instructions never appear in
36
+ * it, and never in `env`: a process's arguments and environment are readable
37
+ * by other processes, so the instruction layer travels through the provider's
38
+ * own session API instead.
39
+ */
40
+ export interface NativeLaunchRequest {
41
+ command: string[];
42
+ cwd: string;
43
+ env?: Record<string, string>;
44
+ }
45
+ /**
46
+ * How the native UI ended. A child that exited on a signal reports the signal
47
+ * and no code, which is how a signalled exit stays distinguishable from
48
+ * status 0.
49
+ */
50
+ export interface NativeLaunchOutcome {
51
+ exitCode?: number;
52
+ signal?: string;
53
+ }
54
+ export interface NativeLauncherHandler {
55
+ reserve(): Operation<void>;
56
+ flush(): Operation<void>;
57
+ launch(request: NativeLaunchRequest): Operation<NativeLaunchOutcome>;
58
+ }
59
+ export declare const NATIVE_LAUNCHER_UNAVAILABLE: string;
60
+ export declare class NativeLauncherUnavailableError extends Error {
61
+ name: string;
62
+ constructor(message?: string);
63
+ }
64
+ export declare const NativeLauncher: Api<NativeLauncherHandler>;
65
+ /** Hold the foreground-terminal lease for the calling scope. */
66
+ export declare function reserveTerminal(): Operation<void>;
67
+ /** Give the reader everything the document has produced so far. */
68
+ export declare function flushOutput(): Operation<void>;
69
+ /** Run one native UI as a foreground child and report how it ended. */
70
+ export declare function nativeLaunch(request: NativeLaunchRequest): Operation<NativeLaunchOutcome>;
71
+ export declare const NO_TERMINAL: string;
72
+ interface ForegroundLauncherOptions {
73
+ /**
74
+ * Whether this host can hand a child the terminal. Read once, when the
75
+ * launcher installs, so a run learns what it is before a document starts.
76
+ */
77
+ isTerminal?: () => boolean;
78
+ /** Everything this host has still to show the reader. */
79
+ drain?: () => Operation<void>;
80
+ }
81
+ /**
82
+ * Install the launcher that hands a native UI this process's own terminal.
83
+ *
84
+ * XMD stays the parent. It does not replace itself with the child, because a
85
+ * process that has execed away cannot cancel the document, reap the child,
86
+ * own its exit status, or continue after the UI closes.
87
+ */
88
+ export declare function installForegroundLauncher(options?: ForegroundLauncherOptions): Operation<void>;
89
+ /**
90
+ * A launcher a host installs when it has no terminal to give away, and no
91
+ * intention of starting a native UI.
92
+ *
93
+ * `record` sees each request in the order the provider made it; `outcome`
94
+ * decides what the child did; and `wait` is the operation the launch blocks
95
+ * on, so a test controls exactly how long the document stays suspended.
96
+ */
97
+ export interface ControlledLauncherOptions {
98
+ record?: (request: NativeLaunchRequest) => void;
99
+ outcome?: (request: NativeLaunchRequest) => NativeLaunchOutcome;
100
+ wait?: (request: NativeLaunchRequest) => Operation<void>;
101
+ onReserve?: () => void;
102
+ onFlush?: () => void;
103
+ }
104
+ export declare function installControlledLauncher(options?: ControlledLauncherOptions): Operation<void>;
105
+ export {};
package/types/mod.d.ts CHANGED
@@ -8,7 +8,7 @@
8
8
  * Seven domain APIs:
9
9
  * - `API.Process` — subprocess execution (`exec`)
10
10
  * - `API.Fs` — the low-level host filesystem (`readTextFile`, `writeTextFile`,
11
- * `stat`, `glob`, `realpath`, `ensureDir`, `rename`, `remove`)
11
+ * `stat`, `lstat`, `glob`, `realpath`, `ensureDir`, `rename`, `remove`)
12
12
  * - `API.Files` — document filesystem access as whole semantic operations,
13
13
  * with no host default. `useHostFiles()` installs the host provider.
14
14
  * - `API.Fetch` — HTTP requests (`fetch`)
@@ -16,14 +16,17 @@
16
16
  * this xmd, and eval-block compilation
17
17
  * (`cwd`, `env`, `platform`, `command`, `compile`)
18
18
  * - `API.Service` — scoped attached service startup (`startService`)
19
+ * - `NativeLauncher` — handing one native agent UI the foreground terminal
20
+ * (`reserveTerminal`, `flushOutput`, `nativeLaunch`)
19
21
  * - `Config` — shared execution config (`timeout`, `timeoutExec`, `timeoutFetch`)
20
22
  *
21
23
  * See `apis.ts` for architecture rationale.
22
24
  * See `@executablemd/runtime/test` for composable test stubs.
23
25
  */
26
+ import "./_dnt.polyfills.js";
24
27
  export { API } from "./apis.js";
25
- export { exec, readTextFile, writeTextFile, stat, glob, realpath, ensureDir, rename, remove, fetch, cwd, env, platform, command, compile, useQuietProcessOutput, } from "./apis.js";
26
- export type { EvalBlock, ResponseHeaders, RuntimeFetchResponse, StatResult } from "./apis.js";
28
+ export { exec, readTextFile, writeTextFile, stat, lstat, glob, realpath, ensureDir, rename, remove, fetch, cwd, env, platform, command, compile, useQuietProcessOutput, } from "./apis.js";
29
+ export type { EvalBlock, FetchInit, FetchOperation, LinkStatResult, ResponseHeaders, RuntimeFetchResponse, StatResult, } from "./apis.js";
27
30
  export { Service, SERVICE_HOSTNAME, SERVICE_READY_PREFIX, ServiceProcessExitBeforeReadyError, ServiceProtocolDuplicateError, ServiceProtocolHostnameMismatchError, ServiceProtocolIncompatibleError, ServiceProtocolMalformedError, ServiceProtocolTokenMismatchError, ServiceProviderError, ServiceStartupTimeoutError, ServiceTeardownError, ServiceUnexpectedExitError, parseServiceReadyRecord, startService, } from "./service.js";
28
31
  export type { ServiceEndpoint, ServiceHandler, ServiceAttachment, ServiceStartOptions, } from "./service.js";
29
32
  export { Config, timeout, timeoutExec, timeoutFetch } from "./config.js";
@@ -32,5 +35,13 @@ export { asDuration, durationError, parseDuration } from "./duration.js";
32
35
  export type { ProcessExecOptions, ProcessOutcome } from "./apis.js";
33
36
  export { asFilesFatal, FILES_ERROR, FILES_ERROR_MESSAGE, FILES_FATAL, FILES_INVARIANT_MESSAGE, FILES_OPERATION_DENIED_MESSAGE, FILES_PROVIDER_UNAVAILABLE_MESSAGE, FILES_WRITE_SUCCESS, Files, FilesError, FilesInvariantError, FilesOperationDeniedError, FilesProviderUnavailableError, fileWriteFailure, fileWriteSuccess, filesFailure, isFilesFatal, parseFilesPhase, parseFilesReason, parseFileWriteFailure, parseFileWritePhase, parseFileWriteSuccess, parseFilesFailure, parseFilesFatal, } from "./files.js";
34
37
  export type { FilePathInput, FilesDeniableOperation, FilesErrorData, FilesFailureData, FilesFatalData, FilesFatalFailure, FilesHandler, FilesInvariantCategory, FilesOperation, FilesPhase, FilesReason, FileWriteFailureData, FileWriteInput, FileWritePhase, FileWriteSuccess, FileWriteTarget, GlobInput, } from "./files.js";
38
+ export { flushOutput, installControlledLauncher, installForegroundLauncher, NATIVE_LAUNCHER_UNAVAILABLE, NativeLauncher, NativeLauncherUnavailableError, nativeLaunch, NO_TERMINAL, reserveTerminal, } from "./launcher.js";
39
+ export type { ControlledLauncherOptions, NativeLauncherHandler, NativeLaunchOutcome, NativeLaunchRequest, } from "./launcher.js";
35
40
  export { hostFilesHandler, useHostFiles } from "./host-files.js";
36
41
  export type { HostFilesEvent, HostFilesObserver, HostFilesOptions } from "./host-files.js";
42
+ export { AgentSessionBusy, agentSessionKeyDigest, AgentSessionRecoveryRequired, parseAgentSessionOwnership, serializeAgentSessionOwnership, } from "./agent-session-coordinator.js";
43
+ export type { AgentSessionCoordinator, AgentSessionKey, AgentSessionOwner, AgentSessionOwnerKind, AgentSessionOwnership, AgentSessionOwnershipRecordV1, } from "./agent-session-coordinator.js";
44
+ export { createDenoAgentSessionCoordinator, hasDenoAgentSessionCoordinator, } from "./deno-agent-session-coordinator.js";
45
+ export { ExecutableObservationError } from "./executable-observer.js";
46
+ export type { ExecutableObserver, ExecutableRefusal, ObservedExecutable, } from "./executable-observer.js";
47
+ export { createDenoExecutableObserver, hasDenoExecutableObserver, } from "./deno-executable-observer.js";
@@ -8,4 +8,5 @@
8
8
  * - `useFailingExec(exitCode, stderr)` — always-failing exec
9
9
  * - `useStubService(endpoint)` — scoped provider-neutral service attachment
10
10
  */
11
+ import "../_dnt.polyfills.js";
11
12
  export { useStubFs, useEchoExec, useFailingExec, useStubService } from "./stubs.js";
@@ -28,6 +28,8 @@ export declare function useStubService(endpoint: ServiceEndpoint): Operation<voi
28
28
  *
29
29
  * - `readTextFile` returns content from the `files` map; throws ENOENT for missing keys.
30
30
  * - `stat` returns `{ exists: true, isFile: true }` for keys in the map.
31
+ * - `lstat` answers the same, with `isSymbolicLink: false`: an in-memory map
32
+ * holds file content, so nothing in it is a link to somewhere else.
31
33
  * - `glob` throws (not stubbed). Install `API.Fs.around()` directly if needed.
32
34
  * - the writing half — `writeTextFile`, `ensureDir`, `rename`, `remove`, and
33
35
  * `realpath` — is not stubbed and reaches the real filesystem. A test that