@intx/hub-sessions 0.2.2 → 0.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.
Files changed (87) hide show
  1. package/README.md +3 -5
  2. package/dist/agent-repo.d.ts +23 -7
  3. package/dist/agent-repo.js +19 -6
  4. package/dist/agent-state-kind.js +18 -63
  5. package/dist/asset-service.d.ts +1 -20
  6. package/dist/asset-service.js +19 -97
  7. package/dist/committed-source-tree.d.ts +10 -0
  8. package/dist/committed-source-tree.js +35 -0
  9. package/dist/credential-push.d.ts +52 -7
  10. package/dist/credential-push.js +178 -22
  11. package/dist/event-collector-registry.d.ts +3 -2
  12. package/dist/event-collector-registry.js +42 -13
  13. package/dist/event-collector.d.ts +12 -2
  14. package/dist/event-collector.js +45 -4
  15. package/dist/hub-session-lookups.d.ts +125 -7
  16. package/dist/hub-session-lookups.js +541 -86
  17. package/dist/hub-session-orchestrator.d.ts +2 -3
  18. package/dist/hub-session-orchestrator.js +22 -56
  19. package/dist/index.d.ts +19 -9
  20. package/dist/index.js +16 -7
  21. package/dist/reconciliation-scheduler.d.ts +14 -0
  22. package/dist/reconciliation-scheduler.js +55 -0
  23. package/dist/repo-store/index.d.ts +2 -1
  24. package/dist/repo-store/index.js +1 -0
  25. package/dist/repo-store/store.d.ts +1 -1
  26. package/dist/repo-store/store.js +138 -1
  27. package/dist/repo-store/subscribe-kind.d.ts +6 -3
  28. package/dist/repo-store/subscribe-kind.js +42 -77
  29. package/dist/repo-store/types.d.ts +94 -6
  30. package/dist/repo-store/user-principal-gate.d.ts +26 -0
  31. package/dist/repo-store/user-principal-gate.js +78 -0
  32. package/dist/session-service.d.ts +252 -126
  33. package/dist/session-service.js +851 -624
  34. package/dist/sidecar-allocation/capability-policy.d.ts +27 -0
  35. package/dist/sidecar-allocation/capability-policy.js +124 -0
  36. package/dist/sidecar-allocation/contracts.d.ts +101 -0
  37. package/dist/sidecar-allocation/contracts.js +26 -0
  38. package/dist/sidecar-allocation/index.d.ts +5 -0
  39. package/dist/sidecar-allocation/index.js +4 -0
  40. package/dist/sidecar-allocation/operation.d.ts +10 -0
  41. package/dist/sidecar-allocation/operation.js +54 -0
  42. package/dist/sidecar-allocation/plugin-registry.d.ts +24 -0
  43. package/dist/sidecar-allocation/plugin-registry.js +61 -0
  44. package/dist/sidecar-allocation/reconciler.d.ts +54 -0
  45. package/dist/sidecar-allocation/reconciler.js +825 -0
  46. package/dist/skill-kind.js +12 -62
  47. package/dist/substrate.d.ts +3 -3
  48. package/dist/substrate.js +1 -1
  49. package/dist/workflow-allocation-service.d.ts +64 -0
  50. package/dist/workflow-allocation-service.js +554 -0
  51. package/dist/workflow-closure-resolution.d.ts +106 -0
  52. package/dist/workflow-closure-resolution.js +123 -0
  53. package/dist/workflow-definition-ensure.d.ts +24 -0
  54. package/dist/workflow-definition-ensure.js +75 -0
  55. package/dist/workflow-dispatch-service.d.ts +42 -0
  56. package/dist/workflow-dispatch-service.js +209 -0
  57. package/dist/workflow-dispatch-settlement.d.ts +29 -0
  58. package/dist/workflow-dispatch-settlement.js +140 -0
  59. package/dist/workflow-kind.d.ts +29 -1
  60. package/dist/workflow-kind.js +143 -139
  61. package/dist/workflow-probe-gate.d.ts +286 -0
  62. package/dist/workflow-probe-gate.js +382 -0
  63. package/dist/workflow-run-kind.d.ts +239 -32
  64. package/dist/workflow-run-kind.js +874 -188
  65. package/dist/workflow-run-reader.d.ts +1 -1
  66. package/dist/workflow-run-reader.js +3 -7
  67. package/dist/workflow-run-restore.d.ts +16 -0
  68. package/dist/workflow-run-restore.js +30 -0
  69. package/dist/workflow-source-closure.d.ts +35 -0
  70. package/dist/workflow-source-closure.js +342 -0
  71. package/dist/workflow-source-pins.d.ts +8 -0
  72. package/dist/workflow-source-pins.js +14 -0
  73. package/dist/ws/index.d.ts +3 -3
  74. package/dist/ws/index.js +2 -2
  75. package/dist/ws/pending-tracker.d.ts +93 -0
  76. package/dist/ws/pending-tracker.js +132 -0
  77. package/dist/ws/sidecar-events.d.ts +139 -37
  78. package/dist/ws/sidecar-events.js +2 -2
  79. package/dist/ws/sidecar-handler.d.ts +218 -60
  80. package/dist/ws/sidecar-handler.js +1713 -732
  81. package/dist/ws/sidecar-handler.test-helpers.d.ts +38 -0
  82. package/dist/ws/sidecar-handler.test-helpers.js +95 -0
  83. package/dist/ws/sidecar-token-authenticator.d.ts +3 -1
  84. package/dist/ws/sidecar-token-authenticator.js +78 -7
  85. package/package.json +14 -13
  86. package/dist/available-skills-stanza.d.ts +0 -21
  87. package/dist/available-skills-stanza.js +0 -32
@@ -0,0 +1,27 @@
1
+ import { type SidecarCapabilityDeclaration, type SidecarCapabilityRule, type TenantSidecarCapabilityPolicy } from "@intx/types";
2
+ export type EffectiveSidecarCapabilityPolicy = {
3
+ readonly tenantPolicies: readonly TenantSidecarCapabilityPolicy[];
4
+ readonly probeRules?: readonly SidecarCapabilityRule[];
5
+ readonly workflowRules: readonly SidecarCapabilityRule[];
6
+ };
7
+ export type SidecarCapabilityMismatch = {
8
+ readonly capability: string;
9
+ readonly expected: "available" | "blocked";
10
+ readonly actual: "available" | "blocked" | "unknown";
11
+ readonly rule: SidecarCapabilityRule;
12
+ readonly source: {
13
+ readonly kind: "tenant";
14
+ readonly tenantId: string;
15
+ } | {
16
+ readonly kind: "probe";
17
+ } | {
18
+ readonly kind: "workflow";
19
+ };
20
+ };
21
+ export type SidecarCapabilityMatch = {
22
+ readonly ok: true;
23
+ } | {
24
+ readonly ok: false;
25
+ readonly mismatches: readonly SidecarCapabilityMismatch[];
26
+ };
27
+ export declare function matchSidecarCapabilityPolicy(policy: EffectiveSidecarCapabilityPolicy, declarations: readonly SidecarCapabilityDeclaration[]): SidecarCapabilityMatch;
@@ -0,0 +1,124 @@
1
+ import { parseSidecarCapabilitySelector, } from "@intx/types";
2
+ export function matchSidecarCapabilityPolicy(policy, declarations) {
3
+ const tenantPolicies = policy.tenantPolicies.map((tenantPolicy) => ({
4
+ ...tenantPolicy,
5
+ rules: tenantPolicy.rules.map(parseRule),
6
+ }));
7
+ const probeRules = (policy.probeRules ?? []).map(parseRule);
8
+ const workflowRules = policy.workflowRules.map(parseRule);
9
+ const parsedDeclarations = declarations.map(parseDeclaration);
10
+ const boundaries = collectBoundaries(tenantPolicies.flatMap(({ rules }) => rules.map(({ selector }) => selector)), probeRules.map(({ selector }) => selector), workflowRules.map(({ selector }) => selector), parsedDeclarations.map(({ selector }) => selector));
11
+ const mismatches = [];
12
+ for (const boundary of boundaries) {
13
+ const capability = formatSelector(boundary);
14
+ for (const tenantPolicy of tenantPolicies) {
15
+ const resolved = resolveRules(tenantPolicy.rules, boundary);
16
+ if (resolved !== null) {
17
+ addMismatch(mismatches, resolved.rule, resolveDeclarations(parsedDeclarations, boundary), capability, { kind: "tenant", tenantId: tenantPolicy.tenantId });
18
+ }
19
+ }
20
+ const probeRule = resolveRules(probeRules, boundary);
21
+ if (probeRule !== null) {
22
+ addMismatch(mismatches, probeRule.rule, resolveDeclarations(parsedDeclarations, boundary), capability, { kind: "probe" });
23
+ }
24
+ const workflowRule = resolveRules(workflowRules, boundary);
25
+ if (workflowRule !== null) {
26
+ addMismatch(mismatches, workflowRule.rule, resolveDeclarations(parsedDeclarations, boundary), capability, { kind: "workflow" });
27
+ }
28
+ }
29
+ return mismatches.length === 0 ? { ok: true } : { ok: false, mismatches };
30
+ }
31
+ function addMismatch(mismatches, rule, declaration, capability, source) {
32
+ const expected = rule.effect === "require" ? "available" : "blocked";
33
+ const actual = declaration?.declaration.state ?? "unknown";
34
+ if (actual === expected)
35
+ return;
36
+ mismatches.push({ capability, expected, actual, rule, source });
37
+ }
38
+ function resolveRules(rules, boundary) {
39
+ let winner = null;
40
+ for (const candidate of rules) {
41
+ if (!selectorAppliesToBoundary(candidate.selector, boundary))
42
+ continue;
43
+ if (winner === null ||
44
+ candidate.specificity > winner.specificity ||
45
+ (candidate.specificity === winner.specificity &&
46
+ candidate.rule.effect === "block" &&
47
+ winner.rule.effect === "require")) {
48
+ winner = candidate;
49
+ }
50
+ }
51
+ return winner;
52
+ }
53
+ function resolveDeclarations(declarations, boundary) {
54
+ let winner = null;
55
+ for (const candidate of declarations) {
56
+ if (!selectorAppliesToBoundary(candidate.selector, boundary))
57
+ continue;
58
+ if (winner === null ||
59
+ candidate.specificity > winner.specificity ||
60
+ (candidate.specificity === winner.specificity &&
61
+ candidate.declaration.state === "blocked" &&
62
+ winner.declaration.state === "available")) {
63
+ winner = candidate;
64
+ }
65
+ }
66
+ return winner;
67
+ }
68
+ function parseRule(rule) {
69
+ const selector = parseSelector(rule.capability);
70
+ return {
71
+ rule,
72
+ selector,
73
+ specificity: selector.segments.length,
74
+ };
75
+ }
76
+ function parseDeclaration(declaration) {
77
+ const selector = parseSelector(declaration.capability);
78
+ return {
79
+ declaration,
80
+ selector,
81
+ specificity: selector.segments.length,
82
+ };
83
+ }
84
+ function parseSelector(capability) {
85
+ const selector = parseSidecarCapabilitySelector(capability);
86
+ if (selector === null) {
87
+ throw new Error(`Invalid sidecar capability selector: ${JSON.stringify(capability)}`);
88
+ }
89
+ return selector;
90
+ }
91
+ function collectBoundaries(...selectorGroups) {
92
+ const boundaries = new Map();
93
+ for (const selectors of selectorGroups) {
94
+ for (const selector of selectors) {
95
+ boundaries.set(`${selector.kind}:${formatSelector(selector)}`, selector);
96
+ }
97
+ }
98
+ return [...boundaries.values()];
99
+ }
100
+ function selectorAppliesToBoundary(selector, boundary) {
101
+ if (selector.kind === "exact") {
102
+ return (boundary.kind === "exact" &&
103
+ segmentsEqual(selector.segments, boundary.segments));
104
+ }
105
+ if (!isSegmentPrefix(selector.segments, boundary.segments))
106
+ return false;
107
+ return (boundary.kind === "prefix" ||
108
+ selector.segments.length < boundary.segments.length);
109
+ }
110
+ function isSegmentPrefix(prefix, segments) {
111
+ return (prefix.length <= segments.length &&
112
+ prefix.every((segment, index) => segment === segments[index]));
113
+ }
114
+ function segmentsEqual(left, right) {
115
+ return (left.length === right.length &&
116
+ left.every((segment, index) => segment === right[index]));
117
+ }
118
+ function formatSelector(selector) {
119
+ if (selector.kind === "exact")
120
+ return selector.segments.join(":");
121
+ if (selector.segments.length === 0)
122
+ return "*";
123
+ return `${selector.segments.join(":")}:*`;
124
+ }
@@ -0,0 +1,101 @@
1
+ import type { SidecarCapabilityDeclaration } from "@intx/types";
2
+ export type EnsureSidecarRequest = {
3
+ /** Cancellation is best effort; a cancelled ensure has an uncertain outcome. */
4
+ readonly signal?: AbortSignal;
5
+ readonly allocationId: string;
6
+ readonly generation: number;
7
+ readonly tenantId: string;
8
+ readonly anchorRunId: string;
9
+ readonly sidecarId: string;
10
+ readonly token: string;
11
+ readonly hubWebSocketUrl: string;
12
+ };
13
+ export type DestroySidecarRequest = {
14
+ /** Cancellation never confirms destruction; the Hub may retry cleanup. */
15
+ readonly signal?: AbortSignal;
16
+ readonly allocationId: string;
17
+ readonly generation: number;
18
+ readonly sidecarId: string;
19
+ /**
20
+ * Optional provider handle recorded after ensure returns. A Hub crash can
21
+ * occur after capacity is created but before this value is persisted, so
22
+ * destroy must always be able to identify the capacity from allocationId,
23
+ * generation, and sidecarId alone.
24
+ */
25
+ readonly externalRef?: string;
26
+ };
27
+ export declare const SidecarOperationFailure: import("arktype/internal/variants/object.ts").ObjectType<{
28
+ kind: "rejected";
29
+ code: string;
30
+ message: string;
31
+ retryable: boolean;
32
+ }, {}>;
33
+ export type SidecarOperationFailure = typeof SidecarOperationFailure.infer;
34
+ /**
35
+ * Acceptance means the requested infrastructure exists, not that it is ready.
36
+ * Rejection means no infrastructure exists for this generation (ensure-only;
37
+ * destroy rejections below carry no such guarantee); a provisioner must throw
38
+ * when it cannot determine whether the request took effect.
39
+ */
40
+ export declare const EnsureSidecarResult: import("arktype/internal/variants/object.ts").ObjectType<{
41
+ kind: "rejected";
42
+ code: string;
43
+ message: string;
44
+ retryable: boolean;
45
+ } | {
46
+ kind: "accepted";
47
+ externalRef?: string;
48
+ }, {}>;
49
+ export type EnsureSidecarResult = typeof EnsureSidecarResult.infer;
50
+ /**
51
+ * Destruction confirms the capacity is gone and older ensure calls are fenced.
52
+ * A non-retryable rejection stops automatic cleanup; capacity may still exist.
53
+ */
54
+ export declare const DestroySidecarResult: import("arktype/internal/variants/object.ts").ObjectType<{
55
+ kind: "rejected";
56
+ code: string;
57
+ message: string;
58
+ retryable: boolean;
59
+ } | {
60
+ kind: "destroyed";
61
+ }, {}>;
62
+ export type DestroySidecarResult = typeof DestroySidecarResult.infer;
63
+ export interface SidecarProvisioner {
64
+ readonly id: string;
65
+ readonly apiVersion: 1;
66
+ /** Stable, non-secret identity for the backend configuration. */
67
+ readonly bindingFingerprint: string;
68
+ readonly capabilities: readonly SidecarCapabilityDeclaration[];
69
+ /**
70
+ * Converges infrastructure for this generation. Implementations must be
71
+ * idempotent and reject generations older than one they have observed.
72
+ * Honour the request signal where possible. A late completion must still
73
+ * respect a concurrent destroy's fence, even if cancellation was ignored.
74
+ */
75
+ ensure(request: EnsureSidecarRequest): Promise<EnsureSidecarResult>;
76
+ /**
77
+ * Idempotently destroys the allocation and fences older ensure calls so a
78
+ * delayed request cannot recreate infrastructure after destruction. It must
79
+ * succeed without an externalRef; that value is only an optional optimization.
80
+ */
81
+ destroy(request: DestroySidecarRequest): Promise<DestroySidecarResult>;
82
+ }
83
+ export type SidecarCredentialIdentity = {
84
+ readonly kind: "allocated";
85
+ readonly sidecarId: string;
86
+ readonly allocationId: string;
87
+ readonly tenantId: string;
88
+ readonly anchorRunId: string;
89
+ readonly workflowRunAddress: string;
90
+ readonly generation: number;
91
+ } | {
92
+ readonly kind: "probe";
93
+ readonly sidecarId: string;
94
+ readonly allocationId: string;
95
+ readonly tenantId: string;
96
+ readonly generation: number;
97
+ };
98
+ export interface SidecarCredentialResolver {
99
+ resolve(token: string): Promise<SidecarCredentialIdentity | null>;
100
+ isCurrent(identity: SidecarCredentialIdentity, use: "registration" | "readiness" | "routing"): Promise<boolean>;
101
+ }
@@ -0,0 +1,26 @@
1
+ import { type } from "arktype";
2
+ // Provisioner results cross a plugin boundary, so they are validated at
3
+ // runtime rather than trusted from the declared return type.
4
+ export const SidecarOperationFailure = type({
5
+ kind: "'rejected'",
6
+ code: "string",
7
+ message: "string",
8
+ retryable: "boolean",
9
+ });
10
+ /**
11
+ * Acceptance means the requested infrastructure exists, not that it is ready.
12
+ * Rejection means no infrastructure exists for this generation (ensure-only;
13
+ * destroy rejections below carry no such guarantee); a provisioner must throw
14
+ * when it cannot determine whether the request took effect.
15
+ */
16
+ export const EnsureSidecarResult = type({
17
+ kind: "'accepted'",
18
+ "externalRef?": "string",
19
+ }).or(SidecarOperationFailure);
20
+ /**
21
+ * Destruction confirms the capacity is gone and older ensure calls are fenced.
22
+ * A non-retryable rejection stops automatic cleanup; capacity may still exist.
23
+ */
24
+ export const DestroySidecarResult = type({
25
+ kind: "'destroyed'",
26
+ }).or(SidecarOperationFailure);
@@ -0,0 +1,5 @@
1
+ export { chooseFirstSidecarProvisioner, createSidecarPluginRegistry, type CreateSidecarPluginRegistryOpts, type SidecarPluginRegistry, type SidecarProvisionerChooser, type SidecarProvisionerSelection, } from "./plugin-registry.js";
2
+ export { matchSidecarCapabilityPolicy, type EffectiveSidecarCapabilityPolicy, type SidecarCapabilityMatch, type SidecarCapabilityMismatch, } from "./capability-policy.js";
3
+ export { DestroySidecarResult, EnsureSidecarResult, SidecarOperationFailure, type DestroySidecarRequest, type EnsureSidecarRequest, type SidecarCredentialIdentity, type SidecarCredentialResolver, type SidecarProvisioner, } from "./contracts.js";
4
+ export { createSidecarAllocationReconciler, type SidecarAllocationReconciler, type SidecarAllocationReconcilerDeps, } from "./reconciler.js";
5
+ export type { SidecarReconciliationContext } from "./operation.js";
@@ -0,0 +1,4 @@
1
+ export { chooseFirstSidecarProvisioner, createSidecarPluginRegistry, } from "./plugin-registry.js";
2
+ export { matchSidecarCapabilityPolicy, } from "./capability-policy.js";
3
+ export { DestroySidecarResult, EnsureSidecarResult, SidecarOperationFailure, } from "./contracts.js";
4
+ export { createSidecarAllocationReconciler, } from "./reconciler.js";
@@ -0,0 +1,10 @@
1
+ export declare const DEFAULT_SIDECAR_OPERATION_TIMEOUT_MS = 120000;
2
+ export type SidecarReconciliationContext = {
3
+ readonly signal: AbortSignal;
4
+ readonly leaseId: string;
5
+ };
6
+ export declare class SidecarOperationTimeoutError extends Error {
7
+ constructor(operation: string, timeoutMs: number);
8
+ }
9
+ /** Stops waiting on cancellation or an optional deadline, even if work ignores the signal. */
10
+ export declare function runSidecarOperation<T>(operation: string, timeoutMs: number | undefined, run: (signal: AbortSignal) => Promise<T>, signal?: AbortSignal): Promise<T>;
@@ -0,0 +1,54 @@
1
+ export const DEFAULT_SIDECAR_OPERATION_TIMEOUT_MS = 120_000;
2
+ export class SidecarOperationTimeoutError extends Error {
3
+ constructor(operation, timeoutMs) {
4
+ super(`${operation} timed out after ${String(timeoutMs)}ms`);
5
+ this.name = "SidecarOperationTimeoutError";
6
+ }
7
+ }
8
+ /** Stops waiting on cancellation or an optional deadline, even if work ignores the signal. */
9
+ export async function runSidecarOperation(operation, timeoutMs, run, signal) {
10
+ if (timeoutMs !== undefined &&
11
+ (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0)) {
12
+ throw new Error("Sidecar operation timeout must be a positive integer");
13
+ }
14
+ signal?.throwIfAborted();
15
+ const controller = new AbortController();
16
+ const cancel = () => {
17
+ controller.abort(signal?.reason instanceof Error
18
+ ? signal.reason
19
+ : new Error(`${operation} cancelled`));
20
+ };
21
+ signal?.addEventListener("abort", cancel, { once: true });
22
+ let rejectAborted;
23
+ const aborted = new Promise((_resolve, reject) => {
24
+ rejectAborted = () => {
25
+ reject(controller.signal.reason);
26
+ };
27
+ controller.signal.addEventListener("abort", rejectAborted, { once: true });
28
+ });
29
+ const timer = timeoutMs === undefined
30
+ ? undefined
31
+ : setTimeout(() => {
32
+ controller.abort(new SidecarOperationTimeoutError(operation, timeoutMs));
33
+ }, timeoutMs);
34
+ // A deadline must never keep the process alive on its own: in-flight work
35
+ // always holds its own handles, and a bare 120s timer would stall teardown
36
+ // and test runners after everything else settled.
37
+ timer?.unref?.();
38
+ try {
39
+ return await Promise.race([
40
+ Promise.resolve().then(() => {
41
+ controller.signal.throwIfAborted();
42
+ return run(controller.signal);
43
+ }),
44
+ aborted,
45
+ ]);
46
+ }
47
+ finally {
48
+ clearTimeout(timer);
49
+ signal?.removeEventListener("abort", cancel);
50
+ if (rejectAborted !== undefined) {
51
+ controller.signal.removeEventListener("abort", rejectAborted);
52
+ }
53
+ }
54
+ }
@@ -0,0 +1,24 @@
1
+ import type { SidecarProvisioner } from "./contracts.js";
2
+ import { type EffectiveSidecarCapabilityPolicy, type SidecarCapabilityMismatch } from "./capability-policy.js";
3
+ export type SidecarProvisionerSelection = {
4
+ readonly ok: true;
5
+ readonly provisioner: SidecarProvisioner;
6
+ } | {
7
+ readonly ok: false;
8
+ readonly reason: "no_match";
9
+ readonly mismatches: Readonly<Record<string, readonly SidecarCapabilityMismatch[]>>;
10
+ };
11
+ /** Selects one provisioner from the capability-matched candidates. */
12
+ export type SidecarProvisionerChooser = (candidates: readonly SidecarProvisioner[]) => SidecarProvisioner | Promise<SidecarProvisioner>;
13
+ export type SidecarPluginRegistry = {
14
+ /** Missing plugins return null so reconciliation can stop fail-closed. */
15
+ getProvisioner(id: string): SidecarProvisioner | null;
16
+ selectProvisioner(policy: EffectiveSidecarCapabilityPolicy): Promise<SidecarProvisionerSelection>;
17
+ };
18
+ export type CreateSidecarPluginRegistryOpts = {
19
+ /** Ordered candidates; the default chooser selects the first match. */
20
+ readonly provisioners: readonly SidecarProvisioner[];
21
+ readonly chooser?: SidecarProvisionerChooser;
22
+ };
23
+ export declare function chooseFirstSidecarProvisioner(candidates: readonly SidecarProvisioner[]): SidecarProvisioner;
24
+ export declare function createSidecarPluginRegistry({ provisioners, chooser, }: CreateSidecarPluginRegistryOpts): SidecarPluginRegistry;
@@ -0,0 +1,61 @@
1
+ import { type } from "arktype";
2
+ import { SidecarCapabilityDeclaration } from "@intx/types";
3
+ import { matchSidecarCapabilityPolicy, } from "./capability-policy.js";
4
+ export function chooseFirstSidecarProvisioner(candidates) {
5
+ const first = candidates[0];
6
+ if (first === undefined) {
7
+ throw new Error("No matching sidecar provisioners are available");
8
+ }
9
+ return first;
10
+ }
11
+ export function createSidecarPluginRegistry({ provisioners, chooser = chooseFirstSidecarProvisioner, }) {
12
+ const provisionersById = new Map();
13
+ for (const provisioner of provisioners) {
14
+ validateId(provisioner.id);
15
+ if (provisioner.apiVersion !== 1) {
16
+ throw new Error(`Unsupported sidecar provisioner API version for ${provisioner.id}: ${String(provisioner.apiVersion)}`);
17
+ }
18
+ if (provisioner.bindingFingerprint.trim() === "") {
19
+ throw new Error(`Sidecar provisioner ${provisioner.id} requires a binding fingerprint`);
20
+ }
21
+ const capabilities = SidecarCapabilityDeclaration.array()(provisioner.capabilities);
22
+ if (capabilities instanceof type.errors) {
23
+ throw new Error(`Invalid capability declarations on sidecar provisioner ${provisioner.id}: ${capabilities.summary}`);
24
+ }
25
+ if (provisionersById.has(provisioner.id)) {
26
+ throw new Error(`Duplicate sidecar provisioner id: ${provisioner.id}`);
27
+ }
28
+ provisionersById.set(provisioner.id, provisioner);
29
+ }
30
+ return {
31
+ getProvisioner(id) {
32
+ return provisionersById.get(id) ?? null;
33
+ },
34
+ async selectProvisioner(policy) {
35
+ const mismatches = {};
36
+ const candidates = [];
37
+ for (const provisioner of provisionersById.values()) {
38
+ const match = matchSidecarCapabilityPolicy(policy, provisioner.capabilities);
39
+ if (match.ok) {
40
+ candidates.push(provisioner);
41
+ }
42
+ else {
43
+ mismatches[provisioner.id] = match.mismatches;
44
+ }
45
+ }
46
+ if (candidates.length === 0) {
47
+ return { ok: false, reason: "no_match", mismatches };
48
+ }
49
+ const provisioner = await chooser(candidates);
50
+ if (!candidates.includes(provisioner)) {
51
+ throw new Error("Sidecar provisioner chooser returned a provisioner outside the matching candidates");
52
+ }
53
+ return { ok: true, provisioner };
54
+ },
55
+ };
56
+ }
57
+ function validateId(id) {
58
+ if (id.trim() === "") {
59
+ throw new Error("Sidecar provisioner id must be non-empty");
60
+ }
61
+ }
@@ -0,0 +1,54 @@
1
+ import type { SidecarAllocation, SidecarAllocationStore } from "@intx/db";
2
+ import type { AllocatedSidecarTarget, SidecarAllocationRouter } from "../ws/sidecar-handler.js";
3
+ import type { SidecarPluginRegistry } from "./plugin-registry.js";
4
+ import { type SidecarReconciliationContext } from "./operation.js";
5
+ type AllocationStore = Pick<SidecarAllocationStore, "beginReplacement" | "beginUnrecoverableRelease" | "bindInitialSidecar" | "bindReplacementSidecar" | "claimNextReconcilable" | "extendReconciliationLease" | "failWithoutInfrastructure" | "listActive" | "isReconciliationLeaseCurrent" | "markAllocated" | "markConnectionLost" | "markConnectionReady" | "markDestroyFailed" | "markReleased" | "parkReconciliation" | "scheduleReconnectIfUnscheduled" | "scheduleRetry" | "wakeReconciliation">;
6
+ export type SidecarAllocationReconcilerDeps = {
7
+ readonly allocationStore: AllocationStore;
8
+ readonly plugins: SidecarPluginRegistry;
9
+ readonly router: Pick<SidecarAllocationRouter, "fenceAllocation" | "isAllocatedSidecarReady" | "retireAllocation" | "waitForAllocatedSidecar">;
10
+ readonly hubWebSocketUrl: string;
11
+ /**
12
+ * Resolve the previous attempt's deferred mail under the newly claimed
13
+ * lease, before connection waits or cleanup.
14
+ */
15
+ readonly onInitializationRecovery?: (allocation: SidecarAllocation, reconciliation: SidecarReconciliationContext) => Promise<void>;
16
+ /** Idempotently restores and deploys one connected allocation generation. */
17
+ readonly onReady?: (allocation: SidecarAllocation, reconciliation: SidecarReconciliationContext) => Promise<void>;
18
+ /**
19
+ * Replace an allocated worker after its reconnect grace expires. Disabled by
20
+ * default because Hub recovery does not restore arbitrary sidecar or
21
+ * isolation-container filesystem state, so automatic continuation could run
22
+ * without state the previous worker produced.
23
+ */
24
+ readonly enableAutomaticReplacementRecovery?: boolean;
25
+ readonly leaseDurationMs?: number;
26
+ readonly connectTimeoutMs?: number;
27
+ readonly operationTimeoutMs?: number;
28
+ /**
29
+ * Bounds admitted claims, active reconciliation, and retained allocation
30
+ * queries or writes.
31
+ */
32
+ readonly maxConcurrentClaims?: number;
33
+ readonly retryDelayMs?: (attempt: number) => number;
34
+ readonly now?: () => Date;
35
+ readonly createSidecarId?: () => string;
36
+ readonly createToken?: () => string;
37
+ readonly createLeaseId?: () => string;
38
+ };
39
+ export type SidecarAllocationReconciler = {
40
+ /** Rebuild all trust fences before accepting allocated connections. */
41
+ initialize(): Promise<void>;
42
+ /** Starts a durable reconnect grace period for the exact lost generation. */
43
+ handleDisconnect(target: AllocatedSidecarTarget): Promise<void>;
44
+ /** Wakes recovery as soon as the exact generation reconnects. */
45
+ handleConnected(target: AllocatedSidecarTarget): Promise<void>;
46
+ /** Repairs allocated generations left unscheduled after a lost event write. */
47
+ repairUnscheduledConnections(): Promise<void>;
48
+ /** Reconcile at most one due allocation. Returns false when none are due. */
49
+ reconcileNext(): Promise<boolean>;
50
+ /** Drain the currently due queue, bounded to catch accidental hot loops. */
51
+ reconcileUntilIdle(maxIterations?: number): Promise<number>;
52
+ };
53
+ export declare function createSidecarAllocationReconciler({ allocationStore, plugins, router, hubWebSocketUrl, onInitializationRecovery, onReady, enableAutomaticReplacementRecovery, leaseDurationMs, connectTimeoutMs, operationTimeoutMs, maxConcurrentClaims, retryDelayMs, now, createSidecarId, createToken, createLeaseId, }: SidecarAllocationReconcilerDeps): SidecarAllocationReconciler;
54
+ export {};