@deepseek-ai/dsh-webhook 0.1.2-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 DeepSeek
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,6 @@
1
+ # Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
2
+ # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
+ # after editing either side, bring the other along and re-record with:
4
+ # pnpm run verify-translation-pairing --write packages/webhook/webhook/README.md
5
+ README.md: e19bb7edc26857312c9249294bcfab23942075c1
6
+ README.zh.md: 39d17eb6e2d1b20e7f05b811d42804c7b3aa435c
package/README.md ADDED
@@ -0,0 +1,86 @@
1
+ ---
2
+ description: "Webhook rule runtime for maintainers registering trusted external-event policies that create Workspace Sessions."
3
+ kind: "package-reference"
4
+ ---
5
+
6
+ # @deepseek-ai/dsh-webhook
7
+
8
+ English | [中文](README.zh.md)
9
+
10
+ ## Summary
11
+
12
+ `dsh-webhook` provides the Host `ctx.webhookRuntime`: a registry for trusted programmatic webhook rules plus the one built-in action, creating an ordinary root Session inside a Web Workspace. The interface stays at `register(rule)` and `dispatch(delivery)`; provider authentication belongs to adapter packages. Use it when a trusted rule must turn an external event into a new agent Session.
13
+
14
+ ## Table of Contents
15
+
16
+ - [Rule interface](#rule-interface)
17
+ - [Session request](#session-request)
18
+ - [Composition](#composition)
19
+ - [Model Experience](#model-experience)
20
+ - [Known Limitations and Deferred Work](#known-limitations-and-deferred-work)
21
+ - [Dev Note](#dev-note)
22
+
23
+ -----
24
+
25
+ <a id="rule-interface"></a>
26
+ ## Rule interface
27
+
28
+ `WebhookRule<K>` has a branded unique `id`, a provider `kind`, and `run(delivery, signal)`. A callback may execute arbitrary trusted code and returns either `null` or one `WebhookSessionRequest`. Rules of the same kind start independently, and one throw or rejection is logged without starving siblings.
29
+
30
+ `VerifiedWebhookDelivery` carries provider kind, configured source id, provider delivery id, normalized lossless JSON, and receipt time. The runtime snapshots and freezes the complete value before sharing it. `deliveryId` is provenance only; repeated delivery runs the rules again.
31
+
32
+ Registration is an effect. Its awaitable disposer first hides the rule, then aborts and drains active callbacks. Callbacks must observe the supplied signal; same-process code that ignores cancellation cannot be forcibly stopped safely.
33
+
34
+ <a id="session-request"></a>
35
+ ## Session request
36
+
37
+ `WebhookSessionRequest` requires `workspacePath`, `title`, `prompt`, `agentPreset`, and `permissionPreset`; optional `model` names an explicit provider/model route plus an output-token cap. An explicit route uses its adapter's reasoning default. Omission snapshots the complete current deployment selection, including reasoning effort, until the first request records its durable header; later Web model changes retain the ordinary session behavior.
38
+
39
+ The runtime validates presets before mutation, resolves or creates the canonical Workspace, creates an Agent with that Workspace path as `SessionHeader.cwd`, mounts the agent preset before publication, and attaches the Session before applying permissions, title, and prompt. Failed attachment disposes the unpublished action. A later pre-prompt failure detaches the Workspace and disposes the Agent on a best-effort rollback.
40
+
41
+ Successful `Agent.followup()` is the webhook operation's commit point. The message uses `source.kind: "webhook"` with provider, source, delivery, and rule provenance. The runtime does not wait for idle, flush specially, inspect the reply, or publish completion state; ordinary Agent and Session behavior owns everything afterward.
42
+
43
+ <a id="composition"></a>
44
+ ## Composition
45
+
46
+ Load the runtime on the Web Host plane after Agents, model defaults, agent presets, permission presets, titles, and the Workspace registry. User-authored rule plugins inject `webhookRuntime` and yield the disposer returned by `register()` through their own effect.
47
+
48
+ The [GitHub review guide](../../../docs/user/guide/github-review.md) shows a rule module, dedicated ingress port, secret setup, and Workspace routing.
49
+
50
+ <a id="model-experience"></a>
51
+ ## Model Experience
52
+
53
+ ### Rule-authored initial prompt
54
+
55
+ #### What the model sees
56
+
57
+ For each matching rule, the model sees exactly the non-empty text returned as `WebhookSessionRequest.prompt`. The generic runtime adds no private framing; a rule incorporating external text owns its trust labeling. The shipped GitHub example labels selected PR fields as untrusted JSON metadata.
58
+
59
+ #### Token effect
60
+
61
+ One data-dependent user-role message is retained in the new Session and contributes tokens until ordinary compaction replaces or removes that history.
62
+
63
+ #### KV Cache effect
64
+
65
+ The initial prompt begins a new Session, so it establishes rather than invalidates that Session's reusable request prefix.
66
+
67
+ ## Known Limitations and Deferred Work
68
+
69
+ <a id="known-limitations-and-deferred-work"></a>
70
+
71
+ - **Process-local fire-and-forget only** — a crash loses rule calls that have not admitted a prompt; there is no queue, replay, or retry.
72
+ - **No built-in deduplication** — repeated provider deliveries may create repeated Sessions; rules that need idempotency own it.
73
+ - **No completion result** — HTTP acceptance and rule settlement do not report Agent success, idle, or output.
74
+ - **Trusted callbacks must cooperate with cancellation** — runtime teardown aborts and awaits them but cannot terminate arbitrary same-process code.
75
+ - **Workspace creation may outlive a failed Session attempt** — an empty Workspace is retained because another concurrent caller may already use it.
76
+
77
+
78
+ <a id="dev-note"></a>
79
+ ### Dev Note
80
+
81
+ <details>
82
+ <summary>Working context for maintainers — click to expand</summary>
83
+
84
+ None.
85
+
86
+ </details>
package/README.zh.md ADDED
@@ -0,0 +1,86 @@
1
+ ---
2
+ description: "面向注册可信外部事件策略并创建 Workspace Session 的维护者,说明 webhook 规则运行时。"
3
+ kind: "package-reference"
4
+ ---
5
+
6
+ # @deepseek-ai/dsh-webhook
7
+
8
+ [English](README.md) | 中文
9
+
10
+ ## 概述
11
+
12
+ `dsh-webhook` 提供 Host 侧的 `ctx.webhookRuntime`:它既是受信任程序化 webhook 规则的注册表,也拥有唯一内置动作——在 Web Workspace 中创建普通根 Session。接口只包含 `register(rule)` 和 `dispatch(delivery)`;提供方身份验证属于适配器包。当受信任规则必须把外部事件变成新的 agent Session 时,请使用它。
13
+
14
+ ## 目录
15
+
16
+ - [规则接口](#rule-interface)
17
+ - [Session 请求](#session-request)
18
+ - [组合](#composition)
19
+ - [Model Experience](#model-experience)
20
+ - [Known Limitations and Deferred Work](#known-limitations-and-deferred-work)
21
+ - [开发备注](#dev-note)
22
+
23
+ -----
24
+
25
+ <a id="rule-interface"></a>
26
+ ## 规则接口
27
+
28
+ `WebhookRule<K>` 具有带品牌类型的唯一 `id`、提供方 `kind` 与 `run(delivery, signal)`。回调可以执行任意受信任代码,并返回 `null` 或一个 `WebhookSessionRequest`。同类规则彼此独立启动;某个规则抛出或拒绝只会记录日志,不会阻止同级规则。
29
+
30
+ `VerifiedWebhookDelivery` 携带提供方种类、已配置来源 id、提供方交付 id、规范化的无损 JSON 与接收时间。runtime 会在共享前快照并冻结完整值。`deliveryId` 仅是来源信息;重复交付会再次运行规则。
31
+
32
+ 注册是一项 effect。它的可等待 disposer 会先隐藏规则,再中止并排空活动回调。回调必须观察所提供的 signal;忽略取消的同进程代码无法被安全强制停止。
33
+
34
+ <a id="session-request"></a>
35
+ ## Session 请求
36
+
37
+ `WebhookSessionRequest` 要求 `workspacePath`、`title`、`prompt`、`agentPreset` 与 `permissionPreset`;可选 `model` 会指定明确的提供方/模型路由与输出 token 上限。明确路由使用其适配器的默认推理强度。省略时会快照包含推理强度的完整当前部署选择,直到首个请求记录持久 header;之后的 Web 模型变更保留普通 Session 行为。
38
+
39
+ runtime 会在变更状态前验证 preset,解析或创建规范 Workspace,以该 Workspace 路径作为 `SessionHeader.cwd` 创建 Agent,在发布前挂载 agent preset,并在应用权限、标题与提示词前附加 Session。附加失败会释放尚未提交动作的 Agent。之后若在提示词前失败,则以尽力而为方式脱离 Workspace 并释放 Agent。
40
+
41
+ 成功的 `Agent.followup()` 是 webhook 操作的提交点。消息使用 `source.kind: "webhook"`,并携带提供方、来源、交付与规则来源信息。runtime 不等待 idle、不执行特殊 flush、不检查回复,也不发布完成状态;之后完全由普通 Agent 与 Session 行为接管。
42
+
43
+ <a id="composition"></a>
44
+ ## 组合
45
+
46
+ 在 Web Host plane 上,于 Agents、模型默认值、agent presets、permission presets、标题与 Workspace 注册表之后加载 runtime。用户编写的规则插件注入 `webhookRuntime`,并通过自己的 effect 交出 `register()` 返回的 disposer。
47
+
48
+ [GitHub 评审指南](../../../docs/user/guide/github-review.zh.md)展示了规则模块、专用入口端口、密钥设置与 Workspace 路由。
49
+
50
+ <a id="model-experience"></a>
51
+ ## Model Experience
52
+
53
+ ### 规则编写的初始提示词
54
+
55
+ #### What the model sees
56
+
57
+ 每个匹配规则都会让模型看到 `WebhookSessionRequest.prompt` 返回的非空文本原文。通用 runtime 不增加私有框架;若规则包含外部文本,则由规则负责标明其信任属性。随附 GitHub 示例会把选定 PR 字段标为不受信任的 JSON 元数据。
58
+
59
+ #### Token effect
60
+
61
+ 一条依赖数据的 user-role 消息保留在新 Session 中,并持续贡献 token,直到普通 compaction 替换或移除该历史。
62
+
63
+ #### KV Cache effect
64
+
65
+ 初始提示词开启一个新 Session,因此它建立而不是使该 Session 的可复用请求前缀失效。
66
+
67
+ ## Known Limitations and Deferred Work
68
+
69
+ <a id="known-limitations-and-deferred-work"></a>
70
+
71
+ - **仅限进程内 fire-and-forget** — 崩溃会丢失尚未接纳提示词的规则调用;不存在队列、重放或重试。
72
+ - **无内置去重** — 提供方重复交付可能创建重复 Session;需要幂等性的规则自行负责。
73
+ - **无完成结果** — HTTP 接受与规则结算都不报告 Agent 成功、idle 或输出。
74
+ - **受信任回调必须配合取消** — runtime teardown 会中止并等待回调,但无法终止任意同进程代码。
75
+ - **Workspace 创建可能比失败的 Session 尝试更长寿** — 空 Workspace 会保留,因为另一个并发调用者可能已经使用它。
76
+
77
+
78
+ <a id="dev-note"></a>
79
+ ### 开发备注
80
+
81
+ <details>
82
+ <summary>维护者工作上下文——点击展开</summary>
83
+
84
+ 无。
85
+
86
+ </details>
package/lib/index.js ADDED
@@ -0,0 +1,284 @@
1
+ import { Service } from "@deepseek-ai/cordis";
2
+ import { boundContextSummary, createUserMessage, errorChain } from "@deepseek-ai/dsh-llm";
3
+ import { deepFreeze, snapshotJsonValue } from "@deepseek-ai/dsh-util-values";
4
+ import { randomUUID } from "node:crypto";
5
+ import { isAbsolute } from "node:path";
6
+ import { brandString } from "@deepseek-ai/dsh-brand";
7
+ //#region lib/types/session.js
8
+ /** Workspace-backed Session creation for one settled webhook rule result. */
9
+ /** Require one non-empty string field from an untyped rule result. */
10
+ function requiredString(record, field) {
11
+ const value = record[field];
12
+ if (typeof value !== "string" || value.trim() === "") throw new TypeError(`webhook Session request ${field} must be a non-empty string`);
13
+ return value;
14
+ }
15
+ /** Snapshot and validate a same-process rule result before crossing awaits. */
16
+ function resolveRequest(ctx, input) {
17
+ const candidate = input;
18
+ if (candidate === null || typeof candidate !== "object" || Array.isArray(candidate)) throw new TypeError("webhook rule result must be null or a Session request object");
19
+ const record = candidate;
20
+ const workspacePath = requiredString(record, "workspacePath");
21
+ if (!isAbsolute(workspacePath)) throw new TypeError(`webhook Session request workspacePath must be absolute, got ${JSON.stringify(workspacePath)}`);
22
+ const title = requiredString(record, "title");
23
+ const prompt = requiredString(record, "prompt");
24
+ const agentPreset = requiredString(record, "agentPreset");
25
+ const permissionPreset = requiredString(record, "permissionPreset");
26
+ const model = record["model"];
27
+ if (model !== void 0 && (model === null || typeof model !== "object" || Array.isArray(model))) throw new TypeError("webhook Session request model must be an object");
28
+ let agentOptions;
29
+ let modelSelection;
30
+ if (model === void 0) {
31
+ const selected = ctx.agentDefaultModel.currentSelection();
32
+ agentOptions = {
33
+ provider: selected.provider,
34
+ model: selected.model
35
+ };
36
+ modelSelection = { ...selected };
37
+ } else {
38
+ const modelRecord = model;
39
+ const provider = requiredString(modelRecord, "provider");
40
+ const modelId = requiredString(modelRecord, "model");
41
+ const maxTokens = modelRecord["maxTokens"];
42
+ if (maxTokens !== void 0 && (typeof maxTokens !== "number" || !Number.isSafeInteger(maxTokens) || maxTokens <= 0)) throw new TypeError("webhook Session request model.maxTokens must be a positive safe integer");
43
+ agentOptions = {
44
+ provider,
45
+ model: modelId,
46
+ ...maxTokens === void 0 ? {} : { maxTokens }
47
+ };
48
+ modelSelection = {
49
+ provider,
50
+ model: modelId
51
+ };
52
+ }
53
+ return {
54
+ workspacePath,
55
+ title,
56
+ prompt,
57
+ agentPreset,
58
+ permissionPreset,
59
+ modelSelection,
60
+ agentOptions
61
+ };
62
+ }
63
+ /** Log a rollback failure without replacing the operation's original failure. */
64
+ function reportRollbackFailure(ctx, subject, error) {
65
+ ctx.logger.warn(`webhook: ${subject} rollback failed: ${errorChain(error)}`);
66
+ }
67
+ /** Apply the creation-time selection until its first durable request header exists. */
68
+ function installInitialModelSelection(agentCtx, selection) {
69
+ agentCtx.on("agent/request", async (_payload, next) => {
70
+ const resolved = await next();
71
+ const agent = agentCtx.agent;
72
+ /* v8 ignore next -- AgentRegistry setup always provides the unpublished scoped Agent. */
73
+ if (agent === void 0) throw new Error("webhook Session setup has no scoped Agent");
74
+ if (agent.session.requestHeader() !== void 0 || resolved.provider !== selection.provider || resolved.model !== selection.model) return resolved;
75
+ const { reasoningEffort: _inheritedEffort, ...withoutInheritedEffort } = resolved;
76
+ return {
77
+ ...withoutInheritedEffort,
78
+ ...selection.reasoningEffort === void 0 ? {} : { reasoningEffort: selection.reasoningEffort }
79
+ };
80
+ });
81
+ }
82
+ /**
83
+ * Create, attach, title, configure, and prompt one ordinary root Session.
84
+ * Successful prompt admission ends webhook ownership of the operation; the
85
+ * Agent remains lifecycle-owned by `ctx` and follows normal Session behavior.
86
+ *
87
+ * @param ctx - untraced runtime context that owns the resulting Agent.
88
+ * @param delivery - exact verified provider delivery used for provenance.
89
+ * @param ruleId - rule that returned the request.
90
+ * @param request - same-process rule result.
91
+ * @param signal - registration lifetime cancellation through publication.
92
+ */
93
+ async function createWebhookSession(ctx, delivery, ruleId, request, signal) {
94
+ const resolved = resolveRequest(ctx, request);
95
+ ctx.permissionPresets.resolve(resolved.permissionPreset);
96
+ const preset = await ctx.agentPresets.resolve(resolved.agentPreset);
97
+ await ctx.agentPresets.standingKeyFor(preset.id);
98
+ signal.throwIfAborted();
99
+ const workspace = await ctx.workspaceRegistry.create(resolved.workspacePath);
100
+ signal.throwIfAborted();
101
+ const sessionId = brandString(`webhook-${randomUUID()}`);
102
+ const handle = await ctx.agents.create({
103
+ sessionId,
104
+ signal,
105
+ meta: {
106
+ cwd: workspace.path,
107
+ agentPreset: preset.id
108
+ },
109
+ agentOptions: resolved.agentOptions,
110
+ setup: async (agentCtx) => {
111
+ await ctx.agentPresets.mount(agentCtx, preset.id);
112
+ installInitialModelSelection(agentCtx, resolved.modelSelection);
113
+ }
114
+ });
115
+ let attached = false;
116
+ try {
117
+ signal.throwIfAborted();
118
+ await workspace.attachSession(sessionId);
119
+ attached = true;
120
+ signal.throwIfAborted();
121
+ ctx.permissionPresets.set(handle.agent.session, resolved.permissionPreset);
122
+ ctx.sessionTitle.rename(handle.agent.session, resolved.title);
123
+ handle.agent.followup(createUserMessage({
124
+ content: [{
125
+ type: "text",
126
+ text: resolved.prompt
127
+ }],
128
+ source: {
129
+ kind: "webhook",
130
+ provider: delivery.kind,
131
+ source: delivery.source,
132
+ deliveryId: delivery.deliveryId,
133
+ ruleId,
134
+ form: "notice",
135
+ summary: boundContextSummary(`${delivery.kind} webhook handled by ${ruleId}`)
136
+ }
137
+ }));
138
+ } catch (error) {
139
+ if (attached) try {
140
+ await workspace.detachSession(sessionId);
141
+ } catch (rollbackError) {
142
+ reportRollbackFailure(ctx, `Workspace detach for Session "${sessionId}"`, rollbackError);
143
+ }
144
+ try {
145
+ await handle.dispose();
146
+ } catch (rollbackError) {
147
+ reportRollbackFailure(ctx, `Agent disposal for Session "${sessionId}"`, rollbackError);
148
+ }
149
+ throw error;
150
+ }
151
+ }
152
+ //#endregion
153
+ //#region lib/types/brand.js
154
+ /** Opaque webhook identities shared by adapters, rules, and Session provenance. */
155
+ /**
156
+ * Brand a webhook rule id.
157
+ * @param value - non-empty rule identifier validated at registration.
158
+ * @returns the same string with its compile-time brand.
159
+ */
160
+ function WebhookRuleId(value) {
161
+ return value;
162
+ }
163
+ /**
164
+ * Brand a configured webhook source id.
165
+ * @param value - non-empty adapter instance identifier validated by its adapter.
166
+ * @returns the same string with its compile-time brand.
167
+ */
168
+ function WebhookSourceId(value) {
169
+ return value;
170
+ }
171
+ /**
172
+ * Brand a provider delivery id.
173
+ * @param value - non-empty provider identity validated by its adapter.
174
+ * @returns the same string with its compile-time brand.
175
+ */
176
+ function WebhookDeliveryId(value) {
177
+ return value;
178
+ }
179
+ //#endregion
180
+ //#region lib/types/index.js
181
+ /** Fire-and-forget webhook rule registry and Workspace-backed Session runtime. */
182
+ /** Validate and detach one delivery before sharing it across arbitrary rules. */
183
+ function snapshotDelivery(delivery) {
184
+ if (typeof delivery.kind !== "string" || delivery.kind.trim() === "") throw new TypeError("webhook delivery kind must be a non-empty string");
185
+ if (typeof delivery.source !== "string" || delivery.source.trim() === "") throw new TypeError("webhook delivery source must be a non-empty string");
186
+ if (typeof delivery.deliveryId !== "string" || delivery.deliveryId.trim() === "") throw new TypeError("webhook delivery id must be a non-empty string");
187
+ if (!Number.isSafeInteger(delivery.receivedAt) || delivery.receivedAt < 0) throw new TypeError("webhook delivery receivedAt must be a non-negative safe integer");
188
+ const snapshot = snapshotJsonValue(delivery);
189
+ if (snapshot === void 0) throw new TypeError("webhook delivery must be lossless JSON");
190
+ return deepFreeze(snapshot);
191
+ }
192
+ /** Fire-and-forget rule runtime. Session creation is the only built-in action. */
193
+ var WebhookRuntime = class extends Service {
194
+ static inject = [
195
+ "agents",
196
+ "agentDefaultModel",
197
+ "agentPresets",
198
+ "permissionPresets",
199
+ "sessionTitle",
200
+ "workspaceRegistry"
201
+ ];
202
+ rules = /* @__PURE__ */ new Map();
203
+ selfCtx;
204
+ closing = false;
205
+ constructor(ctx) {
206
+ super(ctx, "webhookRuntime");
207
+ this.selfCtx = ctx;
208
+ ctx.effect(() => async () => {
209
+ this.closing = true;
210
+ /* v8 ignore next -- caller-owned registration effects normally dispose first; this covers provider-first unload. */
211
+ await Promise.all([...this.rules.values()].map((rule) => this.disposeRegistration(rule)));
212
+ }, "webhookRuntime.lifecycle()");
213
+ }
214
+ /**
215
+ * Register one trusted programmatic rule.
216
+ * @param rule - unique id, provider kind, and arbitrary callback.
217
+ * @returns awaitable effect disposer that aborts and drains this rule's active callbacks.
218
+ */
219
+ register(rule) {
220
+ if (this.closing) throw new Error("webhook runtime is closing");
221
+ if (typeof rule.id !== "string" || rule.id.trim() === "") throw new TypeError("webhook rule id must be a non-empty string");
222
+ if (typeof rule.kind !== "string" || rule.kind.trim() === "") throw new TypeError(`webhook rule "${String(rule.id)}" kind must be a non-empty string`);
223
+ if (typeof rule.run !== "function") throw new TypeError(`webhook rule "${String(rule.id)}" requires run()`);
224
+ const erased = rule;
225
+ let registration;
226
+ const disposeEffect = this.ctx.effect(() => {
227
+ /* v8 ignore next -- no await separates the public liveness check from this initializer. */
228
+ if (this.closing) throw new Error("webhook runtime is closing");
229
+ if (this.rules.has(rule.id)) throw new Error(`webhook rule "${rule.id}" is already registered`);
230
+ registration = {
231
+ rule: erased,
232
+ controller: new AbortController(),
233
+ active: /* @__PURE__ */ new Set(),
234
+ closing: false
235
+ };
236
+ this.rules.set(rule.id, registration);
237
+ return () => this.disposeRegistration(registration);
238
+ }, `webhookRuntime.register(${rule.id})`);
239
+ return async () => {
240
+ await disposeEffect();
241
+ };
242
+ }
243
+ /**
244
+ * Start every currently matching rule and return before any callback settles.
245
+ * @param delivery - authenticated provider data; snapshotted before dispatch.
246
+ * @throws synchronously when the runtime is closing or the delivery is malformed.
247
+ */
248
+ dispatch(delivery) {
249
+ if (this.closing) throw new Error("webhook runtime is closing");
250
+ const snapshot = snapshotDelivery(delivery);
251
+ for (const registration of [...this.rules.values()]) {
252
+ if (registration.closing || registration.rule.kind !== snapshot.kind) continue;
253
+ this.startInvocation(registration, snapshot);
254
+ }
255
+ }
256
+ /** Start one contained invocation and attach it to registration teardown. */
257
+ startInvocation(registration, delivery) {
258
+ const tracked = Promise.resolve().then(async () => {
259
+ registration.controller.signal.throwIfAborted();
260
+ const request = await registration.rule.run(delivery, registration.controller.signal);
261
+ registration.controller.signal.throwIfAborted();
262
+ if (request !== null) await createWebhookSession(this.selfCtx, delivery, registration.rule.id, request, registration.controller.signal);
263
+ }).catch((error) => {
264
+ const invocation = `webhook: provider=${JSON.stringify(delivery.kind)} source=${JSON.stringify(delivery.source)} delivery=${JSON.stringify(delivery.deliveryId)} rule=${JSON.stringify(registration.rule.id)}`;
265
+ if (registration.controller.signal.aborted) this.selfCtx.logger.debug(`${invocation} stopped after disposal: ${errorChain(error)}`);
266
+ else this.selfCtx.logger.warn(`${invocation} failed: ${errorChain(error)}`);
267
+ }).finally(() => {
268
+ registration.active.delete(tracked);
269
+ });
270
+ registration.active.add(tracked);
271
+ }
272
+ /** Memoized registration teardown: hide, abort, then drain. */
273
+ disposeRegistration(registration) {
274
+ registration.disposal ??= (async () => {
275
+ registration.closing = true;
276
+ this.rules.delete(registration.rule.id);
277
+ registration.controller.abort(/* @__PURE__ */ new Error(`webhook rule "${registration.rule.id}" was disposed`));
278
+ while (registration.active.size > 0) await Promise.allSettled([...registration.active]);
279
+ })();
280
+ return registration.disposal;
281
+ }
282
+ };
283
+ //#endregion
284
+ export { WebhookDeliveryId, WebhookRuleId, WebhookRuntime, WebhookRuntime as default, WebhookSourceId };
@@ -0,0 +1,29 @@
1
+ //#region lib/types/invariant.js
2
+ /** Package-owned relationship invariant for webhook-origin prompt admission. */
3
+ const PACKAGE_NAME = "@deepseek-ai/dsh-webhook";
4
+ /** Cordis invariant-companion plugin name. */
5
+ const name = "webhook-invariant";
6
+ /** Registry required before reserving this package's invariant ownership. */
7
+ const inject = ["invariants"];
8
+ /** Verify that one webhook-origin message already belongs to its cwd Workspace. */
9
+ const install = Object.assign(function installWebhookMessages(ctx, fail) {
10
+ ctx.on("internal/dispatch", (_mode, eventName, args) => {
11
+ if (eventName !== "session/event") return;
12
+ const [session, event] = args;
13
+ if (event.type !== "agent/inbox/spliced") return;
14
+ if (event.data.inserted.filter((message) => message.source.kind === "webhook").length === 0) return;
15
+ const cwd = session.header.cwd;
16
+ if (cwd === void 0) return fail(`webhook Session "${session.id}" has no cwd`);
17
+ const owners = ctx.workspaceRegistry.list().filter((workspace) => workspace.sessionIds.includes(session.id));
18
+ if (owners.length !== 1) return fail(`webhook Session "${session.id}" belongs to ${owners.length} Workspaces at prompt admission`);
19
+ if (owners[0]?.path !== cwd) fail(`webhook Session "${session.id}" cwd ${JSON.stringify(cwd)} differs from its Workspace path`);
20
+ }, { global: true });
21
+ }, { inject: ["workspaceRegistry"] });
22
+ /**
23
+ * Register this package's relationship invariant.
24
+ * @param ctx - Cordis context carrying the invariant registry.
25
+ * @returns the invariant registration disposer.
26
+ */
27
+ const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
28
+ //#endregion
29
+ export { apply, inject, name };
@@ -0,0 +1,27 @@
1
+ /** Opaque webhook identities shared by adapters, rules, and Session provenance. */
2
+ import type { Branded } from '@deepseek-ai/dsh-brand';
3
+ /** Identifies one programmatic webhook rule. */
4
+ export type WebhookRuleId = Branded<'WebhookRuleId'>;
5
+ /** Identifies one configured webhook adapter instance. */
6
+ export type WebhookSourceId = Branded<'WebhookSourceId'>;
7
+ /** Identifies one provider delivery. The runtime assigns no deduplication semantics. */
8
+ export type WebhookDeliveryId = Branded<'WebhookDeliveryId'>;
9
+ /**
10
+ * Brand a webhook rule id.
11
+ * @param value - non-empty rule identifier validated at registration.
12
+ * @returns the same string with its compile-time brand.
13
+ */
14
+ export declare function WebhookRuleId(value: string): WebhookRuleId;
15
+ /**
16
+ * Brand a configured webhook source id.
17
+ * @param value - non-empty adapter instance identifier validated by its adapter.
18
+ * @returns the same string with its compile-time brand.
19
+ */
20
+ export declare function WebhookSourceId(value: string): WebhookSourceId;
21
+ /**
22
+ * Brand a provider delivery id.
23
+ * @param value - non-empty provider identity validated by its adapter.
24
+ * @returns the same string with its compile-time brand.
25
+ */
26
+ export declare function WebhookDeliveryId(value: string): WebhookDeliveryId;
27
+ //# sourceMappingURL=brand.d.ts.map
@@ -0,0 +1,26 @@
1
+ /** Opaque webhook identities shared by adapters, rules, and Session provenance. */
2
+ /**
3
+ * Brand a webhook rule id.
4
+ * @param value - non-empty rule identifier validated at registration.
5
+ * @returns the same string with its compile-time brand.
6
+ */
7
+ export function WebhookRuleId(value) {
8
+ return value;
9
+ }
10
+ /**
11
+ * Brand a configured webhook source id.
12
+ * @param value - non-empty adapter instance identifier validated by its adapter.
13
+ * @returns the same string with its compile-time brand.
14
+ */
15
+ export function WebhookSourceId(value) {
16
+ return value;
17
+ }
18
+ /**
19
+ * Brand a provider delivery id.
20
+ * @param value - non-empty provider identity validated by its adapter.
21
+ * @returns the same string with its compile-time brand.
22
+ */
23
+ export function WebhookDeliveryId(value) {
24
+ return value;
25
+ }
26
+ //# sourceMappingURL=brand.js.map
@@ -0,0 +1,36 @@
1
+ /** Fire-and-forget webhook rule registry and Workspace-backed Session runtime. */
2
+ import { Context, Service } from '@deepseek-ai/cordis';
3
+ import type { VerifiedWebhookDelivery, WebhookRule } from './types.ts';
4
+ export * from './brand.ts';
5
+ export type * from './types.ts';
6
+ declare module '@deepseek-ai/cordis' {
7
+ interface Context {
8
+ webhookRuntime: WebhookRuntime;
9
+ }
10
+ }
11
+ /** Fire-and-forget rule runtime. Session creation is the only built-in action. */
12
+ export declare class WebhookRuntime extends Service {
13
+ static inject: string[];
14
+ private readonly rules;
15
+ private readonly selfCtx;
16
+ private closing;
17
+ constructor(ctx: Context);
18
+ /**
19
+ * Register one trusted programmatic rule.
20
+ * @param rule - unique id, provider kind, and arbitrary callback.
21
+ * @returns awaitable effect disposer that aborts and drains this rule's active callbacks.
22
+ */
23
+ register<K extends string>(rule: WebhookRule<K>): () => Promise<void>;
24
+ /**
25
+ * Start every currently matching rule and return before any callback settles.
26
+ * @param delivery - authenticated provider data; snapshotted before dispatch.
27
+ * @throws synchronously when the runtime is closing or the delivery is malformed.
28
+ */
29
+ dispatch<K extends string>(delivery: VerifiedWebhookDelivery<K>): void;
30
+ /** Start one contained invocation and attach it to registration teardown. */
31
+ private startInvocation;
32
+ /** Memoized registration teardown: hide, abort, then drain. */
33
+ private disposeRegistration;
34
+ }
35
+ export default WebhookRuntime;
36
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,138 @@
1
+ /** Fire-and-forget webhook rule registry and Workspace-backed Session runtime. */
2
+ import { Service } from '@deepseek-ai/cordis';
3
+ import { errorChain } from '@deepseek-ai/dsh-llm';
4
+ import { deepFreeze, snapshotJsonValue } from '@deepseek-ai/dsh-util-values';
5
+ import { createWebhookSession } from "./session.js";
6
+ export * from "./brand.js";
7
+ /** Validate and detach one delivery before sharing it across arbitrary rules. */
8
+ function snapshotDelivery(delivery) {
9
+ if (typeof delivery.kind !== 'string' || delivery.kind.trim() === '') {
10
+ throw new TypeError('webhook delivery kind must be a non-empty string');
11
+ }
12
+ if (typeof delivery.source !== 'string' || delivery.source.trim() === '') {
13
+ throw new TypeError('webhook delivery source must be a non-empty string');
14
+ }
15
+ if (typeof delivery.deliveryId !== 'string' || delivery.deliveryId.trim() === '') {
16
+ throw new TypeError('webhook delivery id must be a non-empty string');
17
+ }
18
+ if (!Number.isSafeInteger(delivery.receivedAt) || delivery.receivedAt < 0) {
19
+ throw new TypeError('webhook delivery receivedAt must be a non-negative safe integer');
20
+ }
21
+ const snapshot = snapshotJsonValue(delivery);
22
+ if (snapshot === undefined)
23
+ throw new TypeError('webhook delivery must be lossless JSON');
24
+ return deepFreeze(snapshot);
25
+ }
26
+ /** Fire-and-forget rule runtime. Session creation is the only built-in action. */
27
+ export class WebhookRuntime extends Service {
28
+ static inject = [
29
+ 'agents',
30
+ 'agentDefaultModel',
31
+ 'agentPresets',
32
+ 'permissionPresets',
33
+ 'sessionTitle',
34
+ 'workspaceRegistry',
35
+ ];
36
+ rules = new Map();
37
+ selfCtx;
38
+ closing = false;
39
+ constructor(ctx) {
40
+ super(ctx, 'webhookRuntime');
41
+ this.selfCtx = ctx;
42
+ ctx.effect(() => async () => {
43
+ this.closing = true;
44
+ /* v8 ignore next -- caller-owned registration effects normally dispose first; this covers provider-first unload. */
45
+ await Promise.all([...this.rules.values()].map(rule => this.disposeRegistration(rule)));
46
+ }, 'webhookRuntime.lifecycle()');
47
+ }
48
+ /**
49
+ * Register one trusted programmatic rule.
50
+ * @param rule - unique id, provider kind, and arbitrary callback.
51
+ * @returns awaitable effect disposer that aborts and drains this rule's active callbacks.
52
+ */
53
+ register(rule) {
54
+ if (this.closing)
55
+ throw new Error('webhook runtime is closing');
56
+ if (typeof rule.id !== 'string' || rule.id.trim() === '') {
57
+ throw new TypeError('webhook rule id must be a non-empty string');
58
+ }
59
+ if (typeof rule.kind !== 'string' || rule.kind.trim() === '') {
60
+ throw new TypeError(`webhook rule "${String(rule.id)}" kind must be a non-empty string`);
61
+ }
62
+ if (typeof rule.run !== 'function') {
63
+ throw new TypeError(`webhook rule "${String(rule.id)}" requires run()`);
64
+ }
65
+ // The public generic preserves adapter-specific authoring types. The runtime
66
+ // stores one erased callback after validating the shared provider tag.
67
+ const erased = rule;
68
+ let registration;
69
+ const disposeEffect = this.ctx.effect(() => {
70
+ /* v8 ignore next -- no await separates the public liveness check from this initializer. */
71
+ if (this.closing)
72
+ throw new Error('webhook runtime is closing');
73
+ if (this.rules.has(rule.id))
74
+ throw new Error(`webhook rule "${rule.id}" is already registered`);
75
+ registration = {
76
+ rule: erased,
77
+ controller: new AbortController(),
78
+ active: new Set(),
79
+ closing: false,
80
+ };
81
+ this.rules.set(rule.id, registration);
82
+ return () => this.disposeRegistration(registration);
83
+ }, `webhookRuntime.register(${rule.id})`);
84
+ return async () => { await disposeEffect(); };
85
+ }
86
+ /**
87
+ * Start every currently matching rule and return before any callback settles.
88
+ * @param delivery - authenticated provider data; snapshotted before dispatch.
89
+ * @throws synchronously when the runtime is closing or the delivery is malformed.
90
+ */
91
+ dispatch(delivery) {
92
+ if (this.closing)
93
+ throw new Error('webhook runtime is closing');
94
+ const snapshot = snapshotDelivery(delivery);
95
+ for (const registration of [...this.rules.values()]) {
96
+ if (registration.closing || registration.rule.kind !== snapshot.kind)
97
+ continue;
98
+ this.startInvocation(registration, snapshot);
99
+ }
100
+ }
101
+ /** Start one contained invocation and attach it to registration teardown. */
102
+ startInvocation(registration, delivery) {
103
+ const tracked = Promise.resolve().then(async () => {
104
+ registration.controller.signal.throwIfAborted();
105
+ const request = await registration.rule.run(delivery, registration.controller.signal);
106
+ registration.controller.signal.throwIfAborted();
107
+ if (request !== null) {
108
+ await createWebhookSession(this.selfCtx, delivery, registration.rule.id, request, registration.controller.signal);
109
+ }
110
+ }).catch((error) => {
111
+ const invocation = `webhook: provider=${JSON.stringify(delivery.kind)} source=${JSON.stringify(delivery.source)} `
112
+ + `delivery=${JSON.stringify(delivery.deliveryId)} rule=${JSON.stringify(registration.rule.id)}`;
113
+ if (registration.controller.signal.aborted) {
114
+ this.selfCtx.logger.debug(`${invocation} stopped after disposal: ${errorChain(error)}`);
115
+ }
116
+ else {
117
+ this.selfCtx.logger.warn(`${invocation} failed: ${errorChain(error)}`);
118
+ }
119
+ }).finally(() => {
120
+ registration.active.delete(tracked);
121
+ });
122
+ registration.active.add(tracked);
123
+ }
124
+ /** Memoized registration teardown: hide, abort, then drain. */
125
+ disposeRegistration(registration) {
126
+ registration.disposal ??= (async () => {
127
+ registration.closing = true;
128
+ this.rules.delete(registration.rule.id);
129
+ registration.controller.abort(new Error(`webhook rule "${registration.rule.id}" was disposed`));
130
+ while (registration.active.size > 0) {
131
+ await Promise.allSettled([...registration.active]);
132
+ }
133
+ })();
134
+ return registration.disposal;
135
+ }
136
+ }
137
+ export default WebhookRuntime;
138
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,13 @@
1
+ /** Package-owned relationship invariant for webhook-origin prompt admission. */
2
+ import type { Context } from '@deepseek-ai/cordis';
3
+ /** Cordis invariant-companion plugin name. */
4
+ export declare const name = "webhook-invariant";
5
+ /** Registry required before reserving this package's invariant ownership. */
6
+ export declare const inject: string[];
7
+ /**
8
+ * Register this package's relationship invariant.
9
+ * @param ctx - Cordis context carrying the invariant registry.
10
+ * @returns the invariant registration disposer.
11
+ */
12
+ export declare const apply: (ctx: Context) => Promise<() => void>;
13
+ //# sourceMappingURL=invariant.d.ts.map
@@ -0,0 +1,38 @@
1
+ /** Package-owned relationship invariant for webhook-origin prompt admission. */
2
+ const PACKAGE_NAME = '@deepseek-ai/dsh-webhook';
3
+ /** Cordis invariant-companion plugin name. */
4
+ export const name = 'webhook-invariant';
5
+ /** Registry required before reserving this package's invariant ownership. */
6
+ export const inject = ['invariants'];
7
+ /** Verify that one webhook-origin message already belongs to its cwd Workspace. */
8
+ const install = Object.assign(function installWebhookMessages(ctx, fail) {
9
+ ctx.on('internal/dispatch', (_mode, eventName, args) => {
10
+ if (eventName !== 'session/event')
11
+ return;
12
+ const [session, event] = args;
13
+ if (event.type !== 'agent/inbox/spliced')
14
+ return;
15
+ const webhookMessages = event.data.inserted.filter(message => message.source.kind === 'webhook');
16
+ if (webhookMessages.length === 0)
17
+ return;
18
+ const cwd = session.header.cwd;
19
+ if (cwd === undefined)
20
+ return fail(`webhook Session "${session.id}" has no cwd`);
21
+ const owners = ctx.workspaceRegistry.list().filter(workspace => workspace.sessionIds.includes(session.id));
22
+ if (owners.length !== 1) {
23
+ return fail(`webhook Session "${session.id}" belongs to ${owners.length} Workspaces at prompt admission`);
24
+ }
25
+ if (owners[0]?.path !== cwd) {
26
+ fail(`webhook Session "${session.id}" cwd ${JSON.stringify(cwd)} differs from its Workspace path`);
27
+ }
28
+ }, { global: true });
29
+ }, {
30
+ inject: ['workspaceRegistry'],
31
+ });
32
+ /**
33
+ * Register this package's relationship invariant.
34
+ * @param ctx - Cordis context carrying the invariant registry.
35
+ * @returns the invariant registration disposer.
36
+ */
37
+ export const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
38
+ //# sourceMappingURL=invariant.js.map
@@ -0,0 +1,17 @@
1
+ /** Workspace-backed Session creation for one settled webhook rule result. */
2
+ import type { Context } from '@deepseek-ai/cordis';
3
+ import type { WebhookRuleId } from './brand.ts';
4
+ import type { VerifiedWebhookDelivery, WebhookSessionRequest } from './types.ts';
5
+ /**
6
+ * Create, attach, title, configure, and prompt one ordinary root Session.
7
+ * Successful prompt admission ends webhook ownership of the operation; the
8
+ * Agent remains lifecycle-owned by `ctx` and follows normal Session behavior.
9
+ *
10
+ * @param ctx - untraced runtime context that owns the resulting Agent.
11
+ * @param delivery - exact verified provider delivery used for provenance.
12
+ * @param ruleId - rule that returned the request.
13
+ * @param request - same-process rule result.
14
+ * @param signal - registration lifetime cancellation through publication.
15
+ */
16
+ export declare function createWebhookSession(ctx: Context, delivery: VerifiedWebhookDelivery, ruleId: WebhookRuleId, request: WebhookSessionRequest, signal: AbortSignal): Promise<void>;
17
+ //# sourceMappingURL=session.d.ts.map
@@ -0,0 +1,150 @@
1
+ /** Workspace-backed Session creation for one settled webhook rule result. */
2
+ import { randomUUID } from 'node:crypto';
3
+ import { isAbsolute } from 'node:path';
4
+ import { brandString } from '@deepseek-ai/dsh-brand';
5
+ import { boundContextSummary, createUserMessage, errorChain } from '@deepseek-ai/dsh-llm';
6
+ /** Require one non-empty string field from an untyped rule result. */
7
+ function requiredString(record, field) {
8
+ const value = record[field];
9
+ if (typeof value !== 'string' || value.trim() === '') {
10
+ throw new TypeError(`webhook Session request ${field} must be a non-empty string`);
11
+ }
12
+ return value;
13
+ }
14
+ /** Snapshot and validate a same-process rule result before crossing awaits. */
15
+ function resolveRequest(ctx, input) {
16
+ const candidate = input;
17
+ if (candidate === null || typeof candidate !== 'object' || Array.isArray(candidate)) {
18
+ throw new TypeError('webhook rule result must be null or a Session request object');
19
+ }
20
+ const record = candidate;
21
+ const workspacePath = requiredString(record, 'workspacePath');
22
+ if (!isAbsolute(workspacePath)) {
23
+ throw new TypeError(`webhook Session request workspacePath must be absolute, got ${JSON.stringify(workspacePath)}`);
24
+ }
25
+ const title = requiredString(record, 'title');
26
+ const prompt = requiredString(record, 'prompt');
27
+ const agentPreset = requiredString(record, 'agentPreset');
28
+ const permissionPreset = requiredString(record, 'permissionPreset');
29
+ const model = record['model'];
30
+ if (model !== undefined && (model === null || typeof model !== 'object' || Array.isArray(model))) {
31
+ throw new TypeError('webhook Session request model must be an object');
32
+ }
33
+ let agentOptions;
34
+ let modelSelection;
35
+ if (model === undefined) {
36
+ const selected = ctx.agentDefaultModel.currentSelection();
37
+ agentOptions = { provider: selected.provider, model: selected.model };
38
+ modelSelection = { ...selected };
39
+ }
40
+ else {
41
+ const modelRecord = model;
42
+ const provider = requiredString(modelRecord, 'provider');
43
+ const modelId = requiredString(modelRecord, 'model');
44
+ const maxTokens = modelRecord['maxTokens'];
45
+ if (maxTokens !== undefined
46
+ && (typeof maxTokens !== 'number' || !Number.isSafeInteger(maxTokens) || maxTokens <= 0)) {
47
+ throw new TypeError('webhook Session request model.maxTokens must be a positive safe integer');
48
+ }
49
+ agentOptions = {
50
+ provider,
51
+ model: modelId,
52
+ ...(maxTokens === undefined ? {} : { maxTokens }),
53
+ };
54
+ modelSelection = { provider, model: modelId };
55
+ }
56
+ return { workspacePath, title, prompt, agentPreset, permissionPreset, modelSelection, agentOptions };
57
+ }
58
+ /** Log a rollback failure without replacing the operation's original failure. */
59
+ function reportRollbackFailure(ctx, subject, error) {
60
+ ctx.logger.warn(`webhook: ${subject} rollback failed: ${errorChain(error)}`);
61
+ }
62
+ /** Apply the creation-time selection until its first durable request header exists. */
63
+ function installInitialModelSelection(agentCtx, selection) {
64
+ agentCtx.on('agent/request', async (_payload, next) => {
65
+ const resolved = await next();
66
+ const agent = agentCtx.agent;
67
+ /* v8 ignore next -- AgentRegistry setup always provides the unpublished scoped Agent. */
68
+ if (agent === undefined)
69
+ throw new Error('webhook Session setup has no scoped Agent');
70
+ if (agent.session.requestHeader() !== undefined
71
+ || resolved.provider !== selection.provider
72
+ || resolved.model !== selection.model)
73
+ return resolved;
74
+ const { reasoningEffort: _inheritedEffort, ...withoutInheritedEffort } = resolved;
75
+ return {
76
+ ...withoutInheritedEffort,
77
+ ...selection.reasoningEffort === undefined ? {} : { reasoningEffort: selection.reasoningEffort },
78
+ };
79
+ });
80
+ }
81
+ /**
82
+ * Create, attach, title, configure, and prompt one ordinary root Session.
83
+ * Successful prompt admission ends webhook ownership of the operation; the
84
+ * Agent remains lifecycle-owned by `ctx` and follows normal Session behavior.
85
+ *
86
+ * @param ctx - untraced runtime context that owns the resulting Agent.
87
+ * @param delivery - exact verified provider delivery used for provenance.
88
+ * @param ruleId - rule that returned the request.
89
+ * @param request - same-process rule result.
90
+ * @param signal - registration lifetime cancellation through publication.
91
+ */
92
+ export async function createWebhookSession(ctx, delivery, ruleId, request, signal) {
93
+ const resolved = resolveRequest(ctx, request);
94
+ ctx.permissionPresets.resolve(resolved.permissionPreset);
95
+ const preset = await ctx.agentPresets.resolve(resolved.agentPreset);
96
+ await ctx.agentPresets.standingKeyFor(preset.id);
97
+ signal.throwIfAborted();
98
+ const workspace = await ctx.workspaceRegistry.create(resolved.workspacePath);
99
+ signal.throwIfAborted();
100
+ const sessionId = brandString(`webhook-${randomUUID()}`);
101
+ const handle = await ctx.agents.create({
102
+ sessionId,
103
+ signal,
104
+ meta: { cwd: workspace.path, agentPreset: preset.id },
105
+ agentOptions: resolved.agentOptions,
106
+ setup: async (agentCtx) => {
107
+ await ctx.agentPresets.mount(agentCtx, preset.id);
108
+ installInitialModelSelection(agentCtx, resolved.modelSelection);
109
+ },
110
+ });
111
+ let attached = false;
112
+ try {
113
+ signal.throwIfAborted();
114
+ await workspace.attachSession(sessionId);
115
+ attached = true;
116
+ signal.throwIfAborted();
117
+ ctx.permissionPresets.set(handle.agent.session, resolved.permissionPreset);
118
+ ctx.sessionTitle.rename(handle.agent.session, resolved.title);
119
+ handle.agent.followup(createUserMessage({
120
+ content: [{ type: 'text', text: resolved.prompt }],
121
+ source: {
122
+ kind: 'webhook',
123
+ provider: delivery.kind,
124
+ source: delivery.source,
125
+ deliveryId: delivery.deliveryId,
126
+ ruleId,
127
+ form: 'notice',
128
+ summary: boundContextSummary(`${delivery.kind} webhook handled by ${ruleId}`),
129
+ },
130
+ }));
131
+ }
132
+ catch (error) {
133
+ if (attached) {
134
+ try {
135
+ await workspace.detachSession(sessionId);
136
+ }
137
+ catch (rollbackError) {
138
+ reportRollbackFailure(ctx, `Workspace detach for Session "${sessionId}"`, rollbackError);
139
+ }
140
+ }
141
+ try {
142
+ await handle.dispose();
143
+ }
144
+ catch (rollbackError) {
145
+ reportRollbackFailure(ctx, `Agent disposal for Session "${sessionId}"`, rollbackError);
146
+ }
147
+ throw error;
148
+ }
149
+ }
150
+ //# sourceMappingURL=session.js.map
@@ -0,0 +1,74 @@
1
+ /** Provider-neutral webhook deliveries, rules, and Session requests. */
2
+ import type { JsonValue } from '@deepseek-ai/dsh-util-values';
3
+ import type { WebhookDeliveryId, WebhookRuleId, WebhookSourceId } from './brand.ts';
4
+ /** Provider adapters add their normalized event type through declaration merging. */
5
+ export interface WebhookEventMap {
6
+ }
7
+ /** Event value for a known provider kind, or generic lossless JSON for an out-of-tree kind. */
8
+ export type WebhookEventOf<K extends string> = K extends keyof WebhookEventMap ? WebhookEventMap[K] : JsonValue;
9
+ /** One authenticated and parsed provider delivery. */
10
+ export interface VerifiedWebhookDelivery<K extends string = string> {
11
+ /** Provider family such as `github`. */
12
+ readonly kind: K;
13
+ /** Configured adapter instance such as `primary-github`. */
14
+ readonly source: WebhookSourceId;
15
+ /** Provider identity exposed as provenance, never as built-in deduplication state. */
16
+ readonly deliveryId: WebhookDeliveryId;
17
+ /** Provider-normalized lossless JSON. */
18
+ readonly event: WebhookEventOf<K>;
19
+ /** Host receipt time in Unix epoch milliseconds. */
20
+ readonly receivedAt: number;
21
+ }
22
+ /** Optional explicit model route and output cap for a webhook-created Agent. */
23
+ export interface WebhookModelSelection {
24
+ /** Registered provider route. */
25
+ readonly provider: string;
26
+ /** Provider-owned model id. */
27
+ readonly model: string;
28
+ /** Optional positive output-token cap. */
29
+ readonly maxTokens?: number;
30
+ }
31
+ /** The sole runtime action: create and prompt one root Session. */
32
+ export interface WebhookSessionRequest {
33
+ /** Existing local directory to resolve or create as a Web Workspace. */
34
+ readonly workspacePath: string;
35
+ /** Explicit Session title. */
36
+ readonly title: string;
37
+ /** Non-empty initial text prompt. */
38
+ readonly prompt: string;
39
+ /** Agent composition mounted before publication. */
40
+ readonly agentPreset: string;
41
+ /** Sandbox and approval preset applied before prompt admission. */
42
+ readonly permissionPreset: string;
43
+ /** Optional explicit route; omission uses the complete current default, including reasoning effort. */
44
+ readonly model?: WebhookModelSelection;
45
+ }
46
+ /** Trusted code that optionally creates one Session for a delivery. */
47
+ export interface WebhookRule<K extends string = string> {
48
+ /** Globally unique diagnostic identity. */
49
+ readonly id: WebhookRuleId;
50
+ /** Provider kind this rule receives. */
51
+ readonly kind: K;
52
+ /**
53
+ * Run arbitrary trusted code and optionally request one Session.
54
+ * @param delivery - immutable authenticated provider data.
55
+ * @param signal - aborts when this registration or the runtime unloads.
56
+ * @returns one Session request, or `null` for no action.
57
+ */
58
+ run(delivery: Readonly<VerifiedWebhookDelivery<K>>, signal: AbortSignal): WebhookSessionRequest | null | Promise<WebhookSessionRequest | null>;
59
+ }
60
+ declare module '@deepseek-ai/dsh-llm' {
61
+ interface MessageSourceMap {
62
+ /** Programmatic input admitted from one verified webhook rule. */
63
+ webhook: {
64
+ readonly kind: 'webhook';
65
+ readonly provider: string;
66
+ readonly source: WebhookSourceId;
67
+ readonly deliveryId: WebhookDeliveryId;
68
+ readonly ruleId: WebhookRuleId;
69
+ readonly form: 'notice';
70
+ readonly summary: string;
71
+ };
72
+ }
73
+ }
74
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1,3 @@
1
+ /** Provider-neutral webhook deliveries, rules, and Session requests. */
2
+ export {};
3
+ //# sourceMappingURL=types.js.map
package/package.json ADDED
@@ -0,0 +1,69 @@
1
+ {
2
+ "name": "@deepseek-ai/dsh-webhook",
3
+ "description": "Fire-and-forget webhook rule runtime that creates Workspace-backed DeepSeek Harness Sessions",
4
+ "version": "0.1.2-alpha.2",
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
11
+ "directory": "packages/webhook/webhook"
12
+ },
13
+ "type": "module",
14
+ "main": "lib/index.js",
15
+ "types": "lib/types/index.d.ts",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./lib/types/index.d.ts",
19
+ "default": "./lib/index.js"
20
+ },
21
+ "./types": {
22
+ "types": "./lib/types/types.d.ts",
23
+ "default": "./lib/types/types.js"
24
+ },
25
+ "./invariant": {
26
+ "types": "./lib/types/invariant.d.ts",
27
+ "default": "./lib/invariant.js"
28
+ },
29
+ "./src/*": "./src/*",
30
+ "./package.json": "./package.json"
31
+ },
32
+ "files": [
33
+ "lib/index.js",
34
+ "lib/invariant.js",
35
+ "lib/types/**/*.js",
36
+ "lib/types/**/*.d.ts"
37
+ ],
38
+ "license": "MIT",
39
+ "peerDependencies": {
40
+ "@deepseek-ai/cordis": "^4.0.2",
41
+ "@deepseek-ai/dsh-agent": "^0.1.2-alpha.2",
42
+ "@deepseek-ai/dsh-agent-default-model": "^0.1.2-alpha.2",
43
+ "@deepseek-ai/dsh-agent-presets": "^0.1.2-alpha.2",
44
+ "@deepseek-ai/dsh-invariants": "^0.1.2-alpha.2",
45
+ "@deepseek-ai/dsh-llm": "^0.1.2-alpha.2",
46
+ "@deepseek-ai/dsh-permission-presets": "^0.1.2-alpha.2",
47
+ "@deepseek-ai/dsh-session": "^0.1.2-alpha.2",
48
+ "@deepseek-ai/dsh-session-title": "^0.1.2-alpha.2",
49
+ "@deepseek-ai/dsh-workspace": "^0.1.2-alpha.2"
50
+ },
51
+ "devDependencies": {
52
+ "@deepseek-ai/cordis": "^4.0.2",
53
+ "@deepseek-ai/cordis-plugin-include": "^1.0.7",
54
+ "@deepseek-ai/cordis-plugin-loader": "^1.0.3",
55
+ "@deepseek-ai/dsh-agent": "^0.1.2-alpha.2",
56
+ "@deepseek-ai/dsh-agent-presets": "^0.1.2-alpha.2",
57
+ "@deepseek-ai/dsh-invariants": "^0.1.2-alpha.2",
58
+ "@deepseek-ai/dsh-llm": "^0.1.2-alpha.2",
59
+ "@deepseek-ai/dsh-permission-presets": "^0.1.2-alpha.2",
60
+ "@deepseek-ai/dsh-session": "^0.1.2-alpha.2",
61
+ "@deepseek-ai/dsh-session-title": "^0.1.2-alpha.2",
62
+ "@deepseek-ai/dsh-agent-default-model": "^0.1.2-alpha.2",
63
+ "@deepseek-ai/dsh-workspace": "^0.1.2-alpha.2"
64
+ },
65
+ "dependencies": {
66
+ "@deepseek-ai/dsh-brand": "^0.1.2-alpha.2",
67
+ "@deepseek-ai/dsh-util-values": "^0.1.2-alpha.2"
68
+ }
69
+ }