@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
@@ -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.13.1",
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",
@@ -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({