@danypops/vehicle-core 0.18.2 → 0.18.4
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/dist/cli-safety/assert-no-leading-flag-char.d.ts +21 -0
- package/dist/cli-safety/assert-no-leading-flag-char.js +31 -0
- package/dist/cli-safety/index.d.ts +1 -0
- package/dist/cli-safety/index.js +1 -0
- package/dist/index.d.ts +7 -3
- package/dist/index.js +7 -3
- package/dist/resource-pool/bounded-resource-pool.d.ts +139 -0
- package/dist/resource-pool/bounded-resource-pool.js +427 -0
- package/dist/resource-pool/errors.d.ts +24 -0
- package/dist/resource-pool/errors.js +44 -0
- package/dist/resource-pool/index.d.ts +3 -0
- package/dist/resource-pool/index.js +3 -0
- package/dist/resource-pool/resource-policy.d.ts +28 -0
- package/dist/resource-pool/resource-policy.js +1 -0
- package/package.json +1 -1
- package/src/cli-safety/assert-no-leading-flag-char.ts +32 -0
- package/src/cli-safety/index.ts +1 -0
- package/src/index.ts +7 -3
- package/src/resource-pool/bounded-resource-pool.ts +506 -0
- package/src/resource-pool/errors.ts +45 -0
- package/src/resource-pool/index.ts +3 -0
- package/src/resource-pool/resource-policy.ts +29 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Guards against the argv-injection class behind a well-known family of CLI-wrapper CVEs
|
|
3
|
+
* (simple-git's own history includes several): a caller-influenced string that starts with `-`
|
|
4
|
+
* can be parsed by the target CLI as a *flag* (`--upload-pack`, `--exec`, `--template`, `-c`
|
|
5
|
+
* config override) instead of the literal ref/path/pattern value the caller intended. Any Vehicle
|
|
6
|
+
* operation that hands a caller-supplied string to a shelled-out CLI's argv should run it through
|
|
7
|
+
* this check first, at the exact position it reaches that argv.
|
|
8
|
+
*
|
|
9
|
+
* This is a hard rejection with no exceptions -- it has no opinion about *why* a value starting
|
|
10
|
+
* with `-` might be needed and does not attempt to allow-list specific flags. A caller whose CLI
|
|
11
|
+
* genuinely accepts caller-influenced flag-shaped arguments needs a purpose-built allow-list of
|
|
12
|
+
* its own; this primitive only ever covers the common case of a value that should always be a
|
|
13
|
+
* literal.
|
|
14
|
+
*/
|
|
15
|
+
export declare class UnsafeCliArgument extends Error {
|
|
16
|
+
readonly value: string;
|
|
17
|
+
readonly fieldName?: string | undefined;
|
|
18
|
+
constructor(value: string, fieldName?: string | undefined);
|
|
19
|
+
}
|
|
20
|
+
/** Throws UnsafeCliArgument if `value` starts with `-`. `fieldName`, when given, names the field in the thrown error's own message for a caller with several distinct argv positions to check. */
|
|
21
|
+
export declare function assertNoLeadingFlagChar(value: string, fieldName?: string): void;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Guards against the argv-injection class behind a well-known family of CLI-wrapper CVEs
|
|
3
|
+
* (simple-git's own history includes several): a caller-influenced string that starts with `-`
|
|
4
|
+
* can be parsed by the target CLI as a *flag* (`--upload-pack`, `--exec`, `--template`, `-c`
|
|
5
|
+
* config override) instead of the literal ref/path/pattern value the caller intended. Any Vehicle
|
|
6
|
+
* operation that hands a caller-supplied string to a shelled-out CLI's argv should run it through
|
|
7
|
+
* this check first, at the exact position it reaches that argv.
|
|
8
|
+
*
|
|
9
|
+
* This is a hard rejection with no exceptions -- it has no opinion about *why* a value starting
|
|
10
|
+
* with `-` might be needed and does not attempt to allow-list specific flags. A caller whose CLI
|
|
11
|
+
* genuinely accepts caller-influenced flag-shaped arguments needs a purpose-built allow-list of
|
|
12
|
+
* its own; this primitive only ever covers the common case of a value that should always be a
|
|
13
|
+
* literal.
|
|
14
|
+
*/
|
|
15
|
+
export class UnsafeCliArgument extends Error {
|
|
16
|
+
value;
|
|
17
|
+
fieldName;
|
|
18
|
+
constructor(value, fieldName) {
|
|
19
|
+
super(fieldName
|
|
20
|
+
? `"${value}" cannot be used as ${fieldName} -- it would be interpreted as a CLI flag, not a literal value`
|
|
21
|
+
: `"${value}" cannot be used as a CLI argument -- it would be interpreted as a flag, not a literal value`);
|
|
22
|
+
this.value = value;
|
|
23
|
+
this.fieldName = fieldName;
|
|
24
|
+
this.name = "UnsafeCliArgument";
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/** Throws UnsafeCliArgument if `value` starts with `-`. `fieldName`, when given, names the field in the thrown error's own message for a caller with several distinct argv positions to check. */
|
|
28
|
+
export function assertNoLeadingFlagChar(value, fieldName) {
|
|
29
|
+
if (value.startsWith("-"))
|
|
30
|
+
throw new UnsafeCliArgument(value, fieldName);
|
|
31
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./assert-no-leading-flag-char.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./assert-no-leading-flag-char.js";
|
package/dist/index.d.ts
CHANGED
|
@@ -6,13 +6,16 @@
|
|
|
6
6
|
* classification, invocation context), events, manifest, client (the port a
|
|
7
7
|
* caller programs against), approvals (the Approval Gate's wire shapes), jobs
|
|
8
8
|
* (Vehicle Jobs' pure pieces), schedules, watches, persistence (atomic
|
|
9
|
-
* JSON),
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* JSON), concurrency (timer-based scheduling primitives), cli-safety
|
|
10
|
+
* (argv-injection guards for any operation shelling out to a CLI), and
|
|
11
|
+
* resource-pool (bounded admission/eviction/leasing for a pooled expensive
|
|
12
|
+
* stateful resource) -- the latter four are technical utilities, not
|
|
13
|
+
* Vehicle protocol capabilities, kept distinct for that reason. Every symbol below is re-exported unchanged
|
|
12
14
|
* from its historical flat-file home, so root-level `import { X } from
|
|
13
15
|
* "@danypops/vehicle-core"` usage is completely unaffected by this layout.
|
|
14
16
|
*/
|
|
15
17
|
export * from "./approvals/index.js";
|
|
18
|
+
export * from "./cli-safety/index.js";
|
|
16
19
|
export * from "./client/index.js";
|
|
17
20
|
export * from "./concurrency/index.js";
|
|
18
21
|
export * from "./content/index.js";
|
|
@@ -23,6 +26,7 @@ export * from "./jobs/index.js";
|
|
|
23
26
|
export * from "./manifest/index.js";
|
|
24
27
|
export * from "./operations/index.js";
|
|
25
28
|
export * from "./persistence/index.js";
|
|
29
|
+
export * from "./resource-pool/index.js";
|
|
26
30
|
export * from "./schedules/index.js";
|
|
27
31
|
export * from "./schemas/index.js";
|
|
28
32
|
export * from "./watches/index.js";
|
package/dist/index.js
CHANGED
|
@@ -6,13 +6,16 @@
|
|
|
6
6
|
* classification, invocation context), events, manifest, client (the port a
|
|
7
7
|
* caller programs against), approvals (the Approval Gate's wire shapes), jobs
|
|
8
8
|
* (Vehicle Jobs' pure pieces), schedules, watches, persistence (atomic
|
|
9
|
-
* JSON),
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* JSON), concurrency (timer-based scheduling primitives), cli-safety
|
|
10
|
+
* (argv-injection guards for any operation shelling out to a CLI), and
|
|
11
|
+
* resource-pool (bounded admission/eviction/leasing for a pooled expensive
|
|
12
|
+
* stateful resource) -- the latter four are technical utilities, not
|
|
13
|
+
* Vehicle protocol capabilities, kept distinct for that reason. Every symbol below is re-exported unchanged
|
|
12
14
|
* from its historical flat-file home, so root-level `import { X } from
|
|
13
15
|
* "@danypops/vehicle-core"` usage is completely unaffected by this layout.
|
|
14
16
|
*/
|
|
15
17
|
export * from "./approvals/index.js";
|
|
18
|
+
export * from "./cli-safety/index.js";
|
|
16
19
|
export * from "./client/index.js";
|
|
17
20
|
export * from "./concurrency/index.js";
|
|
18
21
|
export * from "./content/index.js";
|
|
@@ -23,6 +26,7 @@ export * from "./jobs/index.js";
|
|
|
23
26
|
export * from "./manifest/index.js";
|
|
24
27
|
export * from "./operations/index.js";
|
|
25
28
|
export * from "./persistence/index.js";
|
|
29
|
+
export * from "./resource-pool/index.js";
|
|
26
30
|
export * from "./schedules/index.js";
|
|
27
31
|
export * from "./schemas/index.js";
|
|
28
32
|
export * from "./watches/index.js";
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { ResourceAdmissionQueueFull, ResourceAdmissionQueueTimedOut, ResourceCapacityExceeded, ResourceInUse } from "./errors.js";
|
|
2
|
+
import type { ResourcePoolActiveCeilingSource, ResourcePoolResourcePolicy } from "./resource-policy.js";
|
|
3
|
+
export { ResourceAdmissionQueueFull, ResourceAdmissionQueueTimedOut, ResourceCapacityExceeded, ResourceInUse };
|
|
4
|
+
/** A resource the pool can shut down when it goes cold. costHandle, when present, is an opaque token a cost-sampling hook may key off (e.g. a subprocess pid) -- undefined for a resource with nothing external to sample. */
|
|
5
|
+
export interface PooledResource {
|
|
6
|
+
close(): Promise<void>;
|
|
7
|
+
isAlive?(): boolean;
|
|
8
|
+
readonly costHandle?: unknown;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Distinguishes an interactive human/agent-facing request from a self-scheduled background one.
|
|
12
|
+
* Foreground admission is never queued or reduced below reservedForegroundSlots' effective
|
|
13
|
+
* ceiling -- background is the only work kind that ever waits. Defaults to "foreground": a caller
|
|
14
|
+
* that never opts in gets today's exact unreserved behavior.
|
|
15
|
+
*/
|
|
16
|
+
export type ResourceWorkKind = "foreground" | "background";
|
|
17
|
+
export type ResourcePoolEvent = {
|
|
18
|
+
readonly kind: "admission-evicted" | "dead-replaced" | "resource-pressure-evicted";
|
|
19
|
+
readonly partitionKey: string;
|
|
20
|
+
} | {
|
|
21
|
+
readonly kind: "close-failed";
|
|
22
|
+
readonly reason: "admission" | "dead-replacement" | "idle-reap" | "resource-pressure";
|
|
23
|
+
readonly partitionKey: string;
|
|
24
|
+
readonly errorName: string;
|
|
25
|
+
};
|
|
26
|
+
export interface ResourcePoolStatus<Status = unknown> {
|
|
27
|
+
readonly active: number;
|
|
28
|
+
readonly leased: number;
|
|
29
|
+
readonly maxActive: number;
|
|
30
|
+
/** The count ceiling actually in effect for the most recent admission -- may exceed maxActive when a resource policy's own soft ceiling raised it, never exceeds absoluteMaxActive. */
|
|
31
|
+
readonly effectiveMaxActive: number;
|
|
32
|
+
readonly activeCeilingSource: ResourcePoolActiveCeilingSource;
|
|
33
|
+
readonly absoluteMaxActive: number;
|
|
34
|
+
readonly byPartition: Readonly<Record<string, number>>;
|
|
35
|
+
readonly resources?: Status;
|
|
36
|
+
/** How many background admissions are currently waiting for a slot reserved for foreground work -- count-only, zero whenever reservedForegroundSlots is unset or nothing is contending. */
|
|
37
|
+
readonly waitingBackgroundAdmissions: number;
|
|
38
|
+
}
|
|
39
|
+
export interface PoolLease<Value> extends AsyncDisposable {
|
|
40
|
+
readonly value: Value;
|
|
41
|
+
}
|
|
42
|
+
export interface ResourcePoolOptions<Status = unknown> {
|
|
43
|
+
readonly maxActive?: number;
|
|
44
|
+
readonly partitionLimits?: Readonly<Record<string, number>>;
|
|
45
|
+
readonly resourcePolicy?: ResourcePoolResourcePolicy<Status>;
|
|
46
|
+
/** The hard structural ceiling a resource policy's own soft ceiling can never raise maxActive past. Defaults to 32. Must be >= maxActive. */
|
|
47
|
+
readonly absoluteMaxActive?: number;
|
|
48
|
+
readonly observe?: (event: ResourcePoolEvent) => void;
|
|
49
|
+
/** Slots background admission alone can never grow into. Default 0 (no reservation). */
|
|
50
|
+
readonly reservedForegroundSlots?: number;
|
|
51
|
+
/** How long a queued background admission waits for a slot before giving up with ResourceAdmissionQueueTimedOut. Default 10s. */
|
|
52
|
+
readonly backgroundAdmissionQueueTimeoutMs?: number;
|
|
53
|
+
/** How many background admissions may be simultaneously waiting before a new one fails fast with ResourceAdmissionQueueFull instead of growing the wait queue further. Default 8. */
|
|
54
|
+
readonly maxQueuedBackgroundAdmissions?: number;
|
|
55
|
+
/** Fed one (partitionKey, costHandle) pair per active entry with a costHandle on calibrateCosts(). */
|
|
56
|
+
readonly costRecorder?: {
|
|
57
|
+
recordSample(partitionKey: string, costHandle: unknown): void;
|
|
58
|
+
};
|
|
59
|
+
readonly now?: () => number;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Owns the bounded lifecycle of pooled, expensive, stateful resources partitioned by an
|
|
63
|
+
* (ownerKey, partitionKey) pair -- e.g. one warm language-server process per (workspace,
|
|
64
|
+
* language), one warm browser context per (tenant, profile). Never knows how to build a resource:
|
|
65
|
+
* `acquire()` takes a lazy factory invoked only on an actual cache miss, so every caller-specific
|
|
66
|
+
* concept (what a resource even is, how to construct one) lives entirely in the caller's own
|
|
67
|
+
* closure at each call site.
|
|
68
|
+
*/
|
|
69
|
+
export declare class BoundedResourcePool<OwnerKey extends string, Resource extends PooledResource, Status = unknown> {
|
|
70
|
+
private readonly options;
|
|
71
|
+
private readonly entries;
|
|
72
|
+
private readonly now;
|
|
73
|
+
private readonly maxActive;
|
|
74
|
+
private readonly absoluteMaxActive;
|
|
75
|
+
private lastActiveCeilingSource;
|
|
76
|
+
private lastEffectiveMaxActive;
|
|
77
|
+
private readonly partitionLimits;
|
|
78
|
+
private admissionTail;
|
|
79
|
+
private nextSequence;
|
|
80
|
+
private readonly reservedForegroundSlots;
|
|
81
|
+
private readonly backgroundAdmissionQueueTimeoutMs;
|
|
82
|
+
private readonly maxQueuedBackgroundAdmissions;
|
|
83
|
+
private readonly admissionWaiters;
|
|
84
|
+
private queuedBackgroundAdmissions;
|
|
85
|
+
private readonly waitingCounts;
|
|
86
|
+
constructor(options?: ResourcePoolOptions<Status>);
|
|
87
|
+
private key;
|
|
88
|
+
private partitionLimit;
|
|
89
|
+
private countPartition;
|
|
90
|
+
private activePartitions;
|
|
91
|
+
private leastRecentlyUsedIdle;
|
|
92
|
+
private evict;
|
|
93
|
+
/** Wakes every queued background admission to re-check the real state -- called whenever an entry is removed OR a lease completes (an idle candidate an admit() retry might now be able to evict). A false wake just re-checks and re-waits; never a correctness issue, only a wasted retry. */
|
|
94
|
+
private notifyAdmissionWaiters;
|
|
95
|
+
/** True while at least one background admission for this owner is currently waiting for a reserved-slot conflict to clear. */
|
|
96
|
+
waitingForAdmission(ownerKey: OwnerKey): boolean;
|
|
97
|
+
/**
|
|
98
|
+
* Runs entirely outside the serialized admission lock -- admissionTail is the single global
|
|
99
|
+
* admission mutex, and this wait can legitimately take up to backgroundAdmissionQueueTimeoutMs.
|
|
100
|
+
* Holding that lock for the whole wait would block every other admission request, foreground
|
|
101
|
+
* included, which is the exact starvation this exists to prevent.
|
|
102
|
+
*/
|
|
103
|
+
private waitForAdmissionRoom;
|
|
104
|
+
private admit;
|
|
105
|
+
private serialized;
|
|
106
|
+
/** Acquires a lease for (ownerKey, partitionKey), reusing an already-admitted resource if one is warm, or admitting a fresh one via `create()` -- called only on an actual cache miss, never speculatively. workKind defaults to "foreground". */
|
|
107
|
+
acquire(ownerKey: OwnerKey, partitionKey: string, create: () => Resource, workKind?: ResourceWorkKind): Promise<PoolLease<Resource>>;
|
|
108
|
+
private lease;
|
|
109
|
+
has(ownerKey: OwnerKey, partitionKey: string): boolean;
|
|
110
|
+
hasAny(ownerKey: OwnerKey): boolean;
|
|
111
|
+
/** Every currently active resource belonging to `ownerKey` -- lets a caller's own fan-out (file-watch notifications, etc) stay pool-backed instead of duplicating the entry map. */
|
|
112
|
+
activeResourcesForOwner(ownerKey: OwnerKey): readonly Resource[];
|
|
113
|
+
/**
|
|
114
|
+
* Derives the count ceiling actually in effect right now, independent of any particular
|
|
115
|
+
* admission attempt -- the resource policy's own soft ceiling raises maxActive when it reports
|
|
116
|
+
* more room, clamped to absoluteMaxActive, and falls back to maxActive alone (source
|
|
117
|
+
* "configured") on any metric loss -- fails closed, never treated as "unlimited room."
|
|
118
|
+
*/
|
|
119
|
+
private baseActiveCeiling;
|
|
120
|
+
status(): ResourcePoolStatus<Status>;
|
|
121
|
+
/** Samples every currently active entry with a real costHandle and folds it into the configured recorder, if any -- a no-op without one. Read-only over the entry map, so it deliberately does not run inside serialized(). */
|
|
122
|
+
calibrateCosts(): void;
|
|
123
|
+
/** Unconditional force-close of every one of `ownerKey`'s resources, regardless of any active lease -- for the case of a remote resource swapped out from under an already-warm one, where correctness requires closing regardless of who still holds it. */
|
|
124
|
+
closeOwner(ownerKey: OwnerKey): Promise<void>;
|
|
125
|
+
/** Unconditional force-close of one (ownerKey, partitionKey) resource, if any -- the single-partition sibling of closeOwner, for a caller that has already identified exactly which partition needs invalidating. */
|
|
126
|
+
closePartition(ownerKey: OwnerKey, partitionKey: string): Promise<void>;
|
|
127
|
+
/**
|
|
128
|
+
* The safe sibling of closeOwner: refuses (does not evict anything) while any of this owner's
|
|
129
|
+
* resources has an active lease. Serialized against concurrent admission so a lease can't be
|
|
130
|
+
* granted between the check and the close.
|
|
131
|
+
*/
|
|
132
|
+
releaseOwnerIfIdle(ownerKey: OwnerKey): Promise<{
|
|
133
|
+
readonly closed: number;
|
|
134
|
+
}>;
|
|
135
|
+
closeAll(): Promise<void>;
|
|
136
|
+
private reconcileResourcesUnsafe;
|
|
137
|
+
reconcileResources(): Promise<number>;
|
|
138
|
+
reapIdle(maxIdleMs: number): Promise<number>;
|
|
139
|
+
}
|