@danypops/vehicle-core 0.13.1 → 0.15.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.
|
@@ -26,6 +26,14 @@ export type VehicleSchemaResult<T> = {
|
|
|
26
26
|
readonly success: false;
|
|
27
27
|
readonly issues?: readonly VehicleSchemaIssue[];
|
|
28
28
|
};
|
|
29
|
+
/**
|
|
30
|
+
* A serializable, descriptive `jsonSchema` (surfaced to a client or Pi tool
|
|
31
|
+
* projection) paired with a real `safeParse` that actually enforces it at
|
|
32
|
+
* runtime -- a Vehicle registry's own `invoke()` only ever calls
|
|
33
|
+
* `safeParse`; `jsonSchema` alone is never itself enforced, so a codec that
|
|
34
|
+
* only sets `jsonSchema` without a matching `safeParse` is a documentation
|
|
35
|
+
* gesture, not an honest contract.
|
|
36
|
+
*/
|
|
29
37
|
export interface VehicleSchemaCodec<T> {
|
|
30
38
|
readonly jsonSchema: JsonSchema;
|
|
31
39
|
safeParse(value: unknown): VehicleSchemaResult<T>;
|
|
@@ -93,6 +101,7 @@ export interface VehicleLimits {
|
|
|
93
101
|
readonly maxRequestBytes: number;
|
|
94
102
|
readonly maxResponseBytes: number;
|
|
95
103
|
}
|
|
104
|
+
/** One structured, documented failure mode a {@link VehicleOperationDescriptor} declares up front -- part of the operation's own serializable contract, not an ad hoc thrown Error a caller has to reverse-engineer from a message string. */
|
|
96
105
|
export interface VehicleFailureDescriptor {
|
|
97
106
|
readonly code: string;
|
|
98
107
|
readonly description: string;
|
|
@@ -103,6 +112,16 @@ export interface VehicleBackgroundCapability {
|
|
|
103
112
|
readonly defaultWakeBudget: VehicleJobWakeBudget;
|
|
104
113
|
readonly maxWakeBudget: VehicleJobWakeBudget;
|
|
105
114
|
}
|
|
115
|
+
/**
|
|
116
|
+
* The serializable half of a Vehicle operation -- name, version, schemas,
|
|
117
|
+
* ownership-implying permissions, effect classification, idempotency,
|
|
118
|
+
* streaming/long-running capability, request/response limits, and declared
|
|
119
|
+
* {@link VehicleFailureDescriptor} failure modes. Kept separate from the
|
|
120
|
+
* executable {@link VehicleOperationHandler} on purpose: a manifest, a Pi
|
|
121
|
+
* tool projection, or a client's own capability check can all inspect this
|
|
122
|
+
* shape without ever touching (or needing to trust) the implementation
|
|
123
|
+
* behind it.
|
|
124
|
+
*/
|
|
106
125
|
export interface VehicleOperationDescriptor {
|
|
107
126
|
readonly name: string;
|
|
108
127
|
readonly version: number;
|
|
@@ -117,6 +136,21 @@ export interface VehicleOperationDescriptor {
|
|
|
117
136
|
readonly limits: VehicleLimits;
|
|
118
137
|
readonly errors: readonly VehicleFailureDescriptor[];
|
|
119
138
|
readonly background?: VehicleBackgroundCapability;
|
|
139
|
+
/**
|
|
140
|
+
* Owner-declared override for whether this specific operation is ever a candidate for
|
|
141
|
+
* approval gating, independent of its `effect`. Undefined (the default) means "derive it
|
|
142
|
+
* from `effect` against the registry's own requireApprovalForEffects set instead" --
|
|
143
|
+
* VehicleRegistry.manifest()'s own resolution rule, unchanged for every existing
|
|
144
|
+
* operation that never sets this.
|
|
145
|
+
*
|
|
146
|
+
* Exists because VehicleEffect's five values are coarse enough that two operations a
|
|
147
|
+
* real owner classifies very differently (e.g. "restart an already-installed,
|
|
148
|
+
* already-vetted service" vs. "sync a read-only catalog mirror") can land in the same
|
|
149
|
+
* effect bucket (both external-write) -- no single requireApprovalForEffects set can
|
|
150
|
+
* gate one without also gating the other. The owner who registers the operation knows
|
|
151
|
+
* its real risk far better than a 5-value enum can; this lets them say so directly.
|
|
152
|
+
*/
|
|
153
|
+
readonly requiresApproval?: boolean;
|
|
120
154
|
}
|
|
121
155
|
export interface VehicleOperation<Input, Output> {
|
|
122
156
|
readonly descriptor: VehicleOperationDescriptor;
|
|
@@ -137,14 +171,31 @@ export interface DefineVehicleOperationOptions<Input, Output> {
|
|
|
137
171
|
readonly limits: VehicleLimits;
|
|
138
172
|
readonly errors?: readonly VehicleFailureDescriptor[];
|
|
139
173
|
readonly background?: VehicleBackgroundCapability;
|
|
174
|
+
/** See {@link VehicleOperationDescriptor.requiresApproval}. */
|
|
175
|
+
readonly requiresApproval?: boolean;
|
|
140
176
|
}
|
|
141
177
|
export interface VehiclePrincipal {
|
|
142
178
|
readonly id: string;
|
|
143
179
|
readonly claims?: Readonly<Record<string, JsonValue>>;
|
|
144
180
|
}
|
|
181
|
+
/**
|
|
182
|
+
* callerSessionId/callerProjectRoot identify the real host session (e.g. one Pi TUI process) that
|
|
183
|
+
* originated this call, and its working directory at call time -- a generic ownership/attribution
|
|
184
|
+
* hook any operation handler can read (e.g. scoping a background subscription to the session or
|
|
185
|
+
* project that created it), distinct from both:
|
|
186
|
+
* - correlationId: a caller-CHOSEN id deliberately meant to span several separate invoke() calls
|
|
187
|
+
* (a batch/business-transaction id), not an automatically-derived caller identity.
|
|
188
|
+
* - principal: broader identity/claims used for permission and approval decisions, usually a
|
|
189
|
+
* fixed per-extension value (e.g. {id: "pi-pipes"}), not a distinguishing per-session id.
|
|
190
|
+
* A Pi projection layer (see vehicle-client-pi's invokeVehicleOperation) auto-derives both from
|
|
191
|
+
* context.sessionManager.getSessionId()/context.cwd on every call, the same way it already
|
|
192
|
+
* auto-derives correlationId -- a handler that never reads them pays nothing extra.
|
|
193
|
+
*/
|
|
145
194
|
export interface VehicleInvocationOptions {
|
|
146
195
|
readonly operationId?: string;
|
|
147
196
|
readonly correlationId?: string;
|
|
197
|
+
readonly callerSessionId?: string;
|
|
198
|
+
readonly callerProjectRoot?: string;
|
|
148
199
|
readonly signal?: AbortSignal;
|
|
149
200
|
readonly deadline?: number;
|
|
150
201
|
readonly permissions?: readonly string[];
|
|
@@ -158,6 +209,10 @@ export interface VehicleOperationContext<Input> {
|
|
|
158
209
|
readonly input: Input;
|
|
159
210
|
readonly operationId: string;
|
|
160
211
|
readonly correlationId?: string;
|
|
212
|
+
/** See VehicleInvocationOptions's own doc comment. */
|
|
213
|
+
readonly callerSessionId?: string;
|
|
214
|
+
/** See VehicleInvocationOptions's own doc comment. */
|
|
215
|
+
readonly callerProjectRoot?: string;
|
|
161
216
|
readonly signal: AbortSignal;
|
|
162
217
|
readonly deadline: number;
|
|
163
218
|
readonly permissions: readonly string[];
|
|
@@ -192,6 +247,24 @@ export interface VehicleManifestIdentity {
|
|
|
192
247
|
export interface VehicleManifestOperation extends VehicleOperationDescriptor {
|
|
193
248
|
readonly available: boolean;
|
|
194
249
|
readonly unavailableReason?: string;
|
|
250
|
+
/**
|
|
251
|
+
* The registry's own, live, fully-resolved answer to "does invoking this operation right
|
|
252
|
+
* now require approval" -- accounts for the registry's current approval policy being
|
|
253
|
+
* enabled/disabled, this operation's own `requiresApproval` override when set, and the
|
|
254
|
+
* effect-derived default otherwise. A real VehicleRegistry.manifest() always sets this
|
|
255
|
+
* (false when the registry never called configureApprovals() at all) -- unlike
|
|
256
|
+
* `requiresApproval` (the static, author-declared override on the descriptor itself),
|
|
257
|
+
* this always reflects the current instant, so a client re-fetching the manifest after a
|
|
258
|
+
* live policy change (VehicleRegistry.updateApprovalPolicy) sees the new answer with no
|
|
259
|
+
* separate sync mechanism needed.
|
|
260
|
+
*
|
|
261
|
+
* Optional purely for backward compatibility with every hand-authored VehicleManifest
|
|
262
|
+
* test fixture across the ecosystem that predates this field (the same reason
|
|
263
|
+
* VehicleManifest.events is optional) -- a consumer reading it should treat undefined the
|
|
264
|
+
* same as a caller of classifyVehicleOperationSafety does: fall back to the effect-level
|
|
265
|
+
* default, never assume false.
|
|
266
|
+
*/
|
|
267
|
+
readonly approvalRequired?: boolean;
|
|
195
268
|
}
|
|
196
269
|
/**
|
|
197
270
|
* A named, schema'd event type a provider declares as part of its
|
package/dist/vehicle-contract.js
CHANGED
|
@@ -140,6 +140,7 @@ export function defineVehicleOperation(options) {
|
|
|
140
140
|
longRunning: options.longRunning ?? false,
|
|
141
141
|
limits: Object.freeze({ ...options.limits }),
|
|
142
142
|
errors: Object.freeze((options.errors ?? []).map((failure) => Object.freeze({ ...failure }))),
|
|
143
|
+
...(options.requiresApproval !== undefined ? { requiresApproval: options.requiresApproval } : {}),
|
|
143
144
|
...(options.background
|
|
144
145
|
? {
|
|
145
146
|
background: Object.freeze({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@danypops/vehicle-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"description": "Vehicle's runtime-neutral wire contract: operation descriptors, schema codecs, failure shapes. Zero runtime dependencies, zero Bun-specific code -- the one thing every Vehicle client and server package depends on.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
package/src/vehicle-contract.ts
CHANGED
|
@@ -42,6 +42,14 @@ export type VehicleSchemaResult<T> =
|
|
|
42
42
|
| { readonly success: true; readonly value: T }
|
|
43
43
|
| { readonly success: false; readonly issues?: readonly VehicleSchemaIssue[] };
|
|
44
44
|
|
|
45
|
+
/**
|
|
46
|
+
* A serializable, descriptive `jsonSchema` (surfaced to a client or Pi tool
|
|
47
|
+
* projection) paired with a real `safeParse` that actually enforces it at
|
|
48
|
+
* runtime -- a Vehicle registry's own `invoke()` only ever calls
|
|
49
|
+
* `safeParse`; `jsonSchema` alone is never itself enforced, so a codec that
|
|
50
|
+
* only sets `jsonSchema` without a matching `safeParse` is a documentation
|
|
51
|
+
* gesture, not an honest contract.
|
|
52
|
+
*/
|
|
45
53
|
export interface VehicleSchemaCodec<T> {
|
|
46
54
|
readonly jsonSchema: JsonSchema;
|
|
47
55
|
safeParse(value: unknown): VehicleSchemaResult<T>;
|
|
@@ -163,6 +171,7 @@ export interface VehicleLimits {
|
|
|
163
171
|
readonly maxResponseBytes: number;
|
|
164
172
|
}
|
|
165
173
|
|
|
174
|
+
/** One structured, documented failure mode a {@link VehicleOperationDescriptor} declares up front -- part of the operation's own serializable contract, not an ad hoc thrown Error a caller has to reverse-engineer from a message string. */
|
|
166
175
|
export interface VehicleFailureDescriptor {
|
|
167
176
|
readonly code: string;
|
|
168
177
|
readonly description: string;
|
|
@@ -175,6 +184,16 @@ export interface VehicleBackgroundCapability {
|
|
|
175
184
|
readonly maxWakeBudget: VehicleJobWakeBudget;
|
|
176
185
|
}
|
|
177
186
|
|
|
187
|
+
/**
|
|
188
|
+
* The serializable half of a Vehicle operation -- name, version, schemas,
|
|
189
|
+
* ownership-implying permissions, effect classification, idempotency,
|
|
190
|
+
* streaming/long-running capability, request/response limits, and declared
|
|
191
|
+
* {@link VehicleFailureDescriptor} failure modes. Kept separate from the
|
|
192
|
+
* executable {@link VehicleOperationHandler} on purpose: a manifest, a Pi
|
|
193
|
+
* tool projection, or a client's own capability check can all inspect this
|
|
194
|
+
* shape without ever touching (or needing to trust) the implementation
|
|
195
|
+
* behind it.
|
|
196
|
+
*/
|
|
178
197
|
export interface VehicleOperationDescriptor {
|
|
179
198
|
readonly name: string;
|
|
180
199
|
readonly version: number;
|
|
@@ -189,6 +208,21 @@ export interface VehicleOperationDescriptor {
|
|
|
189
208
|
readonly limits: VehicleLimits;
|
|
190
209
|
readonly errors: readonly VehicleFailureDescriptor[];
|
|
191
210
|
readonly background?: VehicleBackgroundCapability;
|
|
211
|
+
/**
|
|
212
|
+
* Owner-declared override for whether this specific operation is ever a candidate for
|
|
213
|
+
* approval gating, independent of its `effect`. Undefined (the default) means "derive it
|
|
214
|
+
* from `effect` against the registry's own requireApprovalForEffects set instead" --
|
|
215
|
+
* VehicleRegistry.manifest()'s own resolution rule, unchanged for every existing
|
|
216
|
+
* operation that never sets this.
|
|
217
|
+
*
|
|
218
|
+
* Exists because VehicleEffect's five values are coarse enough that two operations a
|
|
219
|
+
* real owner classifies very differently (e.g. "restart an already-installed,
|
|
220
|
+
* already-vetted service" vs. "sync a read-only catalog mirror") can land in the same
|
|
221
|
+
* effect bucket (both external-write) -- no single requireApprovalForEffects set can
|
|
222
|
+
* gate one without also gating the other. The owner who registers the operation knows
|
|
223
|
+
* its real risk far better than a 5-value enum can; this lets them say so directly.
|
|
224
|
+
*/
|
|
225
|
+
readonly requiresApproval?: boolean;
|
|
192
226
|
}
|
|
193
227
|
|
|
194
228
|
export interface VehicleOperation<Input, Output> {
|
|
@@ -211,6 +245,8 @@ export interface DefineVehicleOperationOptions<Input, Output> {
|
|
|
211
245
|
readonly limits: VehicleLimits;
|
|
212
246
|
readonly errors?: readonly VehicleFailureDescriptor[];
|
|
213
247
|
readonly background?: VehicleBackgroundCapability;
|
|
248
|
+
/** See {@link VehicleOperationDescriptor.requiresApproval}. */
|
|
249
|
+
readonly requiresApproval?: boolean;
|
|
214
250
|
}
|
|
215
251
|
|
|
216
252
|
export interface VehiclePrincipal {
|
|
@@ -218,9 +254,24 @@ export interface VehiclePrincipal {
|
|
|
218
254
|
readonly claims?: Readonly<Record<string, JsonValue>>;
|
|
219
255
|
}
|
|
220
256
|
|
|
257
|
+
/**
|
|
258
|
+
* callerSessionId/callerProjectRoot identify the real host session (e.g. one Pi TUI process) that
|
|
259
|
+
* originated this call, and its working directory at call time -- a generic ownership/attribution
|
|
260
|
+
* hook any operation handler can read (e.g. scoping a background subscription to the session or
|
|
261
|
+
* project that created it), distinct from both:
|
|
262
|
+
* - correlationId: a caller-CHOSEN id deliberately meant to span several separate invoke() calls
|
|
263
|
+
* (a batch/business-transaction id), not an automatically-derived caller identity.
|
|
264
|
+
* - principal: broader identity/claims used for permission and approval decisions, usually a
|
|
265
|
+
* fixed per-extension value (e.g. {id: "pi-pipes"}), not a distinguishing per-session id.
|
|
266
|
+
* A Pi projection layer (see vehicle-client-pi's invokeVehicleOperation) auto-derives both from
|
|
267
|
+
* context.sessionManager.getSessionId()/context.cwd on every call, the same way it already
|
|
268
|
+
* auto-derives correlationId -- a handler that never reads them pays nothing extra.
|
|
269
|
+
*/
|
|
221
270
|
export interface VehicleInvocationOptions {
|
|
222
271
|
readonly operationId?: string;
|
|
223
272
|
readonly correlationId?: string;
|
|
273
|
+
readonly callerSessionId?: string;
|
|
274
|
+
readonly callerProjectRoot?: string;
|
|
224
275
|
readonly signal?: AbortSignal;
|
|
225
276
|
readonly deadline?: number;
|
|
226
277
|
readonly permissions?: readonly string[];
|
|
@@ -235,6 +286,10 @@ export interface VehicleOperationContext<Input> {
|
|
|
235
286
|
readonly input: Input;
|
|
236
287
|
readonly operationId: string;
|
|
237
288
|
readonly correlationId?: string;
|
|
289
|
+
/** See VehicleInvocationOptions's own doc comment. */
|
|
290
|
+
readonly callerSessionId?: string;
|
|
291
|
+
/** See VehicleInvocationOptions's own doc comment. */
|
|
292
|
+
readonly callerProjectRoot?: string;
|
|
238
293
|
readonly signal: AbortSignal;
|
|
239
294
|
readonly deadline: number;
|
|
240
295
|
readonly permissions: readonly string[];
|
|
@@ -273,6 +328,24 @@ export interface VehicleManifestIdentity {
|
|
|
273
328
|
export interface VehicleManifestOperation extends VehicleOperationDescriptor {
|
|
274
329
|
readonly available: boolean;
|
|
275
330
|
readonly unavailableReason?: string;
|
|
331
|
+
/**
|
|
332
|
+
* The registry's own, live, fully-resolved answer to "does invoking this operation right
|
|
333
|
+
* now require approval" -- accounts for the registry's current approval policy being
|
|
334
|
+
* enabled/disabled, this operation's own `requiresApproval` override when set, and the
|
|
335
|
+
* effect-derived default otherwise. A real VehicleRegistry.manifest() always sets this
|
|
336
|
+
* (false when the registry never called configureApprovals() at all) -- unlike
|
|
337
|
+
* `requiresApproval` (the static, author-declared override on the descriptor itself),
|
|
338
|
+
* this always reflects the current instant, so a client re-fetching the manifest after a
|
|
339
|
+
* live policy change (VehicleRegistry.updateApprovalPolicy) sees the new answer with no
|
|
340
|
+
* separate sync mechanism needed.
|
|
341
|
+
*
|
|
342
|
+
* Optional purely for backward compatibility with every hand-authored VehicleManifest
|
|
343
|
+
* test fixture across the ecosystem that predates this field (the same reason
|
|
344
|
+
* VehicleManifest.events is optional) -- a consumer reading it should treat undefined the
|
|
345
|
+
* same as a caller of classifyVehicleOperationSafety does: fall back to the effect-level
|
|
346
|
+
* default, never assume false.
|
|
347
|
+
*/
|
|
348
|
+
readonly approvalRequired?: boolean;
|
|
276
349
|
}
|
|
277
350
|
|
|
278
351
|
/**
|
|
@@ -384,6 +457,7 @@ export function defineVehicleOperation<Input, Output>(
|
|
|
384
457
|
longRunning: options.longRunning ?? false,
|
|
385
458
|
limits: Object.freeze({ ...options.limits }),
|
|
386
459
|
errors: Object.freeze((options.errors ?? []).map((failure) => Object.freeze({ ...failure }))),
|
|
460
|
+
...(options.requiresApproval !== undefined ? { requiresApproval: options.requiresApproval } : {}),
|
|
387
461
|
...(options.background
|
|
388
462
|
? {
|
|
389
463
|
background: Object.freeze({
|