@agent-network-protocol/dsh-anp 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,201 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright [yyyy] [name of copyright owner]
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
package/README.md CHANGED
@@ -1,3 +1,152 @@
1
- # Temporary Holding Version
1
+ # DSH ANP
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ **ANP discovery and authenticated agent-to-agent calls for DeepSeek Harness (DSH).**
4
+
5
+ English | [简体中文](README.zh-CN.md)
6
+
7
+ [Agent Network Protocol](https://github.com/agent-network-protocol/anp) · [Shared identity](https://github.com/agent-network-protocol/anp-identity) · [Three-protocol example server](https://github.com/AgentConnect/service-agent) · [Apache-2.0](LICENSE)
8
+
9
+ ## Why this exists
10
+
11
+ A personal agent should not need a different account and private key at every service it contacts. DSH ANP lets a DeepSeek Harness agent **reuse a user-created, locally held identity** to discover other agents and call their advertised APIs. The user approves which installed plugin may use that identity and which HTTPS destinations it may contact.
12
+
13
+ This is one part of a larger interoperable identity flow:
14
+
15
+ | Protocol | How the shared identity is used | Client |
16
+ | --- | --- | --- |
17
+ | **ANP** | A published `did:wba` identity authenticates individual HTTP requests using local signing capabilities. No OAuth token is required for this path. | **This plugin** |
18
+ | **MCP** | Public CIMD/JWKS metadata identifies the OAuth client; `private_key_jwt` and `client_credentials` can obtain a policy-limited machine access token. | [DSH MCP integration](https://github.com/AgentConnect/dsh-harness-identity) |
19
+ | **PAP 0.3** | The identity authorizes a Delegation and signed `Poppy-Session`; account access, when supported, is a separate explicit authorization. | [DSH PAP](https://github.com/AgentConnect/dsh-pap) |
20
+
21
+ These are **protocol-specific proofs and permissions**, not interchangeable credentials or a universal bypass of a service's admission policy. Agent private keys stay in local custody, not in this plugin or the destination service.
22
+
23
+ ## What the plugin does
24
+
25
+ 1. `anp_discover({ domain })` reads the public `https://<domain>/.well-known/agent-descriptions` directory, including paginated results.
26
+ 2. `anp_describe({ agentRef })` authenticates the request and reads an advertised agent description, including linked information and interfaces.
27
+ 3. `anp_read({ resourceRef })` authenticates access to a linked resource. JSON OpenAPI 3.x descriptions yield callable operation references.
28
+ 4. `anp_call({ operationRef, path?, query?, body? })` calls an operation declared by the discovered OpenAPI document and signs that exact request.
29
+
30
+ The agent uses opaque references returned by earlier tools; it does **not** invent URLs or transmit private keys. Unsupported interface types can be discovered but are not executed.
31
+
32
+ ## Requirements and installation
33
+
34
+ - Node.js **22.13+**, a DSH Host with Cordis 4.1 and a tools registry.
35
+ - The [ANP Identity](https://github.com/agent-network-protocol/anp-identity) shared-identity service installed in the **same Host**.
36
+ - An existing local identity (`storeId`, `identityId`, `did`), with its public identity material published through an identity provider such as [AWiki](https://awiki.me).
37
+ - Explicit user approval for this plugin and its target HTTPS origins.
38
+
39
+ To install the DSH bundle when the package is available in your package registry:
40
+
41
+ ```sh
42
+ pnpm dsh plugin --profile <profile> add @agent-network-protocol/dsh-anp
43
+ ```
44
+
45
+ Or build and test from source:
46
+
47
+ ```sh
48
+ git clone https://github.com/agent-network-protocol/dsh-anp.git
49
+ cd dsh-anp
50
+ npm ci
51
+ npm run build
52
+ npm test
53
+ ```
54
+
55
+ For a source-pinned Host dependency, install the Git package at a specific full commit SHA; the package's prepare hook builds its exports:
56
+
57
+ ```sh
58
+ npm install '@agent-network-protocol/dsh-anp@git+https://github.com/agent-network-protocol/dsh-anp.git#<full-commit-sha>'
59
+ ```
60
+
61
+ **Installation alone does not create an identity, approve access, or enable signing.** The packaged Cordis bundle starts disabled.
62
+
63
+ ## Configure the DSH Host
64
+
65
+ After setting up the shared identity service, add the installed plugin consumer to ANP Identity's `allowConsumers` and `userConsumers`, and configure its `httpAllowedOrigins` policy. The identity service and the plugin both enforce destination permissions.
66
+
67
+ Example Host plugin row (replace placeholders with your existing published identity):
68
+
69
+ ```yaml
70
+ - id: anp
71
+ name: '@agent-network-protocol/dsh-anp/plugin'
72
+ config:
73
+ enabled: true
74
+ allowedOrigins:
75
+ - 'https://agent.agent-network-protocol.com'
76
+ identityRef:
77
+ storeId: '<existing-store-id>'
78
+ identityId: '<existing-identity-id>'
79
+ did: '<existing-did:wba>'
80
+ ```
81
+
82
+ The identity service must separately authorize this **exact consumer name** and the intended HTTPS origin. On its first signed request, the plugin requests permission through the local identity manager. If approval is pending, approve it in the identity manager and retry. Permanent grants can be reused; one-time grants apply to one operation. Revocation is checked on every dispatch.
83
+
84
+ For an explicitly unrestricted HTTPS destination policy, both the plugin's `allowedOrigins` and ANP Identity's `httpAllowedOrigins['@agent-network-protocol/dsh-anp/plugin']` must contain `'*'`; the consumer must still be user-approved. Prefer exact origins. HTTP, URL credentials, fragments and redirects remain disallowed.
85
+
86
+ ## Try the live ANP service
87
+
88
+ The [Agent Network Protocol service](https://agent.agent-network-protocol.com/ready) offers a three-protocol example with versioned ANP documentation, messages and public posts. Its [website](https://agent-network-protocol.com/) displays published content; [public posts](https://agent-network-protocol.com/zh/posts) can be viewed in a browser.
89
+
90
+ Once identity and permissions are configured, ask the agent to run this **tool sequence**:
91
+
92
+ ```text
93
+ anp_discover({ "domain": "agent.agent-network-protocol.com" })
94
+ anp_describe({ "agentRef": "<ref returned by discover>" })
95
+ anp_read({ "resourceRef": "<OpenAPI interface ref returned by describe>" })
96
+ anp_call({ "operationRef": "<post_create operation ref returned by read>",
97
+ "body": { "content": "Hello from DSH via ANP" } })
98
+ ```
99
+
100
+ The values in angle brackets are **references from previous tool responses**, not literal inputs. You can first use the discovered `protocol_list` / `protocol_get` operation to read versioned documentation, then publish a post with `post_create`. Posting requires the user's approval and a valid signed request. Confirm a successful publication via the [website posts page](https://agent-network-protocol.com/zh/posts).
101
+
102
+ ## Library usage
103
+
104
+ `AnpClient` accepts an `authenticatedDispatch(Request)` supplied by the Host's
105
+ authorized identity. `publicDispatch` defaults to fetch and is used only for the
106
+ public discovery directory. Transport injection supports isolated HTTPS tests;
107
+ it does not provide an alternative signing or private-key storage implementation.
108
+
109
+ ## Service Agent example
110
+
111
+ https://agent-network-protocol.com is the project's example Service Agent for
112
+ MCP, ANP and PAP. The example is mentioned in the Agent-visible discovery/status
113
+ tool descriptions. To see ANP in action, discover `agent-network-protocol.com`,
114
+ read the returned descriptions/interfaces, then invoke a read-only discovered
115
+ operation. For PAP, explicitly discover that Company in settings, connect and
116
+ enable the desired operation. MCP uses a separately configured server connection.
117
+ No reference here grants identity access, enables an operation, bypasses protocol
118
+ validation, or sends a request automatically. The website deployment is managed
119
+ by the service operator and is outside this UI change's acceptance scope.
120
+
121
+ ## Architecture and security
122
+
123
+ ```text
124
+ DSH agent
125
+ | ANP tool request (no credential material)
126
+ v
127
+ dsh-anp / Cordis tools
128
+ | authorized HTTP dispatch
129
+ v
130
+ ANP Identity (same Host) --- local private-key custody
131
+ | signed HTTPS request
132
+ v
133
+ Remote ANP Service Agent --- shared document/message/post APIs
134
+ ```
135
+
136
+ - Public discovery uses a separate, unauthenticated transport; description, linked-resource and business requests use the Host-authorized transport.
137
+ - The identity manager grants only the permitted HTTP operation. It does not hand the model a raw signing key, device assertion or OAuth assertion.
138
+ - Allowed origins are HTTPS-only. References are held in the plugin instance and must be rediscovered after reload.
139
+ - Redirects and invalid discovery cycles fail closed. Requests that may have side effects, including POST, are **not automatically retried**.
140
+ - The `AnpClient` library accepts an injected `authenticatedDispatch(Request)` for Host integration and isolated tests; this is not a separate key store or signing implementation.
141
+
142
+ ## Related projects
143
+
144
+ - [ANP Identity](https://github.com/agent-network-protocol/anp-identity) — native local key custody and the shared DSH identity service (`packages/dsh-anp-identity`).
145
+ - [AWiki User Service](https://github.com/AgentConnect/user-service) — publishes DID, CIMD and JWKS material without receiving the private key.
146
+ - [DSH PAP](https://github.com/AgentConnect/dsh-pap) — PAP 0.3 Client, Delegation/Session and optional account sign-in.
147
+ - [Service Agent](https://github.com/AgentConnect/service-agent) — ANP, MCP and PAP reference server.
148
+ - [DSH Harness Identity](https://github.com/AgentConnect/dsh-harness-identity) — shared-identity-aware DSH Host and MCP OAuth client.
149
+
150
+ ## License
151
+
152
+ [Apache-2.0](LICENSE).
@@ -0,0 +1,111 @@
1
+ # DSH ANP
2
+
3
+ **DeepSeek Harness(DSH)的 ANP 智能体发现与身份认证调用插件。**
4
+
5
+ [English](README.md) | 简体中文
6
+
7
+ [ANP 协议](https://github.com/agent-network-protocol/anp) · [共享身份底座](https://github.com/agent-network-protocol/anp-identity) · [三协议服务端](https://github.com/AgentConnect/service-agent) · [Apache-2.0](LICENSE)
8
+
9
+ ## 解决什么问题
10
+
11
+ Personal Agent 不应该在每一个服务或 Service Agent 上重复创建账户和管理私钥。本插件让 DSH 智能体复用一套**用户创建、私钥保留在本地的身份**,按用户授权发现外部智能体、读取描述并调用业务接口。
12
+
13
+ 共享身份通过三种协议复用,但各自使用不同的认证机制:
14
+
15
+ | 协议 | 身份与授权机制 | 客户端 |
16
+ | --- | --- | --- |
17
+ | **ANP** | 以已发布的 `did:wba` 身份对 HTTP 请求签名,不要求 ANP OAuth Token。 | **本插件** |
18
+ | **MCP** | CIMD/JWKS 公开客户端身份,`private_key_jwt` + `client_credentials` 按服务端策略申请机器 Token。 | [DSH MCP 集成](https://github.com/AgentConnect/dsh-harness-identity) |
19
+ | **PAP 0.3** | 共享身份签发 Delegation,按请求使用 `Poppy-Session`;账户授权是可选且独立的过程。 | [DSH PAP](https://github.com/AgentConnect/dsh-pap) |
20
+
21
+ 复用身份**不等于绕过服务端准入、资源授权和用户操作授权**;三种协议的 Token 和证明材料不能混用。
22
+
23
+ ## 核心能力
24
+
25
+ | 工具 | 功能 |
26
+ | --- | --- |
27
+ | `anp_discover({domain})` | 获取目标域名下的 `/.well-known/agent-descriptions` 及其分页目录 |
28
+ | `anp_describe({agentRef})` | 使用本地授权身份读取 Agent Description、关联资料和接口 |
29
+ | `anp_read({resourceRef})` | 读取已发现的资料和 OpenAPI 3.x,并生成操作引用 |
30
+ | `anp_call({operationRef, path?, query?, body?})` | 调用 OpenAPI 声明的业务接口并对请求签名 |
31
+
32
+ 调用所需的 `agentRef`、`resourceRef`、`operationRef` 必须来自前一步结果;不会把私钥交给模型。其他接口协议可见,但尚不能直接执行。
33
+
34
+ ## 安装
35
+
36
+ 要求 Node.js **22.13+**、具有 Cordis 4.1 和工具注册表的 DSH Host,以及同一 Host 内已安装的 [ANP Identity](https://github.com/agent-network-protocol/anp-identity)。用户须先建立本地身份,并通过 [AWiki 身份服务](https://awiki.me) 等发布 DID 公共材料。
37
+
38
+ 已有可用 npm 包的情况下,安装 DSH Bundle:
39
+
40
+ ```sh
41
+ pnpm dsh plugin --profile <profile> add @agent-network-protocol/dsh-anp
42
+ ```
43
+
44
+ 从源码构建验证:
45
+
46
+ ```sh
47
+ git clone https://github.com/agent-network-protocol/dsh-anp.git
48
+ cd dsh-anp
49
+ npm ci
50
+ npm run build
51
+ npm test
52
+ ```
53
+
54
+ **安装不等于启用。** 插件默认关闭,不会自动创建身份或获得签名授权。
55
+
56
+ ## Host 配置
57
+
58
+ 先在公共身份插件中将 `@agent-network-protocol/dsh-anp/plugin` 加入 `allowConsumers` 与 `userConsumers`,并针对目标域名配置 `httpAllowedOrigins`。之后配置插件:
59
+
60
+ ```yaml
61
+ - id: anp
62
+ name: '@agent-network-protocol/dsh-anp/plugin'
63
+ config:
64
+ enabled: true
65
+ allowedOrigins:
66
+ - 'https://agent.agent-network-protocol.com'
67
+ identityRef:
68
+ storeId: '<existing-store-id>'
69
+ identityId: '<existing-identity-id>'
70
+ did: '<existing-did:wba>'
71
+ ```
72
+
73
+ 上述身份引用必须来自已存在、已发布的本地身份。首次请求会申请本地授权;等待批准时应在身份管理器中批准,再重试。长期授权可复用,一次性授权只覆盖对应操作;撤销后调用会被拒绝。建议只允许明确的 HTTPS Origin。
74
+
75
+ ## 使用线上实例验证
76
+
77
+ 线上示例:[三协议服务健康检查](https://agent.agent-network-protocol.com/ready)、[ANP 官网](https://agent-network-protocol.com/)、[公开帖子](https://agent-network-protocol.com/zh/posts)。
78
+
79
+ 配置好身份后,在 DSH 中依次使用:
80
+
81
+ ```text
82
+ anp_discover({ "domain": "agent.agent-network-protocol.com" })
83
+ anp_describe({ "agentRef": "<上一步返回的引用>" })
84
+ anp_read({ "resourceRef": "<上一步返回的 OpenAPI 引用>" })
85
+ anp_call({ "operationRef": "<post_create 对应的操作引用>",
86
+ "body": { "content": "Hello from DSH via ANP" } })
87
+ ```
88
+
89
+ 尖括号中的值需要从实际工具返回结果中取得,**不能直接照抄**。还可以通过发现的 `protocol_list`、`protocol_get` 操作读取 ANP 指定版本、指定语言的文档。发布成功后,在[官网帖子页面](https://agent-network-protocol.com/zh/posts)查看结果。
90
+
91
+ ## 原理和安全边界
92
+
93
+ ```text
94
+ DSH Agent → dsh-anp 工具 → 同一 Host 的 ANP Identity
95
+ └ 本地私钥签名
96
+ → HTTPS ANP Service Agent
97
+ → 共享的文档 / 留言 / 帖子业务
98
+ ```
99
+
100
+ - 公共目录发现不需要签名;读取描述、资源和业务调用使用授权签名通道。
101
+ - 模型不能获得原始私钥、设备断言或 OAuth 断言;所有签名都经由本地身份底座。
102
+ - 限制目标 HTTPS Origin;HTTP、带凭据 URL、片段和重定向均被拒绝。`'*'` 通配允许所有 HTTPS Origin,必须在插件和身份服务两端显式配置并取得用户批准。
103
+ - 引用存放在当前插件实例中;重启后须重新发现。POST 等副作用请求不会自动重试。
104
+
105
+ ## 相关项目
106
+
107
+ [ANP Identity](https://github.com/agent-network-protocol/anp-identity) · [AWiki User Service](https://github.com/AgentConnect/user-service) · [DSH PAP](https://github.com/AgentConnect/dsh-pap) · [Service Agent](https://github.com/AgentConnect/service-agent) · [DSH Harness Identity](https://github.com/AgentConnect/dsh-harness-identity)
108
+
109
+ ## 许可
110
+
111
+ [Apache-2.0](LICENSE)。
@@ -0,0 +1,6 @@
1
+ # Configure an existing identity and approved HTTPS origins before enabling.
2
+ - insert:
3
+ - id: anp
4
+ name: '@agent-network-protocol/dsh-anp/plugin'
5
+ config:
6
+ enabled: false
package/lib/index.d.ts ADDED
@@ -0,0 +1,64 @@
1
+ type Json = Record<string, unknown>;
2
+ export type Dispatch = (request: Request) => Promise<Response>;
3
+ export interface AnpClientOptions {
4
+ /** Exact HTTPS origins, or an explicit '*' to allow any HTTPS origin. */
5
+ allowedOrigins: string[];
6
+ authenticatedDispatch: Dispatch;
7
+ publicDispatch?: Dispatch;
8
+ }
9
+ export declare class AnpError extends Error {
10
+ readonly code: string;
11
+ constructor(code: string);
12
+ }
13
+ /** Public references contain no signing capabilities. The Host owns the dispatcher. */
14
+ export declare class AnpClient {
15
+ private readonly options;
16
+ private readonly origins;
17
+ private readonly resources;
18
+ private readonly operations;
19
+ constructor(options: AnpClientOptions);
20
+ private target;
21
+ private add;
22
+ private request;
23
+ discover(domain: string): Promise<{
24
+ count: number;
25
+ complete: boolean;
26
+ agents: {
27
+ agentRef: string;
28
+ name: string;
29
+ url: string;
30
+ }[];
31
+ }>;
32
+ describe(agentRef: string): Promise<{
33
+ links: {
34
+ resourceRef: string;
35
+ kind: string;
36
+ description?: unknown;
37
+ protocol?: string;
38
+ }[];
39
+ url: string;
40
+ contentType: string;
41
+ content: unknown;
42
+ }>;
43
+ read(resourceRef: string): Promise<{
44
+ operations: {
45
+ operationRef: string;
46
+ operationId: string;
47
+ method: string;
48
+ url: string;
49
+ }[];
50
+ url: string;
51
+ contentType: string;
52
+ content: unknown;
53
+ }>;
54
+ call(operationRef: string, args?: {
55
+ path?: Json;
56
+ query?: Json;
57
+ body?: unknown;
58
+ }): Promise<{
59
+ url: string;
60
+ contentType: string;
61
+ content: unknown;
62
+ }>;
63
+ }
64
+ export {};
package/lib/index.js ADDED
@@ -0,0 +1,243 @@
1
+ import { randomUUID } from "node:crypto";
2
+ export class AnpError extends Error {
3
+ code;
4
+ constructor(code) {
5
+ super(code);
6
+ this.code = code;
7
+ }
8
+ }
9
+ const requireValue = (condition, code = "invalid_document") => {
10
+ if (!condition)
11
+ throw new AnpError(code);
12
+ };
13
+ const object = (value) => {
14
+ requireValue(value !== null && typeof value === "object" && !Array.isArray(value));
15
+ return value;
16
+ };
17
+ /** Public references contain no signing capabilities. The Host owns the dispatcher. */
18
+ export class AnpClient {
19
+ options;
20
+ origins;
21
+ resources = new Map();
22
+ operations = new Map();
23
+ constructor(options) {
24
+ this.options = options;
25
+ this.origins = new Set(options.allowedOrigins.map((value) => {
26
+ if (value === "*")
27
+ return value;
28
+ const url = new URL(value);
29
+ requireValue(url.protocol === "https:" && url.origin === value, "invalid_origin");
30
+ return url.origin;
31
+ }));
32
+ }
33
+ target(value, base) {
34
+ const url = new URL(value, base);
35
+ requireValue(url.protocol === "https:" &&
36
+ !url.username &&
37
+ !url.password &&
38
+ !url.hash &&
39
+ (this.origins.has("*") || this.origins.has(url.origin)), "origin_not_approved");
40
+ return url.href;
41
+ }
42
+ add(resource) {
43
+ const entry = [...this.resources].find(([, value]) => value.url === resource.url && value.kind === resource.kind);
44
+ if (entry)
45
+ return entry[0];
46
+ const ref = randomUUID();
47
+ this.resources.set(ref, resource);
48
+ return ref;
49
+ }
50
+ async request(url, init = {}, signed = true) {
51
+ const request = new Request(this.target(url), {
52
+ ...init,
53
+ redirect: "manual",
54
+ signal: AbortSignal.timeout(10000),
55
+ credentials: "omit",
56
+ });
57
+ const response = await (signed
58
+ ? this.options.authenticatedDispatch
59
+ : (this.options.publicDispatch ?? fetch))(request);
60
+ requireValue(response.ok && !response.redirected, "http_request_failed");
61
+ const reader = response.body?.getReader();
62
+ const chunks = [];
63
+ let size = 0;
64
+ if (reader) {
65
+ try {
66
+ while (true) {
67
+ const part = await reader.read();
68
+ if (part.done)
69
+ break;
70
+ size += part.value.length;
71
+ if (size > 4 * 1024 * 1024) {
72
+ await reader.cancel();
73
+ throw new AnpError("response_too_large");
74
+ }
75
+ chunks.push(part.value);
76
+ }
77
+ }
78
+ finally {
79
+ reader.releaseLock();
80
+ }
81
+ }
82
+ const contentType = response.headers.get("content-type") ?? "text/plain";
83
+ const text = Buffer.concat(chunks).toString("utf8");
84
+ return {
85
+ url,
86
+ contentType,
87
+ content: /json/i.test(contentType) ? JSON.parse(text) : text,
88
+ };
89
+ }
90
+ async discover(domain) {
91
+ requireValue(typeof domain === "string" && !!domain && !/[/?#@\\]/.test(domain), "invalid_domain");
92
+ let url = this.target(`https://${domain}/.well-known/agent-descriptions`);
93
+ const visited = new Set();
94
+ const agents = new Map();
95
+ while (url) {
96
+ requireValue(!visited.has(url) && visited.size < 64, "invalid_pagination");
97
+ visited.add(url);
98
+ const page = object((await this.request(url, {}, false)).content);
99
+ requireValue(page["@type"] === "CollectionPage" && Array.isArray(page.items));
100
+ for (const value of page.items) {
101
+ const item = object(value);
102
+ requireValue(typeof item["@id"] === "string" && typeof item.name === "string");
103
+ const target = this.target(item["@id"], url);
104
+ agents.set(target, {
105
+ agentRef: this.add({ url: target, kind: "agent" }),
106
+ name: item.name,
107
+ url: target,
108
+ });
109
+ }
110
+ requireValue(page.next === undefined || typeof page.next === "string");
111
+ url =
112
+ page.next === undefined
113
+ ? undefined
114
+ : this.target(page.next, url);
115
+ }
116
+ return { count: agents.size, complete: true, agents: [...agents.values()] };
117
+ }
118
+ async describe(agentRef) {
119
+ const resource = this.resources.get(agentRef);
120
+ requireValue(resource?.kind === "agent", "unknown_agent");
121
+ const result = await this.request(resource.url);
122
+ const document = object(result.content);
123
+ requireValue(document.protocolType === "ANP" && document.type === "AgentDescription");
124
+ const links = [];
125
+ for (const [field, kind] of [
126
+ ["Infomations", "information"],
127
+ ["interfaces", "interface"],
128
+ ]) {
129
+ requireValue(document[field] === undefined || Array.isArray(document[field]));
130
+ for (const value of document[field] ?? []) {
131
+ const link = object(value);
132
+ if (typeof link.url !== "string")
133
+ continue;
134
+ const target = this.target(link.url, resource.url);
135
+ const protocol = typeof link.protocol === "string" ? link.protocol : undefined;
136
+ links.push({
137
+ resourceRef: this.add({ url: target, kind, protocol }),
138
+ kind,
139
+ ...(link.description === undefined ? {} : { description: link.description }),
140
+ ...(protocol === undefined ? {} : { protocol }),
141
+ });
142
+ }
143
+ }
144
+ return { ...result, links };
145
+ }
146
+ async read(resourceRef) {
147
+ const resource = this.resources.get(resourceRef);
148
+ requireValue(resource && resource.kind !== "agent", "unknown_resource");
149
+ const result = await this.request(resource.url);
150
+ const operations = [];
151
+ if (resource.kind === "interface" &&
152
+ /openapi/i.test(resource.protocol ?? "")) {
153
+ const spec = object(result.content);
154
+ requireValue(typeof spec.openapi === "string" && spec.openapi.startsWith("3."));
155
+ const servers = spec.servers === undefined
156
+ ? [{ url: new URL(resource.url).origin }]
157
+ : spec.servers;
158
+ requireValue(Array.isArray(servers) && servers.length > 0);
159
+ const base = this.target(String(object(servers[0]).url), resource.url).replace(/\/$/, "");
160
+ for (const [path, value] of Object.entries(object(spec.paths))) {
161
+ requireValue(path.startsWith("/") &&
162
+ !path.startsWith("//") &&
163
+ !/[?#\\]/.test(path));
164
+ const item = object(value);
165
+ requireValue(item.servers === undefined && item.$ref === undefined, "unsupported_openapi");
166
+ for (const method of [
167
+ "get",
168
+ "post",
169
+ "put",
170
+ "patch",
171
+ "delete",
172
+ "head",
173
+ "options",
174
+ ]) {
175
+ if (item[method] === undefined)
176
+ continue;
177
+ const operation = object(item[method]);
178
+ requireValue(operation.servers === undefined && operation.$ref === undefined, "unsupported_openapi");
179
+ const parameters = [
180
+ ...(item.parameters ?? []),
181
+ ...(operation.parameters ?? []),
182
+ ].map(object);
183
+ requireValue(parameters.every((p) => p.$ref === undefined &&
184
+ ["path", "query"].includes(String(p.in))), "unsupported_openapi");
185
+ const operationRef = randomUUID();
186
+ const target = this.target(base + path);
187
+ this.operations.set(operationRef, {
188
+ url: target,
189
+ method: method.toUpperCase(),
190
+ parameters,
191
+ hasBody: operation.requestBody !== undefined,
192
+ });
193
+ operations.push({
194
+ operationRef,
195
+ operationId: String(operation.operationId ?? `${method} ${path}`),
196
+ method: method.toUpperCase(),
197
+ url: target,
198
+ });
199
+ }
200
+ }
201
+ }
202
+ return { ...result, operations };
203
+ }
204
+ async call(operationRef, args = {}) {
205
+ const operation = this.operations.get(operationRef);
206
+ requireValue(operation, "unknown_operation");
207
+ let target = operation.url;
208
+ for (const [location, supplied] of [
209
+ ["path", args.path ?? {}],
210
+ ["query", args.query ?? {}],
211
+ ]) {
212
+ for (const key of Object.keys(supplied))
213
+ requireValue(operation.parameters.some((p) => p.in === location && p.name === key), "unknown_parameter");
214
+ for (const parameter of operation.parameters.filter((p) => p.in === location)) {
215
+ const value = supplied[String(parameter.name)];
216
+ requireValue(value !== undefined || parameter.required !== true, "missing_parameter");
217
+ if (value === undefined)
218
+ continue;
219
+ requireValue(["string", "number", "boolean"].includes(typeof value), "invalid_parameter");
220
+ if (location === "path") {
221
+ requireValue(value !== "." && value !== "..", "invalid_parameter");
222
+ target = target
223
+ .replace(`{${parameter.name}}`, encodeURIComponent(String(value)))
224
+ .replace(`%7B${parameter.name}%7D`, encodeURIComponent(String(value)));
225
+ }
226
+ }
227
+ }
228
+ requireValue(!/[{}]/.test(decodeURI(target)), "missing_parameter");
229
+ const url = new URL(target);
230
+ for (const [key, value] of Object.entries(args.query ?? {}))
231
+ url.searchParams.set(key, String(value));
232
+ requireValue(args.body === undefined || operation.hasBody, "unexpected_body");
233
+ return this.request(url.href, {
234
+ method: operation.method,
235
+ ...(args.body === undefined
236
+ ? {}
237
+ : {
238
+ headers: { "content-type": "application/json" },
239
+ body: JSON.stringify(args.body),
240
+ }),
241
+ });
242
+ }
243
+ }
@@ -0,0 +1,14 @@
1
+ import type { Context } from "@deepseek-ai/cordis";
2
+ export declare const name = "@agent-network-protocol/dsh-anp/plugin";
3
+ export declare const inject: string[];
4
+ export interface Config {
5
+ enabled?: boolean;
6
+ allowedOrigins: string[];
7
+ identityRef: {
8
+ storeId: string;
9
+ identityId: string;
10
+ did: string;
11
+ };
12
+ grantId?: string;
13
+ }
14
+ export declare function apply(ctx: Context, config: Config): void;
package/lib/plugin.js ADDED
@@ -0,0 +1,104 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { AnpClient, AnpError } from "./index.js";
3
+ export const name = "@agent-network-protocol/dsh-anp/plugin";
4
+ export const inject = ["anpIdentity", "tools"];
5
+ const identityFailures = {
6
+ provider_incompatible: "identity_provider_incompatible",
7
+ consumer_forbidden: "identity_consumer_forbidden",
8
+ operation_not_permitted: "identity_operation_not_permitted",
9
+ grant_inactive: "identity_authorization_unavailable",
10
+ grant_conflict: "identity_authorization_unavailable",
11
+ };
12
+ function failureCode(error) {
13
+ if (error instanceof AnpError)
14
+ return error.code;
15
+ if (error && typeof error === "object" && "code" in error && typeof error.code === "string") {
16
+ return Object.hasOwn(identityFailures, error.code) ? identityFailures[error.code] : "anp_request_failed";
17
+ }
18
+ return "anp_request_failed";
19
+ }
20
+ export function apply(ctx, config) {
21
+ if (!config.enabled)
22
+ return;
23
+ const identity = ctx.get("anpIdentity").bindUserConsumer(ctx);
24
+ let grantId = config.grantId;
25
+ const pending = new Map();
26
+ const client = new AnpClient({
27
+ allowedOrigins: config.allowedOrigins,
28
+ authenticatedDispatch: async (request) => {
29
+ if (!grantId) {
30
+ const key = `${request.method} ${request.url} ${await request.clone().text()}`;
31
+ const old = pending.get(key);
32
+ const access = old
33
+ ? await identity.getAccessRequest(old)
34
+ : await identity.requestAccess({
35
+ requestId: `anp-${randomUUID()}`,
36
+ identity: config.identityRef,
37
+ purpose: `ANP ${request.method} ${new URL(request.url).origin}`,
38
+ operation: { action: "http", request },
39
+ });
40
+ if (access.status !== "approved" || !access.grantId) {
41
+ if (access.status === "pending")
42
+ pending.set(key, access.id);
43
+ else
44
+ pending.delete(key);
45
+ throw new AnpError("identity_authorization_required");
46
+ }
47
+ pending.delete(key);
48
+ if (access.mode === "permanent")
49
+ grantId = access.grantId;
50
+ const authorized = await identity.openAuthorizedIdentity(access.grantId);
51
+ return authorized.authenticatedHttp.dispatch(request);
52
+ }
53
+ return (await identity.openAuthorizedIdentity(grantId)).authenticatedHttp.dispatch(request);
54
+ },
55
+ });
56
+ let stopped = false;
57
+ ctx.effect(() => () => {
58
+ stopped = true;
59
+ pending.clear();
60
+ });
61
+ const tools = ctx.get("tools");
62
+ const register = (toolName, properties, required, run) => tools.register({
63
+ name: toolName,
64
+ description: `ANP ${toolName.slice(4)} using discovered references and the locally authorized identity.` + (toolName === "anp_discover" ? " Example Service Agent: https://agent-network-protocol.com supports MCP, ANP and PAP. To demonstrate ANP, use anp_discover with domain agent-network-protocol.com, then read discovered descriptions/interfaces and invoke a read-only example. For PAP, ask the user to discover this Company and enable an operation in PAP settings first. MCP requires a configured server connection. Keep all identity approvals and user permissions." : ""),
65
+ parameters: {
66
+ type: "object",
67
+ properties,
68
+ required,
69
+ additionalProperties: false,
70
+ },
71
+ output: {
72
+ schema: { type: "object", additionalProperties: true },
73
+ render: (_args, value) => [
74
+ { type: "text", text: JSON.stringify(value) },
75
+ ],
76
+ },
77
+ execute: async (args, execution) => {
78
+ if (stopped || !execution.agent || execution.signal?.aborted)
79
+ return { ok: false, error: "unavailable" };
80
+ try {
81
+ const value = await run(args);
82
+ return stopped
83
+ ? { ok: false, error: "unavailable" }
84
+ : { ok: true, value };
85
+ }
86
+ catch (error) {
87
+ return {
88
+ ok: false,
89
+ error: failureCode(error),
90
+ };
91
+ }
92
+ },
93
+ });
94
+ const string = { type: "string" };
95
+ register("anp_discover", { domain: string }, ["domain"], (args) => client.discover(String(args.domain)));
96
+ register("anp_describe", { agentRef: string }, ["agentRef"], (args) => client.describe(String(args.agentRef)));
97
+ register("anp_read", { resourceRef: string }, ["resourceRef"], (args) => client.read(String(args.resourceRef)));
98
+ register("anp_call", {
99
+ operationRef: string,
100
+ path: { type: "object" },
101
+ query: { type: "object" },
102
+ body: {},
103
+ }, ["operationRef"], (args) => client.call(String(args.operationRef), args));
104
+ }
package/package.json CHANGED
@@ -1,6 +1,57 @@
1
1
  {
2
2
  "name": "@agent-network-protocol/dsh-anp",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.1",
4
+ "type": "module",
5
+ "description": "ANP discovery and authenticated interaction for DeepSeek Harness",
6
+ "license": "Apache-2.0",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/agent-network-protocol/dsh-anp.git"
10
+ },
11
+ "main": "lib/index.js",
12
+ "types": "lib/index.d.ts",
13
+ "exports": {
14
+ ".": "./lib/index.js",
15
+ "./plugin": "./lib/plugin.js",
16
+ "./cordis.patch.yml": "./cordis.patch.yml"
17
+ },
18
+ "files": [
19
+ "lib",
20
+ "README.md",
21
+ "LICENSE",
22
+ "cordis.patch.yml",
23
+ "README.zh-CN.md"
24
+ ],
25
+ "engines": {
26
+ "node": ">=22.13.0"
27
+ },
28
+ "scripts": {
29
+ "build": "tsc -p tsconfig.json",
30
+ "typecheck": "tsc --noEmit -p tsconfig.json",
31
+ "test": "npm run build && tsx --test tests/*.test.ts",
32
+ "prepare": "npm run build",
33
+ "prepack": "npm run build"
34
+ },
35
+ "dependencies": {
36
+ "@deepseek-ai/cordis": "4.1.0"
37
+ },
38
+ "devDependencies": {
39
+ "typescript": "6.0.3",
40
+ "tsx": "4.20.6",
41
+ "@types/node": "24.3.0"
42
+ },
43
+ "publishConfig": {
44
+ "access": "public",
45
+ "registry": "https://registry.npmjs.org"
46
+ },
47
+ "keywords": [
48
+ "anp",
49
+ "deepseek-harness",
50
+ "dsh-plugin"
51
+ ],
52
+ "dsh": {
53
+ "bundle": {
54
+ "patch": "./cordis.patch.yml"
55
+ }
56
+ }
57
+ }