refactor(cua): move fixed signing setup into a developer skill

This commit is contained in:
creatixchu
2026-10-30 13:41:02 +08:00
parent 1b9b05a367
commit 6d33eb56ca
29 changed files with 243 additions and 277 deletions
@@ -9,11 +9,11 @@
en: a24857a312482724
zh: dbac75d363670e86
/agent-note-development-signing-identity-for-source-built-cua-helpers/decision:
en: f83e1ec19e7b8783
zh: 843c911c6531cb17
en: 6e34d4c1e4816a7c
zh: 1f00b046832e19be
/agent-note-development-signing-identity-for-source-built-cua-helpers/alternatives-considered:
en: 88dc1b86ce9aa018
zh: 4f940dbc16331f3e
en: e30db5bc9c7aab43
zh: fa84883b5527334e
/agent-note-development-signing-identity-for-source-built-cua-helpers/consequences:
en: e2950b9e0698a0b0
zh: c9c304ef59086784
en: fb0e8fe44890bdf9
zh: d9e9ace1a4c10060
@@ -12,17 +12,17 @@ Source checkouts compile `DSH Computer Use.app` with an ad-hoc signature. macOS
The Host builds source checkouts as a development variant with bundle identifier `com.deepseek.harness.computer-use.dev` and name `DSH Computer Use Dev`, in `DSH Computer Use Dev.app`. The macOS privacy list and the helper’s reported launch application use the bundle filename, so that filename also includes Dev. The release variant and its packaging are unchanged. Grants stay with the helper application launched through LaunchServices, as the [shared helper decision](2026-10-09-cua-shared-native-helper.md) requires.
`sourceSigning` belongs to the macOS provider and accepts `ad-hoc`, default `local-identity`, or `{ identity, keychain? }` for an existing certificate. The generated identity and password live under `$DSH_HOME/computer-use-dev-signing`, using `~/.dsh` when no home is configured. The first build creates the certificate and later builds using that home reuse it. The Host uses the shared home-path resolver and passes the result to Python. Ad-hoc and user-certificate modes do not read or create the generated identity; users own unlocking and maintaining their certificates.
`sourceSigning` belongs to the macOS provider and accepts only default `ad-hoc` or `{ identity, keychain }` selecting an existing certificate and absolute keychain path. Builds do not create, unlock or remove identities. Developer credentials stay separate from the user-facing DSH home, which stores only the certificate selection; the shared `cua` package owns no macOS signing configuration.
Setup creates a self-signed certificate in a dedicated keychain, stores a random password in a `0600` file, permits codesign through the key partition list, and adds the keychain to the user search list. Automatic builds unlock it before signing to avoid password or key-access dialogs. A file lock serializes setup and removal within the same home. A recorded identity with a missing keychain is preserved and rejected until the user explicitly removes it. The signing requirement contains the bundle identifier and certificate, independent of code hash or path for builds using that certificate.
The [developer signing skill](../../../skills/dsh-cua-macos-signing/SKILL.md) guides Agents through creating, reusing and unlocking an isolated identity under `~/.cua-macos-keys/`, then selecting its fingerprint and keychain path in user configuration. The tool stores a random password in a `0600` file, permits codesign access, and adds the dedicated keychain to the user search list. A file lock serializes setup and removal for that directory; a missing keychain with an existing record is preserved and rejected.
Certificate signatures embed a signing time even without a timestamp server. Development publication records which generation each unsigned bundle and identity produced, then selects that generation again instead of re-signing, so identical sources keep one executable digest and one shared service. The cache uses certificate fingerprints; explicit certificate names are not reused because renewal can change the certificate behind a name. Setup failures stop the build, and diagnostics omit passwords and complete command arguments. `--identity` selects user-certificate signing and is mutually exclusive with `--signing`.
Certificate signatures embed a signing time even without a timestamp server. Development publication records which generation each unsigned bundle and identity produced, then selects that generation again instead of re-signing, so identical sources keep one executable digest and one shared service. The cache uses certificate fingerprints; explicit certificate names are not reused because renewal can change the certificate behind a name. Signing failures stop publication. Developer setup diagnostics omit passwords and complete command arguments. The build CLI defaults to `--identity -` and accepts an existing certificate with `--identity` and optional `--keychain` for release packaging.
## Alternatives considered
**Grant the launching terminal.** Launching the service directly makes macOS attribute permissions to the terminal or editor that started DSH. Every program that application runs, including commands run by the model, then has the permissions.
**Ad-hoc signing by default.** This avoids storing a private key but can require new grants when binaries change. Automatic fixed signing removes manual certificate setup from ordinary source builds; ad-hoc signing remains an explicit option.
**Automatically create a fixed identity during source builds.** This makes ordinary startup create developer private keys and ties their lifetime to user data. An explicit developer workflow preserves the option to reduce repeated grants while keeping identity creation, removal and selection under developer control.
**Self-signed identity in the login keychain.** The login keychain is already unlocked and on the search list, but allowing `codesign` to use a new key there without a dialog requires the user's login password.
@@ -32,4 +32,4 @@ Certificate signatures embed a signing time even without a timestamp server. Dev
## Consequences
Automatic builds using the same DSH home share a signing requirement; actual permission retention across rebuilds and worktrees still needs manual verification. Other processes of the same user can read the password and sign with the development identity; the release helper uses a different bundle identifier. Deleting DSH home removes the generated identity, so the next build creates another certificate and needs new grants; home reset therefore clears signing state too. System permission entries and the keychain search entry survive directory deletion; subsequent setup removes a stale search entry at the same path. Select ad-hoc or user-certificate signing before explicit removal to prevent regeneration. The real signing e2e checks requirements across output directories, user-selected certificates, and search-list restoration after removal.
Development builds signed by the same certificate share a signing requirement; actual permission retention after grants, rebuilds and worktree changes still requires verification. Other processes of the same user can read the stored password and sign with that identity; the release helper has a different bundle identifier. Deleting DSH home removes its selection, not the developer identity; deleting the identity requires clearing every configuration that references it. The real-signing e2e checks reuse, unlocking, requirements across output directories and search-list restoration after removal.
@@ -12,17 +12,17 @@ Status: implemented
Host 以开发版构建源码检出,Bundle ID 为 `com.deepseek.harness.computer-use.dev`,名称为 `DSH Computer Use Dev`,应用包文件名为 `DSH Computer Use Dev.app`。macOS 隐私列表和 helper 返回的启动应用名称使用应用包文件名,因此文件名也需包含 Dev。正式版及其打包流程不变。按[共享 helper 决策](2026-10-09-cua-shared-native-helper.zh.md)的要求,授权仍归经 LaunchServices 启动的 helper 应用。
`sourceSigning` 由 macOS provider 定义,支持 `ad-hoc`、默认的 `local-identity`,以及指定已有证书的 `{ identity, keychain? }`。自动身份及密码存放在 `$DSH_HOME/computer-use-dev-signing`,未配置 home 时使用 `~/.dsh`;首次构建创建证书,此后同一 home 的构建都复用它。Host 使用公共 home 路径解析器并把结果传给 Python。临时签名和用户证书模式不读取或创建自动身份。用户证书的解锁和维护由用户负责。
`sourceSigning` 由 macOS provider 定义,只接受默认的 `ad-hoc` 和指定已有证书及钥匙串绝对路径的 `{ identity, keychain }`。构建不创建、解锁或移除身份。开发者凭据与面向用户的 DSH home 分开,后者只保存选择证书的配置;共享 `cua` 包不持有 macOS 签名配置。
初始化在专用钥匙串中创建自签名证书,将随机密码存入权限为 `0600` 的文件,通过密钥分区列表允许 `codesign` 访问,并把钥匙串加入用户搜索列表。自动构建每次签名前用保存的密码解锁,避免密码或密钥访问窗口。同一 home 的初始化和移除使用文件锁串行执行。已有身份记录但钥匙串缺失时保留记录并报错,只有用户显式移除后才允许重新生成。签名要求包含 Bundle ID 和证书,使用同一证书的构建不依赖代码哈希或路径。
[开发者签名 skill](../../../skills/dsh-cua-macos-signing/SKILL.md)指导 Agent 在 `~/.cua-macos-keys/` 创建、复用和解锁独立身份,将证书指纹和钥匙串路径写入用户配置。工具把随机密码存入 `0600` 文件,允许 codesign 访问私钥,并把专用钥匙串加入用户搜索列表。文件锁让同一目录的初始化和移除串行执行;已有记录但钥匙串缺失时保留记录并报错。
即使不使用时间戳服务器,证书签名也会写入签名时间。开发版发布时记录每个未签名应用与签名身份对应的目录,再次构建时直接选中该目录,不重新签名,因此相同源码保持同一个可执行文件摘要和同一个共享服务。缓存使用证书指纹;显式证书名称可能对应续期后的证书,因此不复用缓存。签名初始化失败会中止构建;异常信息不包含密码或完整命令参数。`--identity` 选择用户证书签名,与 `--signing` 互斥。
即使不使用时间戳服务器,证书签名也包含签名时间。开发构建记录每份未签名内容与身份对应的已签名目录,再次构建相同内容时复用该目录,保持可执行文件摘要和共享服务一致。缓存使用证书指纹;按证书名称选择时每次签名,避免续期后复用旧证书。签名失败会中止发布。开发者工具的诊断不包含密码或完整命令参数;构建 CLI 默认 `--identity -`,支持指定证书,正式打包也可省略 `--keychain`。
## 考虑过的替代方案
**把权限交给启动终端。** 直接启动服务时,macOS 把权限归属到启动 DSH 的终端或编辑器。该应用运行的所有程序,包括模型执行的命令,都会带上这些权限。
**默认临时签名。** 可以避免保存私钥,但二进制变化后可能需要重新授权。默认自动生成固定身份,让普通源码构建无需手动准备证书;临时签名仍可显式选择。
**源码构建自动创建固定身份。** 这会让普通启动生成开发者私钥,并把身份生命周期绑定到用户数据目录。显式开发者操作保留了减少重新授权的选择,也让证书的创建、移除和使用范围由开发者决定。
**把自签名身份放进登录钥匙串。** 登录钥匙串已解锁且在搜索列表中,但要让 `codesign` 不弹窗地使用其中的新私钥,需要用户的登录密码。
@@ -32,4 +32,4 @@ Host 以开发版构建源码检出,Bundle ID 为 `com.deepseek.harness.comput
## 后果
使用同一 DSH home 的自动签名构建共用签名要求,实际授权在重建和跨 worktree 后是否保留仍需手动验证。同一用户的进程能读取密码并使用开发身份签名,正式 helper 使用不同的 Bundle ID。删除 DSH home 会删除自动身份,下次构建生成新证书并需要重新授权;这也让 home 重置能清除签名状态。系统设置中的授权和钥匙串搜索条目不随目录删除而消失;后续初始化会清理同路径的失效搜索条目。主动移除身份前需选择临时签名或用户证书,以免下次构建重新生成。真实签名 e2e 核对不同输出目录的构建共用要求、用户指定证书可用,以及移除后搜索列表恢复。
同一证书签出的开发构建共用签名要求;授权后重建和跨 worktree 的实际权限保持仍需验证。同一用户的进程可读取保存的密码并使用该身份签名,正式 helper 使用不同的 Bundle ID。删除 DSH home 只移除配置,不删除开发者身份;删除该身份后应清理所有引用它的配置。真实签名 e2e 验证复用、解锁、不同输出目录的签名要求和移除后的搜索列表恢复。
@@ -0,0 +1,64 @@
---
name: dsh-cua-macos-signing
description: Set up, reuse, unlock, or remove a local macOS Computer Use development signing identity and configure source builds to use it across worktrees. Use when developing the CUA helper or reducing repeated macOS permission grants; release signing and notarization use their packaging workflow.
---
# macOS CUA development signing
Source builds default to `sourceSigning: ad-hoc`. They need no developer certificate. An ad-hoc designated requirement includes the code hash, so changed binaries, clean rebuilds, or different worktrees can require fresh Accessibility and Screen Recording grants even when their sources are unchanged. For developers who want fewer repeated grants, configure one fixed certificate and use it across builds. The first grant is still manual; verify actual permission retention rather than claiming that matching signatures alone prove it.
The [macOS provider](../../../packages/experimental/cua-macos/README.md#use-this-package) owns the two configuration choices: `ad-hoc`, or `{ identity, keychain }`. The build consumes an existing certificate; it never creates or unlocks one. Developer key material stays outside DSH home. DSH home stores only the configuration selecting it.
## Create or reuse an identity
Run from a repository checkout on macOS with the selected Apple command-line tools. An explicit request to set up developer signing or reduce repeated development grants authorizes setup and the corresponding local configuration edit. For advice-only requests, explain the choices without creating an identity.
```sh
python3 .agents/skills/dsh-cua-macos-signing/scripts/signing_identity.py setup
```
The script creates or reuses `~/.cua-macos-keys/dev/signing.keychain-db`, stores its password and certificate fingerprint in the adjacent `identity.json` with mode `0600`, and keeps the directory private. It adds only its dedicated keychain to the user search list and enables `codesign` access without prompting for the login password. A sibling `dev.lock` serializes setup and removal across worktrees. Supply `--directory <absolute identity directory>` when the developer chose another location outside DSH home and the checkout.
Setup prints only the certificate fingerprint and absolute keychain path as JSON. Keep the password record out of chat, logs, commits, and configuration. Other processes running as this user can read the stored password and use this development identity; this is the local developer setup's access model.
If an existing record is damaged or its keychain/certificate is missing, setup stops. Do not silently replace that identity: replacement changes the certificate and can require new grants. Follow the reported remove/setup commands only when identity replacement is authorized.
## Configure the source build
Resolve the DSH home used by the developer (`DSH_HOME`, otherwise `~/.dsh`). Merge the two printed values into its `cordis.patch.yml`, targeting the entry that loads `@deepseek-ai/dsh-experimental-cua-macos`. The shipped entry ID is `cua`:
```yaml
- id: cua
config:
sourceSigning:
identity: "<fingerprint printed by setup>"
keychain: "/Users/<user>/.cua-macos-keys/dev/signing.keychain-db"
```
Use the printed absolute path, not a literal `~`. Preserve unrelated patches and every existing field in this entry's effective config: a Cordis patch replaces the complete `config` object rather than deep-merging it. If the provider has `helperPath`, source signing is bypassed; remove that override only when the developer intends to test the source-built helper. Ensure the profile actually loads the provider before adding a patch for its ID.
Launch through the developer's existing `dsh` profile. The home-level patch shares the selection across profiles and worktrees that use that home. Other DSH homes can explicitly select the same certificate. Record the paths changed and the selected public fingerprint, never the password.
## Unlock and verify
After a restart or a manual keychain lock, rerun `setup`; it unlocks and reuses the existing identity. Do not ask the developer for the login-keychain password or store one. A source build does not automatically unlock the keychain and can fail or prompt if it is locked.
Grant Accessibility and Screen Recording to `DSH Computer Use Dev.app` in System Settings. Call `getCapabilities`, change and rebuild the Swift helper, then test another worktree with the same configuration. Report the observed permission flags for each step. Matching `codesign -d -r- <application>` requirements confirm signature continuity but do not replace the permission checks.
For a temporary, permission-free signing mechanism check, run:
```sh
python3 .agents/skills/dsh-cua-macos-signing/scripts/signing_e2e.py
```
It creates an isolated identity, tests reuse and unlocking, signs changed binaries, then removes its keychain and checks search-list restoration. Run it without concurrent keychain-list mutations.
## Remove an identity
First remove the selecting configuration or set `sourceSigning: ad-hoc` in each affected home. For an authorized removal, run:
```sh
python3 .agents/skills/dsh-cua-macos-signing/scripts/signing_identity.py remove
```
Use the same `--directory` for a custom location. Removal deletes the dedicated keychain, its search-list entry, and password record; the empty sibling lock remains so waiting processes still coordinate. Deleting DSH home alone leaves the developer identity intact. System Settings permission entries are separate; remove them only when requested.
@@ -9,13 +9,12 @@ from pathlib import Path
import plistlib
import subprocess
import tempfile
from unittest.mock import patch
NATIVE = Path(__file__).resolve().parents[1]
NATIVE = Path(__file__).resolve().parents[4] / "packages/experimental/cua-macos/native"
def module(name):
spec = importlib.util.spec_from_file_location(name, NATIVE / f"{name}.py")
spec = importlib.util.spec_from_file_location(name, NATIVE / "build_app.py" if name == "build_app" else Path(__file__).with_name("signing_identity.py"))
loaded = importlib.util.module_from_spec(spec)
spec.loader.exec_module(loaded)
return loaded
@@ -31,16 +30,14 @@ def main():
before = signing.user_keychains()
with tempfile.TemporaryDirectory(prefix="cua-dev-signing-e2e-") as temporary:
root = Path(temporary)
home = root / "home"
directory = signing.default_directory(home)
directory = root / "keys/dev"
try:
with patch.object(builder, "load_dev_signing", return_value=signing):
sha1, keychain = builder.signing_identity("dev", None, "local-identity", home)
assert builder.signing_identity("dev", None, "local-identity", home) == (sha1, keychain)
assert keychain == home / "computer-use-dev-signing/signing.keychain-db"
identity = signing.setup(directory)
assert signing.setup(directory) == identity
sha1, keychain = identity.sha1, identity.keychain
assert (directory / "identity.json").stat().st_mode & 0o777 == 0o600
with patch.object(builder, "load_dev_signing", side_effect=AssertionError("ad-hoc accessed keychain")):
assert builder.signing_identity("dev", None, "ad-hoc") == ("-", None)
signing.security("lock-keychain", str(keychain))
assert signing.setup(directory) == identity, "setup must unlock an existing developer keychain"
requirements = []
for status in (0, 1):
(root / "main.c").write_text(f"int main(void) {{ return {status}; }}\n")
@@ -59,12 +56,10 @@ def main():
subprocess.run(["/usr/bin/codesign", "--verify", f"-R={first_requirement}", str(second)], check=True)
selected = json.loads((root / "checkout-1/current.json").read_text())["bundle"]
assert second.parent.name == selected
with patch.object(builder, "load_dev_signing", side_effect=AssertionError("custom signing accessed managed identity")):
for index, (certificate, selected_keychain) in enumerate(((sha1, None), (signing.COMMON_NAME, keychain))):
custom_identity, custom_keychain = builder.signing_identity("dev", certificate, None, keychain=selected_keychain)
custom = builder.publish(root / "helper", root / f"custom-{index}", custom_identity, NATIVE / "Info.plist", "dev", custom_keychain)
assert requirement(custom.parents[2]) == first_requirement
print(f"PASS: DSH-home identity is reused across changed builds; user-supplied signatures share {first_requirement}")
for index, (certificate, selected_keychain) in enumerate(((sha1, keychain), (signing.COMMON_NAME, keychain))):
custom = builder.publish(root / "helper", root / f"custom-{index}", certificate, NATIVE / "Info.plist", "dev", selected_keychain)
assert requirement(custom.parents[2]) == first_requirement
print(f"PASS: developer setup reuses and unlocks its identity; configured signatures across changed builds share {first_requirement}")
finally:
signing.remove(directory)
assert signing.user_keychains() == before, "the user keychain search list was not restored"
@@ -1,4 +1,4 @@
"""Own the signing identity retained in DSH home for source-built Computer Use applications.
"""Manage an explicitly requested developer identity outside DSH home.
macOS keeps Accessibility and Screen Recording grants for a bundle identifier together with the
designated requirement of the signature that was granted. An ad-hoc signature's requirement is the
@@ -28,8 +28,8 @@ import tempfile
COMMON_NAME = "DSH Computer Use Dev (local)"
SECURITY = "/usr/bin/security"
OPENSSL = "/usr/bin/openssl"
SETUP_COMMAND = "pnpm --dir packages/experimental/cua-macos run dev-signing:setup"
REMOVE_COMMAND = "pnpm --dir packages/experimental/cua-macos run dev-signing:remove"
SETUP_COMMAND = "python3 .agents/skills/dsh-cua-macos-signing/scripts/signing_identity.py setup"
REMOVE_COMMAND = "python3 .agents/skills/dsh-cua-macos-signing/scripts/signing_identity.py remove"
CERTIFICATE_CONFIG = f"""[req]
distinguished_name = subject
x509_extensions = extensions
@@ -94,17 +94,9 @@ def keychain_path(directory):
return directory / "signing.keychain-db"
def default_directory(dsh_home=None):
"""Use the Host-resolved home, or DSH_HOME / ~/.dsh for standalone Python commands."""
selected = dsh_home if dsh_home is not None else os.environ.get("DSH_HOME", "")
selected = str(selected)
if not selected.strip():
home = Path.home() / ".dsh"
elif selected == "~" or selected.startswith(("~/", "~\\")):
home = Path.home() / selected[2:]
else:
home = Path(selected)
return Path(os.path.abspath(home)) / "computer-use-dev-signing"
def default_directory():
"""Keep developer credentials independent of DSH profiles, homes, and worktrees."""
return Path.home() / ".cua-macos-keys" / "dev"
def load(directory):
@@ -113,9 +105,9 @@ def load(directory):
Returns None when setup has not run. Raises when setup ran but its keychain is missing or no
longer on the search list, because signing would otherwise fail or prompt for a password.
"""
home_argument = shlex.quote(str(directory.parent))
setup_command = f"{SETUP_COMMAND} --dsh-home {home_argument}"
remove_command = f"{REMOVE_COMMAND} --dsh-home {home_argument}"
directory_argument = shlex.quote(str(directory))
setup_command = f"{SETUP_COMMAND} --directory {directory_argument}"
remove_command = f"{REMOVE_COMMAND} --directory {directory_argument}"
record = record_path(directory)
if not record.exists():
return None
@@ -216,15 +208,15 @@ def remove_locked(directory):
def main():
parser = argparse.ArgumentParser(description="Manage the local signing identity for source-built Computer Use applications.")
parser.add_argument("action", choices=["setup", "remove"])
parser.add_argument("--dsh-home", type=Path, help="DSH home; defaults to DSH_HOME or ~/.dsh")
parser.add_argument("--directory", type=Path, default=default_directory(), help="identity directory; defaults to ~/.cua-macos-keys/dev")
args = parser.parse_args()
directory = default_directory(args.dsh_home)
directory = args.directory.expanduser().resolve()
if args.action == "setup":
identity = setup(directory)
print(f"Prepared \"{COMMON_NAME}\" ({identity.sha1}) in {directory}")
print(json.dumps({"identity": identity.sha1, "keychain": str(identity.keychain)}, indent=2))
else:
remove(directory)
print("Removed the development signing identity; select ad-hoc or a user certificate to prevent recreation")
print("Removed the development signing identity; remove its sourceSigning configuration before the next build")
if __name__ == "__main__":
@@ -10,7 +10,7 @@ import tempfile
import unittest
from unittest.mock import patch
spec = importlib.util.spec_from_file_location("dev_signing", Path(__file__).resolve().parents[1] / "dev_signing.py")
spec = importlib.util.spec_from_file_location("dev_signing", Path(__file__).with_name("signing_identity.py"))
signing = importlib.util.module_from_spec(spec)
spec.loader.exec_module(signing)
@@ -56,28 +56,24 @@ class DevelopmentSigningTests(unittest.TestCase):
self.addCleanup(patcher.stop)
return security
def test_signing_directory_follows_dsh_home_and_explicit_overrides(self):
def test_default_identity_is_outside_dsh_home(self):
home = self.root / "user"
with patch.object(Path, "home", return_value=home):
for value in ("", " "):
for value in ("", str(self.root / "custom-dsh-home")):
with patch.dict(signing.os.environ, {"DSH_HOME": value}):
self.assertEqual(signing.default_directory(), home / ".dsh/computer-use-dev-signing")
for value, expected in ((str(self.root / "custom"), self.root / "custom"),
("~/custom", home / "custom"), ("~", home),
("relative-home", Path(signing.os.path.abspath("relative-home")))):
with patch.dict(signing.os.environ, {"DSH_HOME": value}):
self.assertEqual(signing.default_directory(), expected / "computer-use-dev-signing")
self.assertEqual(signing.default_directory(self.root / "explicit"),
self.root / "explicit/computer-use-dev-signing")
self.assertEqual(signing.default_directory(), home / ".cua-macos-keys/dev")
self.assertFalse(home.exists())
def test_setup_and_remove_commands_target_the_selected_dsh_home(self):
directory = self.root / "home/computer-use-dev-signing"
def test_setup_and_remove_target_the_explicit_identity_directory(self):
directory = self.root / "keys/dev"
identity = signing.SigningIdentity("ABC123", directory / "signing.keychain-db")
for action in ("setup", "remove"):
with patch.object(signing.sys, "argv", ["dev_signing.py", action, "--dsh-home", str(directory.parent)]), \
patch.object(signing, action, return_value=identity) as operation, contextlib.redirect_stdout(io.StringIO()):
with patch.object(signing.sys, "argv", ["signing_identity.py", action, "--directory", str(directory)]), \
patch.object(signing, action, return_value=identity) as operation, contextlib.redirect_stdout(io.StringIO()) as output:
signing.main()
operation.assert_called_once_with(directory)
if action == "setup":
self.assertEqual(json.loads(output.getvalue()), {"identity": "ABC123", "keychain": str(identity.keychain)})
def test_missing_setup_leaves_builds_unconfigured(self):
self.fake([self.login])
@@ -97,7 +93,7 @@ class DevelopmentSigningTests(unittest.TestCase):
def test_unlisted_or_missing_keychains_fail_with_the_setup_command(self):
security = self.configure(listed=False)
with self.assertRaisesRegex(RuntimeError, "search list; run pnpm"):
with self.assertRaisesRegex(RuntimeError, "search list; run python3"):
signing.load(self.directory)
self.keychain.unlink()
with self.assertRaisesRegex(RuntimeError, "keychain is missing"):
@@ -131,41 +127,29 @@ class DevelopmentSigningTests(unittest.TestCase):
before = record.read_bytes()
self.keychain.unlink()
with patch.object(signing, "create_certificate") as create:
with self.assertRaisesRegex(RuntimeError, "keychain is missing: .*; run .*remove --dsh-home .*, then .*setup --dsh-home "):
with self.assertRaisesRegex(RuntimeError, "keychain is missing: .*; run .*remove --directory .*, then .*setup --directory "):
signing.setup(self.directory)
create.assert_not_called()
self.assertEqual(record.read_bytes(), before)
self.assertFalse(self.keychain.exists())
self.assertEqual(security.commands, [])
def test_build_cli_stops_without_replacing_a_recorded_identity_missing_its_keychain(self):
home = self.root / "home"
directory = home / "computer-use-dev-signing"
def test_setup_cli_preserves_a_recorded_identity_missing_its_keychain(self):
directory = self.root / "keys/dev"
directory.mkdir(parents=True)
record = directory / "identity.json"
before = json.dumps({"sha1": "ABC123", "password": "secret-password"}).encode()
record.write_bytes(before)
output = self.root / "applications"
script = """import runpy, sys
from pathlib import Path
from unittest.mock import patch
test_home = Path(sys.argv[2])
Path.home = lambda: test_home
sys.argv = [sys.argv[1], '--variant', 'dev', '--dsh-home', sys.argv[2], '--output-directory', sys.argv[3]]
with patch('subprocess.run', side_effect=AssertionError('unexpected native build or signing command')):
runpy.run_path(sys.argv[0], run_name='__main__')
"""
result = subprocess.run([sys.executable, "-c", script, str(Path(signing.__file__).with_name("build_app.py")), str(home), str(output)],
result = subprocess.run([sys.executable, str(Path(signing.__file__)), "setup", "--directory", str(directory)],
capture_output=True, text=True, timeout=10)
self.assertEqual(result.returncode, 1)
self.assertEqual(len(result.stderr.splitlines()), 1)
self.assertIn("keychain is missing", result.stderr)
self.assertIn(f"run {signing.REMOVE_COMMAND} --dsh-home {home}, then {signing.SETUP_COMMAND} --dsh-home {home}", result.stderr)
self.assertIn(f"run {signing.REMOVE_COMMAND} --directory {directory}, then {signing.SETUP_COMMAND} --directory {directory}", result.stderr)
self.assertNotIn("Traceback", result.stderr)
self.assertNotIn("secret-password", result.stderr)
self.assertEqual(record.read_bytes(), before)
self.assertFalse(signing.keychain_path(directory).exists())
self.assertFalse(output.exists())
def test_remove_restores_the_other_search_list_entries_and_deletes_the_password(self):
security = self.configure()
@@ -178,7 +162,7 @@ with patch('subprocess.run', side_effect=AssertionError('unexpected native build
def test_missing_certificate_requires_explicit_removal_instead_of_changing_identity(self):
self.configure()
(self.directory / "identity.json").write_text(json.dumps({"sha1": "MISSING", "password": "secret"}))
with self.assertRaisesRegex(RuntimeError, "certificate is missing; run .*remove --dsh-home .*, then .*setup --dsh-home "):
with self.assertRaisesRegex(RuntimeError, "certificate is missing; run .*remove --directory .*, then .*setup --directory "):
signing.setup(self.directory)
self.assertTrue(self.keychain.is_file())
@@ -227,18 +211,18 @@ with open(sys.argv[1], 'w') as lock:
with signing.identity_lock(self.directory):
pass
def test_build_cli_reports_signing_timeout_without_traceback_or_password(self):
def test_setup_cli_reports_signing_timeout_without_traceback_or_password(self):
script = """import runpy, sys, subprocess
from unittest.mock import patch
from pathlib import Path
test_home = Path(sys.argv[2])
Path.home = lambda: test_home
sys.argv = [sys.argv[1], '--variant', 'dev', '--dsh-home', sys.argv[2]]
sys.argv = [sys.argv[1], 'setup', '--directory', sys.argv[2]]
with patch('subprocess.run', side_effect=subprocess.TimeoutExpired(['security', 'unlock-keychain', '-p', 'secret-password'], 60)):
runpy.run_path(sys.argv[0], run_name='__main__')
"""
# Fail at the first security command, before setup can modify any keychain.
result = subprocess.run([sys.executable, "-c", script, str(Path(signing.__file__).with_name("build_app.py")), str(self.root / "home")],
result = subprocess.run([sys.executable, "-c", script, str(Path(signing.__file__)), str(self.root / "home")],
capture_output=True, text=True, timeout=10)
self.assertEqual(result.returncode, 1)
self.assertEqual(len(result.stderr.splitlines()), 1)
+6 -6
View File
@@ -1113,21 +1113,21 @@ export interface Config {
- `inject`: `computerUse` · `tools` · `systemPrompt` · `subprocess` · `agents`
- `refs`: [`CuaConfig`](../packages/experimental/cua/src/index.ts)
- `source`: [`packages/experimental/cua-macos/src/index.ts:18`](../packages/experimental/cua-macos/src/index.ts)
- `source`: [`packages/experimental/cua-macos/src/index.ts:17`](../packages/experimental/cua-macos/src/index.ts)
```ts config-catalog
/** Native preparation, source-build signing, operation bounds, and profile-local application grants. */
export interface Config extends CuaConfig {
/**
* Source-build signing: `local-identity` (default) creates or reuses an identity in DSH home;
* `ad-hoc` binds grants to the code hash; an object selects an existing user-managed certificate.
* Source-build signing: `ad-hoc` (default) binds grants to the code hash;
* an object selects an existing certificate in an explicitly configured keychain.
* Installed packages and `helperPath` keep their own signature.
*/
sourceSigning: 'ad-hoc' | 'local-identity' | {
sourceSigning: 'ad-hoc' | {
/** Existing codesign certificate name or SHA-1 fingerprint. */
identity: string
/** Absolute keychain path; omission uses the user's keychain search list. The user owns unlocking it. */
keychain?: string
/** Absolute keychain path. The developer owns creating and unlocking it. */
keychain: string
}
}
```
+6 -6
View File
@@ -1115,21 +1115,21 @@ export interface Config {
- `inject`: `computerUse` · `tools` · `systemPrompt` · `subprocess` · `agents`
- `refs`: [`CuaConfig`](../packages/experimental/cua/src/index.ts)
- `source`: [`packages/experimental/cua-macos/src/index.ts:18`](../packages/experimental/cua-macos/src/index.ts)
- `source`: [`packages/experimental/cua-macos/src/index.ts:17`](../packages/experimental/cua-macos/src/index.ts)
```ts config-catalog
/** Native preparation, source-build signing, operation bounds, and profile-local application grants. */
export interface Config extends CuaConfig {
/**
* Source-build signing: `local-identity` (default) creates or reuses an identity in DSH home;
* `ad-hoc` binds grants to the code hash; an object selects an existing user-managed certificate.
* Source-build signing: `ad-hoc` (default) binds grants to the code hash;
* an object selects an existing certificate in an explicitly configured keychain.
* Installed packages and `helperPath` keep their own signature.
*/
sourceSigning: 'ad-hoc' | 'local-identity' | {
sourceSigning: 'ad-hoc' | {
/** Existing codesign certificate name or SHA-1 fingerprint. */
identity: string
/** Absolute keychain path; omission uses the user's keychain search list. The user owns unlocking it. */
keychain?: string
/** Absolute keychain path. The developer owns creating and unlocking it. */
keychain: string
}
}
```
@@ -9,8 +9,8 @@
en: 72a8ea364f7427fa
zh: 9e7ed2e00425bddb
/source-built-computer-use-helper-identity/change:
en: 14b5363428db53fe
zh: 58f7ba480871197e
en: c1aa84364896b994
zh: a4559c563d995b2b
/source-built-computer-use-helper-identity/migration:
en: e5dd3a1a5825fc8c
zh: 494ef3db1feb9a18
en: a86e330ab60ef800
zh: ce32e1959ca707fc
@@ -9,11 +9,11 @@ English | [中文](guide.zh.md)
## Change
Source checkouts previously built `DSH Computer Use.app` with the release bundle identifier `com.deepseek.harness.computer-use` and an ad-hoc signature. They now build `DSH Computer Use Dev.app` with bundle identifier `com.deepseek.harness.computer-use.dev`. Accessibility and Screen Recording grants for the old entry do not apply to it. Desktop and npm installations are unchanged. Source builds now generate a fixed signing identity under `$DSH_HOME/computer-use-dev-signing` by default and reuse it; an unset `DSH_HOME` uses `~/.dsh`.
Source checkouts previously built `DSH Computer Use.app` with the release bundle identifier `com.deepseek.harness.computer-use` and an ad-hoc signature. They now build `DSH Computer Use Dev.app` with bundle identifier `com.deepseek.harness.computer-use.dev`. Accessibility and Screen Recording grants for the old entry do not apply to it. Desktop and npm installations are unchanged. Source builds still default to ad-hoc signing; developers generate and maintain fixed identities outside DSH home and select them through configuration.
## Migration
1. Default `sourceSigning: local-identity` needs no manual setup; use the same DSH home across worktrees to share the identity. For ad-hoc signing, set `config.sourceSigning: ad-hoc` on `id: cua` in `cordis.patch.yml`. For an existing certificate, use the [user-signing configuration](../../../../packages/experimental/cua-macos/README.md#use-this-package).
1. Keep the default `sourceSigning: ad-hoc` for temporary signatures. To reduce repeated grants after rebuilds and across worktrees, follow the [developer signing skill](../../../../.agents/skills/dsh-cua-macos-signing/SKILL.md) to generate or reuse a certificate, then write `sourceSigning: { identity, keychain }` into the DSH home’s `cordis.patch.yml`; `keychain` must be an absolute path.
2. Start DSH from the source checkout, call Computer Use, and grant Accessibility and Screen Recording to `DSH Computer Use Dev` in System Settings.
3. Confirm that `getCapabilities` reports both permissions as `true`. Remove an old `DSH Computer Use` entry only when no installed DSH uses it.
4. To stop using the generated identity, select ad-hoc or user-certificate signing, then run `pnpm --dir packages/experimental/cua-macos run dev-signing:remove`. Select a custom home with the same `DSH_HOME` or `--dsh-home <path>`. Deleting DSH home causes a new certificate on the next build and requires new grants.
3. Confirm that `getCapabilities` reports both permissions as `true`. With a fixed signature, check again after changing and rebuilding Swift code and from another worktree. Remove an old `DSH Computer Use` entry only when no installed DSH uses it.
4. After the keychain locks, rerun setup as described in the skill to unlock and reuse the identity. To stop using it, select `ad-hoc` or remove the configuration, then remove the identity through the skill; deleting DSH home does not delete the developer keychain.
@@ -9,11 +9,11 @@ description: "源码构建的 Computer Use helper 使用独立的开发版 Bundl
## 变更
此前源码检出构建的 `DSH Computer Use.app` 使用正式版 Bundle ID `com.deepseek.harness.computer-use` 和临时签名。现在构建的是 `DSH Computer Use Dev.app`,Bundle ID 为 `com.deepseek.harness.computer-use.dev`。旧条目上的辅助功能和屏幕录制授权不适用于它。桌面端和 npm 安装版不变。 源码构建现在默认在 `$DSH_HOME/computer-use-dev-signing` 中生成并复用固定签名身份,未设置 `DSH_HOME` 时使用 `~/.dsh`。
源码检出原先以临时签名构建 `DSH Computer Use.app`,使用正式版 Bundle ID `com.deepseek.harness.computer-use`。现在构建 `DSH Computer Use Dev.app`,使用 `com.deepseek.harness.computer-use.dev`。旧条目的辅助功能和屏幕录制授权不能沿用。Desktop 和 npm 安装版不变。源码构建默认仍为临时签名;固定身份由开发者在 DSH home 之外生成和维护,再通过配置指定。
## 迁移
1. 默认的 `sourceSigning: local-identity` 无需手动 setup;各 worktree 使用同一 DSH home 即可复用身份。如需临时签名,在 `cordis.patch.yml` 的 `id: cua` 节点设置 `config.sourceSigning: ad-hoc`;如需已有证书,使用[用户签名配置](../../../../packages/experimental/cua-macos/README.zh.md#use-this-package)。
2. 从源码检出启动 DSH 并调用 Computer Use,在系统设置中给 `DSH Computer Use Dev` 授予辅助功能和屏幕录制权限。
3. 确认 `getCapabilities` 报告两项权限均为 `true`。只有在没有已安装的 DSH 使用旧的 `DSH Computer Use` 条目时才移除它。
4. 停用自动身份时,先改用临时签名或用户证书,再运行 `pnpm --dir packages/experimental/cua-macos run dev-signing:remove`。自定义 home 通过同一个 `DSH_HOME` 或 `--dsh-home <路径>` 指定。删除 DSH home 后会重新生成证书,届时需要重新授权。
1. 保持默认 `sourceSigning: ad-hoc` 即可使用临时签名。需要减少重新编译和跨 worktree 的授权次数时,按[开发者签名 skill](../../../../.agents/skills/dsh-cua-macos-signing/SKILL.md)生成或复用证书,并将 `sourceSigning: { identity, keychain }` 写入 DSH home 的 `cordis.patch.yml`;`keychain` 必须为绝对路径。
2. 从源码检出启动 DSH,调用 Computer Use,在系统设置中给 `DSH Computer Use Dev` 授予辅助功能和屏幕录制权限。
3. 确认 `getCapabilities` 的两项权限均为 `true`。使用固定签名时,在修改 Swift 源码重建和另一个 worktree 中分别复查。只有确认没有安装版 DSH 使用旧条目时,才移除旧 `DSH Computer Use` 条目。
4. 钥匙串锁定后,按 skill 重新运行 setup 解锁并复用现有身份。停止使用时改回 `ad-hoc` 或移除该配置,再按 skill 删除身份;删除 DSH home 不会删除开发者钥匙串。
@@ -15,8 +15,8 @@
en: 6390cd271bbb64fe
zh: 342f4f6aa617809f
/deepseek-ai-dsh-experimental-cua-macos/use-this-package:
en: 7e29c174a0712fa2
zh: 086cb1d75b6d2d08
en: ceb982af46176a87
zh: de39943183dad724
/deepseek-ai-dsh-experimental-cua-macos/use-this-package/connect-an-external-execution-environment:
en: 786b8fd630b11e5f
zh: 55143f15486afce8
+5 -4
View File
@@ -39,23 +39,24 @@ pnpm dsh headless --patch packages/experimental/cua-macos/examples/macos.cordis.
The dump command checks profile composition without a model request. For a real task, replace `--dump-config` with a prompt and configure the profile’s model credentials. The overlay mounts the computer-use registry and this provider; remove an existing competing provider before using it. The helper needs Accessibility permission for AX/input and Screen Recording permission for capture; grant these permissions to the helper when prompted. Source builds use the bundle filename `DSH Computer Use Dev.app` and declare `DSH Computer Use Dev` in Info.plist; select the development entry when granting access. `getCapabilities` reports permissions and session lock state without opening a system dialog. Locked or unavailable desktop sessions reject UI observations and input before dispatch; the user must unlock macOS before resuming. `listApps` and `cua.getState` list running applications. `findApps(query)` searches standard macOS application directories by name, bundle ID or absolute path. `getApp` selects a unique name, bundle ID, path or running PID, obtains application consent, and launches a stopped app in the background when needed. A stopped application’s bundle identifier and executable path are read from disk before approval and rechecked before and after launch; applications without a bundle identifier use their canonical executable path for reusable consent. Selecting a stopped installation requests a new instance of that copy. The helper waits for the launched application to finish launching before returning its PID. A post-launch verification failure expires the pending handle but can leave the process running; inspect running applications before selecting a fresh handle. The returned object reads initial AX without activating the app. Launch cancellation may leave the application running. Model guidance starts with background observation and input, without activation during initialization or before each action. For hidden or minimized windows, try `app.show()` or `window.show()` and read fresh state. This requests unhide and unminimize without activation, raising or switching Spaces; app-level restoration selects the focused, main or sole AX window. The result names the selected `windowId`, or null when no window was selected, and reports `foregroundChanged` by comparing the front app and top window before and after the request. Null means foreground evidence was unavailable; later focus changes remain possible. If restoration fails or the window remains unusable, explain the needed foreground step, call `app.activate()` or an observed window’s `raise` action, then observe again. Necessary foreground access takes priority over preserving focus. Applications may change focus after input; background delivery does not guarantee an undisturbed user session.
Source builds default to `sourceSigning: local-identity`: the first build generates an identity under `$DSH_HOME/computer-use-dev-signing`, and later builds and worktrees using that home reuse it. An unset `DSH_HOME` uses `~/.dsh`. Select `sourceSigning: ad-hoc` for temporary signatures, or choose an existing certificate in the profile’s `cordis.patch.yml` as below; `cua` is the entry ID loading `@deepseek-ai/dsh-experimental-cua-macos`:
Source builds default to `sourceSigning: ad-hoc`, without creating certificates or keychains. Changed binaries can require new permission grants. To reduce repeated grants during rebuilds and across worktrees, select an existing certificate and its absolute keychain path in the DSH home’s `cordis.patch.yml`; `cua` is the entry ID loading `@deepseek-ai/dsh-experimental-cua-macos`:
```yaml
- id: cua
config:
sourceSigning:
identity: "Apple Development: Your name (TEAMID)"
identity: "<certificate SHA-1 fingerprint>"
keychain: "/Users/<user>/.cua-macos-keys/dev/signing.keychain-db"
```
Replace `identity` with an existing certificate name or SHA-1 fingerprint. Optional `keychain` selects an absolute keychain path; omission uses the user search list, and the user owns unlocking it. Custom signing does not create, unlock or modify the generated identity. Automatic signing stores a private key and password accessible to other processes of the same user; see [development signing](native/README.md#build-and-service).
`identity` is an existing certificate name or SHA-1 fingerprint; `keychain` is a required absolute path. Developers own creating and unlocking the keychain; builds only use the selected certificate. Preserve the entry’s other configuration fields when merging this patch. The repository’s [developer signing skill](../../../.agents/skills/dsh-cua-macos-signing/SKILL.md) guides Agents through creating, reusing, and removing identities under `~/.cua-macos-keys/`, with only their selection stored in DSH home.
| Field | Default | Meaning |
| --- | --- | --- |
| `agentPreset` | `ptc` | Register tools and guidance only for Agents bound to this preset; `false` registers globally. |
| `allowedApps` | `[]` | Legacy path/signature grants, imported once for removal; they do not authorize new bundle-ID identities. |
| `helperPath` | package-owned executable | Absolute executable inside the signed helper `.app`; a bare binary fails with `NATIVE_APPLICATION_REQUIRED`. |
| `sourceSigning` | `local-identity` | Source signing: a generated identity, `ad-hoc`, or `{ identity, keychain? }` for an existing certificate. Ignored for installed packages and `helperPath`. |
| `sourceSigning` | `ad-hoc` | Source signing: `ad-hoc`, or `{ identity, keychain }` for an existing certificate and keychain path. Ignored for installed packages and `helperPath`. |
| `buildTimeoutMs` | `300000` | Maximum native preparation time. |
| `timeoutMs` | `30000` | Per-operation deadline; cancellation can leave partial input. |
| `connectTimeoutMs` | `5000` | Deadline for connecting to or starting the shared helper. |
+5 -4
View File
@@ -39,23 +39,24 @@ pnpm dsh headless --patch packages/experimental/cua-macos/examples/macos.cordis.
配置输出命令不发起模型请求。执行真实任务时,将 `--dump-config` 替换为任务提示,并配置该 profile 的模型凭据。配置挂载 computer-use 注册表和本 provider;使用前需移除已有的竞争 provider。helper 需要辅助功能权限来读取 AX 和输入,需要屏幕录制权限来截图;按系统提示给 helper 授权。源码构建的应用包名为 `DSH Computer Use Dev.app`,Info.plist 声明名称 `DSH Computer Use Dev`;授权时选择开发版条目。`getCapabilities` 查询权限和锁屏状态,不弹出系统授权窗口。桌面会话锁定或不可用时,界面观察与输入会在分派前拒绝,需由用户解锁 macOS 后继续。`listApps` 和 `cua.getState` 列出运行中的应用。`findApps(query)` 按名称、bundle ID 或绝对路径搜索 macOS 标准应用目录。`getApp` 按唯一名称、bundle ID、路径或运行中的 PID 选择应用,取得应用许可后,在需要时后台启动尚未运行的应用。授权前从磁盘读取应用的 Bundle ID 和可执行路径,在启动前后重新读取并核对;没有 Bundle ID 的应用使用规范化的可执行路径复用授权。选择尚未运行的安装副本时,会请求为该副本创建新实例。helper 等待应用完成启动后才返回其 PID。启动后验证失败会使待启动句柄失效,但进程可能仍在运行;应先查看运行中的应用,再取得新句柄。返回的对象读取初始 AX,不自动激活应用。启动被取消时,应用可能已经运行。模型指引要求先在后台观察和输入,不在初始化或每次动作前激活应用。窗口隐藏或最小化时,先尝试 `app.show()` 或 `window.show()`,再重新观察。它请求取消隐藏和恢复最小化,不主动激活、抬起窗口或切换桌面空间;应用级恢复选择焦点窗口、主窗口或唯一 AX 窗口。结果通过 `windowId` 指明选中的窗口,未选中时为 null;`foregroundChanged` 比较操作前后的前台应用和最上层窗口,无法取得完整观察时为 null,返回后仍可能发生焦点变化。恢复失败或窗口仍不可用时,说明需要前台的步骤,再调用 `app.activate()` 或窗口的 `raise` 动作并重新观察。必须使用前台时,以完成任务为先。应用收到输入后仍可能改变焦点,后台投递不保证用户操作始终不受干扰。
源码构建默认使用 `sourceSigning: local-identity`:首次构建在 `$DSH_HOME/computer-use-dev-signing` 中生成固定身份,后续构建和使用同一 DSH home 的 worktree 复用它。未设置 `DSH_HOME` 时使用 `~/.dsh`。临时签名可配置为 `sourceSigning: ad-hoc`;已有签名证书可在 profile 的 `cordis.patch.yml` 中这样选择,`cua` 是加载 `@deepseek-ai/dsh-experimental-cua-macos` 的节点 ID:
源码构建默认使用 `sourceSigning: ad-hoc` 临时签名,不创建证书或钥匙串。二进制变化后可能需要重新授权。需要减少开发重建和跨 worktree 的授权次数时,在 DSH home 的 `cordis.patch.yml` 中选择已有证书及其钥匙串绝对路径;`cua` 是加载 `@deepseek-ai/dsh-experimental-cua-macos` 的节点 ID:
```yaml
- id: cua
config:
sourceSigning:
identity: "Apple Development: Your name (TEAMID)"
identity: "<certificate SHA-1 fingerprint>"
keychain: "/Users/<user>/.cua-macos-keys/dev/signing.keychain-db"
```
将 `identity` 替换为已有证书的名称或 SHA-1 指纹。可选的 `keychain` 指定钥匙串绝对路径;省略时使用用户钥匙串搜索列表,解锁由用户负责。自定义证书模式不会创建、解锁或修改自动生成的身份。自动模式会保存私钥和密码,同一用户的进程可以读取并使用它们;详见[开发签名](native/README.zh.md#build-and-service)。
`identity` 是已有证书名称或 SHA-1 指纹,`keychain` 是必填的绝对路径。开发者负责生成和解锁钥匙串;构建只使用所选证书。合并配置时保留该节点已有的其他字段。仓库的[开发者签名 skill](../../../.agents/skills/dsh-cua-macos-signing/SKILL.md)指导 Agent 在 `~/.cua-macos-keys/` 中生成、复用和移除身份,并只将选择身份的配置写入 DSH home。
| 字段 | 默认值 | 含义 |
| --- | --- | --- |
| `agentPreset` | `ptc` | 仅为绑定该 preset 的 Agent 注册工具和指引;`false` 表示全局注册。 |
| `allowedApps` | `[]` | 旧路径和签名许可,只导入 profile 存储一次供撤销,不授予新 Bundle ID 身份访问权限。 |
| `helperPath` | 包内可执行文件 | 已签名 helper `.app` 内可执行文件的绝对路径;裸二进制会触发 `NATIVE_APPLICATION_REQUIRED`。 |
| `sourceSigning` | `local-identity` | 源码签名:自动生成固定身份、`ad-hoc` 临时签名,或 `{ identity, keychain? }` 指定已有证书。安装版和 `helperPath` 忽略此项。 |
| `sourceSigning` | `ad-hoc` | 源码签名:`ad-hoc` 临时签名,或 `{ identity, keychain }` 指定已有证书及钥匙串路径。安装版和 `helperPath` 忽略此项。 |
| `buildTimeoutMs` | `300000` | 原生组件准备的最长时间。 |
| `timeoutMs` | `30000` | 单次操作截止时间,取消后可能已发生部分输入。 |
| `connectTimeoutMs` | `5000` | 连接或启动共享 helper 的截止时间。 |
@@ -15,8 +15,8 @@
en: e8cbc43d38786c40
zh: 52b3ba85d98011b9
/dsh-cua-native-helper/build-and-service:
en: e673418383f64b0c
zh: f27e4ac4b868da16
en: 1dca32fdeba46e0f
zh: c82d4c92ccdde367
/dsh-cua-native-helper/request-protocol:
en: fc8043c425322cdd
zh: cf1edb9a78fda301
@@ -24,8 +24,8 @@
en: 27f23e6f3b77ee64
zh: 9cd1fdc76e9f041e
/dsh-cua-native-helper/verification:
en: c2ad240e2ce1962f
zh: 4a0cb0b30df98b71
en: 32d832fd7a9a60a7
zh: e80b5e47fd60cc8f
/dsh-cua-native-helper/further-exploration:
en: 265e1927952ce6d7
zh: 7c35d9fd98b6e6c8
@@ -25,13 +25,13 @@ This macOS 14+ JSON-lines helper uses Swift 6, AppKit, Accessibility, CoreGraphi
Run `pnpm --dir packages/experimental/cua-macos run build:native` to publish a signed `DSH Computer Use.app` under `native/bin/darwin-<arch>`. The `current.json` receipt selects a complete content-addressed generation. Signing defaults to ad hoc; release packagers can pass `--identity` for Developer ID signing and must notarize the distribution. The Host runs the application executable with `--ensure-shared`, which launches the background service through LaunchServices. The service exits after its last connection finishes cleanup.
Source checkouts build with `--variant dev`, producing `DSH Computer Use Dev.app` with bundle identifier `com.deepseek.harness.computer-use.dev`. Development builds default to `local-identity`, generating a fixed identity on first use. Explicit `--signing ad-hoc` uses a temporary signature without accessing the generated identity. Ad-hoc requirements contain the code hash, so changed binaries can require new grants.
Source checkouts build with `--variant dev`, producing `DSH Computer Use Dev.app` with bundle identifier `com.deepseek.harness.computer-use.dev`. Development and release builds default to ad-hoc signing. Ad-hoc requirements contain the code hash, so changed binaries can require new grants.
Automatic mode creates or reuses the `DSH Computer Use Dev (local)` certificate in a dedicated keychain under `$DSH_HOME/computer-use-dev-signing` and adds the keychain to the user search list. The Host passes `--dsh-home` from the shared home-path resolver; standalone Python commands use `--dsh-home`, a nonblank `DSH_HOME`, then `~/.dsh`. The identity, password and lock file stay inside that home, with setup and removal serialized per home. Each build unlocks the keychain using the adjacent `0600` identity record. Other processes of the same user can read the password and sign code. The designated requirement contains the bundle identifier and certificate; actual grant retention after rebuilds and across worktrees still needs manual verification.
The source provider’s `sourceSigning: { identity, keychain }` selects an existing certificate and its absolute keychain path. The build CLI accepts `--identity <certificate name or SHA-1 fingerprint>` and `--keychain <absolute path>`; release packaging may also omit `--keychain` to use the user search list. Builds do not create, unlock or remove identities; signing fails when the selected certificate is unavailable.
User-certificate mode uses `sourceSigning: { identity, keychain? }`, or CLI `--identity <certificate name or SHA-1 fingerprint>`. Optional `--keychain <absolute path>` selects a keychain; otherwise the user search list applies. The build does not create certificates, read the generated identity or unlock user keychains. `--identity` and `--signing` are mutually exclusive. Release builds retain their ad-hoc default and support explicit certificates.
The repository’s [developer signing skill](../../../../.agents/skills/dsh-cua-macos-signing/SKILL.md) owns the Agent workflow: generate an isolated identity under `~/.cua-macos-keys/`, write its selection into DSH home, and unlock or remove it when requested. Developers maintain the private key and password separately; deleting DSH home does not delete them. Other processes of the same user can read the stored password and sign with that identity.
`pnpm --dir packages/experimental/cua-macos run dev-signing:setup` can generate the automatic identity in advance. `dev-signing:remove` deletes that home’s identity, password and keychain search entry, retaining the empty lock file. Both commands accept `--dsh-home <path>`. Select ad-hoc or user-certificate signing before removal to prevent regeneration. Deleting DSH home also deletes its identity; a new certificate needs new grants. An existing record with a missing keychain or certificate is preserved and fails with commands to explicitly remove and initialize that home again. Remove system permission entries separately in System Settings.
Builds using the same certificate share a designated requirement containing the bundle identifier and certificate. The first permission grant remains manual in System Settings; equal requirements do not replace actual permission checks after rebuilds and across worktrees.
Certificate signatures include a signing time. Development builds record the generation produced by each unsigned bundle and certificate fingerprint under `sources/` and reuse it for unchanged content. Ad-hoc builds use a separate record. Explicit certificate names are re-signed because renewal can change the certificate behind a name.
@@ -129,7 +129,7 @@ Desktop checks require grants for the selected helper: release builds declare `D
Run `python3 packages/experimental/cua-macos/native/tests/startup_e2e.py` to check immediate exit, typed startup errors, stale readiness rejection and termination of stalled new instances through LaunchServices. It uses owned applications and requests no system permissions. Publication unit tests mock signing; `build:native` invokes real `codesign --verify --strict`, and `launcher_identity_e2e.py` verifies LaunchServices attribution.
Use `python3 packages/experimental/cua-macos/native/tests/test_swift.py` for offline unit checks with the selected Apple toolchain’s Swift Testing framework/runtime paths and `python3 packages/experimental/cua-macos/native/tests/protocol_e2e.py` for JSONL admission, ordered replies and EOF shutdown without desktop operations. Application and protocol e2e checks accept `--helper` to select a separately built comparison binary. `python3 packages/experimental/cua-macos/native/tests/test_build_app.py` covers application publication, the development variant, signed-generation reuse, signing failure and corrupted-generation rejection. `python3 packages/experimental/cua-macos/native/tests/test_dev_signing.py` covers identity setup, unlocking and removal with a simulated `security` command. `python3 packages/experimental/cua-macos/native/tests/dev_signing_e2e.py` creates a temporary identity, signs two different builds, checks that both have the same designated requirement, then removes the identity and verifies the restored keychain search list.
Use `python3 packages/experimental/cua-macos/native/tests/test_swift.py` for offline unit checks with the selected Apple toolchain’s Swift Testing framework/runtime paths and `python3 packages/experimental/cua-macos/native/tests/protocol_e2e.py` for JSONL admission, ordered replies and EOF shutdown without desktop operations. Application and protocol e2e checks accept `--helper` to select a separately built comparison binary. `python3 packages/experimental/cua-macos/native/tests/test_build_app.py` covers application publication, the development variant, signed-generation reuse, signing failure and corrupted-generation rejection. Developer signing unit tests run through `scripts/cua-macos-signing.spec.ts`; the [developer signing skill](../../../../.agents/skills/dsh-cua-macos-signing/SKILL.md) owns the real-signing verification command.
Run `python3 packages/experimental/cua-macos/native/tests/shared_helper_e2e.py --helper /absolute/helper` to verify concurrent startup, a shared service PID, independent handles, same-application handoff, request-local screenshot deadlines, connection cleanup and signal shutdown with an owned AppKit fixture. Existing Accessibility and Screen Recording grants for the selected release or development helper are required. `--without-desktop` limits the check to protocol and process lifetime.
@@ -25,13 +25,13 @@ kind: "package-reference"
运行 `pnpm --dir packages/experimental/cua-macos run build:native`,在 `native/bin/darwin-<arch>` 下生成带签名的 `DSH Computer Use.app`,`current.json` 选择完整的内容摘要目录。默认使用临时签名;正式打包可传入 `--identity` 指定 Developer ID,并由发布流程完成公证。Host 运行应用内的 `--ensure-shared` 入口,经 LaunchServices 后台启动服务。最后一条连接完成清理后,服务退出。
源码检出以 `--variant dev` 构建,应用包为 `DSH Computer Use Dev.app`,Bundle ID 为 `com.deepseek.harness.computer-use.dev`。开发版默认使用 `local-identity`,首次构建自动生成固定身份;显式 `--signing ad-hoc` 使用临时签名,不访问自动身份。临时签名的指定要求包含代码哈希,二进制变化后可能需要重新授权。
源码检出以 `--variant dev` 构建,应用包为 `DSH Computer Use Dev.app`,Bundle ID 为 `com.deepseek.harness.computer-use.dev`。开发版和正式版默认临时签名。临时签名的指定要求包含代码哈希,二进制变化后可能需要重新授权。
自动模式在 `$DSH_HOME/computer-use-dev-signing` 下的专用钥匙串中创建或复用 `DSH Computer Use Dev (local)` 证书,并把钥匙串加入用户搜索列表。Host 通过公共 home 路径解析器传入 `--dsh-home`;独立 Python 命令依次使用 `--dsh-home`、非空 `DSH_HOME`、`~/.dsh`。身份、密码和锁文件都在该 home 内,文件锁让同一 home 的初始化与移除串行执行。每次构建使用旁边权限为 `0600` 的身份记录解锁钥匙串。同一用户的进程可以读取密码并签名。指定要求包含 Bundle ID 和证书,重新编译和跨 worktree 后实际保留授权的效果仍需手动验证。
源码 provider 的 `sourceSigning: { identity, keychain }` 选择已有证书及其钥匙串绝对路径。构建 CLI 使用 `--identity <证书名称或 SHA-1 指纹>` 和 `--keychain <绝对路径>`;正式打包也可省略 `--keychain`,使用用户搜索列表。构建不生成、解锁或移除签名身份;所选证书不可用时签名失败。
用户证书模式使用 `sourceSigning: { identity, keychain? }`,或命令行 `--identity <证书名称或 SHA-1 指纹>`。可选的 `--keychain <绝对路径>` 指定钥匙串,否则使用用户搜索列表。构建不创建证书,不读取自动身份,也不解锁用户钥匙串。`--identity` 与 `--signing` 不能同时指定;正式构建仍默认临时签名,并支持指定证书。
仓库的[开发者签名 skill](../../../../.agents/skills/dsh-cua-macos-signing/SKILL.md)负责 Agent 操作流程:在 `~/.cua-macos-keys/` 生成独立身份,将指向它的配置写入 DSH home,并按需解锁或移除。私钥和密码由开发者单独维护,删除 DSH home 不会删除它们。同一用户的进程可以读取保存的密码并使用该身份签名。
`pnpm --dir packages/experimental/cua-macos run dev-signing:setup` 可提前生成自动身份;`dev-signing:remove` 删除当前 DSH home 中的身份、密码和钥匙串搜索条目,保留空锁文件。两条命令均接受 `--dsh-home <路径>`。停止使用自动身份时,先选择临时签名或用户证书,否则下次构建会重新生成身份。删除 DSH home 也会删除身份,新证书需要重新授权。已有记录但钥匙串或证书缺失时,构建保留记录并报错,提示使用该 home 明确移除后重新初始化;系统设置中的授权需单独移除。
使用相同证书的构建共用包含 Bundle ID 和证书的指定要求。首次授权仍需在系统设置中完成;签名要求相同不代替授权后重建、跨 worktree 的实际权限验证。
证书签名包含签名时间。开发构建在 `sources/` 中记录每个未签名应用与证书指纹对应的目录,内容不变时复用该目录。临时签名使用独立记录。显式传入证书名称时会重新签名,因为续期可能改变名称对应的证书。
@@ -129,7 +129,7 @@ AppKit 在主事件循环上运行。阻塞式 socket 接收与握手使用后
运行 `python3 packages/experimental/cua-macos/native/tests/startup_e2e.py`,通过 LaunchServices 检查启动即退出、原始错误传递、过期就绪记录拒绝,以及卡住的新实例退出。检查使用自建应用,不申请系统权限。发布单元测试模拟签名;`build:native` 调用真实的 `codesign --verify --strict`,`launcher_identity_e2e.py` 验证 LaunchServices 授权归属。
使用 `python3 packages/experimental/cua-macos/native/tests/test_swift.py` 通过选定 Apple 工具链的 Swift Testing 框架和运行时路径执行离线单元检查;使用 `python3 packages/experimental/cua-macos/native/tests/protocol_e2e.py` 检查 JSONL 接纳、有序回复和 EOF 退出,不执行桌面操作。应用与协议端到端检查接受 `--helper` 以选择单独构建的对比二进制。 `python3 packages/experimental/cua-macos/native/tests/test_build_app.py` 验证应用发布、开发版构建、已签名目录复用、签名失败和损坏副本拒绝。`python3 packages/experimental/cua-macos/native/tests/test_dev_signing.py` 用模拟的 `security` 命令验证身份初始化、解锁和移除。`python3 packages/experimental/cua-macos/native/tests/dev_signing_e2e.py` 创建临时身份,签出两个不同的构建并检查二者的指定要求相同,随后移除身份并核对钥匙串搜索列表已恢复。
使用 `python3 packages/experimental/cua-macos/native/tests/test_swift.py` 通过选定 Apple 工具链的 Swift Testing 框架和运行时路径执行离线单元检查;使用 `python3 packages/experimental/cua-macos/native/tests/protocol_e2e.py` 检查 JSONL 接纳、有序回复和 EOF 退出,不执行桌面操作。应用与协议端到端检查接受 `--helper` 以选择单独构建的对比二进制。 `python3 packages/experimental/cua-macos/native/tests/test_build_app.py` 验证应用发布、开发版构建、已签名目录复用、签名失败和损坏副本拒绝。开发者签名工具的单测由 `scripts/cua-macos-signing.spec.ts` 执行;真实签名验证命令见[开发者签名 skill](../../../../.agents/skills/dsh-cua-macos-signing/SKILL.md)。
运行 `python3 packages/experimental/cua-macos/native/tests/shared_helper_e2e.py --helper /absolute/helper`,通过测试自建的 AppKit fixture 核实并发启动、共享服务 PID、独立句柄、同一应用交接、请求级截图期限、连接清理和信号退出。需要已经给所选的正式版或开发版 helper 授予辅助功能和屏幕录制权限。`--without-desktop` 将检查限定为协议与进程生命期。
@@ -1,7 +1,6 @@
"""Build, sign and publish immutable Computer Use application generations."""
import argparse
import hashlib
import importlib.util
import json
import os
from pathlib import Path
@@ -114,26 +113,6 @@ def publish(binary, output, identity, plist, variant="release", keychain=None):
return select(output, temporary, key, name)
def load_dev_signing():
"""Load the sibling module whether this file runs as a script or is imported by path."""
location = Path(__file__).resolve().with_name("dev_signing.py")
spec = importlib.util.spec_from_file_location("dev_signing", location)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
def signing_identity(variant, identity, signing, dsh_home=None, keychain=None):
"""Select a user certificate, an ad-hoc signature, or an identity retained in DSH home."""
if identity is not None:
return identity, keychain
if variant == "release" or signing == "ad-hoc":
return "-", None
signing_module = load_dev_signing()
local = signing_module.setup(signing_module.default_directory(dsh_home))
return local.sha1, local.keychain
def main():
root = Path(__file__).resolve().parent
parser = argparse.ArgumentParser(description=__doc__)
@@ -142,26 +121,20 @@ def main():
parser.add_argument("--output-directory", type=Path)
parser.add_argument("--variant", choices=["release", "dev"], default="release",
help="dev uses a separate bundle identifier")
signing_options = parser.add_mutually_exclusive_group()
signing_options.add_argument("--signing", choices=["ad-hoc", "local-identity"],
help="defaults to local-identity for dev and ad-hoc for release")
signing_options.add_argument("--identity", help="existing codesign certificate name or SHA-1 fingerprint")
parser.add_argument("--identity", default="-", help="existing codesign certificate name or SHA-1 fingerprint; defaults to ad-hoc (-)")
parser.add_argument("--keychain", type=Path, help="absolute keychain path for --identity; the user owns unlocking it")
parser.add_argument("--dsh-home", type=Path, help="home for local-identity; defaults to DSH_HOME or ~/.dsh")
args = parser.parse_args()
if args.identity is not None and not args.identity.strip():
if not args.identity.strip():
parser.error("--identity must name an existing certificate")
if args.keychain is not None and (args.identity is None or not args.keychain.is_absolute()):
if args.keychain is not None and (args.identity == "-" or not args.keychain.is_absolute()):
parser.error("--keychain requires --identity and an absolute path")
architecture = "x64" if args.arch == "x86_64" else "arm64"
scratch = args.scratch_path or root / ".build" / f"runtime-{architecture}"
output = args.output_directory or root / "bin" / f"darwin-{architecture}"
signing = args.signing or ("local-identity" if args.variant == "dev" else "ad-hoc")
identity, keychain = signing_identity(args.variant, args.identity, signing, args.dsh_home, args.keychain)
subprocess.run(["/usr/bin/xcrun", "swift", "build", "--package-path", str(root),
"--scratch-path", str(scratch), "--configuration", "release",
"--product", "dsh-cua-native", "--arch", args.arch], check=True)
executable = publish(scratch / "release/dsh-cua-native", output, identity, root / "Info.plist", args.variant, keychain)
executable = publish(scratch / "release/dsh-cua-native", output, args.identity, root / "Info.plist", args.variant, args.keychain)
print(executable)
@@ -8,7 +8,7 @@ import plistlib
import secrets
import tempfile
import unittest
from unittest.mock import Mock, patch
from unittest.mock import patch
spec = importlib.util.spec_from_file_location("build_app", Path(__file__).resolve().parents[1] / "build_app.py")
builder = importlib.util.module_from_spec(spec)
@@ -168,7 +168,7 @@ class DevelopmentPublicationTests(PublicationCases, unittest.TestCase):
self.assertNotEqual(self.publish("Apple Development: Example"), first)
self.assertEqual(len(self.signings()), 2)
def test_switching_between_ad_hoc_and_local_identity_selects_the_matching_generation(self):
def test_switching_between_ad_hoc_and_configured_identity_selects_the_matching_generation(self):
ad_hoc = self.publish("-")
local = self.publish()
self.assertNotEqual(local, ad_hoc)
@@ -183,65 +183,30 @@ class DevelopmentPublicationTests(PublicationCases, unittest.TestCase):
self.assertEqual(len(self.signings()), 2)
class SigningIdentityTests(unittest.TestCase):
def test_ad_hoc_and_release_builds_never_load_a_local_identity(self):
with patch.object(builder, "load_dev_signing") as load:
for variant, mode in (("dev", "ad-hoc"), ("release", "ad-hoc"), ("release", "local-identity")):
self.assertEqual(builder.signing_identity(variant, None, mode), ("-", None))
load.assert_not_called()
def test_local_identity_builds_create_or_reuse_the_identity(self):
local = type("Identity", (), {"sha1": "ABC", "keychain": Path("/keychains/dev.keychain-db")})
signing = Mock()
signing.setup.return_value = local
signing.default_directory.return_value = Path("/dsh/computer-use-dev-signing")
with patch.object(builder, "load_dev_signing", return_value=signing):
self.assertEqual(builder.signing_identity("dev", None, "local-identity", Path("/dsh")), ("ABC", local.keychain))
signing.default_directory.assert_called_once_with(Path("/dsh"))
signing.setup.assert_called_once_with(Path("/dsh/computer-use-dev-signing"))
def test_user_certificates_do_not_load_or_modify_generated_identities(self):
with patch.object(builder, "load_dev_signing") as load:
for keychain in (None, Path("/keychains/custom.keychain-db")):
self.assertEqual(builder.signing_identity("dev", "Apple Development: Example", None, keychain=keychain),
("Apple Development: Example", keychain))
load.assert_not_called()
def test_local_identity_failure_does_not_fall_back_to_ad_hoc(self):
signing = Mock()
signing.setup.side_effect = RuntimeError("identity unavailable")
with patch.object(builder, "load_dev_signing", return_value=signing):
with self.assertRaisesRegex(RuntimeError, "identity unavailable"):
builder.signing_identity("dev", None, "local-identity")
def test_command_line_defaults_to_local_identity_and_passes_the_selected_mode(self):
for flags, mode in (([], "local-identity"), (["--signing", "ad-hoc"], "ad-hoc"), (["--signing", "local-identity"], "local-identity")):
with patch.object(builder.sys, "argv", ["build_app.py", "--variant", "dev", *flags]), \
patch.object(builder, "signing_identity", return_value=("-", None)) as identity, \
patch.object(builder.subprocess, "run"), patch.object(builder, "publish", return_value="helper"), \
class SigningOptionsTests(unittest.TestCase):
def test_default_dev_and_release_builds_use_ad_hoc_without_keychain_commands(self):
for variant in ("dev", "release"):
with self.subTest(variant=variant), patch.object(builder.sys, "argv", ["build_app.py", "--variant", variant]), \
patch.object(builder.subprocess, "run") as run, patch.object(builder, "publish", return_value="helper") as publish, \
contextlib.redirect_stdout(io.StringIO()):
builder.main()
identity.assert_called_once_with("dev", None, mode, None, None)
self.assertEqual(len(run.call_args_list), 1)
self.assertEqual(run.call_args.args[0][:3], ["/usr/bin/xcrun", "swift", "build"])
self.assertEqual(publish.call_args.args[2], "-")
self.assertEqual(publish.call_args.args[4:], (variant, None))
def test_command_line_passes_a_custom_identity_and_keychain(self):
with patch.object(builder.sys, "argv", ["build_app.py", "--variant", "dev", "--identity", "Example", "--keychain", "/keychains/user.keychain-db"]), \
patch.object(builder, "signing_identity", return_value=("Example", Path("/keychains/user.keychain-db"))) as identity, \
patch.object(builder.subprocess, "run"), patch.object(builder, "publish", return_value="helper"), \
contextlib.redirect_stdout(io.StringIO()):
builder.main()
identity.assert_called_once_with("dev", "Example", "local-identity", None, Path("/keychains/user.keychain-db"))
def test_custom_certificate_and_keychain_reach_the_signer(self):
for flags, keychain in (([], None), (["--keychain", "/keychains/user.keychain-db"], Path("/keychains/user.keychain-db"))):
with patch.object(builder.sys, "argv", ["build_app.py", "--variant", "dev", "--identity", "Example", *flags]), \
patch.object(builder.subprocess, "run"), patch.object(builder, "publish", return_value="helper") as publish, \
contextlib.redirect_stdout(io.StringIO()):
builder.main()
self.assertEqual(publish.call_args.args[2], "Example")
self.assertEqual(publish.call_args.args[4:], ("dev", keychain))
def test_release_command_line_keeps_ad_hoc_signing_by_default(self):
with patch.object(builder.sys, "argv", ["build_app.py"]), \
patch.object(builder, "signing_identity", return_value=("-", None)) as identity, \
patch.object(builder.subprocess, "run"), patch.object(builder, "publish", return_value="helper"), \
contextlib.redirect_stdout(io.StringIO()):
builder.main()
identity.assert_called_once_with("release", None, "ad-hoc", None, None)
def test_invalid_user_signing_options_fail_before_building(self):
for flags in (["--identity", "Example", "--signing", "ad-hoc"], ["--keychain", "/keychains/user"],
["--identity", "Example", "--keychain", "relative"], ["--identity", " "]):
def test_invalid_or_retired_signing_options_fail_before_building(self):
for flags in (["--signing", "local-identity"], ["--dsh-home", "/home/example"],
["--keychain", "/keychains/user"], ["--identity", "Example", "--keychain", "relative"], ["--identity", " "]):
with self.subTest(flags=flags), patch.object(builder.sys, "argv", ["build_app.py", *flags]), \
patch.object(builder.subprocess, "run") as run, contextlib.redirect_stderr(io.StringIO()):
with self.assertRaises(SystemExit) as error:
+1 -5
View File
@@ -31,7 +31,6 @@
"native/Info.plist",
"native/DSHComputerUse.icns",
"native/build_app.py",
"native/dev_signing.py",
"native/Sources",
"native/tests/swift",
"native/tests/test_swift.py",
@@ -57,8 +56,7 @@
"@deepseek-ai/schemastery": "workspace:~",
"zod": "^4.4.3",
"@deepseek-ai/dsh-atomic-write": "workspace:*",
"@deepseek-ai/dsh-cua": "workspace:*",
"@deepseek-ai/dsh-home-paths": "workspace:*"
"@deepseek-ai/dsh-cua": "workspace:*"
},
"devDependencies": {
"@deepseek-ai/dsh-working-directory": "workspace:*",
@@ -100,8 +98,6 @@
},
"scripts": {
"build:native": "xcrun python3 native/build_app.py",
"dev-signing:setup": "xcrun python3 native/dev_signing.py setup",
"dev-signing:remove": "xcrun python3 native/dev_signing.py remove",
"build:icon": "node native/generate_icon.mjs"
},
"os": [
+9 -10
View File
@@ -4,7 +4,6 @@ import { fileURLToPath } from 'node:url'
import type { Context } from '@deepseek-ai/cordis'
import Schema from '@deepseek-ai/schemastery'
import { CuaConfig, installCua } from '@deepseek-ai/dsh-cua'
import { resolveDshHome } from '@deepseek-ai/dsh-home-paths'
import { nativeHostArchitecture, prepareNativeExecutable } from './native-build.ts'
import { createSharedNativeChannel } from './shared-native.ts'
import { GUIDANCE } from './guidance.ts'
@@ -17,15 +16,15 @@ export const inject = ['computerUse', 'tools', 'systemPrompt', 'subprocess', 'ag
/** Native preparation, source-build signing, operation bounds, and profile-local application grants. */
export interface Config extends CuaConfig {
/**
* Source-build signing: `local-identity` (default) creates or reuses an identity in DSH home;
* `ad-hoc` binds grants to the code hash; an object selects an existing user-managed certificate.
* Source-build signing: `ad-hoc` (default) binds grants to the code hash;
* an object selects an existing certificate in an explicitly configured keychain.
* Installed packages and `helperPath` keep their own signature.
*/
sourceSigning: 'ad-hoc' | 'local-identity' | {
sourceSigning: 'ad-hoc' | {
/** Existing codesign certificate name or SHA-1 fingerprint. */
identity: string
/** Absolute keychain path; omission uses the user's keychain search list. The user owns unlocking it. */
keychain?: string
/** Absolute keychain path. The developer owns creating and unlocking it. */
keychain: string
}
}
/** Validate shared operation bounds and macOS source signing before preparing the helper. */
@@ -33,12 +32,12 @@ export const Config = Schema.object({
// Schemastery erases object field types on dict; the shared Config declares their output types.
...CuaConfig.dict as { [Key in keyof CuaConfig]: Schema<unknown, CuaConfig[Key]> },
sourceSigning: Schema.union([
Schema.const('ad-hoc'), Schema.const('local-identity'),
Schema.const('ad-hoc'),
Schema.object({
identity: Schema.string().pattern(/\S/).required(),
keychain: Schema.string().pattern(/^\/.+/),
keychain: Schema.string().pattern(/^\/.+/).required(),
}),
]).default('local-identity'),
]).default('ad-hoc'),
}) satisfies Schema<Record<string, unknown>, Config>
const description = 'Inspect macOS permissions, select an application and window, read AX state, capture screenshots or dispatch input. Use appHandle from getApp; verify actions by reading fresh state.'
@@ -59,7 +58,7 @@ export async function apply(ctx: Context, config: Config): Promise<void> {
const controller = new AbortController()
const prepared = prepareNativeExecutable({
packageRoot: resolve(fileURLToPath(new URL('..', import.meta.url))),
architecture, sourceSigning: config.sourceSigning, dshHome: resolveDshHome(), subprocess: ctx.subprocess,
architecture, sourceSigning: config.sourceSigning, subprocess: ctx.subprocess,
timeoutMs: config.buildTimeoutMs, graceMs: config.graceMs, signal: controller.signal,
})
ctx.effect(() => async () => {
@@ -30,8 +30,6 @@ export interface NativeBuildSpec {
architecture: NativeArchitecture
/** Signature of a source-checkout build; installed packages ignore it. */
sourceSigning: Config['sourceSigning']
/** Resolved DSH home for the automatically generated signing identity. */
dshHome: string
subprocess: Pick<SubprocessRuntime, 'spawn'>
signal: AbortSignal
timeoutMs: number
@@ -86,8 +84,8 @@ export async function buildNativeExecutable(spec: NativeBuildSpec): Promise<stri
return withFileLock(join(nativeRoot, '.build', 'application-build'), async () => {
const signing = spec.sourceSigning
const signingArgs = typeof signing === 'string'
? ['--signing', signing, ...(signing === 'local-identity' ? ['--dsh-home', spec.dshHome] : [])]
: ['--identity', signing.identity, ...(signing.keychain === undefined ? [] : ['--keychain', signing.keychain])]
? ['--identity', '-']
: ['--identity', signing.identity, '--keychain', signing.keychain]
const handle = spec.subprocess.spawn({
argv: ['/usr/bin/xcrun', 'python3', join(nativeRoot, 'build_app.py'),
'--scratch-path', join(nativeRoot, '.build', `runtime-${spec.architecture}`),
@@ -41,7 +41,7 @@ async function fixture(diagnostics = true) {
return outcome
})(), terminate, waitForExit,
}))
const spec = { packageRoot, architecture: 'arm64' as const, sourceSigning: 'ad-hoc' as const, dshHome: join(root, 'dsh-home'), subprocess: { spawn }, signal: controller.signal, timeoutMs: 60_000, graceMs: 100 }
const spec = { packageRoot, architecture: 'arm64' as const, sourceSigning: 'ad-hoc' as const, subprocess: { spawn }, signal: controller.signal, timeoutMs: 60_000, graceMs: 100 }
const directory = join(packageRoot, 'native/.build/prepared/arm64')
return { root, spec, directory, spawn, output, controller, terminate, waitForExit,
setOutput: (value: Buffer) => { output = value },
@@ -77,7 +77,7 @@ it('builds the development variant and returns its executable only after the man
expect(f.spawn.mock.calls[0]![0].argv).toEqual([
'/usr/bin/xcrun', 'python3', join(f.spec.packageRoot, 'native/build_app.py'),
'--scratch-path', join(f.spec.packageRoot, 'native/.build/runtime-arm64'),
'--output-directory', f.directory, '--arch', 'arm64', '--variant', 'dev', '--signing', 'ad-hoc',
'--output-directory', f.directory, '--arch', 'arm64', '--variant', 'dev', '--identity', '-',
])
expect(f.terminate).toHaveBeenCalledTimes(1)
expect(f.waitForExit).toHaveBeenCalledTimes(1)
@@ -86,24 +86,25 @@ it('builds the development variant and returns its executable only after the man
it('checks source builds on activation and maps Intel architecture to the compiler target', async () => {
const f = await fixture()
await writeFile(join(f.root, 'package.json'), '{"name":"@deepseek-ai/dsh-root"}')
const path = await prepareNativeExecutable({ ...f.spec, architecture: 'x64', sourceSigning: 'local-identity' })
const path = await prepareNativeExecutable({ ...f.spec, architecture: 'x64' })
expect(f.spawn.mock.calls[0]![0].argv).toContain('x86_64')
expect(f.spawn.mock.calls[0]![0].argv.slice(-4)).toEqual(['--signing', 'local-identity', '--dsh-home', f.spec.dshHome])
expect(f.spawn.mock.calls[0]![0].argv.slice(-2)).toEqual(['--identity', '-'])
expect(path).toContain(join('native', '.build', 'prepared', 'x64'))
})
it.each([undefined, '/keychains/custom.keychain-db'])('uses a configured certificate with keychain %s', async (keychain) => {
it('uses a configured certificate in the specified keychain', async () => {
const keychain = '/keychains/custom.keychain-db'
const f = await fixture()
await buildNativeExecutable({ ...f.spec, sourceSigning: { identity: 'Apple Development: Example', ...(keychain === undefined ? {} : { keychain }) } })
await buildNativeExecutable({ ...f.spec, sourceSigning: { identity: 'Apple Development: Example', keychain } })
const argv = f.spawn.mock.calls[0]![0].argv
expect(argv.slice(argv.indexOf('--identity'))).toEqual([
'--identity', 'Apple Development: Example', ...(keychain === undefined ? [] : ['--keychain', keychain]),
'--identity', 'Apple Development: Example', '--keychain', keychain,
])
expect(argv).not.toContain('--dsh-home')
expect(argv).not.toContain('--signing')
})
it.each(['local-identity', { identity: 'Apple Development: Example' }] as const)('ignores signing choice %j for an installed application', async (sourceSigning) => {
it.each(['ad-hoc', { identity: 'Apple Development: Example', keychain: '/keychains/custom.keychain-db' }] as const)('ignores signing choice %j for an installed application', async (sourceSigning) => {
const f = await fixture()
const path = await publish(join(f.spec.packageRoot, 'native/bin/darwin-arm64'), f.output, 'release')
expect(await prepareNativeExecutable({ ...f.spec, sourceSigning })).toBe(path)
@@ -10,13 +10,6 @@ it.skipIf(process.platform === 'win32')('preserves signed generations and reject
expect(output).toBe('')
})
it.skipIf(process.platform === 'win32')('keeps one unlocked development signing identity on the user keychain search list', () => {
const output = execFileSync('python3', [fileURLToPath(new URL('../native/tests/test_dev_signing.py', import.meta.url))], {
encoding: 'utf8', timeout: 30_000, stdio: ['ignore', 'pipe', 'pipe'],
})
expect(output).toBe('')
})
it.skipIf(process.platform === 'win32')('accepts shallow bare reference paths in native test helpers', () => {
const output = execFileSync('python3', [fileURLToPath(new URL('../native/tests/test_native_test_support.py', import.meta.url))], {
encoding: 'utf8', timeout: 30_000, stdio: ['ignore', 'pipe', 'pipe'],
@@ -62,20 +62,19 @@ vi.mock('@deepseek-ai/dsh-cua/native-client', async importOriginal => ({
const config = {
agentPreset: false,
helperPath: resolve('fixture-helper'), sourceSigning: 'local-identity', buildTimeoutMs: 300_000, timeoutMs: 10_000, connectTimeoutMs: 5000, screenshotTimeoutMs: 5000, axTimeoutMs: 3000, maxRequestBytes: 1024, maxResponseBytes: 4096, maxClipboardBytes: 8192, pasteReadTimeoutMs: 1000, maxAppMatches: 20,
helperPath: resolve('fixture-helper'), sourceSigning: 'ad-hoc', buildTimeoutMs: 300_000, timeoutMs: 10_000, connectTimeoutMs: 5000, screenshotTimeoutMs: 5000, axTimeoutMs: 3000, maxRequestBytes: 1024, maxResponseBytes: 4096, maxClipboardBytes: 8192, pasteReadTimeoutMs: 1000, maxAppMatches: 20,
graceMs: 10, textEventIntervalMs: 8, maxClickHoldMs: 7000, maxElements: 64, maxDepth: 8, dragDurationMs: 200, dragSteps: 10,
} satisfies Omit<NativeProvider.Config, 'allowedApps'>
const { helperPath: _helperPath, ...automaticConfig } = config
it('defaults to a generated identity and validates all three signing choices', () => {
expect(NativeProvider.Config({}).sourceSigning).toBe('local-identity')
it('defaults to ad-hoc signing and requires a keychain path for certificate signing', () => {
expect(NativeProvider.Config({}).sourceSigning).toBe('ad-hoc')
expect(NativeProvider.Config({ sourceSigning: 'ad-hoc' }).sourceSigning).toBe('ad-hoc')
expect(NativeProvider.Config({ sourceSigning: 'local-identity' }).sourceSigning).toBe('local-identity')
for (const sourceSigning of [{ identity: 'Apple Development: Example' }, { identity: 'ABC123', keychain: '/keychains/custom.keychain-db' }]) {
for (const sourceSigning of [{ identity: 'Apple Development: Example', keychain: '/keychains/custom.keychain-db' }, { identity: 'ABC123', keychain: '/keychains/custom.keychain-db' }]) {
expect(NativeProvider.Config({ sourceSigning }).sourceSigning).toEqual(sourceSigning)
}
for (const sourceSigning of ['developer-id', {}, { identity: '' }, { identity: ' ' }, { identity: 'Example', keychain: 'relative' }]) {
for (const sourceSigning of ['local-identity', 'developer-id', {}, { identity: 'Example' }, { identity: '' }, { identity: ' ' }, { identity: 'Example', keychain: 'relative' }]) {
expect(() => NativeProvider.Config({ sourceSigning } as never)).toThrow()
}
})
@@ -88,8 +87,8 @@ it('projects shared controls and macOS signing without adding signing to the sha
}
expect(CuaConfig({})).not.toHaveProperty('sourceSigning')
expect(CuaConfig.dict).not.toHaveProperty('sourceSigning')
expect(NativeProvider.Config({ sourceSigning: 'local-identity', timeoutMs: 1234 })).toMatchObject({
agentPreset: 'ptc', sourceSigning: 'local-identity', timeoutMs: 1234,
expect(NativeProvider.Config({ sourceSigning: 'ad-hoc', timeoutMs: 1234 })).toMatchObject({
agentPreset: 'ptc', sourceSigning: 'ad-hoc', timeoutMs: 1234,
})
})
@@ -609,7 +608,7 @@ it('prepares the selected native component before registering CUA and creates no
native.prepare.mockReset().mockResolvedValue(resolve('prepared-helper'))
await NativeProvider.apply(f.ctx, { ...automaticConfig, allowedApps: [] })
expect(native.prepare).toHaveBeenCalledTimes(1)
expect(native.prepare.mock.calls[0]![0].sourceSigning).toBe('local-identity')
expect(native.prepare.mock.calls[0]![0].sourceSigning).toBe('ad-hoc')
expect(native.instances).toHaveLength(0)
await f.execute()
expect(native.instances).toHaveLength(1)
@@ -22,9 +22,6 @@
},
{
"path": "../../util/atomic-write"
},
{
"path": "../../util/home-paths"
}
]
}
-3
View File
@@ -9879,9 +9879,6 @@ importers:
'@deepseek-ai/dsh-cua':
specifier: workspace:*
version: link:../cua
'@deepseek-ai/dsh-home-paths':
specifier: workspace:*
version: link:../../util/home-paths
'@deepseek-ai/schemastery':
specifier: link:../../../vendor/schemastery
version: link:../../../vendor/schemastery
+11
View File
@@ -0,0 +1,11 @@
/** Exercise developer signing tooling without reading or mutating real keychains. */
import { execFileSync } from 'node:child_process'
import { fileURLToPath } from 'node:url'
import { expect, it } from 'vitest'
it.skipIf(process.platform === 'win32')('creates, reuses, and removes developer identities outside DSH home', () => {
const output = execFileSync('python3', [fileURLToPath(new URL('../.agents/skills/dsh-cua-macos-signing/scripts/test_signing_identity.py', import.meta.url))], {
encoding: 'utf8', timeout: 30_000, stdio: ['ignore', 'pipe', 'pipe'],
})
expect(output).toBe('')
})