@plurnk/plurnk-execs 1.3.12 → 1.4.0
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/.env.defaults +4 -0
- package/README.md +78 -36
- package/SPEC.md +379 -260
- package/dist/BaseExecutor.d.ts.map +1 -1
- package/dist/BaseExecutor.js +12 -13
- package/dist/BaseExecutor.js.map +1 -1
- package/dist/ErrorDetail.d.ts +8 -0
- package/dist/ErrorDetail.d.ts.map +1 -0
- package/dist/ErrorDetail.js +25 -0
- package/dist/ErrorDetail.js.map +1 -0
- package/dist/Notice.d.ts +3 -0
- package/dist/Notice.d.ts.map +1 -0
- package/dist/Notice.js +2 -0
- package/dist/Notice.js.map +1 -0
- package/dist/RuntimeTag.d.ts +5 -0
- package/dist/RuntimeTag.d.ts.map +1 -0
- package/dist/RuntimeTag.js +28 -0
- package/dist/RuntimeTag.js.map +1 -0
- package/dist/SubprocessExecutor.d.ts +1 -1
- package/dist/SubprocessExecutor.d.ts.map +1 -1
- package/dist/SubprocessExecutor.js +67 -20
- package/dist/SubprocessExecutor.js.map +1 -1
- package/dist/advertise.d.ts +4 -0
- package/dist/advertise.d.ts.map +1 -1
- package/dist/advertise.js +4 -2
- package/dist/advertise.js.map +1 -1
- package/dist/discover.d.ts.map +1 -1
- package/dist/discover.js +52 -84
- package/dist/discover.js.map +1 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -6
- package/dist/index.js.map +1 -1
- package/dist/policy.d.ts +1 -0
- package/dist/policy.d.ts.map +1 -1
- package/dist/policy.js +21 -20
- package/dist/policy.js.map +1 -1
- package/dist/runtime.js +1 -1
- package/dist/runtime.js.map +1 -1
- package/dist/tokenizeArgv.d.ts +5 -0
- package/dist/tokenizeArgv.d.ts.map +1 -0
- package/dist/tokenizeArgv.js +69 -0
- package/dist/tokenizeArgv.js.map +1 -0
- package/dist/types.d.ts +6 -4
- package/dist/types.d.ts.map +1 -1
- package/package.json +11 -15
- package/dist/TelemetryEvent.d.ts +0 -18
- package/dist/TelemetryEvent.d.ts.map +0 -1
- package/dist/TelemetryEvent.js +0 -2
- package/dist/TelemetryEvent.js.map +0 -1
package/SPEC.md
CHANGED
|
@@ -1,292 +1,411 @@
|
|
|
1
1
|
# plurnk-execs — Specification
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
Author contract for `@plurnk/plurnk-execs-*` runtime packages. Core consumes
|
|
4
|
+
the framework; executor leaves implement it.
|
|
5
|
+
|
|
6
|
+
## §executor-role Role and ownership
|
|
7
|
+
|
|
8
|
+
An executor owns the runtime-specific mapping from an EXEC body and optional
|
|
9
|
+
target to work, declared output channels, environment availability, and effect
|
|
10
|
+
classification. The consumer owns dispatch, proposal policy, storage,
|
|
11
|
+
subscriptions, deadlines, polling, cancellation delivery, packet projection,
|
|
12
|
+
and all reads of stored output.
|
|
13
|
+
|
|
14
|
+
```mermaid
|
|
15
|
+
flowchart LR
|
|
16
|
+
Manifest["package runtime declarations"] --> Discover["framework discovery"]
|
|
17
|
+
Discover --> Registry["runtime-tag registry"]
|
|
18
|
+
Registry --> Probe["consumer instantiates and probes each tag"]
|
|
19
|
+
Request["EXEC[tag]"] --> Effect["executor effect fact"]
|
|
20
|
+
Effect --> Admission["consumer proposal policy"]
|
|
21
|
+
Admission --> Stream["consumer creates tag-addressed stream"]
|
|
22
|
+
Stream --> Run["executor run with consumer sinks"]
|
|
23
|
+
Run --> Channels["stored output channels"]
|
|
24
|
+
Channels --> Packet["consumer observation and READ/FIND"]
|
|
25
|
+
```
|
|
8
26
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
27
|
+
`SubprocessExecutor` is the concrete base for command runtimes. It owns child
|
|
28
|
+
process spawning, stdout/stderr streaming, environment handoff, exit status,
|
|
29
|
+
and process-group cancellation. A subprocess leaf normally overrides only its
|
|
30
|
+
spawn recipe and availability binary.
|
|
12
31
|
|
|
13
|
-
## §
|
|
32
|
+
## §executor-contract Author surface
|
|
14
33
|
|
|
15
34
|
```ts
|
|
16
35
|
abstract class BaseExecutor {
|
|
17
|
-
readonly runtime: string;
|
|
36
|
+
readonly runtime: string;
|
|
18
37
|
readonly glyph: string;
|
|
19
|
-
constructor(metadata: { runtime: string; glyph: string });
|
|
20
38
|
|
|
21
|
-
// Channels this executor writes to; the scheme seeds the exec entry from
|
|
22
|
-
// these (§2.1). Subprocess runtimes declare { stdout, stderr }; search
|
|
23
|
-
// declares { results }.
|
|
24
39
|
abstract get channels(): Readonly<Record<string, ChannelDecl>>;
|
|
25
|
-
|
|
40
|
+
get defaultChannel(): string;
|
|
26
41
|
abstract run(args: ExecArgs): Promise<ExecResult>;
|
|
42
|
+
probe(signal?: AbortSignal): Promise<RuntimeAvailability>;
|
|
43
|
+
effect(target: string | null): Effect;
|
|
27
44
|
}
|
|
28
45
|
|
|
29
|
-
interface ChannelDecl {
|
|
30
|
-
|
|
46
|
+
interface ChannelDecl {
|
|
47
|
+
mimetype: string;
|
|
48
|
+
defaultState?: "active" | "closed" | "errored";
|
|
49
|
+
}
|
|
31
50
|
|
|
32
51
|
interface ExecArgs {
|
|
33
|
-
runtime: string;
|
|
34
|
-
command: string;
|
|
35
|
-
cwd: string | null;
|
|
36
|
-
target: string | null;
|
|
37
|
-
env?: NodeJS.ProcessEnv;
|
|
38
|
-
signal: AbortSignal;
|
|
39
|
-
write
|
|
40
|
-
setState
|
|
41
|
-
emit
|
|
42
|
-
entry
|
|
52
|
+
runtime: string;
|
|
53
|
+
command: string;
|
|
54
|
+
cwd: string | null;
|
|
55
|
+
target: string | null;
|
|
56
|
+
env?: NodeJS.ProcessEnv;
|
|
57
|
+
signal: AbortSignal;
|
|
58
|
+
write(channel: string, chunk: string, mimetype?: string): void;
|
|
59
|
+
setState(channel: string, state: ChannelState): void;
|
|
60
|
+
emit(notice: Notice): void;
|
|
61
|
+
entry?(
|
|
62
|
+
path: string,
|
|
63
|
+
content: string | null,
|
|
64
|
+
opts: { tags: string[]; mimetype?: string },
|
|
65
|
+
): Promise<string>;
|
|
43
66
|
}
|
|
44
67
|
|
|
45
|
-
interface
|
|
46
|
-
|
|
47
|
-
|
|
68
|
+
interface RuntimeAvailability {
|
|
69
|
+
available: boolean;
|
|
70
|
+
detail?: string;
|
|
48
71
|
}
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
`run()` must not throw for an expected runtime failure: surface it through `emit` + an `errored` channel state + a non-200 `status`.
|
|
52
|
-
|
|
53
|
-
### §2.1 Channel topology is executor-declared
|
|
54
|
-
|
|
55
|
-
The executor declares its channels; the consuming scheme seeds the exec entry from `executor.channels` rather than from a static scheme-level manifest (plurnk-service#174 Q1). This keeps channel names honest — `search` exposes `{ results: { mimetype: "application/json" } }`, and the model reads `exec://<coord>/EXEC#results` instead of an overloaded `#stdout`. `write` / `setState` are generic over channel name; writing to an undeclared channel is a contract violation.
|
|
56
|
-
|
|
57
|
-
### §2.2 Availability probe
|
|
58
|
-
|
|
59
|
-
```ts
|
|
60
|
-
interface RuntimeAvailability { available: boolean; detail?: string; }
|
|
61
72
|
|
|
62
|
-
abstract class BaseExecutor {
|
|
63
|
-
async probe(signal?: AbortSignal): Promise<RuntimeAvailability> { return { available: true }; } // default: available
|
|
64
|
-
}
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
`probe()` reports whether the runtime's *environment* is usable here — distinct from whether the *package* is installed (`discover()`). Pure / in-process runtimes (node, sqlite) inherit the available default; runtimes depending on an external binary (`python` → `python3 --version`) or config (`search` → `SEARXNG_URL`) override. `SubprocessExecutor` probes its `binary` getter (`null` = always available).
|
|
68
|
-
|
|
69
|
-
Consumer contract (plurnk-service#181): probe **once at boot, per package** (not per tag — stamp all of a package's tags with the one result), **concurrently under a per-probe timeout**, and **cache**. The per-probe deadline/cancellation is delivered as the optional **`signal`** — a probe that spawns a child or opens a connection MUST hand `signal` to it, so a resolved or timed-out probe **reaps** it immediately (else an in-flight `--version` write can `EPIPE` after host teardown, plurnk-execs#16). `signal` is ignore-safe: a probe that spawns nothing needs nothing. `probe()` MAY reject; the consumer treats rejection as `{ available: false, detail: <error> }`, so a buggy probe degrades only its runtime. The model is offered a positive list of available runtimes; an attempt at an unavailable one returns **501 carrying `detail`** (so `detail` is model-facing — terse and actionable). A configured default runtime that probes unavailable is a **fail-hard boot error**.
|
|
70
|
-
|
|
71
|
-
### §2.3 Effect (proposal gating)
|
|
72
|
-
|
|
73
|
-
```ts
|
|
74
73
|
type Effect = "pure" | "read" | "host";
|
|
75
|
-
|
|
76
|
-
abstract class BaseExecutor {
|
|
77
|
-
effect(target: string | null): Effect { return "host"; } // default: conservative
|
|
78
|
-
}
|
|
79
74
|
```
|
|
80
75
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
76
|
+
The framework constructs one executor per matched runtime tag with
|
|
77
|
+
`{ runtime, glyph }`. A package that declares several tags may branch on
|
|
78
|
+
`this.runtime`; it must not retain per-run state on the executor instance. The
|
|
79
|
+
derived addressable runtime scheme retains `glyph` for client discovery
|
|
80
|
+
({§manifest-client-display}); runtime aliases remain excluded from scheme
|
|
81
|
+
model-teaching because EXEC examples own that hot-path surface.
|
|
82
|
+
|
|
83
|
+
### §executor-sinks Inputs and sinks
|
|
84
|
+
|
|
85
|
+
| Field | Contract |
|
|
86
|
+
| ---------- | ------------------------------------------------------------------------------------------------------- |
|
|
87
|
+
| `runtime` | The matched tag. |
|
|
88
|
+
| `command` | The EXEC body: source, query, filter, or command according to the leaf contract. |
|
|
89
|
+
| `cwd` | Consumer-selected project working directory, or `null` for a runtime that has none. |
|
|
90
|
+
| `target` | Consumer-resolved optional EXEC target. The leaf defines how target and body map to its tool. |
|
|
91
|
+
| `env` | Exact child environment when supplied. A subprocess must use it instead of reconstructing host policy. |
|
|
92
|
+
| `signal` | Consumer cancellation. Every executor must honor it at each cancellable boundary. |
|
|
93
|
+
| `write` | Append to a declared channel. An optional mimetype replaces that channel's per-call output type. |
|
|
94
|
+
| `setState` | Move a declared channel from `active` to terminal `closed` or `errored`. |
|
|
95
|
+
| `emit` | Publish a transient, nonterminal Notice. |
|
|
96
|
+
| `entry` | Optionally request consumer-owned entry materialization and receive its canonical model-facing address. |
|
|
97
|
+
|
|
98
|
+
The executor receives callbacks, never a database, subscription registry,
|
|
99
|
+
packet builder, or wake mechanism. Related lifecycle behavior remains on the
|
|
100
|
+
consumer side of {§executor-role}.
|
|
101
|
+
|
|
102
|
+
### §executor-channels Channels
|
|
103
|
+
|
|
104
|
+
The executor declares every channel it may write. The consumer seeds a stream
|
|
105
|
+
entry from that declaration and rejects writes or state transitions for an
|
|
106
|
+
undeclared channel. `defaultChannel` is the fragmentless READ channel and
|
|
107
|
+
defaults to the first declared channel. Subprocess executors declare
|
|
108
|
+
`stdout` and `stderr`, with `stdout` first; logical runtimes commonly declare
|
|
109
|
+
`results`.
|
|
110
|
+
|
|
111
|
+
### §executor-results Results, failures, and notices
|
|
112
|
+
|
|
113
|
+
`run()` resolves one universal `SchemeResult`; `ExecResult` adds an optional
|
|
114
|
+
subprocess `exitCode`. A result with `status >= 400` carries exactly one RFC
|
|
115
|
+
9457 Problem whose status agrees with the result. Expected runtime failures
|
|
116
|
+
resolve as failure results and leave affected channels `errored`; they do not
|
|
117
|
+
throw. The consumer validates the boundary and converts a throw or invalid
|
|
118
|
+
result into its own durable failure before closing the stream.
|
|
119
|
+
|
|
120
|
+
A nonzero subprocess exit directs the caller to inspect both stdout and stderr
|
|
121
|
+
because either may contain the useful diagnostic. Third-party diagnostic text
|
|
122
|
+
entering a Problem is bounded with `ErrorDetail` and the required
|
|
123
|
+
`PLURNK_EXECS_ERROR_DETAIL_LIMIT`. Missing or invalid configuration is itself
|
|
124
|
+
an `invalid-configuration` failure. Structured diagnostic facts remain
|
|
125
|
+
separate Problem extensions.
|
|
126
|
+
|
|
127
|
+
`emit` is only for transient progress or other nonterminal observations. It
|
|
128
|
+
cannot own failure truth, change the terminal result, or drive the scheduler.
|
|
129
|
+
The runtime-neutral Notice types are re-exported from
|
|
130
|
+
`@plurnk/plurnk-contracts`; they are not redefined by this package.
|
|
131
|
+
|
|
132
|
+
### §executor-effect Effect is an admission fact
|
|
133
|
+
|
|
134
|
+
`effect(target)` is pure, synchronous, cheap, and target-classified. The
|
|
135
|
+
consumer calls it exactly once for an admitted invocation, before proposal
|
|
136
|
+
policy, then preserves that exact fact through application and effect-sensitive
|
|
137
|
+
bookkeeping. An installed executor does not parse authored code or commands to
|
|
138
|
+
infer safety. Unknown or unclassifiable work remains `host` through the
|
|
139
|
+
conservative default.
|
|
140
|
+
|
|
141
|
+
`target` is the consumer-canonical logical target. It is normally the same
|
|
142
|
+
local value later supplied to `run()`. If the consumer will materialize an
|
|
143
|
+
addressed source only after acceptance, it may classify against a stable opaque
|
|
144
|
+
target-present identity; the executor neither resolves that address nor
|
|
145
|
+
reclassifies the materialized path.
|
|
146
|
+
|
|
147
|
+
| Effect | Executor fact | Default consumer admission |
|
|
148
|
+
| ------ | -------------------------------------------- | -------------------------- |
|
|
149
|
+
| `host` | Runs host code or may mutate host state. | Human proposal gate. |
|
|
150
|
+
| `read` | Observes external state without mutating it. | Automatically accepted. |
|
|
151
|
+
| `pure` | Has no externally observable side effect. | Automatically accepted. |
|
|
152
|
+
|
|
153
|
+
The classification controls admission policy and may be reused as operational
|
|
154
|
+
metadata; it never changes within the invocation. After acceptance, every EXEC
|
|
155
|
+
uses the same background stream path: output is not returned in the dispatching
|
|
156
|
+
turn. Core owns that composed behavior in {§exec-host-proposes},
|
|
157
|
+
{§exec-readpure-ungated}, and {§exec-stream}.
|
|
158
|
+
|
|
159
|
+
### §executor-probe Availability is per runtime tag
|
|
160
|
+
|
|
161
|
+
`probe(signal?)` reports whether this runtime tag can work in the current
|
|
162
|
+
environment. The default is `{ available: true }`; leaves override it for
|
|
163
|
+
external binaries or required configuration. `detail` must be terse and
|
|
164
|
+
actionable because the consumer may return it with a 501 failure.
|
|
165
|
+
|
|
166
|
+
The consumer constructs and probes one executor per tag at boot, concurrently
|
|
167
|
+
under a per-probe timeout, then caches each verdict. This is intentionally
|
|
168
|
+
per-tag: a multi-tag package can expose only the interpreters actually present.
|
|
169
|
+
A probe that spawns or opens a connection passes the supplied signal through so
|
|
170
|
+
completion or timeout reaps its work. Probe rejection or timeout makes only
|
|
171
|
+
that tag unavailable; a configured default runtime that is absent or
|
|
172
|
+
unavailable is a fail-hard boot error.
|
|
173
|
+
|
|
174
|
+
## §executor-output-address Tag-addressed output
|
|
175
|
+
|
|
176
|
+
`exec` is the consumer's internal EXEC dispatcher; it is not a model-facing
|
|
177
|
+
output namespace. Every available runtime receives one derived read-only
|
|
178
|
+
scheme face from its tag, channels, and default channel. The executor authors
|
|
179
|
+
no second scheme manifest.
|
|
180
|
+
|
|
181
|
+
| Output | Model-facing address |
|
|
182
|
+
| ------------------------- | ------------------------------------------------------------------ |
|
|
183
|
+
| Calling worker's stream | `<tag>:///<loop>/<turn>/<sequence>#<channel>` |
|
|
184
|
+
| Fragmentless read | `<tag>:///<loop>/<turn>/<sequence>` → the declared default channel |
|
|
185
|
+
| Example subprocess stdout | `sh:///1/2/3#stdout` |
|
|
186
|
+
| Example structured result | `sqlite:///1/2/3#results` |
|
|
187
|
+
|
|
188
|
+
The executor only produces through `write` and `setState`. The consumer stores
|
|
189
|
+
the entry and serves every later READ/FIND through uniform entry machinery.
|
|
190
|
+
An executor does not implement a private read face, orientation receipt,
|
|
191
|
+
slicer, or index. Owner-qualified cross-worker addresses and packet projection
|
|
192
|
+
belong to core's {§stream-owner-scoped} and {§exec-stream} contracts.
|
|
193
|
+
|
|
194
|
+
### §executor-entry-sink Optional materialization
|
|
195
|
+
|
|
196
|
+
`entry(path, content, { tags, mimetype? })` is a consumer-implemented sink, not
|
|
197
|
+
executor access to storage. The consumer materializes or updates an entry,
|
|
198
|
+
unions tags, announces it through its ordinary ambience, and resolves the
|
|
199
|
+
canonical model-facing address. `content === null` requests consumer-sourced
|
|
200
|
+
acquisition. Rejection means only that this materialization failed; it does not
|
|
201
|
+
invalidate the executor's upstream result. When the sink is absent, the
|
|
202
|
+
executor preserves its result without inventing a materialization verdict.
|
|
203
|
+
|
|
204
|
+
## §executor-discovery Discovery and registration
|
|
205
|
+
|
|
206
|
+
`discover(options?)` scans every scoped and unscoped package under the nearest
|
|
207
|
+
`node_modules` for the exact declaration `plurnk.kind === "exec"`. It returns
|
|
208
|
+
`{ registry, packageAttributions, skipped, disabled }`, where the registry maps
|
|
209
|
+
each flat runtime tag to its declaration and package owner and the package map
|
|
210
|
+
carries the canonical attribution fact from {§plugin-attribution}.
|
|
131
211
|
|
|
132
212
|
```json
|
|
133
213
|
{
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
214
|
+
"name": "@acme/acme-execs-cobol",
|
|
215
|
+
"plurnk": {
|
|
216
|
+
"kind": "exec",
|
|
217
|
+
"runtimes": [
|
|
218
|
+
{
|
|
219
|
+
"name": "cobol",
|
|
220
|
+
"glyph": "🗄",
|
|
221
|
+
"example": "<<EXEC[cobol]:DISPLAY 'HI'.:EXEC"
|
|
222
|
+
}
|
|
223
|
+
]
|
|
224
|
+
}
|
|
142
225
|
}
|
|
143
226
|
```
|
|
144
227
|
|
|
145
|
-
A
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
|
191
|
-
|
|
192
|
-
|
|
|
193
|
-
|
|
|
194
|
-
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
The
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
`
|
|
240
|
-
|
|
228
|
+
A tag collision is a fail-hard installation error naming both claiming
|
|
229
|
+
packages. A package may declare multiple tags backed by the same default
|
|
230
|
+
export. Discovery is scope-agnostic; third-party packages participate through
|
|
231
|
+
the same contract.
|
|
232
|
+
|
|
233
|
+
### §executor-installation Installed capability lifecycle
|
|
234
|
+
|
|
235
|
+
Configuration cannot enable a package that is absent from the consumer-visible
|
|
236
|
+
module graph. An executor leaf must be installed under the `node_modules` found
|
|
237
|
+
from the running service before discovery can inspect its declaration. The
|
|
238
|
+
service assembles every installed member's package-owned `.env.defaults` under
|
|
239
|
+
operator configuration {§operator-config-env-defaults}; a leaf may therefore
|
|
240
|
+
ship default-disabled without transferring ownership of its key to the host.
|
|
241
|
+
|
|
242
|
+
| Package state | Effective policy and probe | Runtime result |
|
|
243
|
+
| ------------- | -------------------------------------- | ------------------------------------------------------------------- |
|
|
244
|
+
| Absent | Any configuration | Not discovered, registered, advertised, or dispatchable. |
|
|
245
|
+
| Installed | Tag disabled in any policy layer | Listed in `Discovery.disabled`; not registered or advertised. |
|
|
246
|
+
| Installed | Tag admitted; probe unavailable | Registered as unavailable; not advertised or dispatchable. |
|
|
247
|
+
| Installed | Tag admitted; probe available | Registered, advertised through `availableRuntimes()`, dispatchable. |
|
|
248
|
+
|
|
249
|
+
An operator value of `PLURNK_EXECS_<TAG>=1` can outrank a package-owned `0`
|
|
250
|
+
floor. It does not restore a tag removed by `PLURNK_EXECS_ONLY`, another
|
|
251
|
+
consumer layer, or a service hard ceiling. Discovery and policy are evaluated
|
|
252
|
+
at boot; changing package membership or configuration requires a restart.
|
|
253
|
+
|
|
254
|
+
### §executor-runtime-declaration Runtime metadata
|
|
255
|
+
|
|
256
|
+
| Field | Meaning |
|
|
257
|
+
| --------------- | -------------------------------------------------------------------------------------------------- |
|
|
258
|
+
| `name` | Canonical runtime tag and derived output-scheme name, admitted below. |
|
|
259
|
+
| `glyph` | Optional presentation glyph. |
|
|
260
|
+
| `example` | Optional compact, verbatim `plurnk` snippet. Each line is a complete `<<`-delimited operation. |
|
|
261
|
+
| `documentation` | Optional full Markdown reference. `docs/<tag>.md` wins over the inline manifest field. |
|
|
262
|
+
| `attribution` | Published per-tag projection of the validated package declaration ({§plugin-attribution}). |
|
|
263
|
+
| `packageName` | Package that owns and default-exports the executor implementation. |
|
|
264
|
+
|
|
265
|
+
The framework carries example and documentation content unchanged. The
|
|
266
|
+
consumer decides when and how to present either surface. A multi-tag package
|
|
267
|
+
appears at most once in `Discovery.packageAttributions`, and only when at least
|
|
268
|
+
one of its tags survives discovery policy. An instantiated executor may add
|
|
269
|
+
attempt-time tags through the shared runtime hook ({§plugin-attribution}).
|
|
270
|
+
|
|
271
|
+
Runtime-name admission is one identity contract:
|
|
272
|
+
|
|
273
|
+
| Constraint | Contract |
|
|
274
|
+
| ----------- | ------------------------------------------------------------------------------------------------------------- |
|
|
275
|
+
| Syntax | `[a-z][a-z0-9+.-]*`: canonical lowercase RFC-scheme syntax that is also an admitted EXEC identifier. |
|
|
276
|
+
| Identity | The exact name is the EXEC selector, registry key, `docs/<tag>.md` basename, and output URI-scheme name. |
|
|
277
|
+
| Reservation | `only` is unavailable because `PLURNK_EXECS_ONLY` owns that case-insensitive configuration key. |
|
|
278
|
+
|
|
279
|
+
Installed static declarations, trusted dynamic-hook declarations, and
|
|
280
|
+
module-owned declarations use the same validator before documentation lookup,
|
|
281
|
+
policy filtering, or registry mutation. A malformed claimed declaration fails
|
|
282
|
+
hard and names its package or module boundary; discovery still ignores an
|
|
283
|
+
unreadable package that never forms an executor manifest.
|
|
284
|
+
|
|
285
|
+
### §executor-dynamic-runtimes Dynamic declarations
|
|
286
|
+
|
|
287
|
+
A trusted package whose tags depend on deployment configuration may declare
|
|
288
|
+
`plurnk.runtimesModule` as an exported subpath instead of a static
|
|
289
|
+
`plurnk.runtimes[]` array. Its named `runtimes` export, or default export,
|
|
290
|
+
returns `RuntimeDecl[] | Promise<RuntimeDecl[]>`. Static declarations win when
|
|
291
|
+
both fields are present.
|
|
292
|
+
|
|
293
|
+
Discovery resolves the subpath through the package export map and imports it
|
|
294
|
+
only after the trust gate. An unloadable hook, missing function export, thrown
|
|
295
|
+
hook, or non-array result is a fail-hard contract violation by the admitted
|
|
296
|
+
package. The hook enumerates configuration; reachability remains the per-tag
|
|
297
|
+
probe's job.
|
|
298
|
+
|
|
299
|
+
### §executor-trust Trust precedes executable discovery hooks
|
|
300
|
+
|
|
301
|
+
The shared plugin trust predicate runs before a dynamic runtime module is
|
|
302
|
+
imported. A withheld package executes no hook and is returned in
|
|
303
|
+
`Discovery.skipped` for consumer presentation. Discovery silently ignores a
|
|
304
|
+
package that does not form a readable executor manifest; once an admitted
|
|
305
|
+
package declares executable hook code, its broken hook is surfaced rather than
|
|
306
|
+
swallowed.
|
|
307
|
+
|
|
308
|
+
### §executor-policy Subtractive runtime policy
|
|
309
|
+
|
|
310
|
+
Discovery applies the daemon's registration policy to every tag. The exported
|
|
311
|
+
`Policy` parser can apply the same grammar to additional consumer-owned layers.
|
|
312
|
+
|
|
313
|
+
| Variable | Enforced effect |
|
|
314
|
+
| ---------------------------------------- | ----------------------------------------------------------- |
|
|
315
|
+
| `PLURNK_EXECS_<TAG>=0` or `false` | Remove that tag. Keys are matched case-insensitively. |
|
|
316
|
+
| `PLURNK_EXECS_ONLY=<list>` | When present, remove every unlisted tag; empty removes all. |
|
|
317
|
+
| `Policy.isKey(key)` | Admit only `ONLY` or a canonical runtime-tag suffix. |
|
|
318
|
+
| `Policy.enabledAcross(tag, [a, b, ...])` | Keep the tag only when every supplied layer enables it. |
|
|
319
|
+
|
|
320
|
+
Policy is purely subtractive: no layer can re-enable a tag removed by another.
|
|
321
|
+
Boot-disabled tags are absent from the registry and returned in
|
|
322
|
+
`Discovery.disabled`. Workspace and loop admission remain consumer concerns;
|
|
323
|
+
the framework does not define a second Active/Available state machine.
|
|
324
|
+
|
|
325
|
+
### §executor-advertise-compat Frozen `Advertise` compatibility
|
|
326
|
+
|
|
327
|
+
| Surface | 1.x contract |
|
|
328
|
+
|--------------------------------------|-------------------------------------------------------------------------------------------------------------|
|
|
329
|
+
| `Advertise.contribute(registry, fn)` | Filters the supplied registry; returns `NO_EXECS_NOTICE` only when the filtered result is empty. |
|
|
330
|
+
| Architectural ownership | None. It does not discover, probe, admit, dispatch, or render; the composed service never calls it. |
|
|
331
|
+
| Evolution | Frozen. New consumers compose discovery and `Policy` with their own presentation. Removal is SemVer-major. |
|
|
332
|
+
|
|
333
|
+
## §executor-default-inventory Current installed set
|
|
334
|
+
|
|
335
|
+
The default `@plurnk/plurnk-service` composition installs the following
|
|
336
|
+
`@plurnk/plurnk-execs-<leaf>` packages. The lean framework owns no leaf
|
|
337
|
+
dependency edges. A probe and consumer policy still determine which declared
|
|
338
|
+
tags are offered in a particular workspace ({§bundled-set}).
|
|
339
|
+
|
|
340
|
+
| Leaf | Declared tags | Effect by target | Channels / mimetype |
|
|
341
|
+
| -------- | ---------------------------------------------------------------------------------------------------------- | ------------------------------- | ---------------------------------- |
|
|
342
|
+
| `common` | `sh`, `bash`, `node`, `python`, `python3`, `perl`, `ruby`, `php`, `lua`, `deno`, `bun`, `tcl`, `bc`, `awk` | `host` | `stdout`, `stderr` / `text/stream` |
|
|
343
|
+
| `git` | `git` | `host` | `stdout`, `stderr` / `text/stream` |
|
|
344
|
+
| `search` | `search`, `images`, `videos`, `news`, `map`, `music`, `it`, `science`, `social`, `downloadable` | `read` | `results` / `application/json` |
|
|
345
|
+
| `jq` | `jq` | no target `pure`; target `read` | `results` / `application/jsonl` |
|
|
346
|
+
| `sqlite` | `sqlite` | memory `pure`; file `host` | `results` / `application/json` |
|
|
347
|
+
| `wasm` | `wat`, `wasm` | no target `pure`; file `read` | `results` / `application/json` |
|
|
348
|
+
|
|
349
|
+
Optional executor packages are not part of this installed set merely because
|
|
350
|
+
they exist in the workspace.
|
|
351
|
+
|
|
352
|
+
## §executor-subprocess Subprocess contract
|
|
353
|
+
|
|
354
|
+
`SubprocessExecutor` translates a runtime, body, and optional target into:
|
|
241
355
|
|
|
242
356
|
```ts
|
|
243
|
-
interface SpawnArgs {
|
|
357
|
+
interface SpawnArgs {
|
|
358
|
+
cmd: string;
|
|
359
|
+
args: string[];
|
|
360
|
+
useShell: boolean;
|
|
361
|
+
stdin?: string;
|
|
362
|
+
}
|
|
244
363
|
```
|
|
245
364
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
|
249
|
-
|
|
|
250
|
-
| `
|
|
251
|
-
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
With a target,
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
365
|
+
### §executor-subprocess-routing Default routing
|
|
366
|
+
|
|
367
|
+
| Runtime | No-target spawn |
|
|
368
|
+
| -------------------------- | -------------------------------------------------------------------- |
|
|
369
|
+
| `sh` / `bash` | Shell command line. |
|
|
370
|
+
| `node` | `node -e <command>`. |
|
|
371
|
+
| `python` / `python3` | `python3 -c <command>`. |
|
|
372
|
+
| Other default-base runtime | `<runtime> -c <command>`; specialized leaves override this fallback. |
|
|
373
|
+
|
|
374
|
+
With a target, the target is the program and the body is its stdin. Core
|
|
375
|
+
stat-routes a directory target to `cwd`; a file remains the executor target.
|
|
376
|
+
Data runtimes define their own target mapping.
|
|
377
|
+
|
|
378
|
+
Subprocess leaves inherit stdout/stderr streaming, scoped-environment handoff,
|
|
379
|
+
availability probing, operation results, exit code, and process-group
|
|
380
|
+
cancellation. `CommandSyntaxError` during spawn translation becomes a durable
|
|
381
|
+
400 `invalid-command`; other translation exceptions remain plugin contract
|
|
382
|
+
violations for the consumer to contain.
|
|
383
|
+
|
|
384
|
+
### §executor-cancellation Cancellation and consumer timing
|
|
385
|
+
|
|
386
|
+
Executors know only the supplied `AbortSignal`. Core owns EXEC timeout and poll
|
|
387
|
+
syntax, timers, wakes, and loop lifetime in {§exec-timeout} and {§exec-poll}.
|
|
388
|
+
Subprocess cancellation signals the process group: a caller-supplied kill code
|
|
389
|
+
is delivered once; ordinary cancellation uses SIGHUP; loop-end housekeeping
|
|
390
|
+
may escalate after the consumer-provided grace period.
|
|
391
|
+
|
|
392
|
+
## §executor-consumer-boundary Required composition
|
|
393
|
+
|
|
394
|
+
The consumer:
|
|
395
|
+
|
|
396
|
+
1. discovers declared tags, instantiates and probes each tag, and caches the verdict;
|
|
397
|
+
2. registers a derived output scheme for every admitted, available tag;
|
|
398
|
+
3. resolves EXEC to exactly one tag and obtains its effect fact;
|
|
399
|
+
4. applies proposal policy, creates the stream entry, and supplies sinks plus cancellation;
|
|
400
|
+
5. validates the terminal result and closes every declared channel and subscription coherently; and
|
|
401
|
+
6. projects stream observations and later reads without calling back into the executor.
|
|
402
|
+
|
|
403
|
+
## §executor-forbidden Forbidden in executor leaves
|
|
404
|
+
|
|
405
|
+
- Direct database, subscription, packet, or wake access.
|
|
406
|
+
- Imports from `@plurnk/plurnk-service/*`.
|
|
407
|
+
- Mutation of `ExecArgs` or state retained across runs.
|
|
408
|
+
- Output through `console.*` instead of declared channels.
|
|
409
|
+
- Writes or state transitions for undeclared channels.
|
|
410
|
+
- Ignoring `args.signal` at a cancellable boundary.
|
|
411
|
+
- A process or network mechanism unrelated to the leaf's declared runtime domain.
|