machine-bridge-mcp 0.12.0 → 0.12.2
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 +31 -0
- package/SECURITY.md +1 -1
- package/browser-extension/manifest.json +2 -2
- package/docs/AGENT_CONTEXT.md +1 -1
- package/docs/ARCHITECTURE.md +8 -2
- package/docs/AUDIT.md +13 -1
- package/docs/ENGINEERING.md +4 -4
- package/docs/LOCAL_AUTOMATION.md +3 -1
- package/docs/OPERATIONS.md +1 -1
- package/docs/PRIVACY.md +8 -3
- package/docs/RELEASING.md +5 -5
- package/docs/TESTING.md +20 -7
- package/package.json +6 -3
- package/scripts/github-release.mjs +28 -0
- package/scripts/privacy-check.mjs +150 -2
- package/scripts/release-ci.mjs +18 -0
- package/src/local/agent-context.mjs +19 -6
- package/src/local/app-automation.mjs +14 -8
- package/src/local/atomic-fs.mjs +14 -2
- package/src/local/browser-bridge.mjs +3 -1
- package/src/shared/tool-catalog.json +10 -0
- package/src/worker/index.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,36 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.12.2 - 2026-07-12
|
|
4
|
+
|
|
5
|
+
### Cross-platform persistence and browser reliability
|
|
6
|
+
|
|
7
|
+
- Extend transient Windows atomic replacement from eight short linear retries to sixteen bounded exponential retries with jitter. The implementation continues to use one same-directory atomic rename and never falls back to deleting the destination, while tolerating longer antivirus/indexer/reader sharing windows.
|
|
8
|
+
- Eliminate a broker-test race that could miss the runtime `hello` message between WebSocket `open` and listener registration. Pairing HTTP requests and WebSocket open/message/close waits are now bounded, and failed proxy candidates are terminated so a handshake cannot retain the test process indefinitely.
|
|
9
|
+
|
|
10
|
+
### CI, release, and supply-chain integrity
|
|
11
|
+
|
|
12
|
+
- Require a completed successful push-triggered GitHub Actions run for the exact `origin/main` commit before creating or verifying a version tag, GitHub Release, or release asset. A local test pass is necessary but no longer sufficient for publication.
|
|
13
|
+
- Pin all third-party GitHub Actions to immutable commit SHAs, retain Dependabot updates, and enforce the pinning plus the release-CI wiring through architecture and dedicated release-gate tests.
|
|
14
|
+
- Run a reachable-history privacy audit in the package-audit job. The scanner covers historical UTF-8 blob contents, historical paths, and commit messages without printing matched values, while narrowly ignoring the standard public Dependabot signing trailer.
|
|
15
|
+
|
|
16
|
+
### Regression coverage and audit follow-up
|
|
17
|
+
|
|
18
|
+
- Add deterministic tests for twelve consecutive transient Windows replacement failures, exponential delay selection, lost browser handshakes, bounded socket waits, portable simulated-Linux launcher paths on Windows, POSIX-versus-Windows daemon-stop semantics, deleted historical credential blobs, public automation trailers, and successful/failed/pending release CI states.
|
|
19
|
+
- Update the architecture, engineering, privacy, testing, release, security, and audit documentation. Record remaining immutable Git-history identity metadata separately from current-tree and active-credential findings.
|
|
20
|
+
|
|
21
|
+
## 0.12.1 - 2026-07-12
|
|
22
|
+
|
|
23
|
+
### Real local automation corrections
|
|
24
|
+
|
|
25
|
+
- Make the fixed macOS JXA helper return JSON as the top-level `osascript` result instead of using `console.log`, which real `osascript` sends to stderr. Empty helper stdout now fails explicitly rather than being interpreted as a successful empty object.
|
|
26
|
+
- Skip recursive macOS menu-bar/menu traversal by default so bounded inspection reaches main-window controls promptly. Add `include_menus` to inspection and actions for explicit menu automation.
|
|
27
|
+
- Normalize hyphens, underscores, dots, and spaces during capability ranking, and strongly boost explicitly named skills and commands. Natural-language references such as `agents progressive disclosure` now select `agents-progressive-disclosure` instead of a generic competing skill.
|
|
28
|
+
|
|
29
|
+
### Live verification and regression coverage
|
|
30
|
+
|
|
31
|
+
- Add regression coverage for missing JXA output, menu-recursion forwarding, and punctuation-normalized Skill selection. Add an opt-in real macOS Calculator smoke path and document isolated-profile MV3 browser validation.
|
|
32
|
+
- Validate the source runtime with a temporary real global instruction file, an existing user Skill, a registered direct-argv command, shell execution, Calculator discovery/open/activation/inspection/click, and a real unpacked browser extension controlling a local form through inspect/fill/click/source/screenshot operations. Test files, browser profiles, temporary global configuration, tabs, and application state are removed afterward.
|
|
33
|
+
|
|
3
34
|
## 0.12.0 - 2026-07-12
|
|
4
35
|
|
|
5
36
|
### Locking, persistence, and process lifecycle
|
package/SECURITY.md
CHANGED
|
@@ -19,7 +19,7 @@ Do not include live MCP passwords, daemon secrets, OAuth tokens, Cloudflare cred
|
|
|
19
19
|
|
|
20
20
|
## Repository and documentation privacy
|
|
21
21
|
|
|
22
|
-
Tests, examples, documentation, release notes, and package metadata are publication surfaces. Use only synthetic hostnames, resource aliases, usernames, paths, and project names. Run `npm run privacy:check` before committing
|
|
22
|
+
Tests, examples, documentation, release notes, and package metadata are publication surfaces. Use only synthetic hostnames, resource aliases, usernames, paths, and project names. Run `npm run privacy:check` before committing, review `npm run privacy:history` before publishing, and maintain private local identifiers in the ignored `.privacy-denylist`; neither scanner mode prints matched values. See [Repository privacy hygiene](docs/PRIVACY.md).
|
|
23
23
|
|
|
24
24
|
Removing a value from the current branch does not remove it from Git history, caches, forks, or an already published npm package. Rotate any exposed credential immediately and coordinate destructive history rewriting separately when its risk is justified.
|
|
25
25
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"manifest_version": 3,
|
|
3
3
|
"name": "Machine Bridge Browser",
|
|
4
|
-
"version": "0.12.
|
|
4
|
+
"version": "0.12.2",
|
|
5
5
|
"description": "Connects the current Chromium browser profile to the local Machine Bridge runtime for user-authorized automation.",
|
|
6
6
|
"permissions": [
|
|
7
7
|
"tabs",
|
|
@@ -29,5 +29,5 @@
|
|
|
29
29
|
"action": {
|
|
30
30
|
"default_title": "Machine Bridge Browser"
|
|
31
31
|
},
|
|
32
|
-
"version_name": "0.12.
|
|
32
|
+
"version_name": "0.12.2"
|
|
33
33
|
}
|
package/docs/AGENT_CONTEXT.md
CHANGED
|
@@ -189,7 +189,7 @@ No persistent skill or project-context index is trusted as authoritative. `sessi
|
|
|
189
189
|
|
|
190
190
|
## Progressive disclosure and task selection
|
|
191
191
|
|
|
192
|
-
`agent_context` returns bounded skill metadata. `load_local_skill` returns full instructions only for one selected bundle. `resolve_task_capabilities` tokenizes the current task, ranks skill names/descriptions and command names/descriptions/argv, returns matches with scores, and loads the leading skill only when its relevance threshold is met.
|
|
192
|
+
`agent_context` returns bounded skill metadata. `load_local_skill` returns full instructions only for one selected bundle. `resolve_task_capabilities` tokenizes the current task, ranks skill names/descriptions and command names/descriptions/argv, returns matches with scores, and loads the leading skill only when its relevance threshold is met. Hyphens, underscores, dots, and whitespace are normalized for matching, while an explicitly named skill or registered command receives a strong deterministic boost. Thus a task that says “agents progressive disclosure” can match `agents-progressive-disclosure` without relying on generic description words.
|
|
193
193
|
|
|
194
194
|
This ranking is deterministic local assistance, not semantic certainty. The model must still evaluate whether the selected skill applies. Machine Bridge does not execute skill scripts implicitly and does not fabricate a dynamically named MCP tool per skill.
|
|
195
195
|
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -40,7 +40,7 @@ A canonical workspace receives an independent profile, Worker name, secret set,
|
|
|
40
40
|
|
|
41
41
|
`RelayConnection` owns remote WebSocket transport, authenticated `hello_ack` readiness, heartbeat liveness, reconnect backoff, and outage logging. Stdio mode invokes `LocalRuntime` directly without that adapter.
|
|
42
42
|
|
|
43
|
-
`daemon-process.mjs` owns workspace-daemon inspection and takeover. It distinguishes platform service state from the lock-owning Node process, validates PID and process-start identity, canonicalizes workspace/state paths before comparison, parses bounded process command lines without executing them, and sends `SIGTERM` only to a verified same-workspace `--daemon-only` process. CLI orchestration never treats a missing launchd/systemd job as proof that the process exited.
|
|
43
|
+
`daemon-process.mjs` owns workspace-daemon inspection and takeover. It distinguishes platform service state from the lock-owning Node process, validates PID and process-start identity, canonicalizes workspace/state paths before comparison, parses bounded process command lines without executing them, and sends `SIGTERM` only to a verified same-workspace `--daemon-only` process. POSIX daemons may ignore that signal and reach the bounded non-escalating timeout; Node's Windows signal mapping terminates the verified process directly. CLI orchestration never treats a missing launchd/systemd job as proof that the process exited.
|
|
44
44
|
|
|
45
45
|
### Agent context and capability resolver
|
|
46
46
|
|
|
@@ -216,7 +216,7 @@ The Worker also treats a new connection as a bounded candidate until it authenti
|
|
|
216
216
|
|
|
217
217
|
## Persistence
|
|
218
218
|
|
|
219
|
-
Local state and global config are owner-only, versioned, and size-bounded. Shared persistence primitives write a complete private temporary file, `fsync` it, and either hard-link it for an exclusive claim or atomically replace the destination. Machine-level browser pairing state is owner-only and shared across workspace runtimes through the local broker; its bearer token is not part of workspace state responses. State, managed-job manager, detached runner, browser pairing, and service definitions share the same flushed atomic-replacement primitive
|
|
219
|
+
Local state and global config are owner-only, versioned, and size-bounded. Shared persistence primitives write a complete private temporary file, `fsync` it, and either hard-link it for an exclusive claim or atomically replace the destination. Machine-level browser pairing state is owner-only and shared across workspace runtimes through the local broker; its bearer token is not part of workspace state responses. State, managed-job manager, detached runner, browser pairing, and service definitions share the same flushed atomic-replacement primitive. Only classified transient Windows sharing failures are retried, using a bounded sixteen-attempt exponential schedule with jitter; the implementation never deletes the destination as a retry fallback, so readers are not intentionally exposed to a missing-file interval.
|
|
220
220
|
|
|
221
221
|
Process locks contain purpose, workspace, ownership token, lock time, and process start time. Stale removal rechecks device/inode/size/mtime and token so an old observer cannot delete a replacement lock. Recent malformed claims receive a grace period. Startup/state operations wait a bounded interval; daemon and runner identity remain process-lifetime locks. Managed-job transition and recovery locks use the same ownership/snapshot principles and support an atomic runner handoff.
|
|
222
222
|
|
|
@@ -236,6 +236,12 @@ Foreground logging defaults to `info`; autostart uses `warn`. Authenticated read
|
|
|
236
236
|
|
|
237
237
|
Cloudflare sampling is size control rather than an audit log. The project intentionally does not claim complete forensic logging. See [LOGGING.md](LOGGING.md).
|
|
238
238
|
|
|
239
|
+
## Release integrity
|
|
240
|
+
|
|
241
|
+
Repository-local checks are necessary but cannot prove behavior on every supported operating system. `scripts/github-release.mjs` therefore queries `.github/workflows/ci.yml` for the exact `origin/main` commit and requires the newest push-triggered run to be completed with `success` before it creates or verifies a version tag, GitHub Release, or package asset. Pull-request runs, older successful runs, pending runs, and successful runs for another SHA do not satisfy the gate. The selection policy is isolated in `scripts/release-ci.mjs` and tested independently.
|
|
242
|
+
|
|
243
|
+
Third-party workflow actions are pinned to immutable commit SHAs. Dependabot proposes reviewed SHA updates, and `architecture:test` rejects a return to movable action tags or removal of the reachable-history package-audit step.
|
|
244
|
+
|
|
239
245
|
## Explicit non-goals
|
|
240
246
|
|
|
241
247
|
- operating-system sandboxing of arbitrary executables;
|
package/docs/AUDIT.md
CHANGED
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
# Engineering and security audit
|
|
2
2
|
|
|
3
|
-
This document records the cross-cutting audit
|
|
3
|
+
This document records the cross-cutting audit initiated for version 0.12.0 and the 0.12.2 follow-up triggered by real cross-platform CI failures. It complements, but does not replace, the continuously enforced contracts in `SECURITY.md`, `docs/ENGINEERING.md`, and the test suite.
|
|
4
|
+
|
|
5
|
+
## 0.12.2 follow-up findings
|
|
6
|
+
|
|
7
|
+
The failed `0.12.1` workflow had two independent causes. Windows exhausted the old approximately half-second atomic-replacement retry window while a stress reader repeatedly held the destination open. Linux and macOS lost an immediately emitted browser runtime `hello` between awaiting `open` and registering the message listener, then waited without a deadline until the 20-minute job timeout. The fixes preserve atomic replacement, extend only classified transient retries, pre-register the browser handshake listener, bound all relevant HTTP/WebSocket waits, and terminate failed proxy candidates.
|
|
8
|
+
|
|
9
|
+
The review also found three governance gaps not represented by the original failure message:
|
|
10
|
+
|
|
11
|
+
- GitHub Releases could be created from a local pass while the exact cross-platform push CI was failing. Publication now requires successful exact-commit push CI before tagging or release verification.
|
|
12
|
+
- Workflow actions used movable major-version tags. They are now pinned to immutable official commit SHAs, with an executable invariant.
|
|
13
|
+
- Repository privacy checks covered only the current tree. Package audit now scans reachable historical paths, bounded UTF-8 blobs, and commit messages; deleted credential fixtures fail without echoing their values.
|
|
14
|
+
|
|
15
|
+
No generic active credential pattern was found in the current tree or reachable history after excluding standard public automation trailers. The developer-local denylist does match legacy historical identifiers, and the Git metadata audit found a legacy non-noreply author/committer identity. These are identity/privacy metadata rather than active credentials. Removing them would require a coordinated history rewrite and force-update of affected refs, which is intentionally not performed as an incidental code fix.
|
|
4
16
|
|
|
5
17
|
## Scope
|
|
6
18
|
|
package/docs/ENGINEERING.md
CHANGED
|
@@ -9,7 +9,7 @@ This document records project-wide decisions that must survive individual fixes,
|
|
|
9
9
|
3. **Machine Bridge authority and host authority are separate.** `full` removes Machine Bridge's own policy, path, shell, and environment restrictions. It cannot override an MCP host, connector gateway, operating system, endpoint-security product, cloud IAM, remote authentication, or `sudo`.
|
|
10
10
|
4. **Publication surfaces contain no real environment metadata.** Source, tests, fixtures, examples, release notes, filenames, package contents, tags, and release assets use synthetic identifiers and reserved example domains.
|
|
11
11
|
5. **Secrets are never operational log data.** Tool arguments, command text, stdin, stdout, stderr, file content, OAuth bodies, credentials, and local resource values are not logged.
|
|
12
|
-
6. **A release is one version
|
|
12
|
+
6. **A release is one version with successful cross-platform evidence.** Package metadata, Worker version, browser-extension version/name, Git tag, GitHub Release, npm version, documentation, and deployed health version must agree, and the exact `origin/main` commit must have a completed successful push-triggered GitHub Actions run before a tag or release is created.
|
|
13
13
|
7. **Generic local automation is structured, not arbitrary evaluation.** Browser/application features may expose broad user authority under canonical `full`, but must not accept caller-provided JavaScript, AppleScript, JXA, or extension code.
|
|
14
14
|
8. **Daily-browser integration uses the existing profile.** The supported primary browser path is the packaged authenticated extension and machine-level loopback broker, preserving current tabs/login state; a separate automation profile is not an equivalent replacement.
|
|
15
15
|
9. **Pairing and resource secrets are not conversation or log data.** Tokens and injected local-resource values must not be returned, embedded in URLs, or written to operational logs.
|
|
@@ -123,7 +123,7 @@ Every defect fix includes a regression test that fails for the original reason.
|
|
|
123
123
|
The required matrix includes:
|
|
124
124
|
|
|
125
125
|
- Linux, macOS, and Windows on the pinned Node/npm baseline;
|
|
126
|
-
-
|
|
126
|
+
- current-tree and reachable-history privacy gates, release-impact enforcement, and exact-commit release-CI gating;
|
|
127
127
|
- package-manifest and sensitive-artifact inspection;
|
|
128
128
|
- generated type checks and recursively discovered JavaScript/shell syntax checks;
|
|
129
129
|
- concurrent exclusive-lock/atomic-replacement tests, PID-reuse/age tests, and fail-closed service-lifecycle tests;
|
|
@@ -134,7 +134,7 @@ The required matrix includes:
|
|
|
134
134
|
- managed-job integrity, recovery, cancellation, cleanup, and redaction;
|
|
135
135
|
- dependency audit, registry signatures/attestations, SBOM, and Worker dry run.
|
|
136
136
|
|
|
137
|
-
Cross-platform tests must not depend on shell syntax, case-sensitive Windows paths, Unix-only executable shims, or timing races when a deterministic scheduler can be injected.
|
|
137
|
+
Cross-platform tests must not depend on shell syntax, case-sensitive Windows paths, Unix-only executable shims, or timing races when a deterministic scheduler can be injected. Local success cannot substitute for the required Linux/macOS/Windows push CI result used by the release gate.
|
|
138
138
|
|
|
139
139
|
## Documentation rules
|
|
140
140
|
|
|
@@ -177,7 +177,7 @@ A thorough review asks:
|
|
|
177
177
|
- Are persistent files atomic, owner-only, size-bounded, and symlink-aware?
|
|
178
178
|
- Can browser/app automation be expressed without arbitrary evaluation, and are pairing/resource values absent from results and logs?
|
|
179
179
|
- Can a stale PID, stale socket, duplicate request, partial write, or ambiguous remote response violate integrity?
|
|
180
|
-
- Are package, CI, Worker, service, and release behavior tested on every supported platform?
|
|
180
|
+
- Are package, CI, Worker, service, and release behavior tested on every supported platform, and does publication verify the exact successful push CI run?
|
|
181
181
|
- Does the complete diff contain any real identifier, path, host, account, or credential-shaped value?
|
|
182
182
|
- Does the change require a new npm version and deployment?
|
|
183
183
|
|
package/docs/LOCAL_AUTOMATION.md
CHANGED
|
@@ -83,7 +83,9 @@ Then use `value_resource` for a text field or `resources` for `browser_upload_fi
|
|
|
83
83
|
- `inspect_local_application` returns a bounded macOS Accessibility tree.
|
|
84
84
|
- `operate_local_application` performs a structured Accessibility action.
|
|
85
85
|
|
|
86
|
-
macOS UI inspection and actions require Accessibility permission for the Node/Machine Bridge process. Application discovery and opening are available on supported desktop platforms; structured UI inspection currently targets macOS. The application backend uses fixed JXA implementation code. The caller selects only an application, structured selector, action, and optional bounded value; NUL-containing action text is rejected. `include_values` never returns values for secure-text roles or controls whose metadata indicates passwords, tokens, one-time codes, or payment-card secrets.
|
|
86
|
+
macOS UI inspection and actions require Accessibility permission for the Node/Machine Bridge process. Application discovery and opening are available on supported desktop platforms; structured UI inspection currently targets macOS. The application backend uses fixed JXA implementation code and requires a non-empty JSON result from `osascript`; missing or malformed helper output fails explicitly. The caller selects only an application, structured selector, action, and optional bounded value; NUL-containing action text is rejected. `include_values` never returns values for secure-text roles or controls whose metadata indicates passwords, tokens, one-time codes, or payment-card secrets.
|
|
87
|
+
|
|
88
|
+
Menu-bar and menu subtrees are not recursively expanded by default. This keeps main-window controls within the element/time budget for applications with large localized menu hierarchies. Pass `include_menus: true` to `inspect_local_application` or `operate_local_application` only when the target is a menu item.
|
|
87
89
|
|
|
88
90
|
## Capability discovery and automatic selection
|
|
89
91
|
|
package/docs/OPERATIONS.md
CHANGED
|
@@ -61,7 +61,7 @@ Application UI inspection/actions require Accessibility permission for the Node/
|
|
|
61
61
|
|
|
62
62
|
`machine-mcp` is a foreground command. It remains attached to the terminal, defaults to `info` logging, and stops on `Ctrl+C`. `machine-mcp service start` launches the installed platform service in the background and returns to the shell; that service uses `warn` logging.
|
|
63
63
|
|
|
64
|
-
A global npm install changes the CLI files on disk but does not replace an already running Node process. Startup and other state-changing CLI operations use a token/process-identity lock and wait up to 30 seconds for a normal concurrent startup to finish; a short launchd/systemd overlap is therefore serialized rather than reported immediately as an error. On a normal foreground start, Machine Bridge unloads the platform service and then independently examines the workspace daemon lock. This second path handles a detached/orphan `--daemon-only` process that launchd/systemd/task scheduling no longer tracks, including legacy locks without mode/version metadata. Before sending `SIGTERM`, Machine Bridge verifies PID and process start time plus the live command line, entrypoint, canonical workspace, canonical state root, and daemon-only flag. It waits up to 15 seconds for both the PID and lock to disappear and
|
|
64
|
+
A global npm install changes the CLI files on disk but does not replace an already running Node process. Startup and other state-changing CLI operations use a token/process-identity lock and wait up to 30 seconds for a normal concurrent startup to finish; a short launchd/systemd overlap is therefore serialized rather than reported immediately as an error. On a normal foreground start, Machine Bridge unloads the platform service and then independently examines the workspace daemon lock. This second path handles a detached/orphan `--daemon-only` process that launchd/systemd/task scheduling no longer tracks, including legacy locks without mode/version metadata. Before sending `SIGTERM`, Machine Bridge verifies PID and process start time plus the live command line, entrypoint, canonical workspace, canonical state root, and daemon-only flag. It waits up to 15 seconds for both the PID and lock to disappear and does not add a later forced-kill escalation. On POSIX, a daemon that handles and ignores `SIGTERM` therefore reaches the bounded timeout and remains alive. Node maps the supported termination signals to immediate process termination on Windows, so the same verified-daemon stop normally completes rather than exercising the POSIX timeout branch. A foreground or unverifiable process is left untouched; stop a foreground instance with `Ctrl+C`.
|
|
65
65
|
|
|
66
66
|
`machine-mcp service status [WORKSPACE]` reports two independent layers: the platform service (`active`) and `workspace_daemon`, plus `effective_active` and `orphaned_workspace_daemon` summary flags. On macOS it is possible for launchd to report inactive while a prior Node process remains alive with parent PID 1; that is an orphan-daemon condition, not proof that the daemon stopped. `service stop` unloads the provider when present and then terminates only a verified service-style workspace daemon. `service uninstall` and full uninstall are ordered fail-closed operations: provider stop → verified daemon stop(s) → definition removal. A failed or ambiguous stop leaves definitions and state intact. If takeover reaches its deadline, run:
|
|
67
67
|
|
package/docs/PRIVACY.md
CHANGED
|
@@ -8,9 +8,14 @@ Run:
|
|
|
8
8
|
|
|
9
9
|
```sh
|
|
10
10
|
npm run privacy:check
|
|
11
|
+
npm run privacy:history
|
|
11
12
|
```
|
|
12
13
|
|
|
13
|
-
|
|
14
|
+
`privacy:check` scans tracked and unignored new UTF-8 files and relative names for generic/encrypted/algorithm-specific private-key headers, AWS/GitHub/GitLab/npm/Slack/Google/live-payment/API token forms, JWT-shaped bearer values, embedded-credential URLs, absolute user-home paths, non-example email/`user@host` identifiers, credential-shaped filenames, and locally configured private identifiers. A tracked `.npmrc` is parsed: non-secret repository settings such as `engine-strict=true` are allowed, while authentication/identity keys, environment interpolation, and embedded credentials fail closed. Publication-surface symbolic links are rejected rather than followed. Binary, invalid UTF-8, and files above the bounded scanner limit require explicit manual review instead of being silently skipped. Findings report only file, line, and rule; the matched value is never printed.
|
|
15
|
+
|
|
16
|
+
`privacy:history` first runs the same current-tree checks, then scans every reachable historical blob path and bounded UTF-8 blob plus every reachable commit message. It catches values that were committed and later deleted. Standard public Dependabot signing trailers are ignored only in that exact commit-message context; the same non-example address remains disallowed in ordinary files. The local `.privacy-denylist` is also applied to history, so a developer may discover legacy identifiers that CI cannot know. Such a finding is real publication history, but ordinary commits cannot erase it.
|
|
17
|
+
|
|
18
|
+
Git author and committer identity headers are canonical Git metadata rather than blob or commit-message content and are not automatically rejected. Audit them separately with `git log`; changing historical identity metadata requires a coordinated history rewrite and force-update of affected refs.
|
|
14
19
|
|
|
15
20
|
Maintain machine-specific names in an ignored owner-only file:
|
|
16
21
|
|
|
@@ -41,13 +46,13 @@ Before committing or publishing:
|
|
|
41
46
|
|
|
42
47
|
- inspect the complete staged diff, including tests, snapshots, examples, release notes, and generated metadata;
|
|
43
48
|
- use reserved example domains and neutral aliases;
|
|
44
|
-
- run `npm run privacy:check`, `npm run check`, and `npm pack --dry-run`;
|
|
49
|
+
- run `npm run privacy:check`, review `npm run privacy:history`, run `npm run check`, and inspect `npm pack --dry-run`;
|
|
45
50
|
- treat paths, host aliases, usernames, codenames, real browser URLs/page captures, application names tied to a user, and form data as private metadata even when they are not authentication secrets;
|
|
46
51
|
- keep browser pairing-state files and captured source/screenshots out of fixtures, documentation, support logs, and release assets;
|
|
47
52
|
- review any tracked `.npmrc` as configuration code and never commit authentication, registry identity, environment interpolation, cert/key paths, or credential-bearing URLs;
|
|
48
53
|
- inspect package modes and filenames as well as file contents—an empty `.env`, private-key filename, database, or log is still an inappropriate publication artifact.
|
|
49
54
|
|
|
50
|
-
The scanner is heuristic. It cannot identify every personal or organizational name, split/transformed/encrypted value, image, archive, binary fixture, custom credential format,
|
|
55
|
+
The scanner is heuristic. It cannot identify every personal or organizational name, split/transformed/encrypted value, image, archive, binary fixture, custom credential format, unreachable/pruned object, reflog-only object, fork/cache copy, or Git identity header. Passing current-tree and reachable-history checks is a gate, not proof that every publication copy contains no private data.
|
|
51
56
|
|
|
52
57
|
## Incident response
|
|
53
58
|
|
package/docs/RELEASING.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
The release invariant is:
|
|
4
4
|
|
|
5
|
-
- `main` points to the release commit.
|
|
5
|
+
- `main` points to the release commit, and the exact commit has a completed successful push-triggered `.github/workflows/ci.yml` run.
|
|
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.
|
|
@@ -21,8 +21,8 @@ The release invariant is:
|
|
|
21
21
|
```
|
|
22
22
|
|
|
23
23
|
2. Add the matching dated `CHANGELOG.md` section.
|
|
24
|
-
3. Run `npm run release-impact:check`, `npm run privacy:check`, `npm run check`, both dependency audits, `npm audit signatures`, and generate a CycloneDX `npm sbom`.
|
|
25
|
-
4. Inspect the complete diff and `npm pack --dry-run`, including packaged file modes and every helper referenced by package scripts, then commit and push all release changes to `main`.
|
|
24
|
+
3. Run `npm run release-impact:check`, `npm run privacy:check`, review `npm run privacy:history`, run `npm run check`, both dependency audits, `npm audit signatures`, and generate a CycloneDX `npm sbom`.
|
|
25
|
+
4. Inspect the complete diff and `npm pack --dry-run`, including packaged file modes and every helper referenced by package scripts, then commit and push all release changes to `main`. Wait for the push-triggered Linux/macOS/Windows and package-audit checks to complete successfully.
|
|
26
26
|
|
|
27
27
|
A privacy/security documentation correction is not “docs only” for release purposes. It requires a replacement npm version and, when appropriate, deprecation or unpublication of the affected version.
|
|
28
28
|
|
|
@@ -34,7 +34,7 @@ From a clean `main` worktree:
|
|
|
34
34
|
npm run release:publish
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
-
The command validates the project, including repository privacy checks, fast-forwards `origin/main
|
|
37
|
+
The command validates the project, including repository privacy checks, fast-forwards `origin/main` when needed, then requires the latest push-triggered CI run for that exact commit to be completed successfully before it creates or verifies the annotated version tag. It then pushes the tag, builds the npm tarball, creates or updates the GitHub Release, uploads the tarball, and verifies the resulting state. Read-only GitHub operations, Git pushes, and idempotent release updates use bounded retries only for classified transient network failures; ambiguous Release creation responses are resolved by querying server state before continuing.
|
|
38
38
|
|
|
39
39
|
To verify without changing anything:
|
|
40
40
|
|
|
@@ -64,4 +64,4 @@ The npm lifecycle repeats the full project checks and the GitHub synchronization
|
|
|
64
64
|
- An authenticated GitHub CLI session with repository release permission.
|
|
65
65
|
- An npm account that owns the package or has maintainer permission.
|
|
66
66
|
|
|
67
|
-
GitHub Actions is
|
|
67
|
+
GitHub Actions is a required publication boundary. The authenticated GitHub CLI must be able to read Actions runs, and a missing, pending, cancelled, skipped, or failed exact-commit push run blocks both `release:publish` and `release:check`. This prevents a machine-specific local pass from publishing a version that fails another supported platform.
|
package/docs/TESTING.md
CHANGED
|
@@ -13,15 +13,15 @@ The repository requires Node.js 26 and npm 12. `.node-version`, `.nvmrc`, `packa
|
|
|
13
13
|
The suite includes:
|
|
14
14
|
|
|
15
15
|
- release-impact enforcement requiring a new package version and CHANGELOG section for release-relevant changes;
|
|
16
|
-
- release-state diagnostics distinguishing missing local/remote version tags from tags that point to the wrong commit,
|
|
16
|
+
- release-state diagnostics distinguishing missing local/remote version tags from tags that point to the wrong commit, plus a release-CI gate that rejects missing, pending, failed, pull-request-only, stale, or wrong-commit runs;
|
|
17
17
|
- generated Cloudflare Worker types and strict TypeScript checking, including unused-local and unused-parameter rejection;
|
|
18
18
|
- recursive syntax validation for every JavaScript file under the shipped/runtime/test roots plus the shell wrapper;
|
|
19
19
|
- shared tool-catalog schema, annotation, and profile-inventory checks;
|
|
20
20
|
- default working-agreement injection without user files, bounded automatic project metadata, script-body non-disclosure, user-global opt-out, repository opt-out rejection, global `model_instructions_file` injection in stdio/remote initialization, hierarchical precedence, live project/skill rescanning and fingerprints, automatic task ranking/loading, command override/removal, direct argv handling, timeout ceilings, and execution-profile denial;
|
|
21
21
|
- concurrent complete-before-visible lock claims, atomic replacement under active readers, malformed-lock grace, snapshot/token-safe reclamation, absolute-age expiry, PID-reuse detection, and bounded startup-lock waiting;
|
|
22
|
-
- foreground takeover of active and orphaned background daemons, legacy lock identification from canonicalized process arguments, foreground-process protection, actual-PID exit waiting, non-
|
|
22
|
+
- foreground takeover of active and orphaned background daemons, legacy lock identification from canonicalized process arguments, foreground-process protection, actual-PID exit waiting, POSIX non-escalating timeout behavior, Windows verified-daemon stop semantics, daemon lock mode/version/process-start metadata, launchd service-target semantics, and silent idempotent duplicate service starts;
|
|
23
23
|
- fail-closed service lifecycle ordering for provider-stop, all-workspace daemon-stop, and definition removal, including platform/daemon/removal failure injection and normalized macOS/systemd/Windows results;
|
|
24
|
-
- machine-level browser-broker ownership/client proxying, authenticated extension origin/subprotocol, non-cacheable local pairing, pairing-token non-disclosure, resource-backed upload routing,
|
|
24
|
+
- machine-level browser-broker ownership/client proxying, authenticated extension origin/subprotocol, non-cacheable local pairing, pairing-token non-disclosure, resource-backed upload routing, broker result redaction, pre-registered runtime-handshake listeners, and bounded socket/HTTP waits;
|
|
25
25
|
- canonical path and symbolic-link escape tests;
|
|
26
26
|
- relative-path privacy and error-path redaction tests;
|
|
27
27
|
- atomic create/update, optimistic hash, exact edit, and patch transaction tests;
|
|
@@ -36,7 +36,7 @@ The suite includes:
|
|
|
36
36
|
- local resource CLI registration, permission checks, dynamic reload, state-path redaction, and content non-disclosure;
|
|
37
37
|
- real Ed25519 and RSA generation, idempotent reuse, public/private correspondence, mode enforcement, incomplete/mismatched/symlink rejection, and private-content non-disclosure;
|
|
38
38
|
- real-machine canonical-full sandbox acceptance for outside-workspace I/O, direct/shell execution, full environment inheritance, SSH prerequisites, temporary authorized-key writing, and detached cleanup without external state changes;
|
|
39
|
-
- deterministic injected atomic-replace failures and repeated Windows full-sandbox runs
|
|
39
|
+
- deterministic injected atomic-replace failures, sustained transient Windows sharing contention beyond the old retry budget, bounded exponential delay selection, and repeated Windows full-sandbox runs;
|
|
40
40
|
- canonical named-profile repair and full-only tool exposure parity between local and Worker policy filters;
|
|
41
41
|
- managed-job staging/local approval/cancel-before-start, detachment, job-scoped temporary files, resource hash verification/redaction, discard capture, finally execution, descendant-tree escalation, token/snapshot-safe transition locks, runner process identity, plan scrubbing, PID-reuse-safe dead-runner recovery, and legacy PID-file compatibility;
|
|
42
42
|
- daemon/startup locking, successfully-read corrupt-JSON recovery, and explicit propagation/preservation of oversized, symbolic-link, permission, and I/O failures;
|
|
@@ -49,9 +49,22 @@ The suite includes:
|
|
|
49
49
|
- 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;
|
|
50
50
|
- 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.
|
|
51
51
|
|
|
52
|
+
## Opt-in live desktop and browser validation
|
|
53
|
+
|
|
54
|
+
The normal suite uses deterministic mocks for macOS Accessibility and an authenticated in-process extension peer for browser routing. Before a release that changes local UI automation, run the macOS live smoke test on a machine where the invoking Node/terminal process has Accessibility permission:
|
|
55
|
+
|
|
56
|
+
```sh
|
|
57
|
+
npm run app-automation:live-test
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
It opens Calculator, activates it through the fixed JXA helper, verifies structured JSON output, inspects main-window Accessibility controls with menu recursion disabled, clicks the `One` control, and quits the application. It is intentionally excluded from CI because macOS TCC permission and a graphical login session are host state.
|
|
61
|
+
|
|
62
|
+
For browser changes, perform an isolated-profile smoke test with the packaged unpacked extension: pair it to a temporary source runtime, navigate a local no-store page, inspect controls, fill multiple fields, click a button, verify the live DOM, and capture a screenshot. A Playwright persistent Chromium context is an acceptable harness because it loads the real MV3 service worker while keeping the user's daily browser profile untouched.
|
|
63
|
+
|
|
52
64
|
## Additional release checks
|
|
53
65
|
|
|
54
66
|
```sh
|
|
67
|
+
npm run privacy:history
|
|
55
68
|
npm run worker:dry-run
|
|
56
69
|
npm audit --audit-level=high
|
|
57
70
|
npm audit --omit=dev --audit-level=high
|
|
@@ -62,7 +75,7 @@ npm run version:check
|
|
|
62
75
|
npm run release-impact:check
|
|
63
76
|
```
|
|
64
77
|
|
|
65
|
-
GitHub Actions executes the main suite on Linux, macOS, and Windows using the pinned Node 26/npm 12 baseline. Because Node 26 currently bundles npm 11, CI explicitly disables setup-node's automatic package-manager cache and upgrades npm from the runner temporary directory before any project-local npm command can trigger strict `devEngines`. Checkout fetches version tags so the release-impact gate can compare the branch with the latest release. A separate package-audit job audits both the complete dependency graph and the production-only graph, verifies registry signatures and attestations, validates a CycloneDX SBOM written under the runner temporary directory, exercises the documented isolated global installation, then performs a dry-run package build. The macOS matrix job also runs the installation smoke test because Wrangler's optional `fsevents` resolution is platform-specific.
|
|
78
|
+
GitHub Actions executes the main suite on Linux, macOS, and Windows using the pinned Node 26/npm 12 baseline. Because Node 26 currently bundles npm 11, CI explicitly disables setup-node's automatic package-manager cache and upgrades npm from the runner temporary directory before any project-local npm command can trigger strict `devEngines`. Checkout fetches version tags so the release-impact gate can compare the branch with the latest release. A separate package-audit job scans reachable Git history, audits both the complete dependency graph and the production-only graph, verifies registry signatures and attestations, validates a CycloneDX SBOM written under the runner temporary directory, exercises the documented isolated global installation, then performs a dry-run package build. The macOS matrix job also runs the installation smoke test because Wrangler's optional `fsevents` resolution is platform-specific. Third-party Actions are pinned to immutable 40-character commit SHAs; architecture tests reject movable tags, and Dependabot monitors both action and dependency updates.
|
|
66
79
|
|
|
67
80
|
## Test design rules
|
|
68
81
|
|
|
@@ -79,7 +92,7 @@ GitHub Actions executes the main suite on Linux, macOS, and Windows using the pi
|
|
|
79
92
|
|
|
80
93
|
## Privacy hygiene
|
|
81
94
|
|
|
82
|
-
Run `npm run privacy:check` before committing and before packaging. Developers should maintain an ignored owner-only `.privacy-denylist` for machine aliases, usernames, internal codenames, and other private identifiers that a generic scanner cannot know. See [Repository privacy hygiene](PRIVACY.md).
|
|
95
|
+
Run `npm run privacy:check` before committing and before packaging. Run and review `npm run privacy:history` before release; local denylist findings may require an explicit owner decision because ordinary commits cannot remove public history. Developers should maintain an ignored owner-only `.privacy-denylist` for machine aliases, usernames, internal codenames, and other private identifiers that a generic scanner cannot know. See [Repository privacy hygiene](PRIVACY.md).
|
|
83
96
|
|
|
84
97
|
## Package manifest
|
|
85
98
|
|
|
@@ -89,4 +102,4 @@ The stdio integration test also sends an oversized line, verifies bounded reject
|
|
|
89
102
|
|
|
90
103
|
## Architecture and documentation regression checks
|
|
91
104
|
|
|
92
|
-
`npm run architecture:test` rejects local-module dependency cycles, missing static or dynamic relative imports, package scripts that reference missing files, drift from the recursive syntax scanner, incomplete executable package directories, inconsistent installation guidance, obsolete `LocalDaemon`/`daemon.mjs` naming, broken relative Markdown links, invisible ASCII control bytes in repository text, removal of the owner-required default-`full` engineering invariant, and accidental publication of `.project-local/` notes.
|
|
105
|
+
`npm run architecture:test` rejects movable GitHub Action references and loss of the CI history scan, release-gate script drift, local-module dependency cycles, missing static or dynamic relative imports, package scripts that reference missing files, drift from the recursive syntax scanner, incomplete executable package directories, inconsistent installation guidance, obsolete `LocalDaemon`/`daemon.mjs` naming, broken relative Markdown links, invisible ASCII control bytes in repository text, removal of the owner-required default-`full` engineering invariant, and accidental publication of `.project-local/` notes.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "machine-bridge-mcp",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.2",
|
|
4
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",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"worker:types": "wrangler types src/worker/worker-configuration.d.ts",
|
|
43
43
|
"typecheck": "npm run worker:types && tsc -p tsconfig.json --noEmit",
|
|
44
44
|
"syntax": "node scripts/syntax-check.mjs",
|
|
45
|
-
"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 process-lock:test && npm run service-lifecycle:test && 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",
|
|
45
|
+
"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 release-ci: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 process-lock:test && npm run service-lifecycle:test && 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",
|
|
46
46
|
"worker:dry-run": "wrangler deploy --dry-run",
|
|
47
47
|
"worker:integration-test": "node tests/worker-integration-test.mjs",
|
|
48
48
|
"release:check": "node scripts/github-release.mjs --check",
|
|
@@ -55,6 +55,7 @@
|
|
|
55
55
|
"full-access:test": "node tests/full-access-test.mjs",
|
|
56
56
|
"atomic-fs:test": "node tests/atomic-fs-test.mjs",
|
|
57
57
|
"privacy:check": "node scripts/privacy-check.mjs",
|
|
58
|
+
"privacy:history": "node scripts/privacy-check.mjs --history",
|
|
58
59
|
"package:test": "node tests/package-test.mjs",
|
|
59
60
|
"release-impact:check": "node scripts/release-impact-check.mjs",
|
|
60
61
|
"release-impact:test": "node tests/release-impact-test.mjs",
|
|
@@ -65,11 +66,13 @@
|
|
|
65
66
|
"secure-file:test": "node tests/secure-file-test.mjs",
|
|
66
67
|
"architecture:test": "node tests/architecture-test.mjs",
|
|
67
68
|
"release-state:test": "node tests/release-state-test.mjs",
|
|
69
|
+
"release-ci:test": "node tests/release-ci-test.mjs",
|
|
68
70
|
"agent-context:test": "node tests/agent-context-test.mjs",
|
|
69
71
|
"browser-bridge:test": "node tests/browser-bridge-test.mjs",
|
|
70
72
|
"app-automation:test": "node tests/app-automation-test.mjs",
|
|
71
73
|
"process-lock:test": "node tests/process-lock-test.mjs",
|
|
72
|
-
"service-lifecycle:test": "node tests/service-lifecycle-test.mjs"
|
|
74
|
+
"service-lifecycle:test": "node tests/service-lifecycle-test.mjs",
|
|
75
|
+
"app-automation:live-test": "node tests/app-automation-test.mjs --live-macos"
|
|
73
76
|
},
|
|
74
77
|
"dependencies": {
|
|
75
78
|
"wrangler": "4.110.0",
|
|
@@ -10,6 +10,7 @@ import { tmpdir } from "node:os";
|
|
|
10
10
|
import { dirname, join, resolve } from "node:path";
|
|
11
11
|
import { spawnSync } from "node:child_process";
|
|
12
12
|
import { runNetworkCommand } from "./network-retry.mjs";
|
|
13
|
+
import { requireSuccessfulCiRun } from "./release-ci.mjs";
|
|
13
14
|
import { tagSyncError } from "./release-state.mjs";
|
|
14
15
|
import { fileURLToPath } from "node:url";
|
|
15
16
|
|
|
@@ -124,6 +125,31 @@ function remoteTagCommit(tag) {
|
|
|
124
125
|
return (peeled ?? direct)?.[0] ?? null;
|
|
125
126
|
}
|
|
126
127
|
|
|
128
|
+
function assertSuccessfulCi(head) {
|
|
129
|
+
const text = outputNetwork("gh", [
|
|
130
|
+
"run",
|
|
131
|
+
"list",
|
|
132
|
+
"--workflow",
|
|
133
|
+
".github/workflows/ci.yml",
|
|
134
|
+
"--commit",
|
|
135
|
+
head,
|
|
136
|
+
"--event",
|
|
137
|
+
"push",
|
|
138
|
+
"--limit",
|
|
139
|
+
"20",
|
|
140
|
+
"--json",
|
|
141
|
+
"databaseId,status,conclusion,headSha,event,createdAt,url",
|
|
142
|
+
]);
|
|
143
|
+
let runs;
|
|
144
|
+
try { runs = JSON.parse(text); }
|
|
145
|
+
catch { fail("GitHub Actions did not return valid JSON"); }
|
|
146
|
+
let run;
|
|
147
|
+
try { run = requireSuccessfulCiRun(runs, head); }
|
|
148
|
+
catch (error) { fail(String(error?.message || error)); }
|
|
149
|
+
console.log(`GitHub Actions CI succeeded for ${head} (run ${run.databaseId}).`);
|
|
150
|
+
return run;
|
|
151
|
+
}
|
|
152
|
+
|
|
127
153
|
function releaseInfo(tag) {
|
|
128
154
|
const result = runNetwork(
|
|
129
155
|
"gh",
|
|
@@ -148,6 +174,7 @@ function assertCoreSync({ requireReleaseAsset }) {
|
|
|
148
174
|
if (head !== originMain) {
|
|
149
175
|
fail(`HEAD ${head} does not match origin/main ${originMain}`);
|
|
150
176
|
}
|
|
177
|
+
assertSuccessfulCi(head);
|
|
151
178
|
|
|
152
179
|
const localCommit = localTagCommit(tag);
|
|
153
180
|
const localTagError = tagSyncError({ scope: "local", tag, head, commit: localCommit });
|
|
@@ -275,6 +302,7 @@ function publishCurrent() {
|
|
|
275
302
|
}
|
|
276
303
|
runNetwork("git", ["push", "origin", "HEAD:main"]);
|
|
277
304
|
}
|
|
305
|
+
assertSuccessfulCi(head);
|
|
278
306
|
|
|
279
307
|
const existingLocal = localTagCommit(tag);
|
|
280
308
|
if (existingLocal && existingLocal !== head) {
|
|
@@ -5,6 +5,11 @@ import { fileURLToPath } from "node:url";
|
|
|
5
5
|
|
|
6
6
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
7
7
|
const selfPath = "scripts/privacy-check.mjs";
|
|
8
|
+
const requestedOptions = new Set(process.argv.slice(2));
|
|
9
|
+
for (const option of requestedOptions) {
|
|
10
|
+
if (option !== "--history") throw new Error(`unknown privacy-check option: ${option}`);
|
|
11
|
+
}
|
|
12
|
+
const scanHistory = requestedOptions.has("--history");
|
|
8
13
|
const candidates = collectCandidateFiles(root);
|
|
9
14
|
const denylist = loadDenylist(path.join(root, ".privacy-denylist"));
|
|
10
15
|
const findings = [];
|
|
@@ -47,6 +52,8 @@ for (const relativePath of candidates) {
|
|
|
47
52
|
scanDenylist(relativePath, text, denylist, findings);
|
|
48
53
|
}
|
|
49
54
|
|
|
55
|
+
const historySummaryCounts = scanHistory ? scanReachableHistory(root, denylist, findings) : { blobs: 0, commits: 0 };
|
|
56
|
+
|
|
50
57
|
if (findings.length) {
|
|
51
58
|
for (const finding of findings.slice(0, 100)) {
|
|
52
59
|
process.stderr.write(`${redactReportPath(finding.path, denylist)}:${finding.line}: ${finding.rule}\n`);
|
|
@@ -56,7 +63,8 @@ if (findings.length) {
|
|
|
56
63
|
process.exit(1);
|
|
57
64
|
}
|
|
58
65
|
|
|
59
|
-
|
|
66
|
+
const historySummary = scanHistory ? `; ${historySummaryCounts.blobs} reachable history blobs; ${historySummaryCounts.commits} commit messages` : "";
|
|
67
|
+
process.stderr.write(`privacy check ok (${candidates.length} tracked/unignored files; ${denylist.length} local denylist entries${historySummary})\n`);
|
|
60
68
|
|
|
61
69
|
function collectCandidateFiles(directory) {
|
|
62
70
|
try {
|
|
@@ -87,6 +95,136 @@ function collectCandidateFiles(directory) {
|
|
|
87
95
|
}
|
|
88
96
|
}
|
|
89
97
|
|
|
98
|
+
function scanReachableHistory(directory, entries, out) {
|
|
99
|
+
let listing;
|
|
100
|
+
try {
|
|
101
|
+
listing = execFileSync("git", ["-C", directory, "rev-list", "--objects", "--all", "-z"], {
|
|
102
|
+
encoding: "buffer",
|
|
103
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
104
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
105
|
+
});
|
|
106
|
+
} catch {
|
|
107
|
+
throw new Error("privacy history scan could not enumerate reachable Git objects");
|
|
108
|
+
}
|
|
109
|
+
let records;
|
|
110
|
+
try { records = new TextDecoder("utf-8", { fatal: true }).decode(listing).split("\0").filter(Boolean); }
|
|
111
|
+
catch { throw new Error("privacy history scan encountered a non-UTF-8 Git object path"); }
|
|
112
|
+
const objectPaths = new Map();
|
|
113
|
+
let objectHash = "";
|
|
114
|
+
for (const record of records) {
|
|
115
|
+
if (/^[0-9a-f]{40,64}$/.test(record)) {
|
|
116
|
+
objectHash = record;
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (!record.startsWith("path=") || !objectHash) {
|
|
120
|
+
objectHash = "";
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
const relativePath = record.slice(5);
|
|
124
|
+
if (relativePath && !relativePath.includes("\0")) {
|
|
125
|
+
if (!objectPaths.has(objectHash)) objectPaths.set(objectHash, new Set());
|
|
126
|
+
objectPaths.get(objectHash).add(relativePath);
|
|
127
|
+
}
|
|
128
|
+
objectHash = "";
|
|
129
|
+
}
|
|
130
|
+
const hashes = [...objectPaths.keys()];
|
|
131
|
+
let metadata;
|
|
132
|
+
try {
|
|
133
|
+
metadata = execFileSync("git", ["-C", directory, "cat-file", "--batch-check=%(objectname) %(objecttype) %(objectsize)"], {
|
|
134
|
+
input: `${hashes.join("\n")}\n`,
|
|
135
|
+
encoding: "utf8",
|
|
136
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
137
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
138
|
+
}).trim().split(/\r?\n/);
|
|
139
|
+
} catch {
|
|
140
|
+
throw new Error("privacy history scan could not classify reachable Git objects");
|
|
141
|
+
}
|
|
142
|
+
const blobSizes = new Map();
|
|
143
|
+
for (const line of metadata) {
|
|
144
|
+
const match = /^([0-9a-f]{40,64}) blob ([0-9]+)$/.exec(line);
|
|
145
|
+
if (match) blobSizes.set(match[1], Number(match[2]));
|
|
146
|
+
}
|
|
147
|
+
let scanned = 0;
|
|
148
|
+
for (const [hash, paths] of objectPaths) {
|
|
149
|
+
if (!blobSizes.has(hash)) continue;
|
|
150
|
+
const relativePaths = [...paths].sort();
|
|
151
|
+
const reportPath = relativePaths[0] || "unknown";
|
|
152
|
+
const contentPath = relativePaths.find((relativePath) => relativePath !== selfPath) || reportPath;
|
|
153
|
+
for (const relativePath of relativePaths) {
|
|
154
|
+
if (relativePath === ".privacy-denylist") continue;
|
|
155
|
+
const historicalPath = `history/${hash.slice(0, 12)}/${relativePath}`;
|
|
156
|
+
scanDenylistPath(historicalPath, entries, out);
|
|
157
|
+
scanSensitivePath(historicalPath, out);
|
|
158
|
+
}
|
|
159
|
+
if (blobSizes.get(hash) > 5 * 1024 * 1024) {
|
|
160
|
+
out.push({ path: `history/${hash.slice(0, 12)}/${reportPath}`, line: 1, rule: "historical file exceeds privacy scanner size limit and requires manual review" });
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
let buffer;
|
|
164
|
+
try {
|
|
165
|
+
buffer = execFileSync("git", ["-C", directory, "cat-file", "blob", hash], {
|
|
166
|
+
encoding: "buffer",
|
|
167
|
+
maxBuffer: 6 * 1024 * 1024,
|
|
168
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
169
|
+
});
|
|
170
|
+
} catch {
|
|
171
|
+
out.push({ path: `history/${hash.slice(0, 12)}/${reportPath}`, line: 1, rule: "historical file content could not be read" });
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
scanned += 1;
|
|
175
|
+
const historicalPath = `history/${hash.slice(0, 12)}/${contentPath}`;
|
|
176
|
+
if (buffer.includes(0)) {
|
|
177
|
+
out.push({ path: historicalPath, line: 1, rule: "binary file in reachable Git history requires manual review" });
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
let historicalText;
|
|
181
|
+
try { historicalText = new TextDecoder("utf-8", { fatal: true }).decode(buffer); }
|
|
182
|
+
catch {
|
|
183
|
+
out.push({ path: historicalPath, line: 1, rule: "non-UTF-8 file in reachable Git history requires manual review" });
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
if (contentPath !== selfPath) scanBuiltIn(historicalPath, historicalText, out);
|
|
187
|
+
if (relativePaths.some((relativePath) => path.basename(relativePath).toLowerCase() === ".npmrc")) scanNpmrc(historicalPath, historicalText, out);
|
|
188
|
+
scanDenylist(historicalPath, historicalText, entries, out);
|
|
189
|
+
}
|
|
190
|
+
const commitCount = scanReachableCommitMessages(directory, entries, out);
|
|
191
|
+
return { blobs: scanned, commits: commitCount };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function scanReachableCommitMessages(directory, entries, out) {
|
|
195
|
+
let output;
|
|
196
|
+
try {
|
|
197
|
+
output = execFileSync("git", ["-C", directory, "log", "--all", "--format=%H%x00%B%x00"], {
|
|
198
|
+
encoding: "buffer",
|
|
199
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
200
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
201
|
+
});
|
|
202
|
+
} catch {
|
|
203
|
+
throw new Error("privacy history scan could not read reachable commit messages");
|
|
204
|
+
}
|
|
205
|
+
let records;
|
|
206
|
+
try { records = new TextDecoder("utf-8", { fatal: true }).decode(output).split("\0"); }
|
|
207
|
+
catch { throw new Error("privacy history scan encountered a non-UTF-8 commit message"); }
|
|
208
|
+
let count = 0;
|
|
209
|
+
for (let index = 0; index + 1 < records.length; index += 2) {
|
|
210
|
+
const hash = records[index].trim();
|
|
211
|
+
const message = records[index + 1];
|
|
212
|
+
if (!/^[0-9a-f]{40,64}$/.test(hash)) continue;
|
|
213
|
+
const reportPath = `history/commit/${hash.slice(0, 12)}/message`;
|
|
214
|
+
const scannedMessage = removeKnownPublicAutomationTrailers(message);
|
|
215
|
+
scanBuiltIn(reportPath, scannedMessage, out);
|
|
216
|
+
scanDenylist(reportPath, scannedMessage, entries, out);
|
|
217
|
+
count += 1;
|
|
218
|
+
}
|
|
219
|
+
return count;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function removeKnownPublicAutomationTrailers(message) {
|
|
223
|
+
return String(message).split(/\r?\n/).filter((line) => {
|
|
224
|
+
return !/^(?:Signed-off-by|Co-authored-by):\s+dependabot\[bot\]\s+<[^>\s]+@github\.com>\s*$/i.test(line);
|
|
225
|
+
}).join("\n");
|
|
226
|
+
}
|
|
227
|
+
|
|
90
228
|
function redactReportPath(relativePath, entries) {
|
|
91
229
|
let shown = String(relativePath);
|
|
92
230
|
for (const entry of entries) {
|
|
@@ -112,7 +250,6 @@ function scanBuiltIn(relativePath, text, out) {
|
|
|
112
250
|
["live payment API key", /\b(?:sk|rk|pk)_live_[A-Za-z0-9]{16,}\b/g],
|
|
113
251
|
["API secret token", /\bsk-(?:proj-)?[A-Za-z0-9_-]{20,}\b/g],
|
|
114
252
|
["JWT-like bearer token", /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g],
|
|
115
|
-
["URL with embedded credentials", /https?:\/\/[^\s/@:"'<>]+:[^\s/@"'<>]+@[^\s/"'<>]+/gi],
|
|
116
253
|
["absolute macOS/Linux home path", /(?:^|[\s"'=(:,])\/(?:Users|home)\/([^/\s"'<>]+)\//gm],
|
|
117
254
|
["absolute Windows home path", /(?:^|[\s"'=(:,])\b[A-Za-z]:\\Users\\([^\\\s"'<>]+)\\/gm],
|
|
118
255
|
];
|
|
@@ -122,6 +259,11 @@ function scanBuiltIn(relativePath, text, out) {
|
|
|
122
259
|
out.push({ path: relativePath, line: lineNumber(text, match.index || 0), rule });
|
|
123
260
|
}
|
|
124
261
|
}
|
|
262
|
+
const credentialUrl = /https?:\/\/[^\s/@:"'<>]+:[^\s/@"'<>]+@([^\s/"'<>]+)/gi;
|
|
263
|
+
for (const match of text.matchAll(credentialUrl)) {
|
|
264
|
+
if (isReservedExampleHost(stripHostPort(match[1]))) continue;
|
|
265
|
+
out.push({ path: relativePath, line: lineNumber(text, match.index || 0), rule: "URL with embedded credentials" });
|
|
266
|
+
}
|
|
125
267
|
const emailLike = /\b[A-Z0-9._%+-]+@([A-Z0-9.-]+\.[A-Z]{2,})\b/gi;
|
|
126
268
|
for (const match of text.matchAll(emailLike)) {
|
|
127
269
|
if (isReservedExampleHost(match[1]) || String(match[1]).toLowerCase().endsWith(".noreply.github.com")) continue;
|
|
@@ -215,6 +357,12 @@ function hasNearbySshContext(text, offset) {
|
|
|
215
357
|
return /\b(?:ssh|scp|sftp)\b/i.test(text.slice(start, end));
|
|
216
358
|
}
|
|
217
359
|
|
|
360
|
+
function stripHostPort(host) {
|
|
361
|
+
const value = String(host || "");
|
|
362
|
+
if (value.startsWith("[")) return value.slice(1, value.indexOf("]") > 0 ? value.indexOf("]") : undefined);
|
|
363
|
+
return value.replace(/:[0-9]+$/, "");
|
|
364
|
+
}
|
|
365
|
+
|
|
218
366
|
function isReservedExampleHost(host) {
|
|
219
367
|
const value = String(host || "").toLowerCase().replace(/\.$/, "");
|
|
220
368
|
return value === "localhost"
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export function requireSuccessfulCiRun(runs, head) {
|
|
2
|
+
if (!Array.isArray(runs)) throw new Error("GitHub Actions response is not an array");
|
|
3
|
+
if (!/^[0-9a-f]{40,64}$/i.test(String(head || ""))) throw new Error("release commit SHA is invalid");
|
|
4
|
+
const matching = runs
|
|
5
|
+
.filter((run) => run && run.headSha === head && run.event === "push")
|
|
6
|
+
.sort((left, right) => String(right.createdAt || "").localeCompare(String(left.createdAt || "")));
|
|
7
|
+
const run = matching[0];
|
|
8
|
+
if (!run) {
|
|
9
|
+
throw new Error(`no push-triggered CI run exists for release commit ${head}; wait for GitHub Actions to register the run and retry`);
|
|
10
|
+
}
|
|
11
|
+
if (run.status !== "completed") {
|
|
12
|
+
throw new Error(`CI run ${run.databaseId || "unknown"} for release commit ${head} is ${run.status || "unknown"}; wait for completion and retry`);
|
|
13
|
+
}
|
|
14
|
+
if (run.conclusion !== "success") {
|
|
15
|
+
throw new Error(`CI run ${run.databaseId || "unknown"} for release commit ${head} concluded ${run.conclusion || "unknown"}; fix or rerun CI before release`);
|
|
16
|
+
}
|
|
17
|
+
return run;
|
|
18
|
+
}
|
|
@@ -112,12 +112,12 @@ export class AgentContextManager {
|
|
|
112
112
|
const discovered = await this.discoverSkills(state, { maxResults: MAX_SKILL_RESULTS }, context);
|
|
113
113
|
const maxSkills = clampInt(args.max_skills, 10, 1, 50);
|
|
114
114
|
const skillMatches = discovered.skills
|
|
115
|
-
.map((skill) => ({ skill, score: relevanceScore(task, `${skill.name} ${skill.description}
|
|
115
|
+
.map((skill) => ({ skill, score: relevanceScore(task, `${skill.name} ${skill.description}`, skill.name) }))
|
|
116
116
|
.filter((item) => item.score > 0)
|
|
117
117
|
.sort((a, b) => b.score - a.score || a.skill.name.localeCompare(b.skill.name))
|
|
118
118
|
.slice(0, maxSkills);
|
|
119
119
|
const commandMatches = [...state.commands.values()]
|
|
120
|
-
.map((command) => ({ command, score: relevanceScore(task, `${command.name} ${command.description} ${command.argv.join(" ")}
|
|
120
|
+
.map((command) => ({ command, score: relevanceScore(task, `${command.name} ${command.description} ${command.argv.join(" ")}`, command.name) }))
|
|
121
121
|
.filter((item) => item.score > 0)
|
|
122
122
|
.sort((a, b) => b.score - a.score || a.command.name.localeCompare(b.command.name))
|
|
123
123
|
.slice(0, 20);
|
|
@@ -718,7 +718,7 @@ function capabilityFingerprint(state, skills) {
|
|
|
718
718
|
}));
|
|
719
719
|
}
|
|
720
720
|
|
|
721
|
-
function relevanceScore(task, candidate) {
|
|
721
|
+
function relevanceScore(task, candidate, identity = "") {
|
|
722
722
|
const taskTokens = tokenize(task);
|
|
723
723
|
const candidateTokens = tokenize(candidate);
|
|
724
724
|
if (!taskTokens.size || !candidateTokens.size) return 0;
|
|
@@ -726,18 +726,31 @@ function relevanceScore(task, candidate) {
|
|
|
726
726
|
for (const token of taskTokens) {
|
|
727
727
|
if (candidateTokens.has(token)) score += token.length >= 6 ? 2 : 1;
|
|
728
728
|
}
|
|
729
|
-
const
|
|
730
|
-
const
|
|
731
|
-
|
|
729
|
+
const taskComparable = comparableText(task);
|
|
730
|
+
const candidateComparable = comparableText(candidate);
|
|
731
|
+
const identityComparable = comparableText(identity);
|
|
732
|
+
if (candidateComparable.includes(taskComparable) || taskComparable.includes(candidateComparable)) score += 4;
|
|
733
|
+
if (identityComparable.length >= 3 && ` ${taskComparable} `.includes(` ${identityComparable} `)) score += 12;
|
|
732
734
|
return score;
|
|
733
735
|
}
|
|
734
736
|
|
|
737
|
+
function comparableText(value) {
|
|
738
|
+
return String(value || "")
|
|
739
|
+
.toLowerCase()
|
|
740
|
+
.replace(/[^a-z0-9\p{Script=Han}]+/gu, " ")
|
|
741
|
+
.trim()
|
|
742
|
+
.replace(/\s+/g, " ");
|
|
743
|
+
}
|
|
744
|
+
|
|
735
745
|
function tokenize(value) {
|
|
736
746
|
const text = String(value || "").toLowerCase();
|
|
737
747
|
const tokens = new Set();
|
|
738
748
|
for (const raw of text.match(/[a-z0-9_][a-z0-9_.-]{1,}/g) || []) {
|
|
739
749
|
const token = raw.replace(/^[.-]+|[.-]+$/g, "");
|
|
740
750
|
if (token.length >= 2 && !TOKEN_STOP_WORDS.has(token)) tokens.add(token);
|
|
751
|
+
for (const part of token.split(/[._-]+/)) {
|
|
752
|
+
if (part.length >= 2 && !TOKEN_STOP_WORDS.has(part)) tokens.add(part);
|
|
753
|
+
}
|
|
741
754
|
}
|
|
742
755
|
for (const sequence of text.match(/[\p{Script=Han}]{1,}/gu) || []) {
|
|
743
756
|
if (!TOKEN_STOP_WORDS.has(sequence)) tokens.add(sequence);
|
|
@@ -109,6 +109,7 @@ export class AppAutomationManager {
|
|
|
109
109
|
maxDepth: clampInt(args.max_depth, 6, 1, MAX_UI_DEPTH),
|
|
110
110
|
maxElements: clampInt(args.max_elements, 200, 1, MAX_UI_ELEMENTS),
|
|
111
111
|
includeValues: args.include_values === true,
|
|
112
|
+
includeMenus: args.include_menus === true,
|
|
112
113
|
};
|
|
113
114
|
const result = await this.runJxa(payload, clampInt(args.timeout_seconds, 30, 1, 120), context);
|
|
114
115
|
return {
|
|
@@ -143,6 +144,7 @@ export class AppAutomationManager {
|
|
|
143
144
|
value,
|
|
144
145
|
maxDepth: clampInt(args.max_depth, 8, 1, MAX_UI_DEPTH),
|
|
145
146
|
maxElements: MAX_UI_ELEMENTS,
|
|
147
|
+
includeMenus: args.include_menus === true,
|
|
146
148
|
};
|
|
147
149
|
const result = await this.runJxa(payload, clampInt(args.timeout_seconds, 30, 1, 120), context);
|
|
148
150
|
return {
|
|
@@ -168,9 +170,11 @@ export class AppAutomationManager {
|
|
|
168
170
|
undefined,
|
|
169
171
|
input,
|
|
170
172
|
);
|
|
173
|
+
const output = result.stdout.trim();
|
|
174
|
+
if (!output) throw new Error("macOS accessibility helper returned no JSON output");
|
|
171
175
|
let parsed;
|
|
172
176
|
try {
|
|
173
|
-
parsed = JSON.parse(
|
|
177
|
+
parsed = JSON.parse(output);
|
|
174
178
|
} catch {
|
|
175
179
|
throw new Error("macOS accessibility helper returned invalid JSON");
|
|
176
180
|
}
|
|
@@ -337,7 +341,7 @@ function describe(element, index, includeValues) {
|
|
|
337
341
|
return item;
|
|
338
342
|
}
|
|
339
343
|
function childrenOf(element) { return safe(() => element.uiElements(), []); }
|
|
340
|
-
function flatten(root, maxDepth, maxElements, includeValues) {
|
|
344
|
+
function flatten(root, maxDepth, maxElements, includeValues, includeMenus) {
|
|
341
345
|
const output = [];
|
|
342
346
|
const elements = [];
|
|
343
347
|
const stack = [{ element: root, depth: 0 }];
|
|
@@ -346,10 +350,12 @@ function flatten(root, maxDepth, maxElements, includeValues) {
|
|
|
346
350
|
const children = childrenOf(current.element);
|
|
347
351
|
for (let i = children.length - 1; i >= 0; i--) {
|
|
348
352
|
const child = children[i];
|
|
353
|
+
const item = describe(child, output.length, includeValues);
|
|
349
354
|
elements.push(child);
|
|
350
|
-
output.push(
|
|
355
|
+
output.push(item);
|
|
351
356
|
if (output.length >= maxElements) break;
|
|
352
|
-
|
|
357
|
+
const isMenuTree = String(item.role || '').startsWith('AXMenu');
|
|
358
|
+
if (current.depth + 1 < maxDepth && (includeMenus || !isMenuTree)) stack.push({ element: child, depth: current.depth + 1 });
|
|
353
359
|
}
|
|
354
360
|
}
|
|
355
361
|
return { output, elements, truncated: stack.length > 0 || output.length >= maxElements };
|
|
@@ -366,14 +372,14 @@ function main() {
|
|
|
366
372
|
const process = se.applicationProcesses.byName(payload.application);
|
|
367
373
|
if (!safe(() => process.exists(), false)) throw new Error('application process not found or Accessibility access denied');
|
|
368
374
|
if (payload.operation === 'inspect') {
|
|
369
|
-
const flattened = flatten(process, payload.maxDepth, payload.maxElements, payload.includeValues === true);
|
|
370
|
-
return { frontmost: safe(() => process.frontmost(), false), elements: flattened.output, truncated: flattened.truncated };
|
|
375
|
+
const flattened = flatten(process, payload.maxDepth, payload.maxElements, payload.includeValues === true, payload.includeMenus === true);
|
|
376
|
+
return { frontmost: safe(() => process.frontmost(), false), elements: flattened.output, truncated: flattened.truncated, menus_included: payload.includeMenus === true };
|
|
371
377
|
}
|
|
372
378
|
if (payload.action === 'activate') {
|
|
373
379
|
process.frontmost = true;
|
|
374
380
|
return { ok: true, matched: 1 };
|
|
375
381
|
}
|
|
376
|
-
const flattened = flatten(process, payload.maxDepth, payload.maxElements, true);
|
|
382
|
+
const flattened = flatten(process, payload.maxDepth, payload.maxElements, true, payload.includeMenus === true);
|
|
377
383
|
const matchesList = [];
|
|
378
384
|
for (let i = 0; i < flattened.output.length; i++) if (matches(flattened.output[i], payload.selector)) matchesList.push(i);
|
|
379
385
|
const chosen = payload.selector.index !== undefined ? matchesList[payload.selector.index] : matchesList[0];
|
|
@@ -398,5 +404,5 @@ function main() {
|
|
|
398
404
|
}
|
|
399
405
|
return { ok: true, matched: matchesList.length, selected_index: chosen, element: describe(element, chosen, false) };
|
|
400
406
|
}
|
|
401
|
-
try {
|
|
407
|
+
(() => { try { return JSON.stringify(main()); } catch (error) { return JSON.stringify({ error: String(error.message || error) }); } })()
|
|
402
408
|
`;
|
package/src/local/atomic-fs.mjs
CHANGED
|
@@ -5,8 +5,11 @@ const WAIT_BUFFER = new Int32Array(new SharedArrayBuffer(4));
|
|
|
5
5
|
|
|
6
6
|
export function replaceFileSync(source, target, options = {}) {
|
|
7
7
|
const rename = typeof options.rename === "function" ? options.rename : renameSync;
|
|
8
|
-
const
|
|
8
|
+
const sleep = typeof options.sleep === "function" ? options.sleep : sleepSync;
|
|
9
|
+
const random = typeof options.random === "function" ? options.random : Math.random;
|
|
10
|
+
const attempts = clampInteger(options.attempts, 16, 1, 64);
|
|
9
11
|
const baseDelayMs = clampInteger(options.baseDelayMs, 15, 0, 1000);
|
|
12
|
+
const maxDelayMs = Math.max(baseDelayMs, clampInteger(options.maxDelayMs, 250, 0, 2000));
|
|
10
13
|
let lastError;
|
|
11
14
|
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
12
15
|
try {
|
|
@@ -15,7 +18,7 @@ export function replaceFileSync(source, target, options = {}) {
|
|
|
15
18
|
} catch (error) {
|
|
16
19
|
lastError = error;
|
|
17
20
|
if (!isTransientReplaceError(error) || attempt === attempts) throw error;
|
|
18
|
-
|
|
21
|
+
sleep(retryDelayMs(attempt, baseDelayMs, maxDelayMs, random));
|
|
19
22
|
}
|
|
20
23
|
}
|
|
21
24
|
throw lastError || new Error("atomic file replacement failed");
|
|
@@ -25,6 +28,15 @@ export function isTransientReplaceError(error) {
|
|
|
25
28
|
return TRANSIENT_REPLACE_ERRORS.has(String(error?.code || ""));
|
|
26
29
|
}
|
|
27
30
|
|
|
31
|
+
function retryDelayMs(attempt, baseDelayMs, maxDelayMs, random) {
|
|
32
|
+
if (baseDelayMs <= 0 || maxDelayMs <= 0) return 0;
|
|
33
|
+
const exponential = Math.min(baseDelayMs * (2 ** Math.min(attempt - 1, 5)), maxDelayMs);
|
|
34
|
+
const randomValue = Number(random());
|
|
35
|
+
const normalizedRandom = Number.isFinite(randomValue) ? Math.min(Math.max(randomValue, 0), 1) : 0;
|
|
36
|
+
const jitter = Math.floor(exponential * 0.5 * normalizedRandom);
|
|
37
|
+
return Math.min(exponential + jitter, maxDelayMs);
|
|
38
|
+
}
|
|
39
|
+
|
|
28
40
|
function sleepSync(milliseconds) {
|
|
29
41
|
if (milliseconds <= 0) return;
|
|
30
42
|
Atomics.wait(WAIT_BUFFER, 0, 0, milliseconds);
|
|
@@ -355,7 +355,9 @@ export class BrowserBridgeManager {
|
|
|
355
355
|
if (settled) return;
|
|
356
356
|
settled = true;
|
|
357
357
|
clearTimeout(timer);
|
|
358
|
-
if (!ok)
|
|
358
|
+
if (!ok) {
|
|
359
|
+
try { ws.terminate(); } catch { try { ws.close(); } catch {} }
|
|
360
|
+
}
|
|
359
361
|
resolvePromise(ok);
|
|
360
362
|
};
|
|
361
363
|
ws.on("message", (data) => {
|
|
@@ -200,6 +200,11 @@
|
|
|
200
200
|
"type": "boolean",
|
|
201
201
|
"default": false
|
|
202
202
|
},
|
|
203
|
+
"include_menus": {
|
|
204
|
+
"type": "boolean",
|
|
205
|
+
"default": false,
|
|
206
|
+
"description": "Include and recursively inspect the application menu hierarchy. Disabled by default so main-window controls are reached quickly."
|
|
207
|
+
},
|
|
203
208
|
"timeout_seconds": {
|
|
204
209
|
"type": "integer",
|
|
205
210
|
"minimum": 1,
|
|
@@ -286,6 +291,11 @@
|
|
|
286
291
|
"type": "string",
|
|
287
292
|
"pattern": "^[a-z][a-z0-9._-]{0,63}$"
|
|
288
293
|
},
|
|
294
|
+
"include_menus": {
|
|
295
|
+
"type": "boolean",
|
|
296
|
+
"default": false,
|
|
297
|
+
"description": "Include menu-bar and menu subtrees when matching the action selector."
|
|
298
|
+
},
|
|
289
299
|
"max_depth": {
|
|
290
300
|
"type": "integer",
|
|
291
301
|
"minimum": 1,
|
package/src/worker/index.ts
CHANGED
|
@@ -3,7 +3,7 @@ import toolCatalog from "../shared/tool-catalog.json";
|
|
|
3
3
|
import serverMetadata from "../shared/server-metadata.json";
|
|
4
4
|
|
|
5
5
|
const SERVER_NAME = String(serverMetadata.name);
|
|
6
|
-
const SERVER_VERSION = "0.12.
|
|
6
|
+
const SERVER_VERSION = "0.12.2";
|
|
7
7
|
const MCP_PROTOCOL_VERSION = String(serverMetadata.protocolVersion);
|
|
8
8
|
const MCP_SUPPORTED_PROTOCOL_VERSIONS = serverMetadata.supportedProtocolVersions.map((value) => String(value));
|
|
9
9
|
const JSONRPC_VERSION = "2.0";
|