machine-bridge-mcp 0.9.0 → 0.10.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/CHANGELOG.md +21 -0
- package/README.md +44 -23
- package/SECURITY.md +13 -1
- package/browser-extension/manifest.json +33 -0
- package/browser-extension/page-automation.js +246 -0
- package/browser-extension/pairing.js +19 -0
- package/browser-extension/service-worker.js +406 -0
- package/docs/AGENT_CONTEXT.md +65 -131
- package/docs/ARCHITECTURE.md +29 -9
- package/docs/CLIENTS.md +8 -0
- package/docs/ENGINEERING.md +5 -1
- package/docs/LOCAL_AUTOMATION.md +111 -0
- package/docs/LOGGING.md +9 -1
- package/docs/OPERATIONS.md +30 -0
- package/docs/PRIVACY.md +2 -1
- package/docs/RELEASING.md +2 -2
- package/docs/TESTING.md +6 -5
- package/package.json +13 -7
- package/scripts/sync-worker-version.mjs +27 -12
- package/src/local/agent-context.mjs +163 -9
- package/src/local/app-automation.mjs +400 -0
- package/src/local/browser-bridge.mjs +757 -0
- package/src/local/cli.mjs +85 -0
- package/src/local/runtime.mjs +114 -4
- package/src/local/state.mjs +1 -1
- package/src/local/stdio.mjs +12 -7
- package/src/shared/server-metadata.json +4 -1
- package/src/shared/tool-catalog.json +853 -0
- package/src/worker/index.ts +24 -4
package/docs/RELEASING.md
CHANGED
|
@@ -6,7 +6,7 @@ The release invariant is:
|
|
|
6
6
|
- `v<package version>` points to that same commit locally and on GitHub.
|
|
7
7
|
- A final GitHub Release exists for the tag.
|
|
8
8
|
- The GitHub Release contains the npm tarball generated from that commit.
|
|
9
|
-
- `package.json`, `package-lock.json`,
|
|
9
|
+
- `package.json`, `package-lock.json`, the Worker-reported version, and `browser-extension/manifest.json` (`version`/`version_name`) agree.
|
|
10
10
|
- Every release-relevant change since the prior version tag has a higher package version and matching CHANGELOG section.
|
|
11
11
|
- The same reviewed change is present on GitHub and in a new npm version.
|
|
12
12
|
|
|
@@ -14,7 +14,7 @@ The release invariant is:
|
|
|
14
14
|
|
|
15
15
|
## Prepare a version
|
|
16
16
|
|
|
17
|
-
1. Set the new version without creating an automatic npm tag:
|
|
17
|
+
1. Set the new version without creating an automatic npm tag. The npm version hook synchronizes the Worker and packaged browser-extension versions:
|
|
18
18
|
|
|
19
19
|
```sh
|
|
20
20
|
npm version <version> --no-git-tag-version
|
package/docs/TESTING.md
CHANGED
|
@@ -17,7 +17,8 @@ The suite includes:
|
|
|
17
17
|
- generated Cloudflare Worker types and strict TypeScript checking, including unused-local and unused-parameter rejection;
|
|
18
18
|
- syntax validation for every shipped JavaScript entry point;
|
|
19
19
|
- shared tool-catalog schema, annotation, and profile-inventory checks;
|
|
20
|
-
- hierarchical agent-context precedence,
|
|
20
|
+
- global `model_instructions_file` injection in stdio/remote initialization, hierarchical agent-context precedence, live skill rescanning/fingerprints, automatic task ranking/loading, command override/removal, direct argv handling, timeout ceilings, and execution-profile denial;
|
|
21
|
+
- machine-level browser-broker ownership/client proxying, authenticated extension origin/subprotocol, non-cacheable local pairing, pairing-token non-disclosure, resource-backed upload routing, and broker result redaction;
|
|
21
22
|
- canonical path and symbolic-link escape tests;
|
|
22
23
|
- relative-path privacy and error-path redaction tests;
|
|
23
24
|
- atomic create/update, optimistic hash, exact edit, and patch transaction tests;
|
|
@@ -42,8 +43,8 @@ The suite includes:
|
|
|
42
43
|
- deterministic relay connection lifecycle coverage for transport construction/error/deadline, pre-handshake `welcome` validation, authenticated `hello_ack` readiness, identity/version mismatch, retryable Worker handshake errors, fatal protocol errors, autonomous outage-reminder backoff, handshake and heartbeat timeout, brief-outage suppression, sustained-outage escalation, recovery summaries, and supersession;
|
|
43
44
|
- shared no-follow bounded-file reads for normal files, over-limit data, directories, and symbolic links;
|
|
44
45
|
- CLI parsing, policy profiles, and client configuration boundaries;
|
|
45
|
-
- live stdio MCP initialization, discovery, calls, rich content, sessions, cancellation, managed-job acceptance, and a detached job/finally phase that survives stdio shutdown;
|
|
46
|
-
- live local Worker OAuth registration, consent, PKCE, token replay rejection, throttling, CORS, protocol negotiation, dynamic tool advertisement, rich content, daemon replacement, cancellation, malformed daemon JSON/non-object rejection, duplicate hello rejection, and unknown-message closure.
|
|
46
|
+
- live stdio MCP initialization with session instructions, capability resolution, discovery, calls, rich content, sessions, cancellation, managed-job acceptance, and a detached job/finally phase that survives stdio shutdown;
|
|
47
|
+
- live local Worker OAuth registration, consent, PKCE, token replay rejection, throttling, CORS, protocol negotiation, daemon-backed session bootstrap, dynamic tool advertisement, rich content, daemon replacement, cancellation, malformed daemon JSON/non-object rejection, duplicate hello rejection, and unknown-message closure.
|
|
47
48
|
|
|
48
49
|
## Additional release checks
|
|
49
50
|
|
|
@@ -63,7 +64,7 @@ GitHub Actions executes the main suite on Linux, macOS, and Windows using the pi
|
|
|
63
64
|
## Test design rules
|
|
64
65
|
|
|
65
66
|
- Tests should exercise the public boundary rather than only helper functions when practical.
|
|
66
|
-
- Every permission-expanding feature needs a denial test.
|
|
67
|
+
- Every permission-expanding feature needs a denial test. Browser/application features also require a token/value/content non-disclosure assertion.
|
|
67
68
|
- Every bounded resource needs an over-limit test.
|
|
68
69
|
- Every multi-stage mutation needs a no-partial-commit test.
|
|
69
70
|
- Every remote call correlation change needs daemon replacement and cancellation coverage.
|
|
@@ -78,7 +79,7 @@ Run `npm run privacy:check` before committing and before packaging. Developers s
|
|
|
78
79
|
|
|
79
80
|
## Package manifest
|
|
80
81
|
|
|
81
|
-
`npm run package:test` executes a real silent `npm pack --dry-run --json`, requires clean parseable JSON, rejects sensitive local artifacts and credential-like file classes, and verifies that privacy/engineering guidance, the runtime/relay/secure-file modules, contribution discipline, and privacy/release-impact checkers are present in the published package. `npm run install:test` packs the real tarball, installs it into an isolated global prefix with the documented npm 12 options, rejects blocked-script warnings, confirms optional `fsevents` is absent, and runs the installed CLI.
|
|
82
|
+
`npm run package:test` executes a real silent `npm pack --dry-run --json`, requires clean parseable JSON, rejects sensitive local artifacts and credential-like file classes, and verifies that privacy/engineering guidance, the runtime/relay/secure-file/browser/app modules, packaged browser extension, contribution discipline, and privacy/release-impact checkers are present in the published package. `npm run install:test` packs the real tarball, installs it into an isolated global prefix with the documented npm 12 options, rejects blocked-script warnings, confirms optional `fsevents` is absent, and runs the installed CLI.
|
|
82
83
|
|
|
83
84
|
The stdio integration test also sends an oversized line, verifies bounded rejection, and confirms that the next valid request is still processed.
|
|
84
85
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "machine-bridge-mcp",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Cross-client MCP bridge for
|
|
3
|
+
"version": "0.10.0",
|
|
4
|
+
"description": "Cross-client MCP bridge for local agent context, structured browser and application automation, files, Git, processes, resources, and durable jobs over stdio or OAuth relay.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"engines": {
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
"mbm",
|
|
17
17
|
"mbm.cmd",
|
|
18
18
|
"bin",
|
|
19
|
+
"browser-extension",
|
|
19
20
|
"src/local",
|
|
20
21
|
"src/shared",
|
|
21
22
|
"src/worker/index.ts",
|
|
@@ -38,13 +39,13 @@
|
|
|
38
39
|
"self-test": "node tests/local-self-test.mjs",
|
|
39
40
|
"version:sync": "node scripts/sync-worker-version.mjs",
|
|
40
41
|
"version:check": "node scripts/sync-worker-version.mjs --check",
|
|
41
|
-
"version": "node scripts/sync-worker-version.mjs && git add package.json package-lock.json src/worker/index.ts",
|
|
42
|
+
"version": "node scripts/sync-worker-version.mjs && git add package.json package-lock.json src/worker/index.ts browser-extension/manifest.json",
|
|
42
43
|
"prepack": "npm run version:check && npm run privacy:check && npm run release-impact:check",
|
|
43
44
|
"prepublishOnly": "npm run check && npm run version:check && npm run release:check",
|
|
44
45
|
"worker:types": "wrangler types src/worker/worker-configuration.d.ts",
|
|
45
46
|
"typecheck": "npm run worker:types && tsc -p tsconfig.json --noEmit",
|
|
46
|
-
"syntax": "sh -n mbm && node --check bin/machine-mcp.mjs && node --check src/local/cli.mjs && node --check src/local/runtime.mjs && node --check src/local/agent-context.mjs && node --check src/local/relay-connection.mjs && node --check src/local/secure-file.mjs && node --check src/local/patch.mjs && node --check src/local/process-sessions.mjs && node --check src/local/tools.mjs && node --check src/local/stdio.mjs && node --check src/local/state.mjs && node --check src/local/shell.mjs && node --check src/local/service.mjs && node --check src/local/log.mjs && node --check src/local/atomic-fs.mjs && node --check src/local/ssh-key.mjs && node --check src/local/resource-operations.mjs && node --check src/local/full-access-test.mjs && node --check src/local/managed-jobs.mjs && node --check src/local/job-runner.mjs && node --check scripts/sync-worker-version.mjs && node --check scripts/privacy-check.mjs && node --check scripts/release-impact-check.mjs && node --check scripts/network-retry.mjs && node --check scripts/release-state.mjs && node --check scripts/github-release.mjs && node --check tests/worker-integration-test.mjs && node --check tests/stdio-integration-test.mjs && node --check tests/catalog-test.mjs && node --check tests/agent-context-test.mjs && node --check tests/local-self-test.mjs && node --check tests/runtime-self-test.mjs && node --check tests/managed-jobs-test.mjs && node --check tests/ssh-key-test.mjs && node --check tests/full-access-test.mjs && node --check tests/atomic-fs-test.mjs && node --check tests/package-test.mjs && node --check tests/release-impact-test.mjs && node --check tests/release-state-test.mjs && node --check tests/privacy-test.mjs && node --check tests/network-retry-test.mjs && node --check tests/relay-connection-test.mjs && node --check tests/install-smoke-test.mjs && node --check tests/secure-file-test.mjs && node --check tests/architecture-test.mjs",
|
|
47
|
-
"check": "npm run privacy:check && npm run privacy:test && npm run release-impact:check && npm run release-impact:test && npm run release-state:test && npm run network-retry:test && npm run relay:test && npm run secure-file:test && npm run architecture:test && npm run typecheck && npm run syntax && npm run catalog:test && npm run agent-context:test && npm run self-test && npm run atomic-fs:test && npm run package:test && npm run ssh-key:test && npm run full-access:test && npm run managed-jobs:test && npm run stdio:integration-test && npm run worker:integration-test",
|
|
47
|
+
"syntax": "sh -n mbm && node --check bin/machine-mcp.mjs && node --check src/local/cli.mjs && node --check src/local/runtime.mjs && node --check src/local/agent-context.mjs && node --check src/local/browser-bridge.mjs && node --check src/local/app-automation.mjs && node --check src/local/relay-connection.mjs && node --check src/local/secure-file.mjs && node --check src/local/patch.mjs && node --check src/local/process-sessions.mjs && node --check src/local/tools.mjs && node --check src/local/stdio.mjs && node --check src/local/state.mjs && node --check src/local/shell.mjs && node --check src/local/service.mjs && node --check src/local/log.mjs && node --check src/local/atomic-fs.mjs && node --check src/local/ssh-key.mjs && node --check src/local/resource-operations.mjs && node --check src/local/full-access-test.mjs && node --check src/local/managed-jobs.mjs && node --check src/local/job-runner.mjs && node --check scripts/sync-worker-version.mjs && node --check scripts/privacy-check.mjs && node --check scripts/release-impact-check.mjs && node --check scripts/network-retry.mjs && node --check scripts/release-state.mjs && node --check scripts/github-release.mjs && node --check tests/worker-integration-test.mjs && node --check tests/stdio-integration-test.mjs && node --check tests/catalog-test.mjs && node --check tests/agent-context-test.mjs && node --check tests/browser-bridge-test.mjs && node --check tests/app-automation-test.mjs && node --check tests/local-self-test.mjs && node --check tests/runtime-self-test.mjs && node --check tests/managed-jobs-test.mjs && node --check tests/ssh-key-test.mjs && node --check tests/full-access-test.mjs && node --check tests/atomic-fs-test.mjs && node --check tests/package-test.mjs && node --check tests/release-impact-test.mjs && node --check tests/release-state-test.mjs && node --check tests/privacy-test.mjs && node --check tests/network-retry-test.mjs && node --check tests/relay-connection-test.mjs && node --check tests/install-smoke-test.mjs && node --check tests/secure-file-test.mjs && node --check tests/architecture-test.mjs && node --check browser-extension/service-worker.js && node --check browser-extension/pairing.js && node --check browser-extension/page-automation.js",
|
|
48
|
+
"check": "npm run privacy:check && npm run privacy:test && npm run release-impact:check && npm run release-impact:test && npm run release-state:test && npm run network-retry:test && npm run relay:test && npm run secure-file:test && npm run architecture:test && npm run typecheck && npm run syntax && npm run catalog:test && npm run agent-context:test && npm run browser-bridge:test && npm run app-automation:test && npm run self-test && npm run atomic-fs:test && npm run package:test && npm run ssh-key:test && npm run full-access:test && npm run managed-jobs:test && npm run stdio:integration-test && npm run worker:integration-test",
|
|
48
49
|
"worker:dry-run": "wrangler deploy --dry-run",
|
|
49
50
|
"worker:integration-test": "node tests/worker-integration-test.mjs",
|
|
50
51
|
"release:check": "node scripts/github-release.mjs --check",
|
|
@@ -67,7 +68,9 @@
|
|
|
67
68
|
"secure-file:test": "node tests/secure-file-test.mjs",
|
|
68
69
|
"architecture:test": "node tests/architecture-test.mjs",
|
|
69
70
|
"release-state:test": "node tests/release-state-test.mjs",
|
|
70
|
-
"agent-context:test": "node tests/agent-context-test.mjs"
|
|
71
|
+
"agent-context:test": "node tests/agent-context-test.mjs",
|
|
72
|
+
"browser-bridge:test": "node tests/browser-bridge-test.mjs",
|
|
73
|
+
"app-automation:test": "node tests/app-automation-test.mjs"
|
|
71
74
|
},
|
|
72
75
|
"dependencies": {
|
|
73
76
|
"wrangler": "4.110.0",
|
|
@@ -90,7 +93,10 @@
|
|
|
90
93
|
"cross-platform",
|
|
91
94
|
"ssh",
|
|
92
95
|
"automation",
|
|
93
|
-
"managed-jobs"
|
|
96
|
+
"managed-jobs",
|
|
97
|
+
"browser-automation",
|
|
98
|
+
"desktop-automation",
|
|
99
|
+
"agent-skills"
|
|
94
100
|
],
|
|
95
101
|
"repository": {
|
|
96
102
|
"type": "git",
|
|
@@ -7,6 +7,7 @@ import { fileURLToPath } from "node:url";
|
|
|
7
7
|
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
8
8
|
const packagePath = path.join(repoRoot, "package.json");
|
|
9
9
|
const workerPath = path.join(repoRoot, "src", "worker", "index.ts");
|
|
10
|
+
const extensionManifestPath = path.join(repoRoot, "browser-extension", "manifest.json");
|
|
10
11
|
const args = new Set(process.argv.slice(2));
|
|
11
12
|
const checkOnly = args.has("--check");
|
|
12
13
|
const log = message => process.stderr.write(`${message}\n`);
|
|
@@ -17,24 +18,38 @@ if (!/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(expected)) {
|
|
|
17
18
|
fail(`package.json version is not a valid release version: ${JSON.stringify(expected)}`);
|
|
18
19
|
}
|
|
19
20
|
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
const
|
|
23
|
-
if (!
|
|
21
|
+
const workerSource = readFileSync(workerPath, "utf8");
|
|
22
|
+
const workerPattern = /const SERVER_VERSION = "([^"]+)";/;
|
|
23
|
+
const workerMatch = workerSource.match(workerPattern);
|
|
24
|
+
if (!workerMatch) fail("Could not find `const SERVER_VERSION = \"...\";` in src/worker/index.ts");
|
|
24
25
|
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
const extension = JSON.parse(readFileSync(extensionManifestPath, "utf8"));
|
|
27
|
+
const extensionVersion = expected.split(/[+-]/, 1)[0];
|
|
28
|
+
const extensionParts = extensionVersion.split(".").map((value) => Number(value));
|
|
29
|
+
if (extensionParts.length < 1 || extensionParts.length > 4 || extensionParts.some((value) => !Number.isInteger(value) || value < 0 || value > 65535)) {
|
|
30
|
+
fail(`package version cannot be represented as a Chromium extension version: ${expected}`);
|
|
29
31
|
}
|
|
32
|
+
const mismatches = [];
|
|
33
|
+
if (workerMatch[1] !== expected) mismatches.push(`Worker=${workerMatch[1]}`);
|
|
34
|
+
if (String(extension.version || "") !== extensionVersion) mismatches.push(`browser-extension.version=${extension.version || "<missing>"}`);
|
|
35
|
+
if (String(extension.version_name || "") !== expected) mismatches.push(`browser-extension.version_name=${extension.version_name || "<missing>"}`);
|
|
30
36
|
|
|
37
|
+
if (!mismatches.length) {
|
|
38
|
+
log(`Runtime versions are in sync: ${expected}`);
|
|
39
|
+
process.exit(0);
|
|
40
|
+
}
|
|
31
41
|
if (checkOnly) {
|
|
32
|
-
fail(`
|
|
42
|
+
fail(`Version mismatch: package.json=${expected}; ${mismatches.join(", ")}. Run npm run version:sync.`);
|
|
33
43
|
}
|
|
34
44
|
|
|
35
|
-
|
|
36
|
-
writeFileSync(workerPath,
|
|
37
|
-
log(`Updated Worker version: ${
|
|
45
|
+
if (workerMatch[1] !== expected) {
|
|
46
|
+
writeFileSync(workerPath, workerSource.replace(workerPattern, `const SERVER_VERSION = "${expected}";`));
|
|
47
|
+
log(`Updated Worker version: ${workerMatch[1]} -> ${expected}`);
|
|
48
|
+
}
|
|
49
|
+
extension.version = extensionVersion;
|
|
50
|
+
extension.version_name = expected;
|
|
51
|
+
writeFileSync(extensionManifestPath, `${JSON.stringify(extension, null, 2)}\n`);
|
|
52
|
+
log(`Updated browser extension version: ${extensionVersion} (${expected})`);
|
|
38
53
|
|
|
39
54
|
function fail(message) {
|
|
40
55
|
console.error(message);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
+
import { constants as fsConstants } from "node:fs";
|
|
2
3
|
import { lstat, open, opendir, realpath, stat } from "node:fs/promises";
|
|
3
4
|
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
4
5
|
|
|
@@ -21,7 +22,8 @@ const MAX_COMMANDS = 128;
|
|
|
21
22
|
const MAX_COMMAND_ARGV = 128;
|
|
22
23
|
const MAX_COMMAND_ARGUMENT_BYTES = 256 * 1024;
|
|
23
24
|
const COMMAND_NAME_PATTERN = /^[a-z][a-z0-9._-]{0,63}$/;
|
|
24
|
-
const
|
|
25
|
+
const TOKEN_STOP_WORDS = new Set(["the", "and", "for", "with", "from", "this", "that", "use", "using", "into", "of", "to", "a", "an", "in", "on", "is", "are", "or", "及", "和", "的", "了", "在", "用", "使用", "进行", "根据"]);
|
|
26
|
+
const CONFIG_KEYS = new Set(["version", "model_instructions_file", "instruction_files", "instruction_max_bytes", "skill_roots", "commands"]);
|
|
25
27
|
const COMMAND_KEYS = new Set(["description", "argv", "cwd", "timeout_seconds", "allow_extra_args"]);
|
|
26
28
|
|
|
27
29
|
export class AgentContextManager {
|
|
@@ -46,6 +48,12 @@ export class AgentContextManager {
|
|
|
46
48
|
scope_root: this.displayPath(state.scopeRoot),
|
|
47
49
|
precedence: "global guidance first, then scope root to target directory; each directory contributes the first non-empty instruction_files candidate; later directories have higher precedence",
|
|
48
50
|
config_files: state.configFiles.map((file) => this.displayPath(file)),
|
|
51
|
+
model_instructions_file: state.modelInstructions ? {
|
|
52
|
+
path: this.displayPath(state.modelInstructions.path),
|
|
53
|
+
bytes: state.modelInstructions.bytes,
|
|
54
|
+
sha256: state.modelInstructions.sha256,
|
|
55
|
+
...(includeContent ? { content: state.modelInstructions.content } : {}),
|
|
56
|
+
} : null,
|
|
49
57
|
instruction_files: state.instructions.map((item) => ({
|
|
50
58
|
scope: item.scope,
|
|
51
59
|
path: this.displayPath(item.path),
|
|
@@ -65,10 +73,84 @@ export class AgentContextManager {
|
|
|
65
73
|
"Prefer run_local_command for registered repeatable commands. Use run_process or exec_command only when no registered command fits and policy permits it.",
|
|
66
74
|
],
|
|
67
75
|
};
|
|
68
|
-
if (includeContent) result.effective_instructions = renderEffectiveInstructions(state.instructions, this.displayPath);
|
|
76
|
+
if (includeContent) result.effective_instructions = renderEffectiveInstructions([...(state.modelInstructions ? [state.modelInstructions] : []), ...state.instructions], this.displayPath);
|
|
69
77
|
return result;
|
|
70
78
|
}
|
|
71
79
|
|
|
80
|
+
|
|
81
|
+
async sessionBootstrap(args = {}, context = {}) {
|
|
82
|
+
const state = await this.discoverState(args.path || ".", context);
|
|
83
|
+
const fingerprint = capabilityFingerprint(state, []);
|
|
84
|
+
return {
|
|
85
|
+
target: this.displayPath(state.target),
|
|
86
|
+
instructions: renderEffectiveInstructions([...(state.modelInstructions ? [state.modelInstructions] : []), ...state.instructions], this.displayPath),
|
|
87
|
+
model_instructions_file: state.modelInstructions ? this.displayPath(state.modelInstructions.path) : null,
|
|
88
|
+
capability_refresh: {
|
|
89
|
+
strategy: "resolve_task_capabilities-rescans-on-every-call",
|
|
90
|
+
instruction_and_command_fingerprint: fingerprint,
|
|
91
|
+
skills_scanned: false,
|
|
92
|
+
generated_at: new Date().toISOString(),
|
|
93
|
+
},
|
|
94
|
+
guidance: [
|
|
95
|
+
"Call resolve_task_capabilities with the current user task before substantive local work or at the start of a reused-host conversation.",
|
|
96
|
+
"The resolver returns the effective instructions again and rescans skill and command metadata on every call; the runtime supplements application and browser capability metadata.",
|
|
97
|
+
],
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async resolveTaskCapabilities(args = {}, context = {}) {
|
|
102
|
+
const task = requiredString(args.task, "task");
|
|
103
|
+
if (task.length > 20_000) throw new Error("task exceeds 20000 characters");
|
|
104
|
+
const state = await this.discoverState(args.path || ".", context);
|
|
105
|
+
const discovered = await this.discoverSkills(state, { maxResults: MAX_SKILL_RESULTS }, context);
|
|
106
|
+
const maxSkills = clampInt(args.max_skills, 10, 1, 50);
|
|
107
|
+
const skillMatches = discovered.skills
|
|
108
|
+
.map((skill) => ({ skill, score: relevanceScore(task, `${skill.name} ${skill.description}`) }))
|
|
109
|
+
.filter((item) => item.score > 0)
|
|
110
|
+
.sort((a, b) => b.score - a.score || a.skill.name.localeCompare(b.skill.name))
|
|
111
|
+
.slice(0, maxSkills);
|
|
112
|
+
const commandMatches = [...state.commands.values()]
|
|
113
|
+
.map((command) => ({ command, score: relevanceScore(task, `${command.name} ${command.description} ${command.argv.join(" ")}`) }))
|
|
114
|
+
.filter((item) => item.score > 0)
|
|
115
|
+
.sort((a, b) => b.score - a.score || a.command.name.localeCompare(b.command.name))
|
|
116
|
+
.slice(0, 20);
|
|
117
|
+
const selected = skillMatches[0]?.score >= 3 ? skillMatches[0].skill : null;
|
|
118
|
+
let selectedSkill = null;
|
|
119
|
+
if (selected && args.include_selected_skill !== false) {
|
|
120
|
+
const content = await readRegularUtf8(selected.entrypoint, MAX_SKILL_ENTRY_BYTES, "skill entrypoint");
|
|
121
|
+
selectedSkill = { ...publicSkill(selected, this.displayPath), instructions: content.text };
|
|
122
|
+
}
|
|
123
|
+
const recommendedTools = recommendTools(task, commandMatches.length > 0, skillMatches.length > 0);
|
|
124
|
+
const refresh = capabilityFingerprint(state, discovered.skills);
|
|
125
|
+
return {
|
|
126
|
+
task,
|
|
127
|
+
target: this.displayPath(state.target),
|
|
128
|
+
effective_instructions: renderEffectiveInstructions([...(state.modelInstructions ? [state.modelInstructions] : []), ...state.instructions], this.displayPath),
|
|
129
|
+
model_instructions_file: state.modelInstructions ? this.displayPath(state.modelInstructions.path) : null,
|
|
130
|
+
instruction_files: state.instructions.map((item) => ({ path: this.displayPath(item.path), scope: item.scope, bytes: item.bytes, sha256: item.sha256, precedence: item.precedence })),
|
|
131
|
+
instructions_truncated: state.instructionsTruncated,
|
|
132
|
+
refresh: { strategy: "rescan-on-every-call", fingerprint: refresh, generated_at: new Date().toISOString() },
|
|
133
|
+
selected_skill: selectedSkill,
|
|
134
|
+
skill_matches: skillMatches.map(({ skill, score }) => ({ ...publicSkill(skill, this.displayPath), score })),
|
|
135
|
+
command_matches: commandMatches.map(({ command, score }) => ({ ...publicCommands(new Map([[command.name, command]]), this.displayPath)[0], score })),
|
|
136
|
+
recommended_tools: recommendedTools,
|
|
137
|
+
host_semantics: "Machine Bridge can discover, rank, and load capabilities automatically. The MCP host remains responsible for deciding whether to call the recommended tools.",
|
|
138
|
+
warnings: publicSkillWarnings(discovered.warnings, this.displayPath),
|
|
139
|
+
truncated: discovered.truncated,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async collectModelInstructions(state, context) {
|
|
144
|
+
this.throwIfCancelled(context);
|
|
145
|
+
const path = state.modelInstructionsFile;
|
|
146
|
+
const info = await lstat(path).catch((error) => error?.code === "ENOENT" ? null : Promise.reject(error));
|
|
147
|
+
if (!info) throw new Error(`model_instructions_file does not exist: ${path}`);
|
|
148
|
+
if (info.isSymbolicLink() || !info.isFile()) throw new Error(`model_instructions_file must be a regular non-symbolic-link file: ${path}`);
|
|
149
|
+
const content = await readRegularUtf8(path, MAX_INSTRUCTION_FILE_BYTES, "model instructions file");
|
|
150
|
+
if (!content.text.trim()) throw new Error(`model_instructions_file is empty: ${path}`);
|
|
151
|
+
state.modelInstructions = { scope: "model", path, bytes: content.bytes, sha256: sha256(content.text), content: content.text, precedence: 0 };
|
|
152
|
+
}
|
|
153
|
+
|
|
72
154
|
async listLocalSkills(args = {}, context = {}) {
|
|
73
155
|
const state = await this.discoverState(args.path || ".", context);
|
|
74
156
|
const result = await this.discoverSkills(state, {
|
|
@@ -150,31 +232,44 @@ export class AgentContextManager {
|
|
|
150
232
|
instructionMaxBytes: DEFAULT_INSTRUCTION_MAX_BYTES,
|
|
151
233
|
skillRoots: defaultSkillRoots(directories, this.home, this.policy.unrestrictedPaths === true),
|
|
152
234
|
commands: new Map(),
|
|
235
|
+
modelInstructionsFile: "",
|
|
236
|
+
modelInstructions: null,
|
|
153
237
|
configFiles: [],
|
|
154
238
|
instructions: [],
|
|
155
239
|
instructionBytes: 0,
|
|
156
240
|
instructionsTruncated: false,
|
|
157
241
|
};
|
|
158
242
|
|
|
159
|
-
if (this.
|
|
243
|
+
if (this.home) {
|
|
160
244
|
const globalConfig = join(this.home, GLOBAL_CONFIG_RELATIVE_PATH);
|
|
161
245
|
const config = await readOptionalConfig(globalConfig, this.home, false);
|
|
162
|
-
if (config)
|
|
163
|
-
|
|
246
|
+
if (config) {
|
|
247
|
+
if (this.policy.unrestrictedPaths === true) this.applyConfig(state, config, globalConfig, this.home, { global: true });
|
|
248
|
+
else {
|
|
249
|
+
state.configFiles.push(globalConfig);
|
|
250
|
+
if (config.modelInstructionsFile) state.modelInstructionsFile = resolveConfiguredPath(config.modelInstructionsFile, this.home, this.home, this.workspace, true);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
if (state.modelInstructionsFile) await this.collectModelInstructions(state, context);
|
|
254
|
+
if (this.policy.unrestrictedPaths === true && this.codexHome) await this.collectDirectoryInstruction(state, this.codexHome, context, "global");
|
|
164
255
|
}
|
|
165
256
|
|
|
166
257
|
for (const directory of directories) {
|
|
167
258
|
this.throwIfCancelled(context);
|
|
168
259
|
const configPath = join(directory, CONFIG_RELATIVE_PATH);
|
|
169
260
|
const config = await readOptionalConfig(configPath, directory, true);
|
|
170
|
-
if (config) this.applyConfig(state, config, configPath, directory);
|
|
261
|
+
if (config) this.applyConfig(state, config, configPath, directory, { global: false });
|
|
171
262
|
await this.collectDirectoryInstruction(state, directory, context, "project");
|
|
172
263
|
}
|
|
173
264
|
return state;
|
|
174
265
|
}
|
|
175
266
|
|
|
176
|
-
applyConfig(state, config, configPath, baseDir) {
|
|
267
|
+
applyConfig(state, config, configPath, baseDir, { global = false } = {}) {
|
|
177
268
|
state.configFiles.push(configPath);
|
|
269
|
+
if (config.modelInstructionsFile) {
|
|
270
|
+
if (!global) throw new Error(`model_instructions_file is only allowed in the global agent config: ${configPath}`);
|
|
271
|
+
state.modelInstructionsFile = resolveConfiguredPath(config.modelInstructionsFile, baseDir, this.home, this.workspace, true);
|
|
272
|
+
}
|
|
178
273
|
if (config.instructionFiles) state.instructionFiles = [...config.instructionFiles];
|
|
179
274
|
if (config.instructionMaxBytes !== null) {
|
|
180
275
|
state.instructionMaxBytes = config.instructionMaxBytes;
|
|
@@ -385,7 +480,10 @@ function normalizeConfig(value, configPath) {
|
|
|
385
480
|
if (!isPlainRecord(value)) throw new Error(`agent config must be a JSON object: ${configPath}`);
|
|
386
481
|
for (const key of Object.keys(value)) if (!CONFIG_KEYS.has(key)) throw new Error(`unknown agent config field '${key}': ${configPath}`);
|
|
387
482
|
if (value.version !== 1) throw new Error(`agent config version must be 1: ${configPath}`);
|
|
388
|
-
const result = { instructionFiles: null, instructionMaxBytes: null, skillRoots: null, commands: new Map() };
|
|
483
|
+
const result = { modelInstructionsFile: null, instructionFiles: null, instructionMaxBytes: null, skillRoots: null, commands: new Map() };
|
|
484
|
+
if (value.model_instructions_file !== undefined) {
|
|
485
|
+
result.modelInstructionsFile = requiredString(value.model_instructions_file, "model_instructions_file");
|
|
486
|
+
}
|
|
389
487
|
if (value.instruction_files !== undefined) {
|
|
390
488
|
result.instructionFiles = validateInstructionFiles(value.instruction_files, configPath);
|
|
391
489
|
}
|
|
@@ -556,6 +654,62 @@ async function listSkillFiles(root, maxFiles, context, throwIfCancelled) {
|
|
|
556
654
|
return { files, truncated };
|
|
557
655
|
}
|
|
558
656
|
|
|
657
|
+
function capabilityFingerprint(state, skills) {
|
|
658
|
+
return sha256(JSON.stringify({
|
|
659
|
+
configs: state.configFiles,
|
|
660
|
+
instructions: [state.modelInstructions?.sha256 || "", ...state.instructions.map((item) => item.sha256)],
|
|
661
|
+
skills: skills.map((skill) => [skill.id, skill.sha256]),
|
|
662
|
+
commands: [...state.commands.values()].map((command) => [command.name, command.argv]),
|
|
663
|
+
}));
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
function relevanceScore(task, candidate) {
|
|
667
|
+
const taskTokens = tokenize(task);
|
|
668
|
+
const candidateTokens = tokenize(candidate);
|
|
669
|
+
if (!taskTokens.size || !candidateTokens.size) return 0;
|
|
670
|
+
let score = 0;
|
|
671
|
+
for (const token of taskTokens) {
|
|
672
|
+
if (candidateTokens.has(token)) score += token.length >= 6 ? 2 : 1;
|
|
673
|
+
}
|
|
674
|
+
const taskLower = task.toLowerCase();
|
|
675
|
+
const candidateLower = candidate.toLowerCase();
|
|
676
|
+
if (candidateLower.includes(taskLower) || taskLower.includes(candidateLower)) score += 4;
|
|
677
|
+
return score;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
function tokenize(value) {
|
|
681
|
+
const text = String(value || "").toLowerCase();
|
|
682
|
+
const tokens = new Set();
|
|
683
|
+
for (const raw of text.match(/[a-z0-9_][a-z0-9_.-]{1,}/g) || []) {
|
|
684
|
+
const token = raw.replace(/^[.-]+|[.-]+$/g, "");
|
|
685
|
+
if (token.length >= 2 && !TOKEN_STOP_WORDS.has(token)) tokens.add(token);
|
|
686
|
+
}
|
|
687
|
+
for (const sequence of text.match(/[\p{Script=Han}]{1,}/gu) || []) {
|
|
688
|
+
if (!TOKEN_STOP_WORDS.has(sequence)) tokens.add(sequence);
|
|
689
|
+
const minimumSize = sequence.length === 1 ? 1 : 2;
|
|
690
|
+
for (let size = minimumSize; size <= Math.min(3, sequence.length); size += 1) {
|
|
691
|
+
for (let index = 0; index + size <= sequence.length; index += 1) {
|
|
692
|
+
const token = sequence.slice(index, index + size);
|
|
693
|
+
if (!TOKEN_STOP_WORDS.has(token)) tokens.add(token);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
return tokens;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
function recommendTools(task, hasCommands, hasSkills) {
|
|
701
|
+
const lower = task.toLowerCase();
|
|
702
|
+
const tools = ["agent_context"];
|
|
703
|
+
if (hasSkills) tools.push("load_local_skill");
|
|
704
|
+
if (hasCommands) tools.push("run_local_command");
|
|
705
|
+
if (/browser|chrome|edge|brave|网页|浏览器|表单|网站/.test(lower)) tools.push("browser_status", "browser_list_tabs", "browser_inspect_page", "browser_action", "browser_fill_form");
|
|
706
|
+
if (/app|application|gui|window|应用|软件|窗口|界面/.test(lower)) tools.push("list_local_applications", "inspect_local_application", "operate_local_application");
|
|
707
|
+
if (/git|commit|branch|diff|仓库|提交|分支/.test(lower)) tools.push("git_status", "git_diff");
|
|
708
|
+
if (/test|build|lint|command|terminal|测试|构建|命令|终端/.test(lower)) tools.push(hasCommands ? "run_local_command" : "run_process");
|
|
709
|
+
if (/file|code|source|edit|write|文件|代码|源码|修改|写入/.test(lower)) tools.push("read_file", "search_text", "edit_file", "apply_patch");
|
|
710
|
+
return [...new Set(tools)];
|
|
711
|
+
}
|
|
712
|
+
|
|
559
713
|
function publicSkill(skill, displayPath) {
|
|
560
714
|
return {
|
|
561
715
|
id: skill.id,
|
|
@@ -626,7 +780,7 @@ async function readOptionalRegularUtf8(filePath, maxBytes, label) {
|
|
|
626
780
|
}
|
|
627
781
|
|
|
628
782
|
async function readRegularUtf8(filePath, maxBytes, label) {
|
|
629
|
-
const handle = await open(filePath,
|
|
783
|
+
const handle = await open(filePath, fsConstants.O_RDONLY | (fsConstants.O_NOFOLLOW || 0));
|
|
630
784
|
try {
|
|
631
785
|
const info = await handle.stat();
|
|
632
786
|
if (!info.isFile()) throw new Error(`${label} is not a regular file: ${filePath}`);
|