@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.
- package/esm/_dnt.polyfills.js +1 -0
- package/esm/_dnt.shims.js +57 -0
- package/esm/agent-session-coordinator.js +91 -0
- package/esm/apis.js +79 -6
- package/esm/deno-agent-session-coordinator.js +228 -0
- package/esm/deno-executable-observer.js +159 -0
- package/esm/executable-observer.js +42 -0
- package/esm/files.js +10 -2
- package/esm/host-files.js +129 -3
- package/esm/launcher.js +322 -0
- package/esm/mod.js +10 -2
- package/esm/test/mod.js +1 -0
- package/esm/test/stubs.js +6 -0
- package/package.json +1 -1
- package/types/_dnt.polyfills.d.ts +6 -0
- package/types/_dnt.shims.d.ts +1 -0
- package/types/agent-session-coordinator.d.ts +93 -0
- package/types/apis.d.ts +76 -7
- package/types/deno-agent-session-coordinator.d.ts +11 -0
- package/types/deno-executable-observer.d.ts +14 -0
- package/types/executable-observer.d.ts +74 -0
- package/types/files.d.ts +17 -3
- package/types/host-files.d.ts +28 -1
- package/types/launcher.d.ts +105 -0
- package/types/mod.d.ts +14 -3
- package/types/test/mod.d.ts +1 -0
- package/types/test/stubs.d.ts +2 -0
|
@@ -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
|
-
|
|
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. `
|
|
17
|
-
*
|
|
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
|
/**
|
package/types/host-files.d.ts
CHANGED
|
@@ -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";
|
package/types/test/mod.d.ts
CHANGED
package/types/test/stubs.d.ts
CHANGED
|
@@ -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
|