@devrouter/cli 0.5.5 → 0.6.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/README.md +13 -2
- package/assets/preview/github-sweep.yml +23 -0
- package/assets/preview/github.yml +35 -0
- package/assets/preview/gitlab-template.yml +34 -0
- package/assets/preview/gitlab.yml +22 -0
- package/assets/preview/installation.json +54 -0
- package/bin/devrouter-process +29 -2
- package/dist/devrouter-lifecycle-worker.js +1554 -425
- package/dist/devrouter-preview-gitlab-ingress.js +182 -0
- package/dist/devrouter.js +9442 -2701
- package/package.json +11 -2
- package/upgrade-prompts/0.5.6.md +73 -0
- package/upgrade-prompts/0.6.0.md +57 -0
|
@@ -23,8 +23,8 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
23
23
|
));
|
|
24
24
|
|
|
25
25
|
// src/core/devpod-exec.ts
|
|
26
|
-
var
|
|
27
|
-
var
|
|
26
|
+
var import_node_child_process33 = require("child_process");
|
|
27
|
+
var import_node_crypto24 = require("crypto");
|
|
28
28
|
|
|
29
29
|
// src/core/devpod-environment.ts
|
|
30
30
|
var import_node_child_process3 = require("child_process");
|
|
@@ -1982,7 +1982,7 @@ function selectDevpodWorkspace(workspaces, repoPath) {
|
|
|
1982
1982
|
}
|
|
1983
1983
|
|
|
1984
1984
|
// src/core/devsy-exec.ts
|
|
1985
|
-
var
|
|
1985
|
+
var import_node_child_process32 = require("child_process");
|
|
1986
1986
|
|
|
1987
1987
|
// src/core/devsy-exec-proof.ts
|
|
1988
1988
|
var import_node_fs7 = __toESM(require("fs"));
|
|
@@ -3390,6 +3390,12 @@ function validateState(value, repoPath, workspace) {
|
|
|
3390
3390
|
if (typeof candidate.profile !== "string" || candidate.profile.length === 0) {
|
|
3391
3391
|
throw new Error("Managed runtime state has no canonical profile.");
|
|
3392
3392
|
}
|
|
3393
|
+
if (candidate.variant !== void 0 && (typeof candidate.variant !== "string" || candidate.variant.length > 64 || !/^[a-z0-9]+(-[a-z0-9]+)*$/.test(candidate.variant))) {
|
|
3394
|
+
throw new Error("Managed runtime state has an invalid runtime variant.");
|
|
3395
|
+
}
|
|
3396
|
+
if (candidate.hostPortDeclarationsSha256 !== void 0 && (typeof candidate.hostPortDeclarationsSha256 !== "string" || !/^[a-f0-9]{64}$/.test(candidate.hostPortDeclarationsSha256))) {
|
|
3397
|
+
throw new Error("Managed runtime state has an invalid host-port declaration fingerprint.");
|
|
3398
|
+
}
|
|
3393
3399
|
if (!candidate.desired || typeof candidate.desired !== "object") {
|
|
3394
3400
|
throw new Error("Managed runtime state has no desired resource set.");
|
|
3395
3401
|
}
|
|
@@ -3416,6 +3422,8 @@ function validateState(value, repoPath, workspace) {
|
|
|
3416
3422
|
devpodId: candidate.devpodId,
|
|
3417
3423
|
composeProject: candidate.composeProject,
|
|
3418
3424
|
profile: candidate.profile,
|
|
3425
|
+
...candidate.variant !== void 0 ? { variant: candidate.variant } : {},
|
|
3426
|
+
...candidate.hostPortDeclarationsSha256 !== void 0 ? { hostPortDeclarationsSha256: candidate.hostPortDeclarationsSha256 } : {},
|
|
3419
3427
|
desired: {
|
|
3420
3428
|
apps: [...desired.apps],
|
|
3421
3429
|
services: [...desired.services],
|
|
@@ -4055,6 +4063,89 @@ function captureManagedStopBaseline(state, plan, primaryId, expectedEndpoint) {
|
|
|
4055
4063
|
prove(state, baseline);
|
|
4056
4064
|
return baseline;
|
|
4057
4065
|
}
|
|
4066
|
+
function recoverManagedStopBaseline(state, readPlan, beforePersist) {
|
|
4067
|
+
const baseline = validateManagedStopBaseline(state.stopBaseline, state);
|
|
4068
|
+
const plan = readPlan();
|
|
4069
|
+
const owner = registration(state);
|
|
4070
|
+
if (Object.entries(owner).some(
|
|
4071
|
+
([key, value]) => baseline[key] !== value
|
|
4072
|
+
) || baseline.sourceContainer)
|
|
4073
|
+
throw new Error("Stop recovery requires the unchanged provider registration.");
|
|
4074
|
+
const observe = () => {
|
|
4075
|
+
if (!(0, import_node_util3.isDeepStrictEqual)(readManagedRuntimeState(state.repoPath, state.workspace), state) || !(0, import_node_util3.isDeepStrictEqual)(registration(state), owner) || !(0, import_node_util3.isDeepStrictEqual)(readPlan(), plan) || resolveManagedStopEndpoint() !== baseline.endpoint || inspectManagedStopDaemon(baseline.endpoint) !== baseline.daemonId || listDevpodWorkspacesRaw({ allowMissingExecutable: true }).some(
|
|
4076
|
+
(entry) => entry.id === state.devpodId || sameWorkspacePath(entry.source.localFolder, state.repoPath)
|
|
4077
|
+
))
|
|
4078
|
+
throw new Error("Stop recovery authority changed.");
|
|
4079
|
+
const currentOwner = inspectDevsyWorkspaceOwnership(
|
|
4080
|
+
listDevsyWorkspaces(),
|
|
4081
|
+
state.devpodId,
|
|
4082
|
+
state.repoPath
|
|
4083
|
+
);
|
|
4084
|
+
if (currentOwner.status !== "owned") throw new Error("Stop recovery owner is unavailable.");
|
|
4085
|
+
proveLocalDockerSelection(currentOwner.workspace);
|
|
4086
|
+
assertManagedStopContainersAbsent(
|
|
4087
|
+
baseline.endpoint,
|
|
4088
|
+
baseline.containers.map((container) => container.id)
|
|
4089
|
+
);
|
|
4090
|
+
const containers = inspectManagedStopContainers(baseline.project, baseline.endpoint);
|
|
4091
|
+
if (!(0, import_node_util3.isDeepStrictEqual)(
|
|
4092
|
+
containers.map((container) => container.labels["com.docker.compose.service"]).sort(),
|
|
4093
|
+
[...plan.desiredServices].sort()
|
|
4094
|
+
))
|
|
4095
|
+
throw new Error("Stop recovery requires the complete selected service population.");
|
|
4096
|
+
const primary2 = containers.find(
|
|
4097
|
+
(container) => container.labels["com.docker.compose.service"] === plan.primaryService
|
|
4098
|
+
);
|
|
4099
|
+
if (!primary2 || !(0, import_node_util3.isDeepStrictEqual)(
|
|
4100
|
+
inspectProviderRunnerContainers(
|
|
4101
|
+
baseline.endpoint,
|
|
4102
|
+
managedRunnerId(baseline.uid, baseline.providerId)
|
|
4103
|
+
).sort(),
|
|
4104
|
+
[primary2.id]
|
|
4105
|
+
))
|
|
4106
|
+
throw new Error("Stop recovery provider runner population changed.");
|
|
4107
|
+
assertManagedContainerConfigUnchanged({
|
|
4108
|
+
plan,
|
|
4109
|
+
containers,
|
|
4110
|
+
workspace: state.workspace ? {
|
|
4111
|
+
token: state.workspace,
|
|
4112
|
+
gitCommonDir: resolveGitCommonDir(state.repoPath)
|
|
4113
|
+
} : void 0
|
|
4114
|
+
});
|
|
4115
|
+
if (!(0, import_node_util3.isDeepStrictEqual)(registration(state), owner) || !(0, import_node_util3.isDeepStrictEqual)(readManagedRuntimeState(state.repoPath, state.workspace), state) || !(0, import_node_util3.isDeepStrictEqual)(readPlan(), plan) || resolveManagedStopEndpoint() !== baseline.endpoint || inspectManagedStopDaemon(baseline.endpoint) !== baseline.daemonId)
|
|
4116
|
+
throw new Error("Stop recovery authority changed during configuration inspection.");
|
|
4117
|
+
return containers;
|
|
4118
|
+
};
|
|
4119
|
+
const first = observe();
|
|
4120
|
+
const candidate = {
|
|
4121
|
+
...state,
|
|
4122
|
+
sourceConfigSha256: plan.sourceConfigSha256,
|
|
4123
|
+
effectiveConfigSha256: plan.effectiveConfigSha256,
|
|
4124
|
+
status: "degraded",
|
|
4125
|
+
transitionPhase: "stop-recovery",
|
|
4126
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4127
|
+
stopBaseline: void 0
|
|
4128
|
+
};
|
|
4129
|
+
const primary = first.find(
|
|
4130
|
+
(container) => container.labels["com.docker.compose.service"] === plan.primaryService
|
|
4131
|
+
);
|
|
4132
|
+
if (!primary) throw new Error("Stop recovery primary is unavailable.");
|
|
4133
|
+
candidate.stopBaseline = captureManagedStopBaseline(
|
|
4134
|
+
candidate,
|
|
4135
|
+
plan,
|
|
4136
|
+
primary.id,
|
|
4137
|
+
baseline.endpoint
|
|
4138
|
+
);
|
|
4139
|
+
if (!(0, import_node_util3.isDeepStrictEqual)(
|
|
4140
|
+
first.map(identity).sort((a, b) => compareText(a.id, b.id)),
|
|
4141
|
+
observe().map(identity).sort((a, b) => compareText(a.id, b.id))
|
|
4142
|
+
))
|
|
4143
|
+
throw new Error("Stop recovery population changed during inspection.");
|
|
4144
|
+
beforePersist();
|
|
4145
|
+
claimLifecycleEffect();
|
|
4146
|
+
writeManagedRuntimeState(candidate);
|
|
4147
|
+
return candidate;
|
|
4148
|
+
}
|
|
4058
4149
|
function proveRetainedManagedStop(state) {
|
|
4059
4150
|
const baseline = validateManagedStopBaseline(state.stopBaseline, state);
|
|
4060
4151
|
if (!(0, import_node_util3.isDeepStrictEqual)(readManagedRuntimeState(state.repoPath, state.workspace), state)) {
|
|
@@ -5399,7 +5490,7 @@ function retain(session, previous) {
|
|
|
5399
5490
|
|
|
5400
5491
|
// src/core/reliability-operation-store.ts
|
|
5401
5492
|
function installedCliVersion() {
|
|
5402
|
-
return true ? "0.
|
|
5493
|
+
return true ? "0.6.0" : "0.0.0-dev";
|
|
5403
5494
|
}
|
|
5404
5495
|
function compareCliVersions(left, right) {
|
|
5405
5496
|
const parts = (value) => value.split(".").map((part) => Number.parseInt(part, 10) || 0);
|
|
@@ -6158,6 +6249,9 @@ var MAX_CAPACITY_PROFILES = 256;
|
|
|
6158
6249
|
var MAX_CAPACITY_OPERATIONS = 64;
|
|
6159
6250
|
var MAX_CAPACITY_TRANSITIONS = 1024;
|
|
6160
6251
|
var MAX_CAPACITY_NAME_LENGTH = 64;
|
|
6252
|
+
var MAX_RUNTIME_VARIANTS = 32;
|
|
6253
|
+
var MAX_RUNTIME_VARIANT_NAME_LENGTH = 64;
|
|
6254
|
+
var RUNTIME_VARIANT_NAME_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
6161
6255
|
var UPSTREAM_TEMPLATE_RE = /^(?:\$\{WORKSPACE\}|[a-zA-Z0-9._-])+:\d{1,5}$/;
|
|
6162
6256
|
function assertUpstreamSpec(upstream, label) {
|
|
6163
6257
|
if (upstream.includes(WORKSPACE_PLACEHOLDER)) {
|
|
@@ -6634,6 +6728,53 @@ function parsePrepareCommand(value, pathLabel) {
|
|
|
6634
6728
|
return item;
|
|
6635
6729
|
});
|
|
6636
6730
|
}
|
|
6731
|
+
function assertRuntimeVariantName(value, pathLabel) {
|
|
6732
|
+
if (value.length === 0 || value.length > MAX_RUNTIME_VARIANT_NAME_LENGTH || !RUNTIME_VARIANT_NAME_RE.test(value)) {
|
|
6733
|
+
throw new Error(
|
|
6734
|
+
`${pathLabel} must use lowercase letters or digits separated by single hyphens, at most ${MAX_RUNTIME_VARIANT_NAME_LENGTH} characters.`
|
|
6735
|
+
);
|
|
6736
|
+
}
|
|
6737
|
+
if (value === "default") {
|
|
6738
|
+
throw new Error(`${pathLabel} 'default' is implicit and reserved.`);
|
|
6739
|
+
}
|
|
6740
|
+
}
|
|
6741
|
+
function parseRuntimeVariants(value, pathLabel) {
|
|
6742
|
+
if (value === void 0) {
|
|
6743
|
+
return void 0;
|
|
6744
|
+
}
|
|
6745
|
+
if (!Array.isArray(value)) {
|
|
6746
|
+
throw new Error(`${pathLabel} must be an array of strings.`);
|
|
6747
|
+
}
|
|
6748
|
+
if (value.length > MAX_RUNTIME_VARIANTS) {
|
|
6749
|
+
throw new Error(`${pathLabel} accepts at most ${MAX_RUNTIME_VARIANTS} additional names.`);
|
|
6750
|
+
}
|
|
6751
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6752
|
+
return value.map((item, index) => {
|
|
6753
|
+
const itemPath = `${pathLabel}[${index}]`;
|
|
6754
|
+
if (typeof item !== "string") {
|
|
6755
|
+
throw new Error(`${itemPath} must be a string.`);
|
|
6756
|
+
}
|
|
6757
|
+
assertRuntimeVariantName(item, itemPath);
|
|
6758
|
+
if (seen.has(item)) {
|
|
6759
|
+
throw new Error(`${pathLabel} contains duplicate '${item}'.`);
|
|
6760
|
+
}
|
|
6761
|
+
seen.add(item);
|
|
6762
|
+
return item;
|
|
6763
|
+
});
|
|
6764
|
+
}
|
|
6765
|
+
function resolveRuntimeVariant(config, requested) {
|
|
6766
|
+
if (requested === void 0) {
|
|
6767
|
+
return "default";
|
|
6768
|
+
}
|
|
6769
|
+
if (requested === "default") {
|
|
6770
|
+
return requested;
|
|
6771
|
+
}
|
|
6772
|
+
assertRuntimeVariantName(requested, "Runtime variant");
|
|
6773
|
+
if (!config.managedRuntime?.variants?.includes(requested)) {
|
|
6774
|
+
throw new Error(`Runtime variant '${requested}' is not declared in managedRuntime.variants.`);
|
|
6775
|
+
}
|
|
6776
|
+
return requested;
|
|
6777
|
+
}
|
|
6637
6778
|
function parseManagedRuntime(value, configPath) {
|
|
6638
6779
|
if (value === void 0) {
|
|
6639
6780
|
return void 0;
|
|
@@ -6641,9 +6782,13 @@ function parseManagedRuntime(value, configPath) {
|
|
|
6641
6782
|
const managedRuntime = ensureObject(value, `${configPath}.managedRuntime`);
|
|
6642
6783
|
ensureAllowedKeys(
|
|
6643
6784
|
managedRuntime,
|
|
6644
|
-
["devcontainer", "processes", "network"],
|
|
6785
|
+
["devcontainer", "processes", "network", "variants"],
|
|
6645
6786
|
`${configPath}.managedRuntime`
|
|
6646
6787
|
);
|
|
6788
|
+
const variants = parseRuntimeVariants(
|
|
6789
|
+
managedRuntime.variants,
|
|
6790
|
+
`${configPath}.managedRuntime.variants`
|
|
6791
|
+
);
|
|
6647
6792
|
let network;
|
|
6648
6793
|
if (managedRuntime.network !== void 0) {
|
|
6649
6794
|
const value2 = ensureObject(managedRuntime.network, `${configPath}.managedRuntime.network`);
|
|
@@ -6669,7 +6814,7 @@ function parseManagedRuntime(value, configPath) {
|
|
|
6669
6814
|
);
|
|
6670
6815
|
ensureAllowedKeys(
|
|
6671
6816
|
devcontainer,
|
|
6672
|
-
["baseServices", "profileServices", "prepareCommand"],
|
|
6817
|
+
["baseServices", "profileServices", "prepareCommand", "hostPorts"],
|
|
6673
6818
|
`${configPath}.managedRuntime.devcontainer`
|
|
6674
6819
|
);
|
|
6675
6820
|
const baseServices = parseRequiredUniqueStringArray(
|
|
@@ -6691,6 +6836,41 @@ function parseManagedRuntime(value, configPath) {
|
|
|
6691
6836
|
`${configPath}.managedRuntime.devcontainer.baseServices and profileServices overlap: ${overlappingServices.join(", ")}.`
|
|
6692
6837
|
);
|
|
6693
6838
|
}
|
|
6839
|
+
let hostPorts;
|
|
6840
|
+
if (devcontainer.hostPorts !== void 0) {
|
|
6841
|
+
const ports = ensureObject(
|
|
6842
|
+
devcontainer.hostPorts,
|
|
6843
|
+
`${configPath}.managedRuntime.devcontainer.hostPorts`
|
|
6844
|
+
);
|
|
6845
|
+
if (Object.keys(ports).length > 32)
|
|
6846
|
+
throw new Error("managedRuntime.devcontainer.hostPorts accepts at most 32 declarations.");
|
|
6847
|
+
hostPorts = {};
|
|
6848
|
+
const pairs = /* @__PURE__ */ new Set();
|
|
6849
|
+
const registered = /* @__PURE__ */ new Set([...baseServices, ...profileServices]);
|
|
6850
|
+
for (const [name, rawPort] of Object.entries(ports)) {
|
|
6851
|
+
if (name.length > 64 || !/^[a-z0-9]+(-[a-z0-9]+)*$/.test(name))
|
|
6852
|
+
throw new Error(
|
|
6853
|
+
"Host-port names must be lowercase kebab-case identifiers of at most 64 characters."
|
|
6854
|
+
);
|
|
6855
|
+
const port = ensureObject(rawPort, `managedRuntime.devcontainer.hostPorts.${name}`);
|
|
6856
|
+
ensureAllowedKeys(
|
|
6857
|
+
port,
|
|
6858
|
+
["service", "target"],
|
|
6859
|
+
`managedRuntime.devcontainer.hostPorts.${name}`
|
|
6860
|
+
);
|
|
6861
|
+
if (typeof port.service !== "string" || !registered.has(port.service))
|
|
6862
|
+
throw new Error(`Host port '${name}' must reference a registered base or profile service.`);
|
|
6863
|
+
if (!Number.isInteger(port.target) || port.target < 1 || port.target > 65535)
|
|
6864
|
+
throw new Error(`Host port '${name}' target must be an integer from 1 to 65535.`);
|
|
6865
|
+
const pair = `${port.service}:${port.target}`;
|
|
6866
|
+
if (pairs.has(pair)) throw new Error(`Host port '${name}' duplicates a service and target.`);
|
|
6867
|
+
pairs.add(pair);
|
|
6868
|
+
Object.defineProperty(hostPorts, name, {
|
|
6869
|
+
value: { service: port.service, target: port.target },
|
|
6870
|
+
enumerable: true
|
|
6871
|
+
});
|
|
6872
|
+
}
|
|
6873
|
+
}
|
|
6694
6874
|
const processes = parseRequiredUniqueStringArray(
|
|
6695
6875
|
managedRuntime.processes,
|
|
6696
6876
|
`${configPath}.managedRuntime.processes`
|
|
@@ -6705,10 +6885,12 @@ function parseManagedRuntime(value, configPath) {
|
|
|
6705
6885
|
}
|
|
6706
6886
|
return {
|
|
6707
6887
|
...network ? { network } : {},
|
|
6888
|
+
...variants !== void 0 ? { variants } : {},
|
|
6708
6889
|
devcontainer: {
|
|
6709
6890
|
baseServices,
|
|
6710
6891
|
profileServices,
|
|
6711
|
-
...prepareCommand ? { prepareCommand } : {}
|
|
6892
|
+
...prepareCommand ? { prepareCommand } : {},
|
|
6893
|
+
...hostPorts !== void 0 ? { hostPorts } : {}
|
|
6712
6894
|
},
|
|
6713
6895
|
processes
|
|
6714
6896
|
};
|
|
@@ -7396,6 +7578,9 @@ function resolveRepoPath(repoPath) {
|
|
|
7396
7578
|
function getRepoConfigPath(repoPath) {
|
|
7397
7579
|
return import_node_path23.default.join(resolveRepoPath(repoPath), CONFIG_FILE_NAME);
|
|
7398
7580
|
}
|
|
7581
|
+
function parseRepoConfigSource(source, configPath) {
|
|
7582
|
+
return parseConfig(import_yaml3.default.parse(source) ?? {}, configPath);
|
|
7583
|
+
}
|
|
7399
7584
|
function loadRepoConfig(repoPath, read2 = (file) => import_node_fs19.default.readFileSync(file, "utf-8")) {
|
|
7400
7585
|
const resolvedRepoPath = resolveRepoPath(repoPath);
|
|
7401
7586
|
const configPath = getRepoConfigPath(resolvedRepoPath);
|
|
@@ -7405,11 +7590,10 @@ function loadRepoConfig(repoPath, read2 = (file) => import_node_fs19.default.rea
|
|
|
7405
7590
|
);
|
|
7406
7591
|
}
|
|
7407
7592
|
const raw = read2(configPath);
|
|
7408
|
-
const
|
|
7409
|
-
const config = parseConfig(parsed ?? {}, configPath);
|
|
7593
|
+
const config = parseRepoConfigSource(raw, configPath);
|
|
7410
7594
|
const requiredVersion = config.devrouter?.version;
|
|
7411
7595
|
if (requiredVersion && !hasWarnedVersionMismatch) {
|
|
7412
|
-
const cliVersion = true ? "0.
|
|
7596
|
+
const cliVersion = true ? "0.6.0" : "0.0.0-dev";
|
|
7413
7597
|
if (cliVersion !== "0.0.0-dev" && compareSemver2(requiredVersion, cliVersion) > 0) {
|
|
7414
7598
|
hasWarnedVersionMismatch = true;
|
|
7415
7599
|
process.stderr.write(
|
|
@@ -10466,150 +10650,1170 @@ var ExecutionOutcomeError = class extends Error {
|
|
|
10466
10650
|
};
|
|
10467
10651
|
|
|
10468
10652
|
// src/core/reliability-lifecycle.ts
|
|
10469
|
-
var
|
|
10653
|
+
var import_node_crypto23 = require("crypto");
|
|
10470
10654
|
var import_node_path41 = __toESM(require("path"));
|
|
10471
10655
|
var import_node_util7 = require("util");
|
|
10472
10656
|
|
|
10473
10657
|
// src/core/capacity-policy.ts
|
|
10474
10658
|
var import_node_fs27 = __toESM(require("fs"));
|
|
10475
10659
|
var import_node_path31 = __toESM(require("path"));
|
|
10476
|
-
|
|
10477
|
-
|
|
10478
|
-
var
|
|
10479
|
-
var
|
|
10480
|
-
var
|
|
10481
|
-
|
|
10482
|
-
|
|
10483
|
-
|
|
10484
|
-
|
|
10485
|
-
|
|
10486
|
-
|
|
10487
|
-
|
|
10488
|
-
|
|
10489
|
-
|
|
10490
|
-
);
|
|
10491
|
-
|
|
10492
|
-
|
|
10493
|
-
|
|
10494
|
-
|
|
10495
|
-
|
|
10496
|
-
|
|
10497
|
-
function reliabilityAttention(state) {
|
|
10498
|
-
assertReliabilityState(state);
|
|
10499
|
-
const completeStop = state.stopProof.workloadsStopped && state.stopProof.routesRemoved;
|
|
10500
|
-
if (state.desired === "stopped-by-user") return completeStop ? void 0 : "stop-incomplete";
|
|
10501
|
-
if (state.operation?.status === "COMPLETION_UNKNOWN" || state.operation?.status === "INTERRUPTED")
|
|
10502
|
-
return "operation-unknown";
|
|
10503
|
-
if (state.desired === "parked-for-capacity")
|
|
10504
|
-
return completeStop ? "capacity-parked" : "stop-incomplete";
|
|
10505
|
-
if (state.admission === "denied-unadmittable") return "unadmittable";
|
|
10506
|
-
if (state.admission === "waiting") return "capacity-waiting";
|
|
10507
|
-
if (state.phase === "recovering") return "recovering";
|
|
10508
|
-
if (state.phase === "queued" || state.phase === "starting" || state.phase === "verifying")
|
|
10509
|
-
return "starting";
|
|
10510
|
-
return void 0;
|
|
10511
|
-
}
|
|
10512
|
-
|
|
10513
|
-
// src/core/recovery-budget.ts
|
|
10514
|
-
function findRecoveryUnit(incident, unit) {
|
|
10515
|
-
return (incident.units ?? []).find((record5) => record5.key === unit.key);
|
|
10516
|
-
}
|
|
10517
|
-
function restartsFor(kind, limits) {
|
|
10518
|
-
return kind === "service" ? limits.maxServiceRestarts : limits.maxProcessRestarts;
|
|
10519
|
-
}
|
|
10520
|
-
function recoveryBudgetRefusal(input) {
|
|
10521
|
-
const startedAtMs = input.incident.startedAtMs ?? input.nowMs;
|
|
10522
|
-
const elapsed = input.activeElapsedMs ?? Math.max(0, input.nowMs - startedAtMs);
|
|
10523
|
-
if (elapsed > input.limits.windowSeconds * 1e3) return "window-closed";
|
|
10524
|
-
if (input.unit !== void 0) {
|
|
10525
|
-
const record5 = findRecoveryUnit(input.incident, input.unit);
|
|
10526
|
-
if (record5 !== void 0 && record5.actions >= restartsFor(input.unit.kind, input.limits))
|
|
10527
|
-
return "unit-exhausted";
|
|
10660
|
+
var MAX_DOMAINS = 256;
|
|
10661
|
+
var MAX_ENROLLMENTS = 256;
|
|
10662
|
+
var MAX_PROFILES_PER_ENROLLMENT = 64;
|
|
10663
|
+
var MAX_STRING_LENGTH = 256;
|
|
10664
|
+
var MAX_PATH_LENGTH = 4096;
|
|
10665
|
+
function readCapacityPolicy(directory) {
|
|
10666
|
+
const absolute2 = import_node_path31.default.resolve(directory);
|
|
10667
|
+
let current = import_node_path31.default.parse(absolute2).root;
|
|
10668
|
+
for (const part of absolute2.slice(current.length).split(import_node_path31.default.sep)) {
|
|
10669
|
+
current = import_node_path31.default.join(current, part);
|
|
10670
|
+
let stat;
|
|
10671
|
+
try {
|
|
10672
|
+
stat = import_node_fs27.default.lstatSync(current);
|
|
10673
|
+
} catch (error) {
|
|
10674
|
+
if (error.code === "ENOENT") return void 0;
|
|
10675
|
+
throw error;
|
|
10676
|
+
}
|
|
10677
|
+
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
10678
|
+
throw new Error("Unsafe capacity policy directory.");
|
|
10679
|
+
if (current === absolute2 && (stat.uid !== process.getuid?.() || (stat.mode & 63) !== 0))
|
|
10680
|
+
throw new Error("Capacity policy directory is not private.");
|
|
10528
10681
|
}
|
|
10529
|
-
|
|
10530
|
-
|
|
10531
|
-
|
|
10682
|
+
let descriptor;
|
|
10683
|
+
try {
|
|
10684
|
+
descriptor = import_node_fs27.default.openSync(
|
|
10685
|
+
import_node_path31.default.join(absolute2, "capacity-policy.json"),
|
|
10686
|
+
import_node_fs27.default.constants.O_RDONLY | import_node_fs27.default.constants.O_NOFOLLOW | import_node_fs27.default.constants.O_NONBLOCK
|
|
10687
|
+
);
|
|
10688
|
+
} catch (error) {
|
|
10689
|
+
if (error.code === "ENOENT") return void 0;
|
|
10690
|
+
throw error;
|
|
10691
|
+
}
|
|
10692
|
+
try {
|
|
10693
|
+
const stat = import_node_fs27.default.fstatSync(descriptor);
|
|
10694
|
+
const limit = 1048576;
|
|
10695
|
+
if (!stat.isFile() || stat.uid !== process.getuid?.() || (stat.mode & 63) !== 0 || stat.size > limit)
|
|
10696
|
+
throw new Error("Capacity policy is not a bounded private file.");
|
|
10697
|
+
const bytes2 = Buffer.alloc(limit + 1);
|
|
10698
|
+
const count = import_node_fs27.default.readSync(descriptor, bytes2, 0, bytes2.length, 0);
|
|
10699
|
+
if (count > limit) throw new Error("Capacity policy exceeds byte limit.");
|
|
10700
|
+
return parseCapacityPolicy(JSON.parse(bytes2.subarray(0, count).toString("utf8")));
|
|
10701
|
+
} finally {
|
|
10702
|
+
import_node_fs27.default.closeSync(descriptor);
|
|
10703
|
+
}
|
|
10704
|
+
}
|
|
10705
|
+
var DOMAIN_ID_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
10706
|
+
var PROFILE_NAME_RE2 = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
10707
|
+
var WORKSPACE_TOKEN_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
10708
|
+
var SHA256_RE = /^[0-9a-f]{64}$/;
|
|
10709
|
+
var DEFAULT_CAPACITY_RECOVERY = {
|
|
10710
|
+
enabled: false,
|
|
10711
|
+
maxProcessRestarts: 2,
|
|
10712
|
+
maxServiceRestarts: 1,
|
|
10713
|
+
maxCorrectiveActions: 3,
|
|
10714
|
+
windowSeconds: 600,
|
|
10715
|
+
observationSeconds: 30,
|
|
10716
|
+
resumeDwellSeconds: 300
|
|
10717
|
+
};
|
|
10718
|
+
function ensureObject2(value, label) {
|
|
10719
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
10720
|
+
throw new Error(`${label} must be an object.`);
|
|
10721
|
+
}
|
|
10722
|
+
return value;
|
|
10532
10723
|
}
|
|
10533
|
-
function
|
|
10534
|
-
const
|
|
10535
|
-
|
|
10536
|
-
|
|
10537
|
-
|
|
10538
|
-
const index = units.findIndex((record5) => record5.key === input.unit?.key);
|
|
10539
|
-
if (index === -1) {
|
|
10540
|
-
if (units.length >= RELIABILITY_MAX_ITEMS) return { ok: false, reason: "unit-exhausted" };
|
|
10541
|
-
units.push({ ...input.unit, actions: 1, lastActionAtMs: input.nowMs });
|
|
10542
|
-
} else {
|
|
10543
|
-
const record5 = units[index];
|
|
10544
|
-
units[index] = {
|
|
10545
|
-
...record5,
|
|
10546
|
-
actions: record5.actions + 1,
|
|
10547
|
-
lastActionAtMs: input.nowMs
|
|
10548
|
-
};
|
|
10724
|
+
function ensureAllowedKeys2(value, allowedKeys, label) {
|
|
10725
|
+
const allowed = new Set(allowedKeys);
|
|
10726
|
+
for (const key of Object.keys(value)) {
|
|
10727
|
+
if (!allowed.has(key)) {
|
|
10728
|
+
throw new Error(`${label}.${key} is not supported.`);
|
|
10549
10729
|
}
|
|
10550
10730
|
}
|
|
10551
|
-
return {
|
|
10552
|
-
ok: true,
|
|
10553
|
-
incident: {
|
|
10554
|
-
...input.incident,
|
|
10555
|
-
startedAtMs: input.incident.startedAtMs ?? input.nowMs,
|
|
10556
|
-
correctiveActionsTaken: input.incident.correctiveActionsTaken + 1,
|
|
10557
|
-
units
|
|
10558
|
-
}
|
|
10559
|
-
};
|
|
10560
10731
|
}
|
|
10561
|
-
|
|
10562
|
-
|
|
10563
|
-
|
|
10564
|
-
|
|
10565
|
-
|
|
10566
|
-
|
|
10567
|
-
|
|
10732
|
+
function parseBoundedString(value, label, maxLength = MAX_STRING_LENGTH) {
|
|
10733
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
10734
|
+
throw new Error(`${label} must be a non-empty string.`);
|
|
10735
|
+
}
|
|
10736
|
+
if (value !== value.trim()) {
|
|
10737
|
+
throw new Error(`${label} must not have leading or trailing whitespace.`);
|
|
10738
|
+
}
|
|
10739
|
+
if (value.length > maxLength) {
|
|
10740
|
+
throw new Error(`${label} exceeds the maximum length of ${maxLength} characters.`);
|
|
10741
|
+
}
|
|
10742
|
+
if ([...value].some(
|
|
10743
|
+
(character) => character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127
|
|
10744
|
+
)) {
|
|
10745
|
+
throw new Error(`${label} must not contain control characters.`);
|
|
10746
|
+
}
|
|
10747
|
+
return value;
|
|
10568
10748
|
}
|
|
10569
|
-
function
|
|
10570
|
-
|
|
10749
|
+
function parseSafeInteger(value, label, minimum) {
|
|
10750
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || Object.is(value, -0)) {
|
|
10751
|
+
throw new Error(`${label} must be a safe integer.`);
|
|
10752
|
+
}
|
|
10753
|
+
if (value < minimum) {
|
|
10754
|
+
throw new Error(`${label} must be at least ${minimum}.`);
|
|
10755
|
+
}
|
|
10756
|
+
return value;
|
|
10571
10757
|
}
|
|
10572
|
-
function
|
|
10573
|
-
return
|
|
10758
|
+
function parsePositiveInteger(value, label) {
|
|
10759
|
+
return parseSafeInteger(value, label, 1);
|
|
10574
10760
|
}
|
|
10575
|
-
function
|
|
10576
|
-
|
|
10761
|
+
function parseCanonicalAbsolutePath(value, label) {
|
|
10762
|
+
const parsed = parseBoundedString(value, label, MAX_PATH_LENGTH);
|
|
10763
|
+
if (!import_node_path31.default.posix.isAbsolute(parsed)) {
|
|
10764
|
+
throw new Error(`${label} must be an absolute path.`);
|
|
10765
|
+
}
|
|
10766
|
+
if (import_node_path31.default.posix.resolve(parsed) !== parsed) {
|
|
10767
|
+
throw new Error(`${label} must be a canonical absolute path.`);
|
|
10768
|
+
}
|
|
10769
|
+
return parsed;
|
|
10577
10770
|
}
|
|
10578
|
-
function
|
|
10579
|
-
|
|
10771
|
+
function parseDomainId(value, label) {
|
|
10772
|
+
const id5 = parseBoundedString(value, label, 64);
|
|
10773
|
+
if (!DOMAIN_ID_RE.test(id5)) {
|
|
10774
|
+
throw new Error(`${label} must be a lowercase alphanumeric or hyphen domain ID.`);
|
|
10775
|
+
}
|
|
10776
|
+
return id5;
|
|
10580
10777
|
}
|
|
10581
|
-
function
|
|
10582
|
-
|
|
10778
|
+
function parseProfileName(value, label) {
|
|
10779
|
+
const combination = parseBoundedString(value, label, 64);
|
|
10780
|
+
const names = combination.split(",");
|
|
10781
|
+
const seen = /* @__PURE__ */ new Set();
|
|
10782
|
+
for (const [index, name] of names.entries()) {
|
|
10783
|
+
if (!PROFILE_NAME_RE2.test(name)) {
|
|
10784
|
+
throw new Error(
|
|
10785
|
+
`${label}[${index}] must be a lowercase alphanumeric or hyphen profile name.`
|
|
10786
|
+
);
|
|
10787
|
+
}
|
|
10788
|
+
if (seen.has(name)) {
|
|
10789
|
+
throw new Error(`${label} contains duplicate profile '${name}'.`);
|
|
10790
|
+
}
|
|
10791
|
+
seen.add(name);
|
|
10792
|
+
}
|
|
10793
|
+
return [...names].sort().join(",");
|
|
10583
10794
|
}
|
|
10584
|
-
function
|
|
10585
|
-
if (!
|
|
10586
|
-
throw new Error(
|
|
10795
|
+
function parseUniqueProfiles(value, label) {
|
|
10796
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
10797
|
+
throw new Error(`${label} must be a non-empty array of profile names.`);
|
|
10587
10798
|
}
|
|
10588
|
-
|
|
10589
|
-
|
|
10590
|
-
|
|
10591
|
-
|
|
10592
|
-
|
|
10593
|
-
|
|
10594
|
-
|
|
10595
|
-
|
|
10596
|
-
|
|
10597
|
-
|
|
10598
|
-
|
|
10599
|
-
|
|
10600
|
-
|
|
10601
|
-
|
|
10602
|
-
|
|
10603
|
-
|
|
10604
|
-
|
|
10605
|
-
|
|
10606
|
-
|
|
10607
|
-
|
|
10608
|
-
|
|
10609
|
-
|
|
10610
|
-
|
|
10611
|
-
|
|
10612
|
-
|
|
10799
|
+
if (value.length > MAX_PROFILES_PER_ENROLLMENT) {
|
|
10800
|
+
throw new Error(
|
|
10801
|
+
`${label} exceeds the maximum of ${MAX_PROFILES_PER_ENROLLMENT} profile names.`
|
|
10802
|
+
);
|
|
10803
|
+
}
|
|
10804
|
+
const profiles = [];
|
|
10805
|
+
const seen = /* @__PURE__ */ new Set();
|
|
10806
|
+
for (const [index, entry] of value.entries()) {
|
|
10807
|
+
const profile = parseProfileName(entry, `${label}[${index}]`);
|
|
10808
|
+
if (seen.has(profile)) {
|
|
10809
|
+
throw new Error(`${label} contains duplicate or aliased profile combination '${profile}'.`);
|
|
10810
|
+
}
|
|
10811
|
+
seen.add(profile);
|
|
10812
|
+
profiles.push(profile);
|
|
10813
|
+
}
|
|
10814
|
+
return profiles;
|
|
10815
|
+
}
|
|
10816
|
+
function parseDigest(value, label) {
|
|
10817
|
+
const digest = parseBoundedString(value, label, 64);
|
|
10818
|
+
if (!SHA256_RE.test(digest)) {
|
|
10819
|
+
throw new Error(`${label} must be a lowercase SHA-256 hex digest.`);
|
|
10820
|
+
}
|
|
10821
|
+
return digest;
|
|
10822
|
+
}
|
|
10823
|
+
function parseDomain(value, domainId) {
|
|
10824
|
+
const label = `domains.${domainId}`;
|
|
10825
|
+
const domain = ensureObject2(value, label);
|
|
10826
|
+
const kind = parseBoundedString(domain.kind, `${label}.kind`);
|
|
10827
|
+
if (kind === "host") {
|
|
10828
|
+
ensureAllowedKeys2(
|
|
10829
|
+
domain,
|
|
10830
|
+
[
|
|
10831
|
+
"kind",
|
|
10832
|
+
"adapter",
|
|
10833
|
+
"capacityBytes",
|
|
10834
|
+
"protectedHeadroomBytes",
|
|
10835
|
+
"unmanagedAllowanceBytes",
|
|
10836
|
+
"startupSlots",
|
|
10837
|
+
"heavySlots"
|
|
10838
|
+
],
|
|
10839
|
+
label
|
|
10840
|
+
);
|
|
10841
|
+
const adapter = parseBoundedString(domain.adapter, `${label}.adapter`);
|
|
10842
|
+
if (adapter !== "macos-host-v1" && adapter !== "macos-declared-v1") {
|
|
10843
|
+
throw new Error(`${label}.adapter must be 'macos-host-v1' or 'macos-declared-v1'.`);
|
|
10844
|
+
}
|
|
10845
|
+
const capacityBytes = parsePositiveInteger(domain.capacityBytes, `${label}.capacityBytes`);
|
|
10846
|
+
const protectedHeadroomBytes = parsePositiveInteger(
|
|
10847
|
+
domain.protectedHeadroomBytes,
|
|
10848
|
+
`${label}.protectedHeadroomBytes`
|
|
10849
|
+
);
|
|
10850
|
+
if (protectedHeadroomBytes >= capacityBytes) {
|
|
10851
|
+
throw new Error(`${label}.protectedHeadroomBytes must be less than capacityBytes.`);
|
|
10852
|
+
}
|
|
10853
|
+
const startupSlots = parsePositiveInteger(domain.startupSlots, `${label}.startupSlots`);
|
|
10854
|
+
const heavySlots = parsePositiveInteger(domain.heavySlots, `${label}.heavySlots`);
|
|
10855
|
+
if (adapter === "macos-host-v1") {
|
|
10856
|
+
if (Object.hasOwn(domain, "unmanagedAllowanceBytes")) {
|
|
10857
|
+
throw new Error(
|
|
10858
|
+
`${label}.unmanagedAllowanceBytes is only supported for adapter 'macos-declared-v1'.`
|
|
10859
|
+
);
|
|
10860
|
+
}
|
|
10861
|
+
return {
|
|
10862
|
+
kind: "host",
|
|
10863
|
+
adapter: "macos-host-v1",
|
|
10864
|
+
capacityBytes,
|
|
10865
|
+
protectedHeadroomBytes,
|
|
10866
|
+
startupSlots,
|
|
10867
|
+
heavySlots
|
|
10868
|
+
};
|
|
10869
|
+
}
|
|
10870
|
+
const unmanagedAllowanceBytes = parseSafeInteger(
|
|
10871
|
+
domain.unmanagedAllowanceBytes,
|
|
10872
|
+
`${label}.unmanagedAllowanceBytes`,
|
|
10873
|
+
0
|
|
10874
|
+
);
|
|
10875
|
+
if (unmanagedAllowanceBytes > capacityBytes - protectedHeadroomBytes) {
|
|
10876
|
+
throw new Error(
|
|
10877
|
+
`${label}.unmanagedAllowanceBytes must not exceed capacityBytes minus protectedHeadroomBytes.`
|
|
10878
|
+
);
|
|
10879
|
+
}
|
|
10880
|
+
return {
|
|
10881
|
+
kind: "host",
|
|
10882
|
+
adapter: "macos-declared-v1",
|
|
10883
|
+
capacityBytes,
|
|
10884
|
+
protectedHeadroomBytes,
|
|
10885
|
+
unmanagedAllowanceBytes,
|
|
10886
|
+
startupSlots,
|
|
10887
|
+
heavySlots
|
|
10888
|
+
};
|
|
10889
|
+
}
|
|
10890
|
+
if (kind === "runtime") {
|
|
10891
|
+
ensureAllowedKeys2(
|
|
10892
|
+
domain,
|
|
10893
|
+
[
|
|
10894
|
+
"kind",
|
|
10895
|
+
"adapter",
|
|
10896
|
+
"endpoint",
|
|
10897
|
+
"daemonId",
|
|
10898
|
+
"hostDomain",
|
|
10899
|
+
"hostChargeCeilingBytes",
|
|
10900
|
+
"capacityBytes",
|
|
10901
|
+
"protectedHeadroomBytes",
|
|
10902
|
+
"guestUnmanagedAllowanceBytes",
|
|
10903
|
+
"startupSlots",
|
|
10904
|
+
"heavySlots"
|
|
10905
|
+
],
|
|
10906
|
+
label
|
|
10907
|
+
);
|
|
10908
|
+
const adapter = parseBoundedString(domain.adapter, `${label}.adapter`);
|
|
10909
|
+
if (adapter !== "orbstack-local-v1" && adapter !== "orbstack-declared-v1") {
|
|
10910
|
+
throw new Error(`${label}.adapter must be 'orbstack-local-v1' or 'orbstack-declared-v1'.`);
|
|
10911
|
+
}
|
|
10912
|
+
const endpoint = parseCanonicalAbsolutePath(domain.endpoint, `${label}.endpoint`);
|
|
10913
|
+
const daemonId = parseBoundedString(domain.daemonId, `${label}.daemonId`);
|
|
10914
|
+
const hostDomain = parseDomainId(domain.hostDomain, `${label}.hostDomain`);
|
|
10915
|
+
const hostChargeCeilingBytes = parsePositiveInteger(
|
|
10916
|
+
domain.hostChargeCeilingBytes,
|
|
10917
|
+
`${label}.hostChargeCeilingBytes`
|
|
10918
|
+
);
|
|
10919
|
+
const capacityBytes = parsePositiveInteger(domain.capacityBytes, `${label}.capacityBytes`);
|
|
10920
|
+
const protectedHeadroomBytes = parsePositiveInteger(
|
|
10921
|
+
domain.protectedHeadroomBytes,
|
|
10922
|
+
`${label}.protectedHeadroomBytes`
|
|
10923
|
+
);
|
|
10924
|
+
if (protectedHeadroomBytes >= capacityBytes) {
|
|
10925
|
+
throw new Error(`${label}.protectedHeadroomBytes must be less than capacityBytes.`);
|
|
10926
|
+
}
|
|
10927
|
+
const startupSlots = parsePositiveInteger(domain.startupSlots, `${label}.startupSlots`);
|
|
10928
|
+
const heavySlots = parsePositiveInteger(domain.heavySlots, `${label}.heavySlots`);
|
|
10929
|
+
if (adapter === "orbstack-local-v1") {
|
|
10930
|
+
if (Object.hasOwn(domain, "guestUnmanagedAllowanceBytes")) {
|
|
10931
|
+
throw new Error(
|
|
10932
|
+
`${label}.guestUnmanagedAllowanceBytes is only supported for adapter 'orbstack-declared-v1'.`
|
|
10933
|
+
);
|
|
10934
|
+
}
|
|
10935
|
+
return {
|
|
10936
|
+
kind: "runtime",
|
|
10937
|
+
adapter: "orbstack-local-v1",
|
|
10938
|
+
endpoint,
|
|
10939
|
+
daemonId,
|
|
10940
|
+
hostDomain,
|
|
10941
|
+
hostChargeCeilingBytes,
|
|
10942
|
+
capacityBytes,
|
|
10943
|
+
protectedHeadroomBytes,
|
|
10944
|
+
startupSlots,
|
|
10945
|
+
heavySlots
|
|
10946
|
+
};
|
|
10947
|
+
}
|
|
10948
|
+
return {
|
|
10949
|
+
kind: "runtime",
|
|
10950
|
+
adapter: "orbstack-declared-v1",
|
|
10951
|
+
endpoint,
|
|
10952
|
+
daemonId,
|
|
10953
|
+
hostDomain,
|
|
10954
|
+
hostChargeCeilingBytes,
|
|
10955
|
+
capacityBytes,
|
|
10956
|
+
protectedHeadroomBytes,
|
|
10957
|
+
guestUnmanagedAllowanceBytes: parseSafeInteger(
|
|
10958
|
+
domain.guestUnmanagedAllowanceBytes,
|
|
10959
|
+
`${label}.guestUnmanagedAllowanceBytes`,
|
|
10960
|
+
0
|
|
10961
|
+
),
|
|
10962
|
+
startupSlots,
|
|
10963
|
+
heavySlots
|
|
10964
|
+
};
|
|
10965
|
+
}
|
|
10966
|
+
throw new Error(`${label}.kind must be 'host' or 'runtime'.`);
|
|
10967
|
+
}
|
|
10968
|
+
function parseScheduling(value) {
|
|
10969
|
+
const label = "scheduling";
|
|
10970
|
+
const scheduling = ensureObject2(value, label);
|
|
10971
|
+
ensureAllowedKeys2(
|
|
10972
|
+
scheduling,
|
|
10973
|
+
[
|
|
10974
|
+
"maxQueuedPerDomain",
|
|
10975
|
+
"maxQueuedTotal",
|
|
10976
|
+
"queueLifetimeSeconds",
|
|
10977
|
+
"clientWaitSeconds",
|
|
10978
|
+
"maxClientWaitSeconds",
|
|
10979
|
+
"watchSeconds",
|
|
10980
|
+
"sampleIntervalSeconds",
|
|
10981
|
+
"maxSampleAgeSeconds"
|
|
10982
|
+
],
|
|
10983
|
+
label
|
|
10984
|
+
);
|
|
10985
|
+
const result = {
|
|
10986
|
+
maxQueuedPerDomain: parsePositiveInteger(
|
|
10987
|
+
scheduling.maxQueuedPerDomain,
|
|
10988
|
+
`${label}.maxQueuedPerDomain`
|
|
10989
|
+
),
|
|
10990
|
+
maxQueuedTotal: parsePositiveInteger(scheduling.maxQueuedTotal, `${label}.maxQueuedTotal`),
|
|
10991
|
+
queueLifetimeSeconds: parsePositiveInteger(
|
|
10992
|
+
scheduling.queueLifetimeSeconds,
|
|
10993
|
+
`${label}.queueLifetimeSeconds`
|
|
10994
|
+
),
|
|
10995
|
+
// Reserved for future runtime binding: validated and bounded today, but no
|
|
10996
|
+
// consumer reads these scheduling knobs yet. Runtime constants remain
|
|
10997
|
+
// authoritative (controller watch window, CLI admission wait, tick cadence).
|
|
10998
|
+
clientWaitSeconds: parseSafeInteger(
|
|
10999
|
+
scheduling.clientWaitSeconds,
|
|
11000
|
+
`${label}.clientWaitSeconds`,
|
|
11001
|
+
0
|
|
11002
|
+
),
|
|
11003
|
+
maxClientWaitSeconds: parsePositiveInteger(
|
|
11004
|
+
scheduling.maxClientWaitSeconds,
|
|
11005
|
+
`${label}.maxClientWaitSeconds`
|
|
11006
|
+
),
|
|
11007
|
+
watchSeconds: parsePositiveInteger(scheduling.watchSeconds, `${label}.watchSeconds`),
|
|
11008
|
+
sampleIntervalSeconds: parsePositiveInteger(
|
|
11009
|
+
scheduling.sampleIntervalSeconds,
|
|
11010
|
+
`${label}.sampleIntervalSeconds`
|
|
11011
|
+
),
|
|
11012
|
+
maxSampleAgeSeconds: parsePositiveInteger(
|
|
11013
|
+
scheduling.maxSampleAgeSeconds,
|
|
11014
|
+
`${label}.maxSampleAgeSeconds`
|
|
11015
|
+
)
|
|
11016
|
+
};
|
|
11017
|
+
if (result.clientWaitSeconds > result.maxClientWaitSeconds) {
|
|
11018
|
+
throw new Error(`${label}.clientWaitSeconds must not exceed maxClientWaitSeconds.`);
|
|
11019
|
+
}
|
|
11020
|
+
if (result.maxQueuedTotal > 64 || result.maxQueuedPerDomain > 32 || result.queueLifetimeSeconds > 900 || result.maxClientWaitSeconds > 900 || result.watchSeconds > 30 || result.maxSampleAgeSeconds > 15) {
|
|
11021
|
+
throw new Error(`${label} exceeds supported controller bounds.`);
|
|
11022
|
+
}
|
|
11023
|
+
if (result.queueLifetimeSeconds < result.maxClientWaitSeconds) {
|
|
11024
|
+
throw new Error(`${label}.queueLifetimeSeconds must cover maxClientWaitSeconds.`);
|
|
11025
|
+
}
|
|
11026
|
+
if (result.maxSampleAgeSeconds < result.sampleIntervalSeconds) {
|
|
11027
|
+
throw new Error(`${label}.maxSampleAgeSeconds must be at least sampleIntervalSeconds.`);
|
|
11028
|
+
}
|
|
11029
|
+
return result;
|
|
11030
|
+
}
|
|
11031
|
+
function parseRecovery(value) {
|
|
11032
|
+
if (value === void 0) return { ...DEFAULT_CAPACITY_RECOVERY };
|
|
11033
|
+
const label = "recovery";
|
|
11034
|
+
const recovery = ensureObject2(value, label);
|
|
11035
|
+
ensureAllowedKeys2(
|
|
11036
|
+
recovery,
|
|
11037
|
+
[
|
|
11038
|
+
"enabled",
|
|
11039
|
+
"maxProcessRestarts",
|
|
11040
|
+
"maxServiceRestarts",
|
|
11041
|
+
"maxCorrectiveActions",
|
|
11042
|
+
"windowSeconds",
|
|
11043
|
+
"observationSeconds",
|
|
11044
|
+
"resumeDwellSeconds"
|
|
11045
|
+
],
|
|
11046
|
+
label
|
|
11047
|
+
);
|
|
11048
|
+
if (typeof recovery.enabled !== "boolean") {
|
|
11049
|
+
throw new Error(`${label}.enabled must be a boolean.`);
|
|
11050
|
+
}
|
|
11051
|
+
const result = {
|
|
11052
|
+
enabled: recovery.enabled,
|
|
11053
|
+
maxProcessRestarts: parseSafeInteger(
|
|
11054
|
+
recovery.maxProcessRestarts,
|
|
11055
|
+
`${label}.maxProcessRestarts`,
|
|
11056
|
+
0
|
|
11057
|
+
),
|
|
11058
|
+
maxServiceRestarts: parseSafeInteger(
|
|
11059
|
+
recovery.maxServiceRestarts,
|
|
11060
|
+
`${label}.maxServiceRestarts`,
|
|
11061
|
+
0
|
|
11062
|
+
),
|
|
11063
|
+
maxCorrectiveActions: parsePositiveInteger(
|
|
11064
|
+
recovery.maxCorrectiveActions,
|
|
11065
|
+
`${label}.maxCorrectiveActions`
|
|
11066
|
+
),
|
|
11067
|
+
windowSeconds: parsePositiveInteger(recovery.windowSeconds, `${label}.windowSeconds`),
|
|
11068
|
+
observationSeconds: parsePositiveInteger(
|
|
11069
|
+
recovery.observationSeconds,
|
|
11070
|
+
`${label}.observationSeconds`
|
|
11071
|
+
),
|
|
11072
|
+
resumeDwellSeconds: parsePositiveInteger(
|
|
11073
|
+
recovery.resumeDwellSeconds,
|
|
11074
|
+
`${label}.resumeDwellSeconds`
|
|
11075
|
+
)
|
|
11076
|
+
};
|
|
11077
|
+
if (result.maxCorrectiveActions < result.maxProcessRestarts + result.maxServiceRestarts) {
|
|
11078
|
+
throw new Error(
|
|
11079
|
+
`${label}.maxCorrectiveActions must cover maxProcessRestarts and maxServiceRestarts.`
|
|
11080
|
+
);
|
|
11081
|
+
}
|
|
11082
|
+
if (result.maxProcessRestarts > 4 || result.maxServiceRestarts > 2 || result.maxCorrectiveActions > 8 || result.windowSeconds > 3600 || result.observationSeconds > 300 || result.resumeDwellSeconds > 3600) {
|
|
11083
|
+
throw new Error(`${label} exceeds supported controller bounds.`);
|
|
11084
|
+
}
|
|
11085
|
+
return result;
|
|
11086
|
+
}
|
|
11087
|
+
function validateDomainReferences(domains) {
|
|
11088
|
+
const daemonIds = /* @__PURE__ */ new Map();
|
|
11089
|
+
const endpoints = /* @__PURE__ */ new Map();
|
|
11090
|
+
for (const [domainId, domain] of Object.entries(domains)) {
|
|
11091
|
+
if (domain.kind !== "runtime") continue;
|
|
11092
|
+
const previousDaemon = daemonIds.get(domain.daemonId);
|
|
11093
|
+
if (previousDaemon) {
|
|
11094
|
+
throw new Error(
|
|
11095
|
+
`domains.${domainId}.daemonId duplicates runtime domain '${previousDaemon}'.`
|
|
11096
|
+
);
|
|
11097
|
+
}
|
|
11098
|
+
daemonIds.set(domain.daemonId, domainId);
|
|
11099
|
+
const previousEndpoint = endpoints.get(domain.endpoint);
|
|
11100
|
+
if (previousEndpoint) {
|
|
11101
|
+
throw new Error(`domains.${domainId}.endpoint aliases runtime domain '${previousEndpoint}'.`);
|
|
11102
|
+
}
|
|
11103
|
+
endpoints.set(domain.endpoint, domainId);
|
|
11104
|
+
if (!Object.hasOwn(domains, domain.hostDomain)) {
|
|
11105
|
+
throw new Error(
|
|
11106
|
+
`domains.${domainId}.hostDomain references missing domain '${domain.hostDomain}'.`
|
|
11107
|
+
);
|
|
11108
|
+
}
|
|
11109
|
+
const hostDomain = domains[domain.hostDomain];
|
|
11110
|
+
if (hostDomain.kind !== "host") {
|
|
11111
|
+
throw new Error(
|
|
11112
|
+
`domains.${domainId}.hostDomain must reference a host domain, not '${domain.hostDomain}'.`
|
|
11113
|
+
);
|
|
11114
|
+
}
|
|
11115
|
+
const unmanagedAllowanceBytes = hostDomain.adapter === "macos-declared-v1" ? hostDomain.unmanagedAllowanceBytes : 0;
|
|
11116
|
+
const admissibleHostBytes = hostDomain.capacityBytes - hostDomain.protectedHeadroomBytes - unmanagedAllowanceBytes;
|
|
11117
|
+
if (domain.hostChargeCeilingBytes > admissibleHostBytes) {
|
|
11118
|
+
throw new Error(
|
|
11119
|
+
`domains.${domainId}.hostChargeCeilingBytes exceeds admissible capacity of host domain '${domain.hostDomain}'.`
|
|
11120
|
+
);
|
|
11121
|
+
}
|
|
11122
|
+
}
|
|
11123
|
+
}
|
|
11124
|
+
function parseEnrollment(value, index, domains) {
|
|
11125
|
+
const label = `enrollments[${index}]`;
|
|
11126
|
+
const enrollment = ensureObject2(value, label);
|
|
11127
|
+
ensureAllowedKeys2(
|
|
11128
|
+
enrollment,
|
|
11129
|
+
[
|
|
11130
|
+
"repoPath",
|
|
11131
|
+
"gitCommonDir",
|
|
11132
|
+
"workspace",
|
|
11133
|
+
"provider",
|
|
11134
|
+
"providerId",
|
|
11135
|
+
"hostDomain",
|
|
11136
|
+
"runtimeDomain",
|
|
11137
|
+
"profiles",
|
|
11138
|
+
"estimatesDigest",
|
|
11139
|
+
"defaultOperation"
|
|
11140
|
+
],
|
|
11141
|
+
label
|
|
11142
|
+
);
|
|
11143
|
+
const repoPath = parseCanonicalAbsolutePath(enrollment.repoPath, `${label}.repoPath`);
|
|
11144
|
+
const gitCommonDir = parseCanonicalAbsolutePath(enrollment.gitCommonDir, `${label}.gitCommonDir`);
|
|
11145
|
+
const workspace = parseBoundedString(enrollment.workspace, `${label}.workspace`, 32);
|
|
11146
|
+
if (!WORKSPACE_TOKEN_RE.test(workspace)) {
|
|
11147
|
+
throw new Error(`${label}.workspace must be an existing workspace token.`);
|
|
11148
|
+
}
|
|
11149
|
+
const provider = parseBoundedString(enrollment.provider, `${label}.provider`);
|
|
11150
|
+
if (provider !== "devsy" && provider !== "devpod") {
|
|
11151
|
+
throw new Error(`${label}.provider must be 'devsy' or 'devpod'.`);
|
|
11152
|
+
}
|
|
11153
|
+
const providerId = parseBoundedString(enrollment.providerId, `${label}.providerId`);
|
|
11154
|
+
const hostDomain = parseDomainId(enrollment.hostDomain, `${label}.hostDomain`);
|
|
11155
|
+
const runtimeDomain = parseDomainId(enrollment.runtimeDomain, `${label}.runtimeDomain`);
|
|
11156
|
+
if (!Object.hasOwn(domains, hostDomain)) {
|
|
11157
|
+
throw new Error(`${label}.hostDomain references missing domain '${hostDomain}'.`);
|
|
11158
|
+
}
|
|
11159
|
+
const host = domains[hostDomain];
|
|
11160
|
+
if (host.kind !== "host") {
|
|
11161
|
+
throw new Error(`${label}.hostDomain must reference a host domain.`);
|
|
11162
|
+
}
|
|
11163
|
+
if (!Object.hasOwn(domains, runtimeDomain)) {
|
|
11164
|
+
throw new Error(`${label}.runtimeDomain references missing domain '${runtimeDomain}'.`);
|
|
11165
|
+
}
|
|
11166
|
+
const runtime = domains[runtimeDomain];
|
|
11167
|
+
if (runtime.kind !== "runtime") {
|
|
11168
|
+
throw new Error(`${label}.runtimeDomain must reference a runtime domain.`);
|
|
11169
|
+
}
|
|
11170
|
+
if (runtime.hostDomain !== hostDomain) {
|
|
11171
|
+
throw new Error(
|
|
11172
|
+
`${label}.runtimeDomain '${runtimeDomain}' is bound to host domain '${runtime.hostDomain}', not '${hostDomain}'.`
|
|
11173
|
+
);
|
|
11174
|
+
}
|
|
11175
|
+
const profiles = parseUniqueProfiles(enrollment.profiles, `${label}.profiles`);
|
|
11176
|
+
const estimatesDigest = parseDigest(enrollment.estimatesDigest, `${label}.estimatesDigest`);
|
|
11177
|
+
const defaultOperation = ensureObject2(enrollment.defaultOperation, `${label}.defaultOperation`);
|
|
11178
|
+
ensureAllowedKeys2(
|
|
11179
|
+
defaultOperation,
|
|
11180
|
+
["hostIncrementBytes", "runtimeIncrementBytes"],
|
|
11181
|
+
`${label}.defaultOperation`
|
|
11182
|
+
);
|
|
11183
|
+
return {
|
|
11184
|
+
repoPath,
|
|
11185
|
+
gitCommonDir,
|
|
11186
|
+
workspace,
|
|
11187
|
+
provider,
|
|
11188
|
+
providerId,
|
|
11189
|
+
hostDomain,
|
|
11190
|
+
runtimeDomain,
|
|
11191
|
+
profiles,
|
|
11192
|
+
estimatesDigest,
|
|
11193
|
+
defaultOperation: {
|
|
11194
|
+
hostIncrementBytes: parseSafeInteger(
|
|
11195
|
+
defaultOperation.hostIncrementBytes,
|
|
11196
|
+
`${label}.defaultOperation.hostIncrementBytes`,
|
|
11197
|
+
0
|
|
11198
|
+
),
|
|
11199
|
+
runtimeIncrementBytes: parsePositiveInteger(
|
|
11200
|
+
defaultOperation.runtimeIncrementBytes,
|
|
11201
|
+
`${label}.defaultOperation.runtimeIncrementBytes`
|
|
11202
|
+
)
|
|
11203
|
+
}
|
|
11204
|
+
};
|
|
11205
|
+
}
|
|
11206
|
+
function parseCapacityPolicy(value) {
|
|
11207
|
+
const root = ensureObject2(value, "capacity policy");
|
|
11208
|
+
ensureAllowedKeys2(
|
|
11209
|
+
root,
|
|
11210
|
+
["version", "revision", "admissions", "scheduling", "recovery", "domains", "enrollments"],
|
|
11211
|
+
"capacity policy"
|
|
11212
|
+
);
|
|
11213
|
+
const version = parsePositiveInteger(root.version, "capacity policy.version");
|
|
11214
|
+
if (version !== 1) {
|
|
11215
|
+
throw new Error("capacity policy.version must be 1.");
|
|
11216
|
+
}
|
|
11217
|
+
const revision = parsePositiveInteger(root.revision, "capacity policy.revision");
|
|
11218
|
+
const admissions = parseBoundedString(root.admissions, "capacity policy.admissions");
|
|
11219
|
+
if (admissions !== "enabled" && admissions !== "paused") {
|
|
11220
|
+
throw new Error("capacity policy.admissions must be 'enabled' or 'paused'.");
|
|
11221
|
+
}
|
|
11222
|
+
const scheduling = parseScheduling(root.scheduling);
|
|
11223
|
+
const recovery = parseRecovery(root.recovery);
|
|
11224
|
+
const domainsValue = ensureObject2(root.domains, "capacity policy.domains");
|
|
11225
|
+
const domainEntries = Object.entries(domainsValue);
|
|
11226
|
+
if (domainEntries.length > MAX_DOMAINS) {
|
|
11227
|
+
throw new Error(`capacity policy.domains exceeds the maximum of ${MAX_DOMAINS} domains.`);
|
|
11228
|
+
}
|
|
11229
|
+
const domains = {};
|
|
11230
|
+
for (const [domainId, domainValue] of domainEntries) {
|
|
11231
|
+
const parsedId = parseDomainId(domainId, "capacity policy.domains key");
|
|
11232
|
+
domains[parsedId] = parseDomain(domainValue, parsedId);
|
|
11233
|
+
}
|
|
11234
|
+
validateDomainReferences(domains);
|
|
11235
|
+
if (!Array.isArray(root.enrollments)) {
|
|
11236
|
+
throw new Error("capacity policy.enrollments must be an array.");
|
|
11237
|
+
}
|
|
11238
|
+
if (root.enrollments.length > MAX_ENROLLMENTS) {
|
|
11239
|
+
throw new Error(
|
|
11240
|
+
`capacity policy.enrollments exceeds the maximum of ${MAX_ENROLLMENTS} enrollments.`
|
|
11241
|
+
);
|
|
11242
|
+
}
|
|
11243
|
+
const enrollments = [];
|
|
11244
|
+
const enrollmentKeys = /* @__PURE__ */ new Set();
|
|
11245
|
+
const providerIds = /* @__PURE__ */ new Map();
|
|
11246
|
+
for (const [index, enrollmentValue] of root.enrollments.entries()) {
|
|
11247
|
+
const enrollment = parseEnrollment(enrollmentValue, index, domains);
|
|
11248
|
+
const enrollmentKey = [enrollment.repoPath, enrollment.gitCommonDir, enrollment.workspace].join(
|
|
11249
|
+
"\0"
|
|
11250
|
+
);
|
|
11251
|
+
if (enrollmentKeys.has(enrollmentKey)) {
|
|
11252
|
+
throw new Error(`enrollments[${index}] duplicates an existing enrollment.`);
|
|
11253
|
+
}
|
|
11254
|
+
enrollmentKeys.add(enrollmentKey);
|
|
11255
|
+
const providerKey = `${enrollment.provider}\0${enrollment.providerId}`;
|
|
11256
|
+
const previousProviderIndex = providerIds.get(providerKey);
|
|
11257
|
+
if (previousProviderIndex !== void 0) {
|
|
11258
|
+
throw new Error(
|
|
11259
|
+
`enrollments[${index}].providerId duplicates enrollments[${previousProviderIndex}].`
|
|
11260
|
+
);
|
|
11261
|
+
}
|
|
11262
|
+
providerIds.set(providerKey, index);
|
|
11263
|
+
enrollments.push(enrollment);
|
|
11264
|
+
}
|
|
11265
|
+
return {
|
|
11266
|
+
version: 1,
|
|
11267
|
+
revision,
|
|
11268
|
+
admissions,
|
|
11269
|
+
scheduling,
|
|
11270
|
+
recovery,
|
|
11271
|
+
domains,
|
|
11272
|
+
enrollments
|
|
11273
|
+
};
|
|
11274
|
+
}
|
|
11275
|
+
|
|
11276
|
+
// src/core/controller-client.ts
|
|
11277
|
+
var import_node_crypto15 = require("crypto");
|
|
11278
|
+
var import_node_fs28 = __toESM(require("fs"));
|
|
11279
|
+
var import_node_net2 = __toESM(require("net"));
|
|
11280
|
+
var import_node_path33 = __toESM(require("path"));
|
|
11281
|
+
|
|
11282
|
+
// src/core/controller-protocol.ts
|
|
11283
|
+
var import_node_path32 = __toESM(require("path"));
|
|
11284
|
+
var CONTROLLER_FRAME_BYTES = 64 * 1024;
|
|
11285
|
+
var CONTROLLER_MAX_OPERATION_REQUEST_BYTES = 32 * 1024;
|
|
11286
|
+
var CONTROLLER_PROFILE_TOKEN_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
11287
|
+
var CONTROLLER_PROFILE_RE = new RegExp(
|
|
11288
|
+
`^(?:${CONTROLLER_PROFILE_TOKEN_RE.source.slice(1, -1)})(?:,(?:${CONTROLLER_PROFILE_TOKEN_RE.source.slice(1, -1)}))*$`
|
|
11289
|
+
);
|
|
11290
|
+
|
|
11291
|
+
// src/core/controller-monitor.ts
|
|
11292
|
+
var import_node_crypto16 = require("crypto");
|
|
11293
|
+
var import_node_util6 = require("util");
|
|
11294
|
+
|
|
11295
|
+
// src/core/reliability-output.ts
|
|
11296
|
+
function reliabilityAttention(state) {
|
|
11297
|
+
assertReliabilityState(state);
|
|
11298
|
+
const completeStop = state.stopProof.workloadsStopped && state.stopProof.routesRemoved;
|
|
11299
|
+
if (state.desired === "stopped-by-user") return completeStop ? void 0 : "stop-incomplete";
|
|
11300
|
+
if (state.operation?.status === "COMPLETION_UNKNOWN" || state.operation?.status === "INTERRUPTED")
|
|
11301
|
+
return "operation-unknown";
|
|
11302
|
+
if (state.desired === "parked-for-capacity")
|
|
11303
|
+
return completeStop ? "capacity-parked" : "stop-incomplete";
|
|
11304
|
+
if (state.admission === "denied-unadmittable") return "unadmittable";
|
|
11305
|
+
if (state.admission === "waiting") return "capacity-waiting";
|
|
11306
|
+
if (state.phase === "recovering") return "recovering";
|
|
11307
|
+
if (state.phase === "queued" || state.phase === "starting" || state.phase === "verifying")
|
|
11308
|
+
return "starting";
|
|
11309
|
+
return void 0;
|
|
11310
|
+
}
|
|
11311
|
+
|
|
11312
|
+
// src/core/managed-post-start.ts
|
|
11313
|
+
var import_node_child_process20 = require("child_process");
|
|
11314
|
+
var import_node_crypto18 = require("crypto");
|
|
11315
|
+
var import_node_fs29 = __toESM(require("fs"));
|
|
11316
|
+
var import_node_path34 = __toESM(require("path"));
|
|
11317
|
+
|
|
11318
|
+
// src/core/managed-host-ports.ts
|
|
11319
|
+
var import_node_child_process19 = require("child_process");
|
|
11320
|
+
var import_node_crypto17 = require("crypto");
|
|
11321
|
+
var PORT_LOOKUP_TIMEOUT_MS = 1e4;
|
|
11322
|
+
var PORT_LOOKUP_MAX_BUFFER = 64 * 1024;
|
|
11323
|
+
var LOOPBACK_BINDING = /^127\.0\.0\.1:(\d+)$/;
|
|
11324
|
+
function unavailableEntry(declaration, reason2) {
|
|
11325
|
+
return { ...declaration, status: "unavailable", reason: reason2 };
|
|
11326
|
+
}
|
|
11327
|
+
function livePublishedBindings(containerId, target) {
|
|
11328
|
+
const result = (0, import_node_child_process19.spawnSync)("docker", ["port", containerId, `${target}/tcp`], {
|
|
11329
|
+
encoding: "utf-8",
|
|
11330
|
+
timeout: PORT_LOOKUP_TIMEOUT_MS,
|
|
11331
|
+
maxBuffer: PORT_LOOKUP_MAX_BUFFER,
|
|
11332
|
+
...networkDockerOptions()
|
|
11333
|
+
});
|
|
11334
|
+
if (result.error || result.status === null) return void 0;
|
|
11335
|
+
if (result.status !== 0) return "";
|
|
11336
|
+
return typeof result.stdout === "string" ? result.stdout : void 0;
|
|
11337
|
+
}
|
|
11338
|
+
function parseLoopbackBinding(output) {
|
|
11339
|
+
const lines = output.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
11340
|
+
if (lines.length === 0) return { reason: "not-published" };
|
|
11341
|
+
if (lines.length > 1) return { reason: "ambiguous" };
|
|
11342
|
+
const match = LOOPBACK_BINDING.exec(lines[0]);
|
|
11343
|
+
if (!match) return { reason: "not-published" };
|
|
11344
|
+
const port = Number(match[1]);
|
|
11345
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) return { reason: "not-published" };
|
|
11346
|
+
return { endpoint: { host: "127.0.0.1", port } };
|
|
11347
|
+
}
|
|
11348
|
+
function revalidateLiveOwner(containerId, input, composeProject, composeFiles, service) {
|
|
11349
|
+
try {
|
|
11350
|
+
const current = inspectWorkspaceContainers({ ids: [containerId] });
|
|
11351
|
+
if (current.length !== 1 || current[0].id !== containerId) return "unavailable";
|
|
11352
|
+
if (!hasExactComposeIdentity(current[0], {
|
|
11353
|
+
repoPath: input.repoPath,
|
|
11354
|
+
service,
|
|
11355
|
+
composeProject,
|
|
11356
|
+
composeFiles
|
|
11357
|
+
})) {
|
|
11358
|
+
return "unavailable";
|
|
11359
|
+
}
|
|
11360
|
+
return current[0].state.Running ? "available" : "not-running";
|
|
11361
|
+
} catch {
|
|
11362
|
+
return "unavailable";
|
|
11363
|
+
}
|
|
11364
|
+
}
|
|
11365
|
+
function resolveManagedHostPorts(input) {
|
|
11366
|
+
const report = {};
|
|
11367
|
+
const selectedServices = new Set(input.selectedServices);
|
|
11368
|
+
const selectedKeys = [];
|
|
11369
|
+
for (const [key, declaration] of Object.entries(input.declarations)) {
|
|
11370
|
+
if (!selectedServices.has(declaration.service)) {
|
|
11371
|
+
report[key] = { ...declaration, status: "inactive" };
|
|
11372
|
+
continue;
|
|
11373
|
+
}
|
|
11374
|
+
selectedKeys.push(key);
|
|
11375
|
+
report[key] = { ...declaration, status: "unavailable", reason: "unavailable" };
|
|
11376
|
+
}
|
|
11377
|
+
if (selectedKeys.length === 0) return report;
|
|
11378
|
+
const composeProject = input.composeProject;
|
|
11379
|
+
const composeFiles = input.composeFiles;
|
|
11380
|
+
if (typeof composeProject !== "string" || composeProject.length === 0 || composeFiles === void 0 || composeFiles.length === 0 || composeFiles.some((file) => file.length === 0)) {
|
|
11381
|
+
return report;
|
|
11382
|
+
}
|
|
11383
|
+
if (selectedKeys.some((key) => {
|
|
11384
|
+
const target = input.declarations[key].target;
|
|
11385
|
+
return !Number.isInteger(target) || target < 1 || target > 65535;
|
|
11386
|
+
})) {
|
|
11387
|
+
return report;
|
|
11388
|
+
}
|
|
11389
|
+
const liveContainers = input.containers === void 0;
|
|
11390
|
+
let containers = input.containers;
|
|
11391
|
+
if (containers === void 0) {
|
|
11392
|
+
try {
|
|
11393
|
+
containers = inspectWorkspaceContainers({ composeProject });
|
|
11394
|
+
} catch {
|
|
11395
|
+
return report;
|
|
11396
|
+
}
|
|
11397
|
+
}
|
|
11398
|
+
const publishedBindings = input.publishedBindings ?? livePublishedBindings;
|
|
11399
|
+
for (const key of selectedKeys) {
|
|
11400
|
+
const declaration = input.declarations[key];
|
|
11401
|
+
let owners;
|
|
11402
|
+
try {
|
|
11403
|
+
owners = containers.filter(
|
|
11404
|
+
(container) => hasExactComposeIdentity(container, {
|
|
11405
|
+
repoPath: input.repoPath,
|
|
11406
|
+
service: declaration.service,
|
|
11407
|
+
composeProject,
|
|
11408
|
+
composeFiles
|
|
11409
|
+
})
|
|
11410
|
+
);
|
|
11411
|
+
} catch {
|
|
11412
|
+
report[key] = unavailableEntry(declaration, "unavailable");
|
|
11413
|
+
continue;
|
|
11414
|
+
}
|
|
11415
|
+
if (owners.length === 0) {
|
|
11416
|
+
report[key] = unavailableEntry(declaration, "not-found");
|
|
11417
|
+
continue;
|
|
11418
|
+
}
|
|
11419
|
+
if (owners.length > 1) {
|
|
11420
|
+
report[key] = unavailableEntry(declaration, "ambiguous");
|
|
11421
|
+
continue;
|
|
11422
|
+
}
|
|
11423
|
+
const owner = owners[0];
|
|
11424
|
+
if (!owner.state.Running) {
|
|
11425
|
+
report[key] = unavailableEntry(declaration, "not-running");
|
|
11426
|
+
continue;
|
|
11427
|
+
}
|
|
11428
|
+
let output;
|
|
11429
|
+
try {
|
|
11430
|
+
output = publishedBindings(owner.id, declaration.target);
|
|
11431
|
+
} catch {
|
|
11432
|
+
output = void 0;
|
|
11433
|
+
}
|
|
11434
|
+
if (typeof output !== "string") {
|
|
11435
|
+
report[key] = unavailableEntry(declaration, "unavailable");
|
|
11436
|
+
continue;
|
|
11437
|
+
}
|
|
11438
|
+
const binding = parseLoopbackBinding(output);
|
|
11439
|
+
if (!binding.endpoint) {
|
|
11440
|
+
report[key] = unavailableEntry(declaration, binding.reason ?? "unavailable");
|
|
11441
|
+
continue;
|
|
11442
|
+
}
|
|
11443
|
+
if (liveContainers) {
|
|
11444
|
+
const proof = revalidateLiveOwner(
|
|
11445
|
+
owner.id,
|
|
11446
|
+
input,
|
|
11447
|
+
composeProject,
|
|
11448
|
+
composeFiles,
|
|
11449
|
+
declaration.service
|
|
11450
|
+
);
|
|
11451
|
+
if (proof !== "available") {
|
|
11452
|
+
report[key] = unavailableEntry(declaration, proof);
|
|
11453
|
+
continue;
|
|
11454
|
+
}
|
|
11455
|
+
}
|
|
11456
|
+
report[key] = {
|
|
11457
|
+
...declaration,
|
|
11458
|
+
status: "available",
|
|
11459
|
+
endpoint: binding.endpoint
|
|
11460
|
+
};
|
|
11461
|
+
}
|
|
11462
|
+
return report;
|
|
11463
|
+
}
|
|
11464
|
+
function managedHostPortDeclarationsIdentity(declarations) {
|
|
11465
|
+
const keys2 = Object.keys(declarations).sort();
|
|
11466
|
+
if (keys2.length === 0) return void 0;
|
|
11467
|
+
const canonical = keys2.map((key) => [key, declarations[key].service, declarations[key].target]);
|
|
11468
|
+
return (0, import_node_crypto17.createHash)("sha256").update(JSON.stringify(canonical)).digest("hex");
|
|
11469
|
+
}
|
|
11470
|
+
function managedHostPortVariables(report) {
|
|
11471
|
+
return Object.fromEntries(
|
|
11472
|
+
Object.entries(report).map(([key, entry]) => [
|
|
11473
|
+
`DEVROUTER_HOST_PORT_${key.toUpperCase().replaceAll("-", "_")}`,
|
|
11474
|
+
entry.status === "available" && entry.endpoint ? String(entry.endpoint.port) : ""
|
|
11475
|
+
])
|
|
11476
|
+
);
|
|
11477
|
+
}
|
|
11478
|
+
function managedHostPortIdentity(report) {
|
|
11479
|
+
const keys2 = Object.keys(report).sort();
|
|
11480
|
+
if (keys2.length === 0) return void 0;
|
|
11481
|
+
const canonical = keys2.map((key) => {
|
|
11482
|
+
const entry = report[key];
|
|
11483
|
+
return [key, entry.status === "available" ? entry.endpoint?.port ?? null : entry.status];
|
|
11484
|
+
});
|
|
11485
|
+
return (0, import_node_crypto17.createHash)("sha256").update(JSON.stringify(canonical)).digest("hex");
|
|
11486
|
+
}
|
|
11487
|
+
function assertManagedHostPortsAvailable(report) {
|
|
11488
|
+
const unavailable = Object.entries(report).filter(([, entry]) => entry.status === "unavailable").map(([key]) => key).sort();
|
|
11489
|
+
if (unavailable.length > 0) {
|
|
11490
|
+
throw new Error(`Selected managed host ports are unavailable: ${unavailable.join(", ")}.`);
|
|
11491
|
+
}
|
|
11492
|
+
}
|
|
11493
|
+
|
|
11494
|
+
// src/core/managed-post-start.ts
|
|
11495
|
+
var MANAGED_MARKER = "devrouter:managed devcontainer";
|
|
11496
|
+
var MANAGED_ADAPTER_PATH = ".devcontainer/post-start.sh";
|
|
11497
|
+
var RUNTIME_HELPER_PATH = "/tmp/devrouter/bin/devrouter-process";
|
|
11498
|
+
var RUNTIME_VARIANT_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
11499
|
+
var ADAPTER_WRAPPER = `adapter_snapshot="$1"
|
|
11500
|
+
shift
|
|
11501
|
+
readonly DEVROUTER_PROCESS_HELPER DEVROUTER_PROCESS_ADAPTER_SHA256 DEVROUTER_RUNTIME_VARIANT DEVROUTER_PROCESS_HOST_PORTS_SHA256
|
|
11502
|
+
source "$adapter_snapshot"
|
|
11503
|
+
`;
|
|
11504
|
+
function renderDeliveryScript(targetPath) {
|
|
11505
|
+
if (!/^\/tmp\/devrouter\/bin\/[a-zA-Z0-9._-]+$/.test(targetPath)) {
|
|
11506
|
+
throw new Error(`Unsafe managed runtime delivery path: ${targetPath}`);
|
|
11507
|
+
}
|
|
11508
|
+
const temporaryPrefix = import_node_path34.default.posix.basename(targetPath);
|
|
11509
|
+
return `set -eu
|
|
11510
|
+
umask 077
|
|
11511
|
+
runtime_root=/tmp/devrouter
|
|
11512
|
+
runtime_bin="$runtime_root/bin"
|
|
11513
|
+
target="${targetPath}"
|
|
11514
|
+
if [ -L "$runtime_root" ] || [ -L "$runtime_bin" ]; then
|
|
11515
|
+
echo "Refusing symlinked devrouter runtime path." >&2
|
|
11516
|
+
exit 1
|
|
11517
|
+
fi
|
|
11518
|
+
mkdir -p "$runtime_bin"
|
|
11519
|
+
chmod 700 "$runtime_root" "$runtime_bin"
|
|
11520
|
+
temporary="$(mktemp "$runtime_bin/.${temporaryPrefix}.XXXXXX")"
|
|
11521
|
+
trap 'rm -f "$temporary"' EXIT
|
|
11522
|
+
cat > "$temporary"
|
|
11523
|
+
chmod 700 "$temporary"
|
|
11524
|
+
mv -f "$temporary" "$target"
|
|
11525
|
+
trap - EXIT
|
|
11526
|
+
`;
|
|
11527
|
+
}
|
|
11528
|
+
function commandFailure2(result) {
|
|
11529
|
+
return [result.error?.message, result.stderr, result.stdout].filter(Boolean).map((value) => String(value).trim()).filter(Boolean).join("\n");
|
|
11530
|
+
}
|
|
11531
|
+
function deliverRuntimeFile(containerId, targetPath, contents, label) {
|
|
11532
|
+
const result = (0, import_node_child_process20.spawnSync)(
|
|
11533
|
+
"docker",
|
|
11534
|
+
["exec", "-i", containerId, "sh", "-c", renderDeliveryScript(targetPath)],
|
|
11535
|
+
{ input: contents, encoding: "utf-8", ...networkDockerOptions() }
|
|
11536
|
+
);
|
|
11537
|
+
if (result.status !== 0) {
|
|
11538
|
+
const details = commandFailure2(result);
|
|
11539
|
+
throw new Error(`Could not deliver ${label}${details ? `: ${details}` : "."}`);
|
|
11540
|
+
}
|
|
11541
|
+
}
|
|
11542
|
+
function readRegularFileBytes(filePath) {
|
|
11543
|
+
if (!import_node_fs29.default.existsSync(filePath) || !import_node_fs29.default.lstatSync(filePath).isFile()) return void 0;
|
|
11544
|
+
return import_node_fs29.default.readFileSync(filePath);
|
|
11545
|
+
}
|
|
11546
|
+
function readRegularFile(filePath) {
|
|
11547
|
+
return readRegularFileBytes(filePath)?.toString("utf-8");
|
|
11548
|
+
}
|
|
11549
|
+
function adapterFingerprint(adapter) {
|
|
11550
|
+
return (0, import_node_crypto18.createHash)("sha256").update(adapter).digest("hex");
|
|
11551
|
+
}
|
|
11552
|
+
function resolveProcessHelperPath() {
|
|
11553
|
+
const candidates = [
|
|
11554
|
+
import_node_path34.default.resolve(__dirname, "..", "bin", "devrouter-process"),
|
|
11555
|
+
import_node_path34.default.resolve(__dirname, "..", "..", "bin", "devrouter-process")
|
|
11556
|
+
];
|
|
11557
|
+
const helperPath = candidates.find((candidate) => import_node_fs29.default.existsSync(candidate));
|
|
11558
|
+
if (!helperPath) {
|
|
11559
|
+
throw new Error("Could not locate the packaged devrouter-process helper.");
|
|
11560
|
+
}
|
|
11561
|
+
return helperPath;
|
|
11562
|
+
}
|
|
11563
|
+
function resolveManagedPostStartPlan(repoPath) {
|
|
11564
|
+
const adapterPath = import_node_path34.default.join(repoPath, MANAGED_ADAPTER_PATH);
|
|
11565
|
+
const adapterBytes = readRegularFileBytes(adapterPath);
|
|
11566
|
+
const adapterText = adapterBytes?.toString("utf-8");
|
|
11567
|
+
const dockerfilePath = import_node_path34.default.join(repoPath, ".devcontainer", "Dockerfile");
|
|
11568
|
+
const devcontainerPath = import_node_path34.default.join(repoPath, ".devcontainer", "devcontainer.json");
|
|
11569
|
+
const dockerfile = readRegularFile(dockerfilePath) ?? "";
|
|
11570
|
+
const devcontainer = readRegularFile(devcontainerPath) ?? "";
|
|
11571
|
+
const devrouterPattern = [adapterText, dockerfile, devcontainer].filter((value) => value !== void 0).some(
|
|
11572
|
+
(value) => value.includes("DEVROUTER_PROCESS_HELPER") || value.includes("devrouter-process")
|
|
11573
|
+
);
|
|
11574
|
+
if (adapterBytes === void 0) {
|
|
11575
|
+
if (devrouterPattern) {
|
|
11576
|
+
throw new Error(
|
|
11577
|
+
"Devrouter lifecycle wiring is incomplete: add the managed post-start adapter or remove the stale devrouter-process references."
|
|
11578
|
+
);
|
|
11579
|
+
}
|
|
11580
|
+
return { kind: "unmanaged" };
|
|
11581
|
+
}
|
|
11582
|
+
const adapter = adapterBytes.toString("utf-8");
|
|
11583
|
+
if (!adapter.includes(MANAGED_MARKER)) {
|
|
11584
|
+
if (devrouterPattern) {
|
|
11585
|
+
throw new Error(
|
|
11586
|
+
"Devrouter-looking post-start adapter is missing the 'devrouter:managed devcontainer' marker. Regenerate or migrate the devcontainer, then retry devrouter ensure."
|
|
11587
|
+
);
|
|
11588
|
+
}
|
|
11589
|
+
return { kind: "unmanaged" };
|
|
11590
|
+
}
|
|
11591
|
+
assertManagedDevcontainerLifecycle(repoPath);
|
|
11592
|
+
if (adapter.includes("DEVROUTER_PROCESS_HELPER")) {
|
|
11593
|
+
return {
|
|
11594
|
+
kind: "runtime",
|
|
11595
|
+
adapterPath: MANAGED_ADAPTER_PATH,
|
|
11596
|
+
adapterSha256: adapterFingerprint(adapterBytes),
|
|
11597
|
+
adapterContents: adapterBytes
|
|
11598
|
+
};
|
|
11599
|
+
}
|
|
11600
|
+
if (dockerfile.includes("devrouter-process") && devcontainer.includes("postStartCommand")) {
|
|
11601
|
+
return { kind: "legacy" };
|
|
11602
|
+
}
|
|
11603
|
+
throw new Error(
|
|
11604
|
+
"Managed post-start must use DEVROUTER_PROCESS_HELPER before removing the legacy image helper and postStartCommand. Regenerate or migrate the devcontainer, then retry devrouter ensure."
|
|
11605
|
+
);
|
|
11606
|
+
}
|
|
11607
|
+
function runManagedPostStart(options) {
|
|
11608
|
+
if (options.plan.kind !== "runtime") return;
|
|
11609
|
+
const runtimeVariant = options.variant ?? "default";
|
|
11610
|
+
if (runtimeVariant.length > 64 || !RUNTIME_VARIANT_PATTERN.test(runtimeVariant)) {
|
|
11611
|
+
throw new Error(
|
|
11612
|
+
"Managed runtime variant must use lowercase alphanumeric segments separated by hyphens and be at most 64 characters."
|
|
11613
|
+
);
|
|
11614
|
+
}
|
|
11615
|
+
const hostPorts = options.hostPorts ?? {};
|
|
11616
|
+
assertManagedHostPortsAvailable(hostPorts);
|
|
11617
|
+
const portEnvironment = managedHostPortVariables(hostPorts);
|
|
11618
|
+
const portIdentity = managedHostPortIdentity(hostPorts) ?? "";
|
|
11619
|
+
const helper = import_node_fs29.default.readFileSync(resolveProcessHelperPath());
|
|
11620
|
+
deliverRuntimeFile(
|
|
11621
|
+
options.container.id,
|
|
11622
|
+
RUNTIME_HELPER_PATH,
|
|
11623
|
+
helper,
|
|
11624
|
+
"the managed process helper"
|
|
11625
|
+
);
|
|
11626
|
+
const runtimeAdapterPath = `/tmp/devrouter/bin/managed-post-start-${options.plan.adapterSha256}`;
|
|
11627
|
+
deliverRuntimeFile(
|
|
11628
|
+
options.container.id,
|
|
11629
|
+
runtimeAdapterPath,
|
|
11630
|
+
options.plan.adapterContents,
|
|
11631
|
+
"the managed post-start adapter"
|
|
11632
|
+
);
|
|
11633
|
+
const started2 = (0, import_node_child_process20.spawnSync)(
|
|
11634
|
+
"docker",
|
|
11635
|
+
[
|
|
11636
|
+
"exec",
|
|
11637
|
+
"--workdir",
|
|
11638
|
+
options.container.workspacePath,
|
|
11639
|
+
"--env",
|
|
11640
|
+
`DEVROUTER_PROCESS_HELPER=${RUNTIME_HELPER_PATH}`,
|
|
11641
|
+
"--env",
|
|
11642
|
+
`DEVROUTER_PROCESS_ADAPTER_SHA256=${options.plan.adapterSha256}`,
|
|
11643
|
+
"--env",
|
|
11644
|
+
`DEVROUTER_PROCESS_HOST_PORTS_SHA256=${portIdentity}`,
|
|
11645
|
+
...Object.entries(portEnvironment).flatMap(([name, value]) => ["--env", `${name}=${value}`]),
|
|
11646
|
+
"--env",
|
|
11647
|
+
`DEVROUTER_RUNTIME_VARIANT=${runtimeVariant}`,
|
|
11648
|
+
...options.profile ? ["--env", `DEVROUTER_PROFILE=${options.profile}`] : [],
|
|
11649
|
+
...options.processes ? [
|
|
11650
|
+
"--env",
|
|
11651
|
+
`DEVROUTER_PROCESS_SET=${options.processes.join(",")}`,
|
|
11652
|
+
"--env",
|
|
11653
|
+
"DEVROUTER_PROCESS_FINGERPRINT_ENV=DEVROUTER_PROFILE,DEVROUTER_PROCESS_SET"
|
|
11654
|
+
] : [],
|
|
11655
|
+
options.container.id,
|
|
11656
|
+
"bash",
|
|
11657
|
+
"-c",
|
|
11658
|
+
ADAPTER_WRAPPER,
|
|
11659
|
+
options.plan.adapterPath,
|
|
11660
|
+
runtimeAdapterPath
|
|
11661
|
+
],
|
|
11662
|
+
{
|
|
11663
|
+
stdio: options.quiet ? ["ignore", 2, "inherit"] : "inherit",
|
|
11664
|
+
...networkDockerOptions()
|
|
11665
|
+
}
|
|
11666
|
+
);
|
|
11667
|
+
if (started2.status !== 0) {
|
|
11668
|
+
const details = commandFailure2(started2);
|
|
11669
|
+
throw new Error(`Managed post-start failed${details ? `: ${details}.` : "."}`);
|
|
11670
|
+
}
|
|
11671
|
+
}
|
|
11672
|
+
function assertSafeProcessName(name) {
|
|
11673
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(name)) {
|
|
11674
|
+
throw new Error(`Managed process '${name}' is not a safe exact identifier.`);
|
|
11675
|
+
}
|
|
11676
|
+
}
|
|
11677
|
+
function runManagedProcessAction(options) {
|
|
11678
|
+
assertSafeProcessName(options.name);
|
|
11679
|
+
const result = (0, import_node_child_process20.spawnSync)(
|
|
11680
|
+
"docker",
|
|
11681
|
+
[
|
|
11682
|
+
"exec",
|
|
11683
|
+
"--workdir",
|
|
11684
|
+
options.container.workspacePath,
|
|
11685
|
+
"--env",
|
|
11686
|
+
`DEVROUTER_PROCESS_HELPER=${RUNTIME_HELPER_PATH}`,
|
|
11687
|
+
options.container.id,
|
|
11688
|
+
"bash",
|
|
11689
|
+
"-c",
|
|
11690
|
+
'exec "$DEVROUTER_PROCESS_HELPER" "$1" --name "$2"',
|
|
11691
|
+
"devrouter-process",
|
|
11692
|
+
options.action,
|
|
11693
|
+
options.name
|
|
11694
|
+
],
|
|
11695
|
+
{
|
|
11696
|
+
encoding: "utf-8",
|
|
11697
|
+
stdio: options.action === "status" ? ["ignore", "pipe", "pipe"] : ["ignore", 2, "inherit"],
|
|
11698
|
+
...networkDockerOptions()
|
|
11699
|
+
}
|
|
11700
|
+
);
|
|
11701
|
+
if (options.action === "stop") {
|
|
11702
|
+
if (result.status !== 0) {
|
|
11703
|
+
throw new Error(`Could not stop managed process '${options.name}'.`);
|
|
11704
|
+
}
|
|
11705
|
+
return "stopped";
|
|
11706
|
+
}
|
|
11707
|
+
try {
|
|
11708
|
+
const parsed = JSON.parse(result.stdout.trim());
|
|
11709
|
+
if (parsed.status === "running" || parsed.status === "stopped" || parsed.status === "foreign" || parsed.status === "drifted") {
|
|
11710
|
+
return parsed.status;
|
|
11711
|
+
}
|
|
11712
|
+
} catch {
|
|
11713
|
+
}
|
|
11714
|
+
return "drifted";
|
|
11715
|
+
}
|
|
11716
|
+
|
|
11717
|
+
// src/core/recovery-budget.ts
|
|
11718
|
+
function findRecoveryUnit(incident, unit) {
|
|
11719
|
+
return (incident.units ?? []).find((record5) => record5.key === unit.key);
|
|
11720
|
+
}
|
|
11721
|
+
function restartsFor(kind, limits) {
|
|
11722
|
+
return kind === "service" ? limits.maxServiceRestarts : limits.maxProcessRestarts;
|
|
11723
|
+
}
|
|
11724
|
+
function recoveryBudgetRefusal(input) {
|
|
11725
|
+
const startedAtMs = input.incident.startedAtMs ?? input.nowMs;
|
|
11726
|
+
const elapsed = input.activeElapsedMs ?? Math.max(0, input.nowMs - startedAtMs);
|
|
11727
|
+
if (elapsed > input.limits.windowSeconds * 1e3) return "window-closed";
|
|
11728
|
+
if (input.unit !== void 0) {
|
|
11729
|
+
const record5 = findRecoveryUnit(input.incident, input.unit);
|
|
11730
|
+
if (record5 !== void 0 && record5.actions >= restartsFor(input.unit.kind, input.limits))
|
|
11731
|
+
return "unit-exhausted";
|
|
11732
|
+
}
|
|
11733
|
+
if (input.incident.correctiveActionsTaken >= input.incident.actionLimit)
|
|
11734
|
+
return "budget-exhausted";
|
|
11735
|
+
return void 0;
|
|
11736
|
+
}
|
|
11737
|
+
function claimRecoveryUnit(input) {
|
|
11738
|
+
const reason2 = recoveryBudgetRefusal(input);
|
|
11739
|
+
if (reason2 !== void 0) return { ok: false, reason: reason2 };
|
|
11740
|
+
const units = (input.incident.units ?? []).map((record5) => ({ ...record5 }));
|
|
11741
|
+
if (input.unit !== void 0) {
|
|
11742
|
+
const index = units.findIndex((record5) => record5.key === input.unit?.key);
|
|
11743
|
+
if (index === -1) {
|
|
11744
|
+
if (units.length >= RELIABILITY_MAX_ITEMS) return { ok: false, reason: "unit-exhausted" };
|
|
11745
|
+
units.push({ ...input.unit, actions: 1, lastActionAtMs: input.nowMs });
|
|
11746
|
+
} else {
|
|
11747
|
+
const record5 = units[index];
|
|
11748
|
+
units[index] = {
|
|
11749
|
+
...record5,
|
|
11750
|
+
actions: record5.actions + 1,
|
|
11751
|
+
lastActionAtMs: input.nowMs
|
|
11752
|
+
};
|
|
11753
|
+
}
|
|
11754
|
+
}
|
|
11755
|
+
return {
|
|
11756
|
+
ok: true,
|
|
11757
|
+
incident: {
|
|
11758
|
+
...input.incident,
|
|
11759
|
+
startedAtMs: input.incident.startedAtMs ?? input.nowMs,
|
|
11760
|
+
correctiveActionsTaken: input.incident.correctiveActionsTaken + 1,
|
|
11761
|
+
units
|
|
11762
|
+
}
|
|
11763
|
+
};
|
|
11764
|
+
}
|
|
11765
|
+
|
|
11766
|
+
// src/core/reliability-model.ts
|
|
11767
|
+
var INFRASTRUCTURE_VALUES = ["healthy", "failed", "unknown"];
|
|
11768
|
+
var APPLICATION_VALUES = ["verified", "unready", "unverified"];
|
|
11769
|
+
var ADMISSION_VALUES = ["admitted", "waiting", "unknown", "denied-unadmittable"];
|
|
11770
|
+
function record3(value) {
|
|
11771
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
11772
|
+
}
|
|
11773
|
+
function oneOf2(value, values) {
|
|
11774
|
+
return typeof value === "string" && values.includes(value);
|
|
11775
|
+
}
|
|
11776
|
+
function uniqueIds2(values) {
|
|
11777
|
+
return new Set(values).size === values.length && values.every(isReliabilityId);
|
|
11778
|
+
}
|
|
11779
|
+
function isFence(value) {
|
|
11780
|
+
return record3(value) && isReliabilityId(value.environmentId) && isReliabilityCounter(value.intentRevision) && isReliabilityCounter(value.runtimeGeneration) && isReliabilityCounter(value.controllerEpoch);
|
|
11781
|
+
}
|
|
11782
|
+
function isConsumer(value) {
|
|
11783
|
+
return record3(value) && isReliabilityId(value.id) && typeof value.pinned === "boolean" && Array.isArray(value.requiredCapabilities) && value.requiredCapabilities.length <= 128 && uniqueIds2(value.requiredCapabilities);
|
|
11784
|
+
}
|
|
11785
|
+
function isObservation(value) {
|
|
11786
|
+
return record3(value) && isReliabilityId(value.capability) && oneOf2(value.infrastructure, INFRASTRUCTURE_VALUES) && oneOf2(value.application, APPLICATION_VALUES) && isReliabilityCounter(value.observedAtMs) && isReliabilityCounter(value.validForMs);
|
|
11787
|
+
}
|
|
11788
|
+
function assertReliabilityEvent(value) {
|
|
11789
|
+
if (!record3(value) || !isFence(value) || typeof value.type !== "string") {
|
|
11790
|
+
throw new Error("Invalid reliability event.");
|
|
11791
|
+
}
|
|
11792
|
+
let valid = false;
|
|
11793
|
+
switch (value.type) {
|
|
11794
|
+
case "request":
|
|
11795
|
+
valid = oneOf2(value.mode, ["start", "attach"]) && isReliabilityId(value.key) && isReliabilityId(value.operationId) && isReliabilityProfile(value.profile) && isConsumer(value.consumer);
|
|
11796
|
+
break;
|
|
11797
|
+
case "operation-request":
|
|
11798
|
+
valid = oneOf2(value.kind, ["ensure", "exec"]) && isReliabilityId(value.key) && isReliabilityId(value.operationId) && isReliabilityProfile(value.profile) && isConsumer(value.consumer) && typeof value.runtimeRunning === "boolean" && (value.recoverInterruptedEnsure === void 0 || typeof value.recoverInterruptedEnsure === "boolean");
|
|
11799
|
+
break;
|
|
11800
|
+
case "drained":
|
|
11801
|
+
valid = isReliabilityId(value.operationId);
|
|
11802
|
+
break;
|
|
11803
|
+
case "release":
|
|
11804
|
+
valid = isReliabilityId(value.consumerId);
|
|
11805
|
+
break;
|
|
11806
|
+
case "stop":
|
|
11807
|
+
case "park":
|
|
11808
|
+
case "dispatch":
|
|
11809
|
+
valid = true;
|
|
11810
|
+
break;
|
|
11811
|
+
case "admission":
|
|
11812
|
+
valid = oneOf2(value.result, ADMISSION_VALUES);
|
|
11813
|
+
break;
|
|
11814
|
+
case "resume":
|
|
11815
|
+
valid = typeof value.pressureDwellSatisfied === "boolean" && isReliabilityId(value.operationId);
|
|
11816
|
+
break;
|
|
10613
11817
|
case "dispatch-persisted":
|
|
10614
11818
|
case "launched":
|
|
10615
11819
|
case "interrupted":
|
|
@@ -11278,10 +12482,10 @@ function stepReliability(state, event, nowMs) {
|
|
|
11278
12482
|
}
|
|
11279
12483
|
|
|
11280
12484
|
// src/core/reliability-worker.ts
|
|
11281
|
-
var
|
|
11282
|
-
var
|
|
11283
|
-
var
|
|
11284
|
-
var
|
|
12485
|
+
var import_node_child_process21 = require("child_process");
|
|
12486
|
+
var import_node_crypto19 = require("crypto");
|
|
12487
|
+
var import_node_fs30 = __toESM(require("fs"));
|
|
12488
|
+
var import_node_path35 = __toESM(require("path"));
|
|
11285
12489
|
|
|
11286
12490
|
// src/core/lifecycle-progress.ts
|
|
11287
12491
|
var ROLES = {
|
|
@@ -11347,11 +12551,11 @@ function workerGroupAbsent(pid) {
|
|
|
11347
12551
|
if (process.platform !== "linux") return false;
|
|
11348
12552
|
let observedZombie = false;
|
|
11349
12553
|
try {
|
|
11350
|
-
for (const entry of
|
|
12554
|
+
for (const entry of import_node_fs30.default.readdirSync("/proc")) {
|
|
11351
12555
|
if (!/^\d+$/.test(entry)) continue;
|
|
11352
12556
|
let stat;
|
|
11353
12557
|
try {
|
|
11354
|
-
stat =
|
|
12558
|
+
stat = import_node_fs30.default.readFileSync(`/proc/${entry}/stat`, "utf8");
|
|
11355
12559
|
} catch (error) {
|
|
11356
12560
|
if (error.code === "ENOENT") continue;
|
|
11357
12561
|
return false;
|
|
@@ -11371,20 +12575,20 @@ function workerGroupAbsent(pid) {
|
|
|
11371
12575
|
}
|
|
11372
12576
|
|
|
11373
12577
|
// src/core/workspace-ensure.ts
|
|
11374
|
-
var
|
|
12578
|
+
var import_node_child_process31 = require("child_process");
|
|
11375
12579
|
var import_node_fs35 = __toESM(require("fs"));
|
|
11376
12580
|
var import_node_path40 = __toESM(require("path"));
|
|
11377
12581
|
|
|
11378
12582
|
// src/core/devpod-mutation.ts
|
|
11379
|
-
var
|
|
11380
|
-
var
|
|
11381
|
-
var
|
|
11382
|
-
var DEVPOD_MUTATION_LOCK_FILE =
|
|
12583
|
+
var import_node_child_process22 = require("child_process");
|
|
12584
|
+
var import_node_fs31 = __toESM(require("fs"));
|
|
12585
|
+
var import_node_path36 = __toESM(require("path"));
|
|
12586
|
+
var DEVPOD_MUTATION_LOCK_FILE = import_node_path36.default.join(DEVROUTER_HOME, "devpod-mutation.lock");
|
|
11383
12587
|
var DEVPOD_MUTATION_WAIT_MS = 18e5;
|
|
11384
12588
|
var DevpodStartPostconditionError = class extends Error {
|
|
11385
12589
|
};
|
|
11386
12590
|
function withMutationLock2(activity, target, operation) {
|
|
11387
|
-
|
|
12591
|
+
import_node_fs31.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
|
|
11388
12592
|
return withFileLockSync(
|
|
11389
12593
|
DEVPOD_MUTATION_LOCK_FILE,
|
|
11390
12594
|
{
|
|
@@ -11397,20 +12601,20 @@ function withMutationLock2(activity, target, operation) {
|
|
|
11397
12601
|
operation
|
|
11398
12602
|
);
|
|
11399
12603
|
}
|
|
11400
|
-
function
|
|
12604
|
+
function commandFailure3(result) {
|
|
11401
12605
|
return [result.error?.message, result.stdout, result.stderr].filter(Boolean).join("\n").trim();
|
|
11402
12606
|
}
|
|
11403
12607
|
function runDevpodAction(action, devpodId, force = false, repoPath) {
|
|
11404
12608
|
const args = action === "delete" ? [action, devpodId, ...force ? ["--force"] : [], "--ignore-not-found"] : [action, devpodId];
|
|
11405
12609
|
claimLifecycleEffect();
|
|
11406
12610
|
const binding = repoPath ? networkProviderEffectOptions("devpod", devpodId, repoPath) : { args: [] };
|
|
11407
|
-
const result = (0,
|
|
12611
|
+
const result = (0, import_node_child_process22.spawnSync)("devpod", [...args, ...binding.args], {
|
|
11408
12612
|
encoding: "utf-8",
|
|
11409
12613
|
...binding.env ? { env: binding.env } : {}
|
|
11410
12614
|
});
|
|
11411
12615
|
if (result.status !== 0) {
|
|
11412
12616
|
throw new Error(
|
|
11413
|
-
`devpod ${action}${force ? " --force" : ""} failed for '${devpodId}': ${
|
|
12617
|
+
`devpod ${action}${force ? " --force" : ""} failed for '${devpodId}': ${commandFailure3(result) || "unknown error"}`
|
|
11414
12618
|
);
|
|
11415
12619
|
}
|
|
11416
12620
|
}
|
|
@@ -11545,7 +12749,7 @@ async function startDevpodWorkspace(options) {
|
|
|
11545
12749
|
}
|
|
11546
12750
|
claimLifecycleEffect();
|
|
11547
12751
|
network?.beforeDispatch?.();
|
|
11548
|
-
const result = (0,
|
|
12752
|
+
const result = (0, import_node_child_process22.spawnSync)("devpod", args, {
|
|
11549
12753
|
stdio: options.quiet ? ["inherit", 2, "inherit"] : "inherit",
|
|
11550
12754
|
env
|
|
11551
12755
|
});
|
|
@@ -11585,8 +12789,8 @@ async function startDevpodWorkspace(options) {
|
|
|
11585
12789
|
}
|
|
11586
12790
|
|
|
11587
12791
|
// src/core/host-port-claims.ts
|
|
11588
|
-
var
|
|
11589
|
-
var
|
|
12792
|
+
var import_node_child_process23 = require("child_process");
|
|
12793
|
+
var import_node_path37 = __toESM(require("path"));
|
|
11590
12794
|
var COMPOSE_RENDER_TIMEOUT_MS = 3e4;
|
|
11591
12795
|
var COMPOSE_RENDER_MAX_BUFFER = 8 * 1024 * 1024;
|
|
11592
12796
|
var DOCKER_TIMEOUT_MS = 1e4;
|
|
@@ -11689,7 +12893,7 @@ function hostPortBindingsConflict(desired, holder) {
|
|
|
11689
12893
|
}
|
|
11690
12894
|
var HOST_PORT_INSPECT_TEMPLATE = '{"id":{{json .Id}},"name":{{json .Name}},"labels":{"com.docker.compose.project":{{json (index .Config.Labels "com.docker.compose.project")}},"com.docker.compose.project.working_dir":{{json (index .Config.Labels "com.docker.compose.project.working_dir")}}},"ports":{{json .NetworkSettings.Ports}}}';
|
|
11691
12895
|
function runDocker(args) {
|
|
11692
|
-
const result = (0,
|
|
12896
|
+
const result = (0, import_node_child_process23.spawnSync)("docker", args, {
|
|
11693
12897
|
...networkDockerOptions(),
|
|
11694
12898
|
encoding: "utf-8",
|
|
11695
12899
|
timeout: DOCKER_TIMEOUT_MS,
|
|
@@ -11759,7 +12963,7 @@ function listHolderSnapshots() {
|
|
|
11759
12963
|
}
|
|
11760
12964
|
function renderManagedComposeModel(plan, workspace) {
|
|
11761
12965
|
const fileArgs = plan.composeFiles.flatMap((file) => ["-f", file]);
|
|
11762
|
-
const result = (0,
|
|
12966
|
+
const result = (0, import_node_child_process23.spawnSync)(
|
|
11763
12967
|
"docker",
|
|
11764
12968
|
[
|
|
11765
12969
|
"compose",
|
|
@@ -11794,8 +12998,8 @@ function renderManagedComposeModel(plan, workspace) {
|
|
|
11794
12998
|
}
|
|
11795
12999
|
}
|
|
11796
13000
|
function attributeHolderWorktree(workingDir) {
|
|
11797
|
-
if (
|
|
11798
|
-
return comparableWorkspacePath(
|
|
13001
|
+
if (import_node_path37.default.basename(workingDir) !== ".devcontainer") return void 0;
|
|
13002
|
+
return comparableWorkspacePath(import_node_path37.default.dirname(workingDir));
|
|
11799
13003
|
}
|
|
11800
13004
|
function holderAttribution(holder, repoPath) {
|
|
11801
13005
|
const attribution = {};
|
|
@@ -11830,7 +13034,7 @@ function detectHostPortClaimConflicts(options) {
|
|
|
11830
13034
|
);
|
|
11831
13035
|
if (desired.length === 0) return [];
|
|
11832
13036
|
const holders = listHolderSnapshots();
|
|
11833
|
-
const devcontainerDir =
|
|
13037
|
+
const devcontainerDir = import_node_path37.default.join(options.repoPath, ".devcontainer");
|
|
11834
13038
|
const conflicts = [];
|
|
11835
13039
|
for (const binding of desired) {
|
|
11836
13040
|
for (const holder of holders) {
|
|
@@ -11854,7 +13058,7 @@ function detectHostPortClaimConflicts(options) {
|
|
|
11854
13058
|
}
|
|
11855
13059
|
|
|
11856
13060
|
// src/core/http-route-probe.ts
|
|
11857
|
-
var
|
|
13061
|
+
var import_node_child_process24 = require("child_process");
|
|
11858
13062
|
var DEFAULT_MAX_TIME_SECONDS = 5;
|
|
11859
13063
|
var PARENT_TIMEOUT_PADDING_MS = 250;
|
|
11860
13064
|
var CONTRACT_MAX_BUFFER = 1024;
|
|
@@ -11887,7 +13091,7 @@ function probeHttpRoute(host, options = {}) {
|
|
|
11887
13091
|
args.push("--cacert", CERT_FILE);
|
|
11888
13092
|
}
|
|
11889
13093
|
args.push(url);
|
|
11890
|
-
const result = (0,
|
|
13094
|
+
const result = (0, import_node_child_process24.spawnSync)("curl", args, { encoding: "utf-8" });
|
|
11891
13095
|
const stdout = result.stdout?.trim() ?? "";
|
|
11892
13096
|
const status = /^\d{3}$/.test(stdout) ? Number(stdout) : void 0;
|
|
11893
13097
|
const ok = result.status === 0 && status !== void 0 && status >= 100 && status < 500;
|
|
@@ -11906,7 +13110,7 @@ function probeHttpReadinessSync(host, readiness, maxTimeSeconds, repoPath) {
|
|
|
11906
13110
|
}
|
|
11907
13111
|
let result;
|
|
11908
13112
|
try {
|
|
11909
|
-
result = (0,
|
|
13113
|
+
result = (0, import_node_child_process24.spawnSync)("curl", args, {
|
|
11910
13114
|
encoding: "utf-8",
|
|
11911
13115
|
maxBuffer: CONTRACT_MAX_BUFFER,
|
|
11912
13116
|
timeout: Math.ceil(parentTimeoutMs)
|
|
@@ -11990,8 +13194,8 @@ function transportFailure() {
|
|
|
11990
13194
|
}
|
|
11991
13195
|
|
|
11992
13196
|
// src/core/managed-host-preparation.ts
|
|
11993
|
-
var
|
|
11994
|
-
var
|
|
13197
|
+
var import_node_child_process25 = require("child_process");
|
|
13198
|
+
var import_node_fs32 = __toESM(require("fs"));
|
|
11995
13199
|
var PREPARATION_TIMEOUT_MS = 6e4;
|
|
11996
13200
|
var CONFIG_READ_ERROR = "Managed host preparation could not read .devrouter.yml.";
|
|
11997
13201
|
var CONFIG_CHANGED_ERROR = "Managed host preparation changed .devrouter.yml; refusing to continue.";
|
|
@@ -12002,7 +13206,7 @@ function runPreparationCommand(argv, cwd) {
|
|
|
12002
13206
|
return new Promise((resolve) => {
|
|
12003
13207
|
let child;
|
|
12004
13208
|
try {
|
|
12005
|
-
child = (0,
|
|
13209
|
+
child = (0, import_node_child_process25.spawn)(argv[0], argv.slice(1), {
|
|
12006
13210
|
cwd,
|
|
12007
13211
|
detached: true,
|
|
12008
13212
|
shell: false,
|
|
@@ -12034,7 +13238,7 @@ function runPreparationCommand(argv, cwd) {
|
|
|
12034
13238
|
}
|
|
12035
13239
|
function readConfigBytes(configPath) {
|
|
12036
13240
|
try {
|
|
12037
|
-
return
|
|
13241
|
+
return import_node_fs32.default.readFileSync(configPath);
|
|
12038
13242
|
} catch {
|
|
12039
13243
|
return void 0;
|
|
12040
13244
|
}
|
|
@@ -12067,219 +13271,8 @@ async function runManagedHostPreparation(repoPath, config) {
|
|
|
12067
13271
|
}
|
|
12068
13272
|
}
|
|
12069
13273
|
|
|
12070
|
-
// src/core/managed-post-start.ts
|
|
12071
|
-
var import_node_child_process24 = require("child_process");
|
|
12072
|
-
var import_node_crypto18 = require("crypto");
|
|
12073
|
-
var import_node_fs32 = __toESM(require("fs"));
|
|
12074
|
-
var import_node_path37 = __toESM(require("path"));
|
|
12075
|
-
var MANAGED_MARKER = "devrouter:managed devcontainer";
|
|
12076
|
-
var MANAGED_ADAPTER_PATH = ".devcontainer/post-start.sh";
|
|
12077
|
-
var RUNTIME_HELPER_PATH = "/tmp/devrouter/bin/devrouter-process";
|
|
12078
|
-
var ADAPTER_WRAPPER = `adapter_snapshot="$1"
|
|
12079
|
-
shift
|
|
12080
|
-
readonly DEVROUTER_PROCESS_HELPER DEVROUTER_PROCESS_ADAPTER_SHA256
|
|
12081
|
-
source "$adapter_snapshot"
|
|
12082
|
-
`;
|
|
12083
|
-
function renderDeliveryScript(targetPath) {
|
|
12084
|
-
if (!/^\/tmp\/devrouter\/bin\/[a-zA-Z0-9._-]+$/.test(targetPath)) {
|
|
12085
|
-
throw new Error(`Unsafe managed runtime delivery path: ${targetPath}`);
|
|
12086
|
-
}
|
|
12087
|
-
const temporaryPrefix = import_node_path37.default.posix.basename(targetPath);
|
|
12088
|
-
return `set -eu
|
|
12089
|
-
umask 077
|
|
12090
|
-
runtime_root=/tmp/devrouter
|
|
12091
|
-
runtime_bin="$runtime_root/bin"
|
|
12092
|
-
target="${targetPath}"
|
|
12093
|
-
if [ -L "$runtime_root" ] || [ -L "$runtime_bin" ]; then
|
|
12094
|
-
echo "Refusing symlinked devrouter runtime path." >&2
|
|
12095
|
-
exit 1
|
|
12096
|
-
fi
|
|
12097
|
-
mkdir -p "$runtime_bin"
|
|
12098
|
-
chmod 700 "$runtime_root" "$runtime_bin"
|
|
12099
|
-
temporary="$(mktemp "$runtime_bin/.${temporaryPrefix}.XXXXXX")"
|
|
12100
|
-
trap 'rm -f "$temporary"' EXIT
|
|
12101
|
-
cat > "$temporary"
|
|
12102
|
-
chmod 700 "$temporary"
|
|
12103
|
-
mv -f "$temporary" "$target"
|
|
12104
|
-
trap - EXIT
|
|
12105
|
-
`;
|
|
12106
|
-
}
|
|
12107
|
-
function commandFailure3(result) {
|
|
12108
|
-
return [result.error?.message, result.stderr, result.stdout].filter(Boolean).map((value) => String(value).trim()).filter(Boolean).join("\n");
|
|
12109
|
-
}
|
|
12110
|
-
function deliverRuntimeFile(containerId, targetPath, contents, label) {
|
|
12111
|
-
const result = (0, import_node_child_process24.spawnSync)(
|
|
12112
|
-
"docker",
|
|
12113
|
-
["exec", "-i", containerId, "sh", "-c", renderDeliveryScript(targetPath)],
|
|
12114
|
-
{ input: contents, encoding: "utf-8", ...networkDockerOptions() }
|
|
12115
|
-
);
|
|
12116
|
-
if (result.status !== 0) {
|
|
12117
|
-
const details = commandFailure3(result);
|
|
12118
|
-
throw new Error(`Could not deliver ${label}${details ? `: ${details}` : "."}`);
|
|
12119
|
-
}
|
|
12120
|
-
}
|
|
12121
|
-
function readRegularFileBytes(filePath) {
|
|
12122
|
-
if (!import_node_fs32.default.existsSync(filePath) || !import_node_fs32.default.lstatSync(filePath).isFile()) return void 0;
|
|
12123
|
-
return import_node_fs32.default.readFileSync(filePath);
|
|
12124
|
-
}
|
|
12125
|
-
function readRegularFile(filePath) {
|
|
12126
|
-
return readRegularFileBytes(filePath)?.toString("utf-8");
|
|
12127
|
-
}
|
|
12128
|
-
function adapterFingerprint(adapter) {
|
|
12129
|
-
return (0, import_node_crypto18.createHash)("sha256").update(adapter).digest("hex");
|
|
12130
|
-
}
|
|
12131
|
-
function resolveProcessHelperPath() {
|
|
12132
|
-
const candidates = [
|
|
12133
|
-
import_node_path37.default.resolve(__dirname, "..", "bin", "devrouter-process"),
|
|
12134
|
-
import_node_path37.default.resolve(__dirname, "..", "..", "bin", "devrouter-process")
|
|
12135
|
-
];
|
|
12136
|
-
const helperPath = candidates.find((candidate) => import_node_fs32.default.existsSync(candidate));
|
|
12137
|
-
if (!helperPath) {
|
|
12138
|
-
throw new Error("Could not locate the packaged devrouter-process helper.");
|
|
12139
|
-
}
|
|
12140
|
-
return helperPath;
|
|
12141
|
-
}
|
|
12142
|
-
function resolveManagedPostStartPlan(repoPath) {
|
|
12143
|
-
const adapterPath = import_node_path37.default.join(repoPath, MANAGED_ADAPTER_PATH);
|
|
12144
|
-
const adapterBytes = readRegularFileBytes(adapterPath);
|
|
12145
|
-
const adapterText = adapterBytes?.toString("utf-8");
|
|
12146
|
-
const dockerfilePath = import_node_path37.default.join(repoPath, ".devcontainer", "Dockerfile");
|
|
12147
|
-
const devcontainerPath = import_node_path37.default.join(repoPath, ".devcontainer", "devcontainer.json");
|
|
12148
|
-
const dockerfile = readRegularFile(dockerfilePath) ?? "";
|
|
12149
|
-
const devcontainer = readRegularFile(devcontainerPath) ?? "";
|
|
12150
|
-
const devrouterPattern = [adapterText, dockerfile, devcontainer].filter((value) => value !== void 0).some(
|
|
12151
|
-
(value) => value.includes("DEVROUTER_PROCESS_HELPER") || value.includes("devrouter-process")
|
|
12152
|
-
);
|
|
12153
|
-
if (adapterBytes === void 0) {
|
|
12154
|
-
if (devrouterPattern) {
|
|
12155
|
-
throw new Error(
|
|
12156
|
-
"Devrouter lifecycle wiring is incomplete: add the managed post-start adapter or remove the stale devrouter-process references."
|
|
12157
|
-
);
|
|
12158
|
-
}
|
|
12159
|
-
return { kind: "unmanaged" };
|
|
12160
|
-
}
|
|
12161
|
-
const adapter = adapterBytes.toString("utf-8");
|
|
12162
|
-
if (!adapter.includes(MANAGED_MARKER)) {
|
|
12163
|
-
if (devrouterPattern) {
|
|
12164
|
-
throw new Error(
|
|
12165
|
-
"Devrouter-looking post-start adapter is missing the 'devrouter:managed devcontainer' marker. Regenerate or migrate the devcontainer, then retry devrouter ensure."
|
|
12166
|
-
);
|
|
12167
|
-
}
|
|
12168
|
-
return { kind: "unmanaged" };
|
|
12169
|
-
}
|
|
12170
|
-
assertManagedDevcontainerLifecycle(repoPath);
|
|
12171
|
-
if (adapter.includes("DEVROUTER_PROCESS_HELPER")) {
|
|
12172
|
-
return {
|
|
12173
|
-
kind: "runtime",
|
|
12174
|
-
adapterPath: MANAGED_ADAPTER_PATH,
|
|
12175
|
-
adapterSha256: adapterFingerprint(adapterBytes),
|
|
12176
|
-
adapterContents: adapterBytes
|
|
12177
|
-
};
|
|
12178
|
-
}
|
|
12179
|
-
if (dockerfile.includes("devrouter-process") && devcontainer.includes("postStartCommand")) {
|
|
12180
|
-
return { kind: "legacy" };
|
|
12181
|
-
}
|
|
12182
|
-
throw new Error(
|
|
12183
|
-
"Managed post-start must use DEVROUTER_PROCESS_HELPER before removing the legacy image helper and postStartCommand. Regenerate or migrate the devcontainer, then retry devrouter ensure."
|
|
12184
|
-
);
|
|
12185
|
-
}
|
|
12186
|
-
function runManagedPostStart(options) {
|
|
12187
|
-
if (options.plan.kind !== "runtime") return;
|
|
12188
|
-
const helper = import_node_fs32.default.readFileSync(resolveProcessHelperPath());
|
|
12189
|
-
deliverRuntimeFile(
|
|
12190
|
-
options.container.id,
|
|
12191
|
-
RUNTIME_HELPER_PATH,
|
|
12192
|
-
helper,
|
|
12193
|
-
"the managed process helper"
|
|
12194
|
-
);
|
|
12195
|
-
const runtimeAdapterPath = `/tmp/devrouter/bin/managed-post-start-${options.plan.adapterSha256}`;
|
|
12196
|
-
deliverRuntimeFile(
|
|
12197
|
-
options.container.id,
|
|
12198
|
-
runtimeAdapterPath,
|
|
12199
|
-
options.plan.adapterContents,
|
|
12200
|
-
"the managed post-start adapter"
|
|
12201
|
-
);
|
|
12202
|
-
const started2 = (0, import_node_child_process24.spawnSync)(
|
|
12203
|
-
"docker",
|
|
12204
|
-
[
|
|
12205
|
-
"exec",
|
|
12206
|
-
"--workdir",
|
|
12207
|
-
options.container.workspacePath,
|
|
12208
|
-
"--env",
|
|
12209
|
-
`DEVROUTER_PROCESS_HELPER=${RUNTIME_HELPER_PATH}`,
|
|
12210
|
-
"--env",
|
|
12211
|
-
`DEVROUTER_PROCESS_ADAPTER_SHA256=${options.plan.adapterSha256}`,
|
|
12212
|
-
...options.profile ? ["--env", `DEVROUTER_PROFILE=${options.profile}`] : [],
|
|
12213
|
-
...options.processes ? [
|
|
12214
|
-
"--env",
|
|
12215
|
-
`DEVROUTER_PROCESS_SET=${options.processes.join(",")}`,
|
|
12216
|
-
"--env",
|
|
12217
|
-
"DEVROUTER_PROCESS_FINGERPRINT_ENV=DEVROUTER_PROFILE,DEVROUTER_PROCESS_SET"
|
|
12218
|
-
] : [],
|
|
12219
|
-
options.container.id,
|
|
12220
|
-
"bash",
|
|
12221
|
-
"-c",
|
|
12222
|
-
ADAPTER_WRAPPER,
|
|
12223
|
-
options.plan.adapterPath,
|
|
12224
|
-
runtimeAdapterPath
|
|
12225
|
-
],
|
|
12226
|
-
{
|
|
12227
|
-
stdio: options.quiet ? ["ignore", 2, "inherit"] : "inherit",
|
|
12228
|
-
...networkDockerOptions()
|
|
12229
|
-
}
|
|
12230
|
-
);
|
|
12231
|
-
if (started2.status !== 0) {
|
|
12232
|
-
const details = commandFailure3(started2);
|
|
12233
|
-
throw new Error(`Managed post-start failed${details ? `: ${details}.` : "."}`);
|
|
12234
|
-
}
|
|
12235
|
-
}
|
|
12236
|
-
function assertSafeProcessName(name) {
|
|
12237
|
-
if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(name)) {
|
|
12238
|
-
throw new Error(`Managed process '${name}' is not a safe exact identifier.`);
|
|
12239
|
-
}
|
|
12240
|
-
}
|
|
12241
|
-
function runManagedProcessAction(options) {
|
|
12242
|
-
assertSafeProcessName(options.name);
|
|
12243
|
-
const result = (0, import_node_child_process24.spawnSync)(
|
|
12244
|
-
"docker",
|
|
12245
|
-
[
|
|
12246
|
-
"exec",
|
|
12247
|
-
"--workdir",
|
|
12248
|
-
options.container.workspacePath,
|
|
12249
|
-
"--env",
|
|
12250
|
-
`DEVROUTER_PROCESS_HELPER=${RUNTIME_HELPER_PATH}`,
|
|
12251
|
-
options.container.id,
|
|
12252
|
-
"bash",
|
|
12253
|
-
"-c",
|
|
12254
|
-
'exec "$DEVROUTER_PROCESS_HELPER" "$1" --name "$2"',
|
|
12255
|
-
"devrouter-process",
|
|
12256
|
-
options.action,
|
|
12257
|
-
options.name
|
|
12258
|
-
],
|
|
12259
|
-
{
|
|
12260
|
-
encoding: "utf-8",
|
|
12261
|
-
stdio: options.action === "status" ? ["ignore", "pipe", "pipe"] : ["ignore", 2, "inherit"],
|
|
12262
|
-
...networkDockerOptions()
|
|
12263
|
-
}
|
|
12264
|
-
);
|
|
12265
|
-
if (options.action === "stop") {
|
|
12266
|
-
if (result.status !== 0) {
|
|
12267
|
-
throw new Error(`Could not stop managed process '${options.name}'.`);
|
|
12268
|
-
}
|
|
12269
|
-
return "stopped";
|
|
12270
|
-
}
|
|
12271
|
-
try {
|
|
12272
|
-
const parsed = JSON.parse(result.stdout.trim());
|
|
12273
|
-
if (parsed.status === "running" || parsed.status === "stopped" || parsed.status === "foreign" || parsed.status === "drifted") {
|
|
12274
|
-
return parsed.status;
|
|
12275
|
-
}
|
|
12276
|
-
} catch {
|
|
12277
|
-
}
|
|
12278
|
-
return "drifted";
|
|
12279
|
-
}
|
|
12280
|
-
|
|
12281
13274
|
// src/core/managed-mount-nesting.ts
|
|
12282
|
-
var
|
|
13275
|
+
var import_node_child_process26 = require("child_process");
|
|
12283
13276
|
var NESTED_MOUNT_REASON = "no configured mount is nested inside another configured mount";
|
|
12284
13277
|
var UNREADABLE_MOUNT_REASON = "the container's own mount table could not be read";
|
|
12285
13278
|
function normalizeDestination(value) {
|
|
@@ -12352,7 +13345,7 @@ function classifyManagedMountNesting(configured, effectiveDestinations, containe
|
|
|
12352
13345
|
};
|
|
12353
13346
|
}
|
|
12354
13347
|
function readContainerMountinfo(containerId, options) {
|
|
12355
|
-
const result = (0,
|
|
13348
|
+
const result = (0, import_node_child_process26.spawnSync)("docker", ["exec", containerId, "cat", "/proc/self/mountinfo"], {
|
|
12356
13349
|
encoding: "utf-8",
|
|
12357
13350
|
timeout: options.timeoutMs ?? 5e3,
|
|
12358
13351
|
stdio: ["ignore", "pipe", "pipe"],
|
|
@@ -12373,8 +13366,9 @@ function recoveryPath(repoPath) {
|
|
|
12373
13366
|
if (/^[A-Za-z0-9_@%+=:,./-]+$/.test(repoPath)) return repoPath;
|
|
12374
13367
|
return `'${repoPath.replaceAll("'", "'\\''")}'`;
|
|
12375
13368
|
}
|
|
12376
|
-
function reliabilityRecovery(reason2, repoPath) {
|
|
13369
|
+
function reliabilityRecovery(reason2, repoPath, variant) {
|
|
12377
13370
|
const checkout = recoveryPath(repoPath);
|
|
13371
|
+
const variantOption = variant && variant !== "default" ? ` --variant ${variant}` : "";
|
|
12378
13372
|
switch (reason2) {
|
|
12379
13373
|
case "stop-incomplete":
|
|
12380
13374
|
case "operation-unknown":
|
|
@@ -12382,12 +13376,12 @@ function reliabilityRecovery(reason2, repoPath) {
|
|
|
12382
13376
|
case "start-refused":
|
|
12383
13377
|
return [
|
|
12384
13378
|
`Run: devrouter doctor --repo ${checkout} to read the refused fixed host-port claim and its holder`,
|
|
12385
|
-
`Stop the holding workspace or change this repository's own published binding, then run: devrouter ensure ${checkout}`
|
|
13379
|
+
`Stop the holding workspace or change this repository's own published binding, then run: devrouter ensure ${checkout}${variantOption}`
|
|
12386
13380
|
];
|
|
12387
13381
|
case "unadmittable":
|
|
12388
13382
|
return [
|
|
12389
13383
|
`Run: devrouter stop ${checkout}`,
|
|
12390
|
-
`Lower the requested capacity in .devrouter.yml, then run: devrouter ensure ${checkout}`
|
|
13384
|
+
`Lower the requested capacity in .devrouter.yml, then run: devrouter ensure ${checkout}${variantOption}`
|
|
12391
13385
|
];
|
|
12392
13386
|
case "capacity-parked":
|
|
12393
13387
|
return [
|
|
@@ -12396,7 +13390,9 @@ function reliabilityRecovery(reason2, repoPath) {
|
|
|
12396
13390
|
`'devrouter ensure' refuses while the journal stays parked; release the intent instead with: devrouter stop ${checkout}`
|
|
12397
13391
|
];
|
|
12398
13392
|
case "capacity-waiting":
|
|
12399
|
-
return [
|
|
13393
|
+
return [
|
|
13394
|
+
`Run: devrouter ensure ${checkout}${variantOption} (still queued until capacity frees up)`
|
|
13395
|
+
];
|
|
12400
13396
|
default:
|
|
12401
13397
|
return [];
|
|
12402
13398
|
}
|
|
@@ -12437,7 +13433,12 @@ function readReliabilityStatus(options) {
|
|
|
12437
13433
|
correctiveActionsTaken: record5.state.incident.correctiveActionsTaken,
|
|
12438
13434
|
actionLimit: record5.state.incident.actionLimit
|
|
12439
13435
|
} : null,
|
|
12440
|
-
...reason2 ? {
|
|
13436
|
+
...reason2 ? {
|
|
13437
|
+
attention: {
|
|
13438
|
+
reason: reason2,
|
|
13439
|
+
recovery: reliabilityRecovery(reason2, options.repoPath, options.variant)
|
|
13440
|
+
}
|
|
13441
|
+
} : {}
|
|
12441
13442
|
};
|
|
12442
13443
|
}
|
|
12443
13444
|
var EMPTY_RESOURCES = {
|
|
@@ -12762,6 +13763,10 @@ function collectManagedRuntimeStatus(options) {
|
|
|
12762
13763
|
}
|
|
12763
13764
|
const drift = [];
|
|
12764
13765
|
if (stateReadFailed) drift.push("managed runtime state could not be read");
|
|
13766
|
+
if (state && options.variant !== void 0 && (state.variant ?? "default") !== options.variant)
|
|
13767
|
+
drift.push("active runtime variant differs from the selected variant");
|
|
13768
|
+
if (state?.variant && state.variant !== "default" && !managedRuntime.variants?.includes(state.variant))
|
|
13769
|
+
drift.push("active runtime variant is no longer declared");
|
|
12765
13770
|
if (state?.status === "degraded") drift.push("the last managed transition is degraded");
|
|
12766
13771
|
if (state) {
|
|
12767
13772
|
const desiredState = {
|
|
@@ -12843,6 +13848,7 @@ function collectManagedRuntimeStatus(options) {
|
|
|
12843
13848
|
};
|
|
12844
13849
|
const reliability = readReliabilityStatus({
|
|
12845
13850
|
repoPath: options.repoPath,
|
|
13851
|
+
variant: state?.variant,
|
|
12846
13852
|
...options.workspace !== void 0 ? { workspace: options.workspace } : {}
|
|
12847
13853
|
});
|
|
12848
13854
|
return {
|
|
@@ -12850,7 +13856,7 @@ function collectManagedRuntimeStatus(options) {
|
|
|
12850
13856
|
status,
|
|
12851
13857
|
profile: options.profile,
|
|
12852
13858
|
...reliability ? { reliability } : {},
|
|
12853
|
-
...state?.profile ? { activeProfile: state.profile } : {},
|
|
13859
|
+
...state?.profile ? { activeProfile: state.profile, activeVariant: state.variant ?? "default" } : {},
|
|
12854
13860
|
...options.workspace !== void 0 ? { workspace: options.workspace } : {},
|
|
12855
13861
|
...state?.devpodId ? { devpodId: state.devpodId } : {},
|
|
12856
13862
|
...inspection.composeProject ? { composeProject: inspection.composeProject } : {},
|
|
@@ -12874,7 +13880,7 @@ function collectManagedRuntimeStatus(options) {
|
|
|
12874
13880
|
}
|
|
12875
13881
|
|
|
12876
13882
|
// src/core/network-inventory.ts
|
|
12877
|
-
var
|
|
13883
|
+
var import_node_child_process27 = require("child_process");
|
|
12878
13884
|
var NetworkInventoryError = class extends Error {
|
|
12879
13885
|
};
|
|
12880
13886
|
var MAX_IDS = 4096;
|
|
@@ -12886,7 +13892,7 @@ function readDocker(endpoint, args, timeoutMs) {
|
|
|
12886
13892
|
const env = { ...process.env };
|
|
12887
13893
|
delete env.DOCKER_CONTEXT;
|
|
12888
13894
|
delete env.DOCKER_HOST;
|
|
12889
|
-
const result = (0,
|
|
13895
|
+
const result = (0, import_node_child_process27.spawnSync)("docker", ["--host", endpoint, ...args], {
|
|
12890
13896
|
env,
|
|
12891
13897
|
encoding: "utf-8",
|
|
12892
13898
|
timeout: timeoutMs,
|
|
@@ -13050,7 +14056,7 @@ function collectDockerNetworkInventory(options = {}, dependencies = {}) {
|
|
|
13050
14056
|
}
|
|
13051
14057
|
|
|
13052
14058
|
// src/core/network-routes.ts
|
|
13053
|
-
var
|
|
14059
|
+
var import_node_child_process28 = require("child_process");
|
|
13054
14060
|
var import_node_net3 = require("net");
|
|
13055
14061
|
|
|
13056
14062
|
// src/core/network-local-daemon.ts
|
|
@@ -13119,7 +14125,7 @@ function parseLinuxNetworkRoutes(output) {
|
|
|
13119
14125
|
function collectNetworkRoutes(options) {
|
|
13120
14126
|
if ((options.platform ?? process.platform) !== "linux" || !hasLocalDockerNetworkNamespace(options.endpoint))
|
|
13121
14127
|
return { status: "unknown", routes: [] };
|
|
13122
|
-
const result = (0,
|
|
14128
|
+
const result = (0, import_node_child_process28.spawnSync)("ip", ["-j", "-4", "route", "show", "table", "all"], {
|
|
13123
14129
|
encoding: "utf8",
|
|
13124
14130
|
timeout: 5e3,
|
|
13125
14131
|
maxBuffer: 1024 * 1024
|
|
@@ -13155,10 +14161,10 @@ function inspectLegacyNetworkCapacity(input) {
|
|
|
13155
14161
|
}
|
|
13156
14162
|
|
|
13157
14163
|
// src/core/network-managed.ts
|
|
13158
|
-
var
|
|
14164
|
+
var import_node_crypto22 = require("crypto");
|
|
13159
14165
|
|
|
13160
14166
|
// src/core/network-compose.ts
|
|
13161
|
-
var
|
|
14167
|
+
var import_node_crypto20 = require("crypto");
|
|
13162
14168
|
var SAFE_ENDPOINT_UPPER_BOUND_SEMANTICS = "full-lifecycle-demand";
|
|
13163
14169
|
function isRecord5(value) {
|
|
13164
14170
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
@@ -13964,12 +14970,12 @@ function stableValue(value) {
|
|
|
13964
14970
|
}
|
|
13965
14971
|
function fingerprintComposeNetworkModel(input) {
|
|
13966
14972
|
const projection = normalizedFingerprintProjection(input);
|
|
13967
|
-
return (0,
|
|
14973
|
+
return (0, import_node_crypto20.createHash)("sha256").update(stableValue(projection), "utf8").digest("hex");
|
|
13968
14974
|
}
|
|
13969
14975
|
|
|
13970
14976
|
// src/core/network-compose-files.ts
|
|
13971
|
-
var
|
|
13972
|
-
var
|
|
14977
|
+
var import_node_child_process29 = require("child_process");
|
|
14978
|
+
var import_node_crypto21 = require("crypto");
|
|
13973
14979
|
var import_node_fs34 = __toESM(require("fs"));
|
|
13974
14980
|
var import_node_path39 = __toESM(require("path"));
|
|
13975
14981
|
var import_yaml4 = __toESM(require("yaml"));
|
|
@@ -14005,7 +15011,7 @@ function inspectNetworkComposeFiles(input) {
|
|
|
14005
15011
|
const env = managedComposeEnvironment(input.workspace);
|
|
14006
15012
|
delete env.DOCKER_CONTEXT;
|
|
14007
15013
|
delete env.DOCKER_HOST;
|
|
14008
|
-
const result = (0,
|
|
15014
|
+
const result = (0, import_node_child_process29.spawnSync)(
|
|
14009
15015
|
"docker",
|
|
14010
15016
|
[
|
|
14011
15017
|
"--host",
|
|
@@ -14040,7 +15046,7 @@ function prepareNetworkComposeFiles(plan, subnet) {
|
|
|
14040
15046
|
const overlay = deriveComposeNetworkOverlay(subnet);
|
|
14041
15047
|
if (overlay.status !== "ready" || !overlay.overlay) throw new Error("Network subnet is invalid.");
|
|
14042
15048
|
const overlayPath = import_node_path39.default.join(plan.composeDirectory, OVERLAY);
|
|
14043
|
-
const ignored = (0,
|
|
15049
|
+
const ignored = (0, import_node_child_process29.spawnSync)("git", [
|
|
14044
15050
|
"-C",
|
|
14045
15051
|
plan.composeDirectory,
|
|
14046
15052
|
"check-ignore",
|
|
@@ -14070,7 +15076,7 @@ ${JSON.stringify(extended.config, null, 2)}
|
|
|
14070
15076
|
...plan,
|
|
14071
15077
|
contents,
|
|
14072
15078
|
composeFiles: [...plan.composeFiles, overlayPath],
|
|
14073
|
-
effectiveConfigSha256: (0,
|
|
15079
|
+
effectiveConfigSha256: (0, import_node_crypto21.createHash)("sha256").update(contents).digest("hex")
|
|
14074
15080
|
};
|
|
14075
15081
|
return {
|
|
14076
15082
|
plan: result,
|
|
@@ -14082,7 +15088,7 @@ ${JSON.stringify(extended.config, null, 2)}
|
|
|
14082
15088
|
}
|
|
14083
15089
|
|
|
14084
15090
|
// src/core/network-connected-route.ts
|
|
14085
|
-
var
|
|
15091
|
+
var import_node_child_process30 = require("child_process");
|
|
14086
15092
|
function bounds(cidr) {
|
|
14087
15093
|
const canonical = parseIPv4Cidr(cidr);
|
|
14088
15094
|
if (!canonical) throw new Error("Invalid connected route.");
|
|
@@ -14107,7 +15113,7 @@ function inspectClaimedBridge(claim2, networkId) {
|
|
|
14107
15113
|
const env = { ...process.env };
|
|
14108
15114
|
delete env.DOCKER_CONTEXT;
|
|
14109
15115
|
delete env.DOCKER_HOST;
|
|
14110
|
-
const result = (0,
|
|
15116
|
+
const result = (0, import_node_child_process30.spawnSync)(
|
|
14111
15117
|
"docker",
|
|
14112
15118
|
[
|
|
14113
15119
|
"--host",
|
|
@@ -14134,7 +15140,7 @@ function inspectClaimedBridge(claim2, networkId) {
|
|
|
14134
15140
|
const [first, last] = bounds(claim2.subnet);
|
|
14135
15141
|
const [address] = bounds(gateway);
|
|
14136
15142
|
if (address <= first || address >= last) throw new Error();
|
|
14137
|
-
const interfaces = (0,
|
|
15143
|
+
const interfaces = (0, import_node_child_process30.spawnSync)("ip", ["-j", "-d", "-4", "address", "show", "dev", bridge], {
|
|
14138
15144
|
encoding: "utf8",
|
|
14139
15145
|
timeout: 5e3,
|
|
14140
15146
|
maxBuffer: 65536
|
|
@@ -14388,7 +15394,7 @@ function createManagedNetworkSession(input) {
|
|
|
14388
15394
|
"Insufficient endpoint headroom; request an allowed larger network before first allocation."
|
|
14389
15395
|
);
|
|
14390
15396
|
const authority = readNetworkOperationAuthority(identity2);
|
|
14391
|
-
const fingerprint = (0,
|
|
15397
|
+
const fingerprint = (0, import_node_crypto22.createHash)("sha256").update(input.plan().sourceConfigSha256).update(fingerprintComposeNetworkModel(model)).digest("hex");
|
|
14392
15398
|
if (!saved) {
|
|
14393
15399
|
if (policy.status !== "valid")
|
|
14394
15400
|
throw new Error("New network allocation requires valid machine policy.");
|
|
@@ -14890,7 +15896,7 @@ async function waitForContainerPreflight(repoPath, target, upstreamHosts, timeou
|
|
|
14890
15896
|
target
|
|
14891
15897
|
});
|
|
14892
15898
|
if (target.kind === "linked") {
|
|
14893
|
-
const workspaceEnv = (0,
|
|
15899
|
+
const workspaceEnv = (0, import_node_child_process31.spawnSync)(
|
|
14894
15900
|
"docker",
|
|
14895
15901
|
["exec", appContainer.id, "printenv", "WORKSPACE"],
|
|
14896
15902
|
{ encoding: "utf-8", ...networkDockerOptions() }
|
|
@@ -14900,7 +15906,7 @@ async function waitForContainerPreflight(repoPath, target, upstreamHosts, timeou
|
|
|
14900
15906
|
`Workspace app container must expose WORKSPACE='${target.workspace}' (got '${workspaceEnv.stdout.trim() || "(empty)"}').`
|
|
14901
15907
|
);
|
|
14902
15908
|
}
|
|
14903
|
-
const devrouterWorkspaceEnv = (0,
|
|
15909
|
+
const devrouterWorkspaceEnv = (0, import_node_child_process31.spawnSync)(
|
|
14904
15910
|
"docker",
|
|
14905
15911
|
["exec", appContainer.id, "printenv", "DEVROUTER_WORKSPACE"],
|
|
14906
15912
|
{ encoding: "utf-8", ...networkDockerOptions() }
|
|
@@ -14911,7 +15917,7 @@ async function waitForContainerPreflight(repoPath, target, upstreamHosts, timeou
|
|
|
14911
15917
|
);
|
|
14912
15918
|
}
|
|
14913
15919
|
}
|
|
14914
|
-
const gitCheck = (0,
|
|
15920
|
+
const gitCheck = (0, import_node_child_process31.spawnSync)(
|
|
14915
15921
|
"docker",
|
|
14916
15922
|
[
|
|
14917
15923
|
"exec",
|
|
@@ -15073,6 +16079,9 @@ function assertRepairBaseline(options) {
|
|
|
15073
16079
|
};
|
|
15074
16080
|
const reasons = [];
|
|
15075
16081
|
if (state.profile !== profile) reasons.push("profile");
|
|
16082
|
+
if ((state.variant ?? "default") !== (options.variant ?? "default")) reasons.push("variant");
|
|
16083
|
+
if (state.hostPortDeclarationsSha256 !== managedHostPortDeclarationsIdentity(config.managedRuntime?.devcontainer.hostPorts ?? {}))
|
|
16084
|
+
reasons.push("host-ports");
|
|
15076
16085
|
if (state.sourceConfigSha256 !== plan.sourceConfigSha256) reasons.push("source-config");
|
|
15077
16086
|
if (state.effectiveConfigSha256 !== plan.effectiveConfigSha256) reasons.push("effective-config");
|
|
15078
16087
|
for (const resource of ["apps", "services", "processes"]) {
|
|
@@ -15191,7 +16200,7 @@ function isPrimaryCheckout(repoPath) {
|
|
|
15191
16200
|
}
|
|
15192
16201
|
function openUrls(urls) {
|
|
15193
16202
|
for (const url of urls) {
|
|
15194
|
-
const opened = (0,
|
|
16203
|
+
const opened = (0, import_node_child_process31.spawnSync)("open", [url], { encoding: "utf-8" });
|
|
15195
16204
|
if (opened.status !== 0) {
|
|
15196
16205
|
process.stderr.write(`Warning: could not open '${url}'.
|
|
15197
16206
|
`);
|
|
@@ -15262,6 +16271,12 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
15262
16271
|
target.kind === "primary" ? "" : target.workspace,
|
|
15263
16272
|
options.profile ?? (options.repair ? previousManagedState?.profile : void 0)
|
|
15264
16273
|
);
|
|
16274
|
+
const variant = resolveRuntimeVariant(
|
|
16275
|
+
runtime.config,
|
|
16276
|
+
options.variant ?? (options.repair ? previousManagedState?.variant : void 0)
|
|
16277
|
+
);
|
|
16278
|
+
if (variant !== "default" && managedPostStart.kind !== "runtime")
|
|
16279
|
+
throw new Error("Runtime variants require a managed runtime post-start adapter.");
|
|
15265
16280
|
const managedRuntime = runtime.config.managedRuntime;
|
|
15266
16281
|
if (options.repair && !managedRuntime)
|
|
15267
16282
|
throw new Error("Repair requires managedRuntime configuration.");
|
|
@@ -15337,6 +16352,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
15337
16352
|
repoPath,
|
|
15338
16353
|
...target.kind === "linked" ? { workspace: target.workspace } : {},
|
|
15339
16354
|
profile: runtime.profile,
|
|
16355
|
+
variant,
|
|
15340
16356
|
// A refusal precedes the provider start, so the exact id may not
|
|
15341
16357
|
// be known yet on a primary checkout.
|
|
15342
16358
|
devpodId: devpodId ?? "(absent)",
|
|
@@ -15366,6 +16382,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
15366
16382
|
plan: managedPlan,
|
|
15367
16383
|
config: runtime.config,
|
|
15368
16384
|
profile: runtime.profile,
|
|
16385
|
+
variant,
|
|
15369
16386
|
processes: desiredProcesses,
|
|
15370
16387
|
routes: previousRoutes,
|
|
15371
16388
|
workspace: managedWorkspaceEnv
|
|
@@ -15474,7 +16491,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
15474
16491
|
transitionPhase = "service-start";
|
|
15475
16492
|
reportLifecycleProgress("service-start");
|
|
15476
16493
|
claimLifecycleEffect();
|
|
15477
|
-
const result = (0,
|
|
16494
|
+
const result = (0, import_node_child_process31.spawnSync)("docker", ["start", ...stopped.map((entry) => entry.id)], {
|
|
15478
16495
|
...networkDockerOptions(),
|
|
15479
16496
|
encoding: "utf-8",
|
|
15480
16497
|
timeout: DEFAULT_READINESS_TIMEOUT_MS,
|
|
@@ -15584,6 +16601,10 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
15584
16601
|
devpodId: captureId,
|
|
15585
16602
|
composeProject: captureProject,
|
|
15586
16603
|
profile: runtime.profile,
|
|
16604
|
+
variant,
|
|
16605
|
+
hostPortDeclarationsSha256: managedHostPortDeclarationsIdentity(
|
|
16606
|
+
managedRuntime?.devcontainer.hostPorts ?? {}
|
|
16607
|
+
),
|
|
15587
16608
|
desired: {
|
|
15588
16609
|
apps: apps.map((app) => app.name).sort(),
|
|
15589
16610
|
services: [...capturePlan.desiredProfileServices].sort(),
|
|
@@ -15630,6 +16651,10 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
15630
16651
|
devpodId,
|
|
15631
16652
|
composeProject: managedComposeProject,
|
|
15632
16653
|
profile: runtime.profile,
|
|
16654
|
+
variant,
|
|
16655
|
+
hostPortDeclarationsSha256: managedHostPortDeclarationsIdentity(
|
|
16656
|
+
managedRuntime?.devcontainer.hostPorts ?? {}
|
|
16657
|
+
),
|
|
15633
16658
|
desired: {
|
|
15634
16659
|
apps: apps.map((app) => app.name).sort(),
|
|
15635
16660
|
services: [...managedPlan.desiredProfileServices].sort(),
|
|
@@ -15653,7 +16678,17 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
15653
16678
|
container,
|
|
15654
16679
|
quiet: options.quiet,
|
|
15655
16680
|
profile: runtime.profile,
|
|
15656
|
-
|
|
16681
|
+
variant,
|
|
16682
|
+
...managedPlan ? {
|
|
16683
|
+
processes: desiredProcesses,
|
|
16684
|
+
hostPorts: resolveManagedHostPorts({
|
|
16685
|
+
repoPath,
|
|
16686
|
+
declarations: managedRuntime?.devcontainer.hostPorts ?? {},
|
|
16687
|
+
selectedServices: managedPlan.desiredServices,
|
|
16688
|
+
composeProject: managedComposeProject,
|
|
16689
|
+
composeFiles: managedPlan.composeFiles
|
|
16690
|
+
})
|
|
16691
|
+
} : {}
|
|
15657
16692
|
});
|
|
15658
16693
|
if (managedPlan && managedComposeProject) {
|
|
15659
16694
|
const processRegistry = runtime.config.managedRuntime?.processes ?? [];
|
|
@@ -15746,7 +16781,8 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
15746
16781
|
plan: managedPostStart,
|
|
15747
16782
|
container: recoveredContainer,
|
|
15748
16783
|
quiet: options.quiet,
|
|
15749
|
-
profile: runtime.profile
|
|
16784
|
+
profile: runtime.profile,
|
|
16785
|
+
variant
|
|
15750
16786
|
});
|
|
15751
16787
|
recreated = true;
|
|
15752
16788
|
claimLifecycleEffect();
|
|
@@ -15778,6 +16814,10 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
15778
16814
|
devpodId,
|
|
15779
16815
|
composeProject: managedComposeProject,
|
|
15780
16816
|
profile: runtime.profile,
|
|
16817
|
+
variant,
|
|
16818
|
+
hostPortDeclarationsSha256: managedHostPortDeclarationsIdentity(
|
|
16819
|
+
managedRuntime?.devcontainer.hostPorts ?? {}
|
|
16820
|
+
),
|
|
15781
16821
|
desired: {
|
|
15782
16822
|
apps: apps.map((app) => app.name).sort(),
|
|
15783
16823
|
services: [...managedPlan.desiredProfileServices].sort(),
|
|
@@ -15809,6 +16849,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
15809
16849
|
workspace: target.workspace,
|
|
15810
16850
|
config: runtime.config,
|
|
15811
16851
|
profile: runtime.profile,
|
|
16852
|
+
variant,
|
|
15812
16853
|
resolvedProfile: runtime.resolvedProfile,
|
|
15813
16854
|
...options.repair ? { candidateState } : {}
|
|
15814
16855
|
}) : void 0;
|
|
@@ -15826,6 +16867,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
15826
16867
|
repoPath,
|
|
15827
16868
|
workspace: target.workspace,
|
|
15828
16869
|
profile: runtime.profile,
|
|
16870
|
+
variant,
|
|
15829
16871
|
devpodId,
|
|
15830
16872
|
urls,
|
|
15831
16873
|
recreated,
|
|
@@ -16018,13 +17060,32 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
16018
17060
|
throw new Error("Managed post-start plan disappeared during rollback.");
|
|
16019
17061
|
}
|
|
16020
17062
|
if (previousProcesses.length > 0 && previousManagedState?.status !== "degraded") {
|
|
17063
|
+
if (previousManagedState?.hostPortDeclarationsSha256 !== managedHostPortDeclarationsIdentity(
|
|
17064
|
+
managedRuntimeConfig?.managedRuntime?.devcontainer.hostPorts ?? {}
|
|
17065
|
+
)) {
|
|
17066
|
+
throw new Error(
|
|
17067
|
+
"Host-port declarations changed; restore the retained declarations before repair."
|
|
17068
|
+
);
|
|
17069
|
+
}
|
|
16021
17070
|
claimLifecycleEffect();
|
|
16022
17071
|
runManagedPostStart({
|
|
16023
17072
|
plan: managedPostStartPlan,
|
|
16024
17073
|
container: managedContainer,
|
|
16025
17074
|
quiet: options.quiet,
|
|
16026
17075
|
profile: previousManagedState?.profile ?? "full",
|
|
16027
|
-
|
|
17076
|
+
variant: previousManagedState?.variant ?? "default",
|
|
17077
|
+
processes: previousProcesses,
|
|
17078
|
+
hostPorts: resolveManagedHostPorts({
|
|
17079
|
+
repoPath,
|
|
17080
|
+
declarations: managedRuntimeConfig?.managedRuntime?.devcontainer.hostPorts ?? {},
|
|
17081
|
+
selectedServices: [
|
|
17082
|
+
managedPlan.primaryService,
|
|
17083
|
+
...managedPlan.baseServices,
|
|
17084
|
+
...previousServices
|
|
17085
|
+
],
|
|
17086
|
+
composeProject: managedComposeProject,
|
|
17087
|
+
composeFiles: managedPlan.composeFiles
|
|
17088
|
+
})
|
|
16028
17089
|
});
|
|
16029
17090
|
}
|
|
16030
17091
|
for (const process2 of managedProcessRegistry) {
|
|
@@ -16174,7 +17235,9 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
16174
17235
|
ensureNetworkLocked,
|
|
16175
17236
|
() => network?.endpoint
|
|
16176
17237
|
);
|
|
17238
|
+
preflightRuntimeVariant(repoPath, options);
|
|
16177
17239
|
return withLifecycleOperationLock(repoPath, async () => {
|
|
17240
|
+
preflightRuntimeVariant(repoPath, options);
|
|
16178
17241
|
const preparationConfig = loadRepoConfig(repoPath);
|
|
16179
17242
|
if (!options.repair)
|
|
16180
17243
|
recoverUnattachedNetworkReservation(repoPath, Boolean(preparationConfig.managedRuntime));
|
|
@@ -16215,7 +17278,8 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
16215
17278
|
linked ? workspace : "",
|
|
16216
17279
|
requestedOptions.profile
|
|
16217
17280
|
).profile;
|
|
16218
|
-
|
|
17281
|
+
const desiredVariant = resolveRuntimeVariant(preparationConfig, requestedOptions.variant);
|
|
17282
|
+
if (desiredProfile !== retained.profile && desiredVariant === (retained.variant ?? "default")) {
|
|
16219
17283
|
const target = resolveRepairTarget(repoPath, linked);
|
|
16220
17284
|
const baseline = loadRuntimeConfig(repoPath, linked ? workspace : "", retained.profile);
|
|
16221
17285
|
const plan = inspectManagedDevcontainerConfig({
|
|
@@ -16233,6 +17297,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
16233
17297
|
plan,
|
|
16234
17298
|
config: baseline.config,
|
|
16235
17299
|
profile: baseline.profile,
|
|
17300
|
+
variant: retained.variant ?? "default",
|
|
16236
17301
|
processes: desiredManagedProcesses(
|
|
16237
17302
|
baseline.config.managedRuntime,
|
|
16238
17303
|
baseline.resolvedProfile
|
|
@@ -16244,7 +17309,13 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
16244
17309
|
claimLifecycleEffect();
|
|
16245
17310
|
return ensureLocked();
|
|
16246
17311
|
}
|
|
16247
|
-
options = {
|
|
17312
|
+
options = {
|
|
17313
|
+
...requestedOptions,
|
|
17314
|
+
repair: true,
|
|
17315
|
+
profile: retained.profile,
|
|
17316
|
+
variant: retained.variant ?? "default",
|
|
17317
|
+
open: false
|
|
17318
|
+
};
|
|
16248
17319
|
let repaired;
|
|
16249
17320
|
try {
|
|
16250
17321
|
repaired = await repairLocked();
|
|
@@ -16252,7 +17323,8 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
16252
17323
|
options = requestedOptions;
|
|
16253
17324
|
}
|
|
16254
17325
|
claimLifecycleEffect();
|
|
16255
|
-
if (desiredProfile !== repaired.profile
|
|
17326
|
+
if (desiredProfile !== repaired.profile || desiredVariant !== (repaired.variant ?? "default"))
|
|
17327
|
+
return ensureLocked();
|
|
16256
17328
|
if (options.open) openUrls(repaired.urls);
|
|
16257
17329
|
return repaired;
|
|
16258
17330
|
});
|
|
@@ -16338,6 +17410,33 @@ function reconcileDrained(record5) {
|
|
|
16338
17410
|
record5.worker = null;
|
|
16339
17411
|
}
|
|
16340
17412
|
}
|
|
17413
|
+
function preflightRuntimeVariant(repoPath, options) {
|
|
17414
|
+
if (options.variant === void 0 && !options.repair) return "default";
|
|
17415
|
+
const workspace = isLinkedWorktree(repoPath) ? readPersistedWorkspace(repoPath) : void 0;
|
|
17416
|
+
const retained = options.repair ? readManagedRuntimeState(repoPath, workspace) : void 0;
|
|
17417
|
+
const variant = resolveRuntimeVariant(
|
|
17418
|
+
loadRepoConfig(repoPath),
|
|
17419
|
+
options.variant ?? retained?.variant
|
|
17420
|
+
);
|
|
17421
|
+
if (options.repair && variant !== (retained?.variant ?? "default"))
|
|
17422
|
+
throw new Error("Repair requires the retained runtime variant.");
|
|
17423
|
+
if (variant !== "default") {
|
|
17424
|
+
if (resolveManagedPostStartPlan(repoPath).kind !== "runtime")
|
|
17425
|
+
throw new Error("Runtime variants require a managed runtime post-start adapter.");
|
|
17426
|
+
const previous = readReliabilityOperation({
|
|
17427
|
+
repoPath,
|
|
17428
|
+
workspace: workspace ?? null,
|
|
17429
|
+
provider: resolveWorkspaceRuntimeOrDefault(repoPath)
|
|
17430
|
+
});
|
|
17431
|
+
if (capacityPolicyTargets(repoPath) || previous?.state.executionPolicy === "capacity-managed")
|
|
17432
|
+
throw new Error("Non-default runtime variants require manual lifecycle admission.");
|
|
17433
|
+
}
|
|
17434
|
+
return variant;
|
|
17435
|
+
}
|
|
17436
|
+
function capacityPolicyTargets(repoPath) {
|
|
17437
|
+
const policy = readCapacityPolicy(import_node_path41.default.join(DEVROUTER_HOME, "controller"));
|
|
17438
|
+
return policy?.admissions === "enabled" && policy.enrollments.some((entry) => entry.repoPath === repoPath);
|
|
17439
|
+
}
|
|
16341
17440
|
function clearActiveLifecycleStartupWitness() {
|
|
16342
17441
|
const request = activeWorker;
|
|
16343
17442
|
if (!request || request.kind === "stop") return;
|
|
@@ -16395,12 +17494,46 @@ async function executeLifecycleWorker(request, operation) {
|
|
|
16395
17494
|
request.repoPath,
|
|
16396
17495
|
() => proveInitialManagedDevsyAbsence(request.repoPath)
|
|
16397
17496
|
);
|
|
17497
|
+
const markStopWorkStarted = () => updateReliabilityOperation(request.identity, (record5) => {
|
|
17498
|
+
if (!matchesFence(record5, request.fence))
|
|
17499
|
+
throw new Error("Lifecycle intent changed before stop work began.");
|
|
17500
|
+
record5.stopWorkStarted = true;
|
|
17501
|
+
});
|
|
17502
|
+
if (request.options.repair && (!retained?.stopBaseline || request.options.delete || request.identity.provider !== "devsy"))
|
|
17503
|
+
throw new Error("Stop recovery requires a retained Devsy baseline and preserves data.");
|
|
16398
17504
|
if (retained?.stopBaseline) {
|
|
16399
|
-
stopBaselineState =
|
|
16400
|
-
withMutationLock(
|
|
17505
|
+
stopBaselineState = withMutationLock(
|
|
16401
17506
|
"Verify retained stop",
|
|
16402
17507
|
request.repoPath,
|
|
16403
|
-
() =>
|
|
17508
|
+
() => {
|
|
17509
|
+
let needsRecovery = false;
|
|
17510
|
+
if (request.options.repair) {
|
|
17511
|
+
try {
|
|
17512
|
+
proveManagedStop(retained);
|
|
17513
|
+
} catch {
|
|
17514
|
+
needsRecovery = true;
|
|
17515
|
+
}
|
|
17516
|
+
}
|
|
17517
|
+
const candidate = needsRecovery ? recoverManagedStopBaseline(
|
|
17518
|
+
retained,
|
|
17519
|
+
() => {
|
|
17520
|
+
const runtime = loadRuntimeConfig(
|
|
17521
|
+
request.repoPath,
|
|
17522
|
+
request.identity.workspace ?? "",
|
|
17523
|
+
retained.profile
|
|
17524
|
+
);
|
|
17525
|
+
return inspectManagedDevcontainerConfig({
|
|
17526
|
+
repoPath: request.repoPath,
|
|
17527
|
+
config: runtime.config,
|
|
17528
|
+
profile: runtime.resolvedProfile,
|
|
17529
|
+
linked: isLinkedWorktree(request.repoPath)
|
|
17530
|
+
});
|
|
17531
|
+
},
|
|
17532
|
+
markStopWorkStarted
|
|
17533
|
+
) : retained;
|
|
17534
|
+
proveManagedStop(candidate);
|
|
17535
|
+
return candidate;
|
|
17536
|
+
}
|
|
16404
17537
|
);
|
|
16405
17538
|
} else {
|
|
16406
17539
|
const containers = inspectWorkspaceContainers();
|
|
@@ -16418,11 +17551,7 @@ async function executeLifecycleWorker(request, operation) {
|
|
|
16418
17551
|
];
|
|
16419
17552
|
for (const project of stopProjects) inspectManagedStopContainers(project);
|
|
16420
17553
|
}
|
|
16421
|
-
|
|
16422
|
-
if (!matchesFence(record5, request.fence))
|
|
16423
|
-
throw new Error("Lifecycle intent changed before stop work began.");
|
|
16424
|
-
record5.stopWorkStarted = true;
|
|
16425
|
-
});
|
|
17554
|
+
markStopWorkStarted();
|
|
16426
17555
|
} else {
|
|
16427
17556
|
updateReliabilityOperation(request.identity, (record5) => {
|
|
16428
17557
|
stepRecord(record5, {
|
|
@@ -16627,7 +17756,7 @@ async function devsyExecOutcome(repoPath, command, retainedProof) {
|
|
|
16627
17756
|
const launch = () => {
|
|
16628
17757
|
if (retainedProof) revalidateDevsyExecProof(repoPath, retainedProof);
|
|
16629
17758
|
claimLifecycleEffect();
|
|
16630
|
-
return (0,
|
|
17759
|
+
return (0, import_node_child_process32.spawn)("devsy", args, { stdio: "inherit" });
|
|
16631
17760
|
};
|
|
16632
17761
|
child = retainedProof ? withMutationLock("Execute retained workspace", repoPath, launch) : launch();
|
|
16633
17762
|
} catch (error) {
|
|
@@ -16675,7 +17804,7 @@ function ensureGuidance2(repoPath) {
|
|
|
16675
17804
|
return `Run 'devrouter ensure ${repoPath}' first.`;
|
|
16676
17805
|
}
|
|
16677
17806
|
function assertRunningDevpod(devpodId, repoPath) {
|
|
16678
|
-
const result = (0,
|
|
17807
|
+
const result = (0, import_node_child_process33.spawnSync)("devpod", ["status", devpodId, "--output", "json"], {
|
|
16679
17808
|
encoding: "utf-8"
|
|
16680
17809
|
});
|
|
16681
17810
|
if (result.status !== 0) {
|
|
@@ -16722,7 +17851,7 @@ async function devpodExecOutcome(repoPath, command) {
|
|
|
16722
17851
|
}
|
|
16723
17852
|
assertRunningDevpod(devpod.id, repoPath);
|
|
16724
17853
|
const workspaceDirectory = resolveWorkspaceDirectory(repoPath);
|
|
16725
|
-
const statusMarker = `__DEVROUTER_EXIT_${(0,
|
|
17854
|
+
const statusMarker = `__DEVROUTER_EXIT_${(0, import_node_crypto24.randomUUID)()}__:`;
|
|
16726
17855
|
const statusMarkerBytes = Buffer.from(statusMarker, "ascii");
|
|
16727
17856
|
const literalCommand = command.map(quotePosixArg).join(" ");
|
|
16728
17857
|
const wrappedCommand = `${literalCommand}; __devrouter_status=$?; printf '${statusMarker}%s\\n' "$__devrouter_status" >&2; exit 0`;
|
|
@@ -16854,7 +17983,7 @@ async function devpodExecOutcome(repoPath, command) {
|
|
|
16854
17983
|
let child;
|
|
16855
17984
|
try {
|
|
16856
17985
|
claimLifecycleEffect();
|
|
16857
|
-
child = (0,
|
|
17986
|
+
child = (0, import_node_child_process33.spawn)("devpod", args, { stdio: ["inherit", "inherit", "pipe"] });
|
|
16858
17987
|
} catch (error) {
|
|
16859
17988
|
settled = true;
|
|
16860
17989
|
reject(
|
|
@@ -16911,7 +18040,7 @@ async function devpodExecOutcome(repoPath, command) {
|
|
|
16911
18040
|
}
|
|
16912
18041
|
|
|
16913
18042
|
// src/core/workspace-lifecycle.ts
|
|
16914
|
-
var
|
|
18043
|
+
var import_node_child_process34 = require("child_process");
|
|
16915
18044
|
var import_node_fs37 = __toESM(require("fs"));
|
|
16916
18045
|
var import_node_path42 = __toESM(require("path"));
|
|
16917
18046
|
|