machine-bridge-mcp 1.1.5 → 1.2.1
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/CHANGELOG.md +21 -0
- package/CODE_OF_CONDUCT.md +24 -0
- package/CONTRIBUTING.md +3 -1
- package/GOVERNANCE.md +50 -0
- package/README.md +26 -0
- package/SUPPORT.md +31 -0
- package/browser-extension/browser-operations.js +10 -4
- package/browser-extension/devtools-input.js +2 -2
- package/browser-extension/manifest.json +2 -2
- package/browser-extension/page-automation.js +1 -1
- package/docs/ARCHITECTURE.md +14 -17
- package/docs/AUDIT.md +28 -0
- package/docs/ENGINEERING.md +8 -2
- package/docs/PROJECT_STANDARDS.md +4 -2
- package/docs/TESTING.md +9 -3
- package/docs/UPGRADING.md +32 -0
- package/package.json +12 -4
- package/scripts/coverage-check.mjs +27 -10
- package/src/local/account-access.mjs +7 -5
- package/src/local/agent-context.mjs +7 -148
- package/src/local/agent-contract.mjs +206 -0
- package/src/local/browser-bridge.mjs +30 -281
- package/src/local/browser-extension-protocol.mjs +19 -4
- package/src/local/browser-operation-service.mjs +325 -0
- package/src/local/call-registry.mjs +44 -1
- package/src/local/capability-ranking.mjs +13 -0
- package/src/local/cli-local-admin.mjs +74 -38
- package/src/local/cli-options.mjs +18 -18
- package/src/local/cli-policy.mjs +1 -1
- package/src/local/cli-service.mjs +132 -0
- package/src/local/cli.mjs +27 -109
- package/src/local/daemon-process.mjs +1 -1
- package/src/local/full-access-test.mjs +1 -1
- package/src/local/job-runner.mjs +9 -3
- package/src/local/managed-job-plan.mjs +3 -3
- package/src/local/monotonic-deadline.mjs +7 -0
- package/src/local/numbers.mjs +8 -0
- package/src/local/policy.mjs +97 -16
- package/src/local/project-metadata.mjs +7 -1
- package/src/local/project-package.mjs +1 -1
- package/src/local/records.mjs +6 -0
- package/src/local/resource-operations.mjs +1 -1
- package/src/local/runtime-capabilities.mjs +66 -0
- package/src/local/runtime-diagnostics.mjs +91 -0
- package/src/local/runtime-reporting.mjs +113 -0
- package/src/local/runtime.mjs +54 -191
- package/src/local/service-convergence.mjs +1 -1
- package/src/local/service.mjs +1 -1
- package/src/local/state.mjs +1 -1
- package/src/local/windows-service.mjs +1 -1
- package/src/local/worker-deployment.mjs +15 -10
- package/src/local/worker-health.mjs +8 -4
- package/src/worker/access.ts +8 -7
- package/src/worker/account-admin.ts +2 -2
- package/src/worker/authority.ts +3 -3
- package/src/worker/http.ts +2 -2
- package/src/worker/index.ts +29 -337
- package/src/worker/oauth-controller.ts +343 -0
- package/src/worker/oauth-state.ts +1 -1
- package/src/worker/oauth-tokens.ts +2 -2
- package/src/worker/tool-catalog.ts +1 -1
- package/tsconfig.json +2 -1
- package/tsconfig.local.json +27 -0
package/src/local/runtime.mjs
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
|
-
import { randomBytes } from "node:crypto";
|
|
2
1
|
import { mkdirSync, mkdtempSync, realpathSync, rmSync } from "node:fs";
|
|
3
|
-
import { lstat, realpath,
|
|
2
|
+
import { lstat, realpath, stat } from "node:fs/promises";
|
|
4
3
|
import { tmpdir } from "node:os";
|
|
5
4
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
6
5
|
import { RelayConnection } from "./relay-connection.mjs";
|
|
7
6
|
import { ProcessSessionManager } from "./process-sessions.mjs";
|
|
8
7
|
export { MAX_COMMAND_BYTES } from "./process-sessions.mjs";
|
|
9
|
-
import {
|
|
8
|
+
import { MCP_SUPPORTED_PROTOCOL_VERSIONS, normalizePolicy, PolicyGate, SERVER_NAME } from "./tools.mjs";
|
|
10
9
|
import { publicError } from "./errors.mjs";
|
|
11
10
|
import { ProcessTracker } from "./process-tracker.mjs";
|
|
12
11
|
import { CallRegistry } from "./call-registry.mjs";
|
|
@@ -15,7 +14,7 @@ import { ToolExecutor } from "./tool-executor.mjs";
|
|
|
15
14
|
import { boundedErrorMessage, ProcessExecutionService } from "./process-execution.mjs";
|
|
16
15
|
import { GitService } from "./git-service.mjs";
|
|
17
16
|
import { LifecycleController } from "./lifecycle.mjs";
|
|
18
|
-
import { MAX_WRITE_BYTES,
|
|
17
|
+
import { MAX_WRITE_BYTES, sha256, WorkspaceFileService } from "./workspace-file-service.mjs";
|
|
19
18
|
export { MAX_WRITE_BYTES, sha256 } from "./workspace-file-service.mjs";
|
|
20
19
|
import { classifyOperationalError } from "./log.mjs";
|
|
21
20
|
import { inspectResourceFile, ManagedJobManager } from "./managed-jobs.mjs";
|
|
@@ -29,6 +28,12 @@ import { readBoundedRegularFileSync } from "./secure-file.mjs";
|
|
|
29
28
|
import { clampInteger } from "./numbers.mjs";
|
|
30
29
|
import { isPlainRecord } from "./records.mjs";
|
|
31
30
|
import { AccountAccessGate, normalizeAccountRole } from "./account-access.mjs";
|
|
31
|
+
import { buildProjectOverview, buildRuntimeInfo } from "./runtime-reporting.mjs";
|
|
32
|
+
import { diagnoseRuntime as runRuntimeDiagnostics } from "./runtime-diagnostics.mjs";
|
|
33
|
+
import {
|
|
34
|
+
resolveTaskCapabilities as resolveRuntimeTaskCapabilities,
|
|
35
|
+
sessionBootstrap as buildRuntimeSessionBootstrap,
|
|
36
|
+
} from "./runtime-capabilities.mjs";
|
|
32
37
|
|
|
33
38
|
const MAX_WS_MESSAGE_BYTES = 8 * 1024 * 1024;
|
|
34
39
|
const MAX_CONCURRENT_TOOL_CALLS = 16;
|
|
@@ -232,53 +237,21 @@ export class LocalRuntime {
|
|
|
232
237
|
}
|
|
233
238
|
|
|
234
239
|
runtimeInfo() {
|
|
235
|
-
return {
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
supported_protocol_versions: MCP_SUPPORTED_PROTOCOL_VERSIONS,
|
|
239
|
-
workspace: this.displayPath(this.workspace),
|
|
240
|
-
workspace_name: this.policy.exposeAbsolutePaths ? basename(this.workspace) : "workspace",
|
|
240
|
+
return buildRuntimeInfo({
|
|
241
|
+
workspace: this.workspace,
|
|
242
|
+
displayPath: (value) => this.displayPath(value),
|
|
241
243
|
policy: this.policy,
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
tool_delivery: {
|
|
254
|
-
full_profile_scope: "local-daemon-and-relay-advertisement",
|
|
255
|
-
daemon_advertised_tool_count: this.tools().length + 1,
|
|
256
|
-
host_exposed_tools_known_to_server: false,
|
|
257
|
-
host_may_expose_subset: true,
|
|
258
|
-
},
|
|
259
|
-
tools: ["server_info", ...this.tools()],
|
|
260
|
-
observability: {
|
|
261
|
-
relay_readiness: "authenticated-hello-acknowledged",
|
|
262
|
-
brief_relay_interruptions: "debug-only",
|
|
263
|
-
raw_transport_details: "debug-only",
|
|
264
|
-
per_tool_events: "structured-debug-events",
|
|
265
|
-
default_logs_include_tool_failures: false,
|
|
266
|
-
tool_arguments_or_results_logged: false,
|
|
267
|
-
capability_routing: this.capabilityObserver.snapshot(),
|
|
268
|
-
tool_calls: this.observability.snapshot(),
|
|
269
|
-
in_flight_calls: this.callRegistry.snapshot(),
|
|
270
|
-
},
|
|
271
|
-
runtime: {
|
|
272
|
-
environment: this.policy.minimalEnv ? "isolated-minimal" : "full-parent",
|
|
273
|
-
lifecycle: this.lifecycle.snapshot(),
|
|
274
|
-
relay: this.relay?.status?.() || null,
|
|
275
|
-
runtime_dir: this.policy.exposeAbsolutePaths ? this.runtimeDir : "<private-runtime-dir>",
|
|
276
|
-
processes: this.processTracker.snapshot(),
|
|
277
|
-
process_sessions: this.processSessionManager.status(),
|
|
278
|
-
managed_jobs: this.managedJobManager.status(),
|
|
279
|
-
local_resources: this.managedJobManager.resourceInfo(),
|
|
280
|
-
},
|
|
281
|
-
};
|
|
244
|
+
toolNames: this.tools(),
|
|
245
|
+
capabilityObserver: this.capabilityObserver,
|
|
246
|
+
observability: this.observability,
|
|
247
|
+
callRegistry: this.callRegistry,
|
|
248
|
+
lifecycle: this.lifecycle,
|
|
249
|
+
relayStatus: () => this.relay?.status?.() || null,
|
|
250
|
+
runtimeDir: this.runtimeDir,
|
|
251
|
+
processTracker: this.processTracker,
|
|
252
|
+
processSessionManager: this.processSessionManager,
|
|
253
|
+
managedJobManager: this.managedJobManager,
|
|
254
|
+
});
|
|
282
255
|
}
|
|
283
256
|
|
|
284
257
|
async start() {
|
|
@@ -426,18 +399,17 @@ export class LocalRuntime {
|
|
|
426
399
|
}
|
|
427
400
|
|
|
428
401
|
async projectOverview(context = {}) {
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
return {
|
|
433
|
-
workspace: this.displayPath(this.workspace),
|
|
434
|
-
workspaceName: this.policy.exposeAbsolutePaths ? basename(this.workspace) : "workspace",
|
|
435
|
-
gitRoot: git.code === 0 ? this.displayPath(git.stdout.trim()) : "",
|
|
402
|
+
return buildProjectOverview({
|
|
403
|
+
workspace: this.workspace,
|
|
404
|
+
displayPath: (value) => this.displayPath(value),
|
|
436
405
|
policy: this.policy,
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
406
|
+
toolNames: this.tools(),
|
|
407
|
+
capabilityObserver: this.capabilityObserver,
|
|
408
|
+
listTopLevel: (callContext) => this.listDir(".", callContext),
|
|
409
|
+
runProcess: (...args) => this.runProcess(...args),
|
|
410
|
+
safeErrorMessage: (error) => this.safeErrorMessage(error),
|
|
411
|
+
throwIfCancelled: (callContext) => this.throwIfCancelled(callContext),
|
|
412
|
+
}, context);
|
|
441
413
|
}
|
|
442
414
|
|
|
443
415
|
listRoots() { return this.workspaceFileService.listRoots(); }
|
|
@@ -475,82 +447,15 @@ export class LocalRuntime {
|
|
|
475
447
|
}
|
|
476
448
|
|
|
477
449
|
async diagnoseRuntime(context = {}) {
|
|
478
|
-
|
|
479
|
-
const checks = [];
|
|
480
|
-
checks.push({
|
|
481
|
-
layer: "mcp-host-to-daemon",
|
|
482
|
-
ok: true,
|
|
483
|
-
detail: "This diagnostic request reached the local Machine Bridge runtime.",
|
|
484
|
-
});
|
|
485
|
-
checks.push({
|
|
486
|
-
layer: "machine-bridge-policy",
|
|
487
|
-
ok: this.policy.execMode === "direct" || this.policy.execMode === "shell",
|
|
488
|
-
detail: `profile=${this.policy.profile}; exec_mode=${this.policy.execMode}; unrestricted_paths=${this.policy.unrestrictedPaths}`,
|
|
489
|
-
});
|
|
490
|
-
|
|
491
|
-
const probe = join(this.runtimeDir, `.diagnostic-${process.pid}-${randomBytes(6).toString("hex")}`);
|
|
492
|
-
try {
|
|
493
|
-
await writeFile(probe, "ok\n", { mode: 0o600, flag: "wx" });
|
|
494
|
-
const { buffer } = await readBoundedFile(probe, 64, "diagnostic file");
|
|
495
|
-
checks.push({ layer: "local-filesystem", ok: buffer.toString("utf8") === "ok\n", error_class: null });
|
|
496
|
-
} catch (error) {
|
|
497
|
-
checks.push({ layer: "local-filesystem", ok: false, error_class: classifyOperationalError(error) });
|
|
498
|
-
} finally {
|
|
499
|
-
await rm(probe, { force: true }).catch(() => {});
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
if (this.policy.execMode === "direct" || this.policy.execMode === "shell") {
|
|
503
|
-
const direct = await this.runProcess(
|
|
504
|
-
process.execPath,
|
|
505
|
-
["-e", "process.stdout.write('ok')"],
|
|
506
|
-
5000,
|
|
507
|
-
true,
|
|
508
|
-
1024,
|
|
509
|
-
context,
|
|
510
|
-
this.workspace,
|
|
511
|
-
).catch((error) => ({ code: 127, stdout: "", stderr: "", error_class: classifyOperationalError(error) }));
|
|
512
|
-
checks.push({
|
|
513
|
-
layer: "local-process-spawn",
|
|
514
|
-
ok: direct.code === 0 && direct.stdout === "ok",
|
|
515
|
-
error_class: direct.error_class || (direct.code === 0 ? null : classifyOperationalError(direct.stderr || direct.stdout || "execution failed")),
|
|
516
|
-
});
|
|
517
|
-
} else {
|
|
518
|
-
checks.push({ layer: "local-process-spawn", ok: false, skipped: true, error_class: "policy_denied" });
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
if (this.policy.execMode === "shell") {
|
|
522
|
-
const result = await this.processExecutionService.probeShell(context)
|
|
523
|
-
.catch((error) => ({ code: 127, error_class: classifyOperationalError(error) }));
|
|
524
|
-
checks.push({
|
|
525
|
-
layer: "local-shell",
|
|
526
|
-
ok: result.code === 0,
|
|
527
|
-
error_class: result.error_class || (result.code === 0 ? null : classifyOperationalError(result.stderr || result.stdout || "execution failed")),
|
|
528
|
-
});
|
|
529
|
-
} else {
|
|
530
|
-
checks.push({ layer: "local-shell", ok: false, skipped: true, error_class: "policy_denied" });
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
const storage = this.managedJobManager.diagnoseStorage();
|
|
534
|
-
checks.push({ layer: "managed-job-storage", ...storage });
|
|
535
|
-
const resources = this.managedJobManager.listResources();
|
|
536
|
-
checks.push({
|
|
537
|
-
layer: "local-resource-registry",
|
|
538
|
-
ok: resources.resources.every((resource) => resource.available),
|
|
539
|
-
registered: resources.count,
|
|
540
|
-
unavailable: resources.resources.filter((resource) => !resource.available).map((resource) => ({ name: resource.name, error_class: resource.error_class })),
|
|
541
|
-
});
|
|
542
|
-
|
|
543
|
-
return {
|
|
544
|
-
request_reached_local_runtime: true,
|
|
545
|
-
interpretation: {
|
|
546
|
-
tool_call_blocked_before_response: "host/platform or connector gateway",
|
|
547
|
-
diagnostic_reached_daemon_but_spawn_failed: "local OS, endpoint security, shell configuration, or Machine Bridge policy",
|
|
548
|
-
managed_job_accepted_then_later_tools_blocked: "job continues independently; inspect with local CLI or a later read_job call",
|
|
549
|
-
},
|
|
450
|
+
return runRuntimeDiagnostics({
|
|
550
451
|
policy: this.policy,
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
452
|
+
runtimeDir: this.runtimeDir,
|
|
453
|
+
workspace: this.workspace,
|
|
454
|
+
runProcess: (...args) => this.runProcess(...args),
|
|
455
|
+
probeShell: (callContext) => this.processExecutionService.probeShell(callContext),
|
|
456
|
+
managedJobManager: this.managedJobManager,
|
|
457
|
+
throwIfCancelled: (callContext) => this.throwIfCancelled(callContext),
|
|
458
|
+
}, context);
|
|
554
459
|
}
|
|
555
460
|
|
|
556
461
|
async generateSshKeyResource(args = {}, context = {}) {
|
|
@@ -588,48 +493,27 @@ export class LocalRuntime {
|
|
|
588
493
|
};
|
|
589
494
|
}
|
|
590
495
|
|
|
591
|
-
|
|
592
496
|
async sessionBootstrap(args = {}, context = {}) {
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
status_tool: "browser_status",
|
|
600
|
-
} : null,
|
|
601
|
-
};
|
|
602
|
-
this.capabilityObserver.recordBootstrap(bootstrap);
|
|
603
|
-
return bootstrap;
|
|
497
|
+
return buildRuntimeSessionBootstrap({
|
|
498
|
+
agentContextManager: this.agentContextManager,
|
|
499
|
+
appAutomationManager: this.appAutomationManager,
|
|
500
|
+
capabilityObserver: this.capabilityObserver,
|
|
501
|
+
policy: this.policy,
|
|
502
|
+
}, args, context);
|
|
604
503
|
}
|
|
605
504
|
|
|
606
505
|
async resolveTaskCapabilities(args = {}, context = {}) {
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
.map((application) => ({ application, score: applicationMatchScore(lower, application) }))
|
|
614
|
-
.filter((item) => item.score > 0)
|
|
615
|
-
.sort((left, right) => right.score - left.score || left.application.name.localeCompare(right.application.name))
|
|
616
|
-
.slice(0, 20)
|
|
617
|
-
.map(({ application, score }) => ({ ...application, score }));
|
|
618
|
-
} else {
|
|
619
|
-
result.application_matches = [];
|
|
620
|
-
}
|
|
621
|
-
if (result.application_matches.length) {
|
|
622
|
-
result.recommended_tools = [...new Set([...result.recommended_tools, "list_local_applications", "open_local_application", "inspect_local_application", "operate_local_application"])];
|
|
623
|
-
}
|
|
624
|
-
result.browser_backend = this.policy.profile === "full" ? { tool: "browser_status", existing_profile: true, extension_bridge: true } : null;
|
|
625
|
-
result.routing_observability = "Call server_info or project_overview to verify that bootstrap and task capability resolution reached the local runtime.";
|
|
626
|
-
this.capabilityObserver.recordResolution(task, result);
|
|
627
|
-
return result;
|
|
506
|
+
return resolveRuntimeTaskCapabilities({
|
|
507
|
+
agentContextManager: this.agentContextManager,
|
|
508
|
+
appAutomationManager: this.appAutomationManager,
|
|
509
|
+
capabilityObserver: this.capabilityObserver,
|
|
510
|
+
policy: this.policy,
|
|
511
|
+
}, args, context);
|
|
628
512
|
}
|
|
629
513
|
|
|
630
514
|
readLocalResourceBinary(name) {
|
|
631
515
|
const registry = this.managedJobManager.currentResources();
|
|
632
|
-
const resource = registry[name];
|
|
516
|
+
const resource = Object.hasOwn(registry, name) ? registry[name] : null;
|
|
633
517
|
if (!resource) throw new Error(`unknown local resource: ${name}`);
|
|
634
518
|
const inspected = inspectResourceFile(resource.path, { allowInsecurePermissions: resource.allowInsecurePermissions === true });
|
|
635
519
|
if (inspected.size > 1024 * 1024) throw new Error("local resource exceeds 1 MiB browser injection limit");
|
|
@@ -749,25 +633,6 @@ export class LocalRuntime {
|
|
|
749
633
|
}
|
|
750
634
|
}
|
|
751
635
|
|
|
752
|
-
function applicationMatchScore(task, application) {
|
|
753
|
-
const name = String(application.name || "").toLowerCase();
|
|
754
|
-
const id = String(application.id || "").toLowerCase();
|
|
755
|
-
if (!name) return 0;
|
|
756
|
-
if (task.includes(name)) return 10 + Math.min(name.length, 20);
|
|
757
|
-
const words = name.split(/[^\p{L}\p{N}]+/u).filter((word) => word.length >= 2);
|
|
758
|
-
return words.reduce((score, word) => score + (task.includes(word) ? 2 : 0), id && task.includes(id) ? 5 : 0);
|
|
759
|
-
}
|
|
760
|
-
|
|
761
|
-
function policyMatchesNamedProfile(policy) {
|
|
762
|
-
const named = POLICY_PROFILES[policy.profile];
|
|
763
|
-
if (!named) return false;
|
|
764
|
-
return policy.allowWrite === named.allowWrite
|
|
765
|
-
&& policy.execMode === named.execMode
|
|
766
|
-
&& policy.unrestrictedPaths === named.unrestrictedPaths
|
|
767
|
-
&& policy.minimalEnv === named.minimalEnv
|
|
768
|
-
&& policy.exposeAbsolutePaths === named.exposeAbsolutePaths;
|
|
769
|
-
}
|
|
770
|
-
|
|
771
636
|
function stateRootFromProfileStatePath(statePath) {
|
|
772
637
|
const absolute = resolve(statePath);
|
|
773
638
|
if (basename(absolute) !== "state.json") throw new Error("local resource state path is invalid");
|
|
@@ -854,8 +719,6 @@ function normalizeRelayAuthorization(value) {
|
|
|
854
719
|
return Object.freeze({ account_id: accountId, account_version: accountVersion, role });
|
|
855
720
|
}
|
|
856
721
|
|
|
857
|
-
|
|
858
|
-
|
|
859
722
|
function collectToolPathCandidates(error, toolArgs, workspace) {
|
|
860
723
|
const candidates = new Set();
|
|
861
724
|
for (const value of [error?.path, error?.dest]) if (typeof value === "string" && value) candidates.add(value);
|
package/src/local/service.mjs
CHANGED
|
@@ -48,7 +48,7 @@ export async function uninstallAutostart({ stateRoot, logger = console } = {}) {
|
|
|
48
48
|
return uninstallSystemd(logger);
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
export async function autostartStatus(
|
|
51
|
+
export async function autostartStatus() {
|
|
52
52
|
if (process.platform === "darwin") return statusLaunchd();
|
|
53
53
|
if (process.platform === "win32") return statusWindowsTask({ run: serviceRun });
|
|
54
54
|
return statusSystemd();
|
package/src/local/state.mjs
CHANGED
|
@@ -279,7 +279,7 @@ export async function acquireStartupLockWithWait(state, options = {}) {
|
|
|
279
279
|
logger.info?.(`waiting for ${ownerPid} to finish the current startup/state operation`);
|
|
280
280
|
const deadline = createMonotonicDeadline(timeoutMs);
|
|
281
281
|
while (!deadline.expired()) {
|
|
282
|
-
await new Promise((resolvePromise) => setTimeout(resolvePromise, Math.min(pollMs, Math.max(1, deadline.remainingMs()))));
|
|
282
|
+
await new Promise((resolvePromise) => { setTimeout(resolvePromise, Math.min(pollMs, Math.max(1, deadline.remainingMs()))); });
|
|
283
283
|
lock = acquireStartupLock(state, metadata);
|
|
284
284
|
if (lock.acquired) {
|
|
285
285
|
logger.info?.("the previous startup/state operation finished; continuing");
|
|
@@ -220,7 +220,7 @@ function windowsServiceFailureReason(command, status) {
|
|
|
220
220
|
async function waitForWindowsStatus(predicate, options) {
|
|
221
221
|
const attempts = boundedPositiveInteger(options.statusAttempts, 10);
|
|
222
222
|
const delayMs = boundedPositiveInteger(options.statusDelayMs, 100);
|
|
223
|
-
const sleep = typeof options.sleep === "function" ? options.sleep : milliseconds => new Promise(resolve => setTimeout(resolve, milliseconds));
|
|
223
|
+
const sleep = typeof options.sleep === "function" ? options.sleep : milliseconds => new Promise(resolve => { setTimeout(resolve, milliseconds); });
|
|
224
224
|
let status = await statusWindowsTask(options);
|
|
225
225
|
for (let index = 1; index < attempts && !predicate(status); index += 1) {
|
|
226
226
|
await sleep(delayMs);
|
|
@@ -5,6 +5,7 @@ import { runWrangler } from "./shell.mjs";
|
|
|
5
5
|
import { packageRoot, saveState } from "./state.mjs";
|
|
6
6
|
import { withWorkerSecretsFile } from "./worker-secret-file.mjs";
|
|
7
7
|
import {
|
|
8
|
+
normalizeWorkerOrigin,
|
|
8
9
|
retryWorkerHealth,
|
|
9
10
|
workerHealthRequiresRedeploy,
|
|
10
11
|
workerHealthUserReason,
|
|
@@ -52,7 +53,7 @@ export async function ensureWorkerDeployment(state, args = {}, options = {}) {
|
|
|
52
53
|
"--secrets-file", secretFile,
|
|
53
54
|
], { capture: true }));
|
|
54
55
|
|
|
55
|
-
const detectedUrl = extractWorkerUrl(deploy.stdout) || extractWorkerUrl(deploy.stderr);
|
|
56
|
+
const detectedUrl = extractWorkerUrl(deploy.stdout, state.worker.name) || extractWorkerUrl(deploy.stderr, state.worker.name);
|
|
56
57
|
const recordedUrl = workerUrlMatchesName(state.worker.url, state.worker.name) ? state.worker.url : "";
|
|
57
58
|
const workerUrl = detectedUrl || recordedUrl;
|
|
58
59
|
if (!workerUrl) {
|
|
@@ -93,20 +94,24 @@ export function workerDeploymentFingerprint(state, options = {}) {
|
|
|
93
94
|
return fingerprint.digest("hex");
|
|
94
95
|
}
|
|
95
96
|
|
|
96
|
-
export function extractWorkerUrl(text = "") {
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
const
|
|
100
|
-
|
|
97
|
+
export function extractWorkerUrl(text = "", workerName = "") {
|
|
98
|
+
const candidates = [...String(text).matchAll(/https:\/\/[^\s"'<>]+/g)]
|
|
99
|
+
.map((match) => match[0].replace(/[),.;:!?]+$/, ""));
|
|
100
|
+
for (const candidate of candidates.reverse()) {
|
|
101
|
+
try {
|
|
102
|
+
return normalizeWorkerOrigin(candidate, workerName);
|
|
103
|
+
} catch {
|
|
104
|
+
// Wrangler output may contain unrelated links; only a canonical matching workers.dev origin is deployment evidence.
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return "";
|
|
101
108
|
}
|
|
102
109
|
|
|
103
110
|
export function workerUrlMatchesName(workerUrl, workerName) {
|
|
104
111
|
if (!workerUrl || !workerName) return false;
|
|
105
112
|
try {
|
|
106
|
-
|
|
107
|
-
return
|
|
108
|
-
&& url.hostname.endsWith(".workers.dev")
|
|
109
|
-
&& url.hostname.startsWith(`${String(workerName)}.`);
|
|
113
|
+
normalizeWorkerOrigin(workerUrl, workerName);
|
|
114
|
+
return true;
|
|
110
115
|
} catch {
|
|
111
116
|
return false;
|
|
112
117
|
}
|
|
@@ -53,8 +53,8 @@ export async function retryWorkerHealth(workerUrl, expectedVersion, attempts, op
|
|
|
53
53
|
return last;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
export function
|
|
57
|
-
const base = new URL(
|
|
56
|
+
export function normalizeWorkerOrigin(workerUrl, expectedWorkerName = "") {
|
|
57
|
+
const base = new URL(String(workerUrl));
|
|
58
58
|
const hostname = base.hostname.toLowerCase();
|
|
59
59
|
const expectedName = String(expectedWorkerName || "").toLowerCase();
|
|
60
60
|
if (base.protocol !== "https:" || base.port || base.username || base.password || base.search || base.hash || base.pathname !== "/") {
|
|
@@ -64,7 +64,11 @@ export function workerHealthUrl(workerUrl, expectedWorkerName = "") {
|
|
|
64
64
|
if (expectedName && (!WORKER_NAME.test(expectedName) || hostname.split(".")[0] !== expectedName)) {
|
|
65
65
|
throw new Error("Worker URL hostname does not match the recorded Worker name");
|
|
66
66
|
}
|
|
67
|
-
return `https://${hostname}
|
|
67
|
+
return `https://${hostname}`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function workerHealthUrl(workerUrl, expectedWorkerName = "") {
|
|
71
|
+
return `${normalizeWorkerOrigin(workerUrl, expectedWorkerName)}/healthz`;
|
|
68
72
|
}
|
|
69
73
|
|
|
70
74
|
export function isRetryableWorkerHealthError(value) {
|
|
@@ -242,5 +246,5 @@ function boundedTimeout(value) {
|
|
|
242
246
|
}
|
|
243
247
|
|
|
244
248
|
function sleep(ms) {
|
|
245
|
-
return new Promise((resolvePromise) => setTimeout(resolvePromise, ms));
|
|
249
|
+
return new Promise((resolvePromise) => { setTimeout(resolvePromise, ms); });
|
|
246
250
|
}
|
package/src/worker/access.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import accessContract from "../shared/access-contract.json";
|
|
2
|
-
import policyContract from "../shared/policy-contract.json";
|
|
3
|
-
import toolCatalog from "../shared/tool-catalog.json";
|
|
4
|
-
import { policyAllowsAvailability, type DaemonPolicy } from "./policy";
|
|
1
|
+
import accessContract from "../shared/access-contract.json" with { type: "json" };
|
|
2
|
+
import policyContract from "../shared/policy-contract.json" with { type: "json" };
|
|
3
|
+
import toolCatalog from "../shared/tool-catalog.json" with { type: "json" };
|
|
4
|
+
import { policyAllowsAvailability, type DaemonPolicy } from "./policy.ts";
|
|
5
5
|
|
|
6
6
|
export type AccountRole = keyof typeof accessContract.roles;
|
|
7
7
|
|
|
@@ -11,16 +11,17 @@ const toolAvailability = new Map((toolCatalog as Array<{ name: string; availabil
|
|
|
11
11
|
|
|
12
12
|
export const ACCOUNT_ACCESS_REVISION = Number(accessContract.revision);
|
|
13
13
|
export const ACCOUNT_ROLES = Object.freeze(Object.keys(roles));
|
|
14
|
+
export const DEFAULT_ACCOUNT_ROLE = String(accessContract.defaultRole) as AccountRole;
|
|
14
15
|
export const OWNER_ACCOUNT_ROLE = String(accessContract.ownerRole) as AccountRole;
|
|
15
16
|
|
|
16
17
|
export function normalizeAccountRole(value: unknown): AccountRole | null {
|
|
17
18
|
const role = String(value ?? "").trim().toLowerCase();
|
|
18
|
-
return role
|
|
19
|
+
return Object.hasOwn(roles, role) ? role as AccountRole : null;
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
export function accountRolePolicy(role: AccountRole): DaemonPolicy {
|
|
22
|
-
const profileName = roles[role]
|
|
23
|
-
const profile = profiles[profileName];
|
|
23
|
+
const profileName = Object.hasOwn(roles, role) ? roles[role].profile : undefined;
|
|
24
|
+
const profile = profileName && Object.hasOwn(profiles, profileName) ? profiles[profileName] : undefined;
|
|
24
25
|
if (!profile) throw new Error(`account role references an unknown policy profile: ${role}`);
|
|
25
26
|
return {
|
|
26
27
|
profile: String(profile.profile),
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { json, methodNotAllowed, parseRequestBody } from "./http";
|
|
1
|
+
import { json, methodNotAllowed, parseRequestBody } from "./http.ts";
|
|
2
2
|
import {
|
|
3
3
|
accountByName, createAccount, publicAccount, replaceAccountPassword, revokeAccountCredentials,
|
|
4
4
|
safeEqual, updateAccount, type AccountRecord, type OAuthStore,
|
|
5
|
-
} from "./oauth-state";
|
|
5
|
+
} from "./oauth-state.ts";
|
|
6
6
|
|
|
7
7
|
const BODY_LIMIT_BYTES = 64 * 1024;
|
|
8
8
|
const MAX_ACCOUNTS = 64;
|
package/src/worker/authority.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import policyContract from "../shared/policy-contract.json";
|
|
2
|
-
import { accountRolePolicy, accountRoleToolNames, type AccountRole } from "./access";
|
|
3
|
-
import type { DaemonPolicy } from "./policy";
|
|
1
|
+
import policyContract from "../shared/policy-contract.json" with { type: "json" };
|
|
2
|
+
import { accountRolePolicy, accountRoleToolNames, type AccountRole } from "./access.ts";
|
|
3
|
+
import type { DaemonPolicy } from "./policy.ts";
|
|
4
4
|
|
|
5
5
|
type EffectivePolicy = {
|
|
6
6
|
scope: "authenticated_account_effective_authority";
|
package/src/worker/http.ts
CHANGED
|
@@ -218,9 +218,9 @@ export function searchParamsEntries(params: URLSearchParams): Array<[string, str
|
|
|
218
218
|
}
|
|
219
219
|
|
|
220
220
|
export function searchParamsObject(params: URLSearchParams): Record<string, unknown> {
|
|
221
|
-
const out
|
|
221
|
+
const out = Object.create(null) as Record<string, unknown>;
|
|
222
222
|
params.forEach((value, key) => {
|
|
223
|
-
if (out
|
|
223
|
+
if (!Object.hasOwn(out, key)) out[key] = value;
|
|
224
224
|
else if (Array.isArray(out[key])) (out[key] as string[]).push(value);
|
|
225
225
|
else out[key] = [out[key] as string, value];
|
|
226
226
|
});
|