@krischoichoi/channel-harness 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +110 -0
- package/lib/access/controller.d.ts +37 -0
- package/lib/access/controller.d.ts.map +1 -0
- package/lib/access/controller.js +51 -0
- package/lib/access/controller.js.map +1 -0
- package/lib/access/decision.d.ts +18 -0
- package/lib/access/decision.d.ts.map +1 -0
- package/lib/access/decision.js +2 -0
- package/lib/access/decision.js.map +1 -0
- package/lib/access/resolver.d.ts +41 -0
- package/lib/access/resolver.d.ts.map +1 -0
- package/lib/access/resolver.js +31 -0
- package/lib/access/resolver.js.map +1 -0
- package/lib/agent-manager.d.ts +314 -0
- package/lib/agent-manager.d.ts.map +1 -0
- package/lib/agent-manager.js +603 -0
- package/lib/agent-manager.js.map +1 -0
- package/lib/agent-router.d.ts +37 -0
- package/lib/agent-router.d.ts.map +1 -0
- package/lib/agent-router.js +33 -0
- package/lib/agent-router.js.map +1 -0
- package/lib/bind-support.d.ts +23 -0
- package/lib/bind-support.d.ts.map +1 -0
- package/lib/bind-support.js +63 -0
- package/lib/bind-support.js.map +1 -0
- package/lib/binding-store.d.ts +104 -0
- package/lib/binding-store.d.ts.map +1 -0
- package/lib/binding-store.js +354 -0
- package/lib/binding-store.js.map +1 -0
- package/lib/bridge.d.ts +248 -0
- package/lib/bridge.d.ts.map +1 -0
- package/lib/bridge.js +961 -0
- package/lib/bridge.js.map +1 -0
- package/lib/channel-label.d.ts +46 -0
- package/lib/channel-label.d.ts.map +1 -0
- package/lib/channel-label.js +89 -0
- package/lib/channel-label.js.map +1 -0
- package/lib/channel-session-factory.d.ts +76 -0
- package/lib/channel-session-factory.d.ts.map +1 -0
- package/lib/channel-session-factory.js +187 -0
- package/lib/channel-session-factory.js.map +1 -0
- package/lib/commands/bind.d.ts +21 -0
- package/lib/commands/bind.d.ts.map +1 -0
- package/lib/commands/bind.js +91 -0
- package/lib/commands/bind.js.map +1 -0
- package/lib/commands/help.d.ts +15 -0
- package/lib/commands/help.d.ts.map +1 -0
- package/lib/commands/help.js +89 -0
- package/lib/commands/help.js.map +1 -0
- package/lib/commands/index.d.ts +122 -0
- package/lib/commands/index.d.ts.map +1 -0
- package/lib/commands/index.js +63 -0
- package/lib/commands/index.js.map +1 -0
- package/lib/commands/locale.d.ts +9 -0
- package/lib/commands/locale.d.ts.map +1 -0
- package/lib/commands/locale.js +27 -0
- package/lib/commands/locale.js.map +1 -0
- package/lib/commands/mirror.d.ts +14 -0
- package/lib/commands/mirror.d.ts.map +1 -0
- package/lib/commands/mirror.js +44 -0
- package/lib/commands/mirror.js.map +1 -0
- package/lib/commands/model.d.ts +20 -0
- package/lib/commands/model.d.ts.map +1 -0
- package/lib/commands/model.js +106 -0
- package/lib/commands/model.js.map +1 -0
- package/lib/commands/models.d.ts +14 -0
- package/lib/commands/models.d.ts.map +1 -0
- package/lib/commands/models.js +67 -0
- package/lib/commands/models.js.map +1 -0
- package/lib/commands/new.d.ts +14 -0
- package/lib/commands/new.d.ts.map +1 -0
- package/lib/commands/new.js +37 -0
- package/lib/commands/new.js.map +1 -0
- package/lib/commands/status.d.ts +12 -0
- package/lib/commands/status.d.ts.map +1 -0
- package/lib/commands/status.js +30 -0
- package/lib/commands/status.js.map +1 -0
- package/lib/commands/stop.d.ts +16 -0
- package/lib/commands/stop.d.ts.map +1 -0
- package/lib/commands/stop.js +27 -0
- package/lib/commands/stop.js.map +1 -0
- package/lib/commands/version.d.ts +25 -0
- package/lib/commands/version.d.ts.map +1 -0
- package/lib/commands/version.js +57 -0
- package/lib/commands/version.js.map +1 -0
- package/lib/config.d.ts +119 -0
- package/lib/config.d.ts.map +1 -0
- package/lib/config.js +60 -0
- package/lib/config.js.map +1 -0
- package/lib/debug-logger.d.ts +8 -0
- package/lib/debug-logger.d.ts.map +1 -0
- package/lib/debug-logger.js +32 -0
- package/lib/debug-logger.js.map +1 -0
- package/lib/dsh-home.d.ts +12 -0
- package/lib/dsh-home.d.ts.map +1 -0
- package/lib/dsh-home.js +31 -0
- package/lib/dsh-home.js.map +1 -0
- package/lib/failure-display.d.ts +10 -0
- package/lib/failure-display.d.ts.map +1 -0
- package/lib/failure-display.js +42 -0
- package/lib/failure-display.js.map +1 -0
- package/lib/file-provider.d.ts +96 -0
- package/lib/file-provider.d.ts.map +1 -0
- package/lib/file-provider.js +44 -0
- package/lib/file-provider.js.map +1 -0
- package/lib/index.d.ts +31 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +31 -0
- package/lib/index.js.map +1 -0
- package/lib/interactions/index.d.ts +13 -0
- package/lib/interactions/index.d.ts.map +1 -0
- package/lib/interactions/index.js +13 -0
- package/lib/interactions/index.js.map +1 -0
- package/lib/interactions/question-backend.d.ts +133 -0
- package/lib/interactions/question-backend.d.ts.map +1 -0
- package/lib/interactions/question-backend.js +37 -0
- package/lib/interactions/question-backend.js.map +1 -0
- package/lib/interactions/question-presenter.d.ts +68 -0
- package/lib/interactions/question-presenter.d.ts.map +1 -0
- package/lib/interactions/question-presenter.js +425 -0
- package/lib/interactions/question-presenter.js.map +1 -0
- package/lib/interactions/question-renderer.d.ts +37 -0
- package/lib/interactions/question-renderer.d.ts.map +1 -0
- package/lib/interactions/question-renderer.js +79 -0
- package/lib/interactions/question-renderer.js.map +1 -0
- package/lib/interactions/question-state.d.ts +106 -0
- package/lib/interactions/question-state.d.ts.map +1 -0
- package/lib/interactions/question-state.js +99 -0
- package/lib/interactions/question-state.js.map +1 -0
- package/lib/interactions/question-text-answer.d.ts +37 -0
- package/lib/interactions/question-text-answer.d.ts.map +1 -0
- package/lib/interactions/question-text-answer.js +70 -0
- package/lib/interactions/question-text-answer.js.map +1 -0
- package/lib/interactions/question-waterfall-backend.d.ts +42 -0
- package/lib/interactions/question-waterfall-backend.d.ts.map +1 -0
- package/lib/interactions/question-waterfall-backend.js +162 -0
- package/lib/interactions/question-waterfall-backend.js.map +1 -0
- package/lib/lifecycle.d.ts +36 -0
- package/lib/lifecycle.d.ts.map +1 -0
- package/lib/lifecycle.js +303 -0
- package/lib/lifecycle.js.map +1 -0
- package/lib/loggable-error.d.ts +10 -0
- package/lib/loggable-error.d.ts.map +1 -0
- package/lib/loggable-error.js +32 -0
- package/lib/loggable-error.js.map +1 -0
- package/lib/message-converter.d.ts +64 -0
- package/lib/message-converter.d.ts.map +1 -0
- package/lib/message-converter.js +191 -0
- package/lib/message-converter.js.map +1 -0
- package/lib/model-selection.d.ts +54 -0
- package/lib/model-selection.d.ts.map +1 -0
- package/lib/model-selection.js +116 -0
- package/lib/model-selection.js.map +1 -0
- package/lib/outbox/binding-resolver.d.ts +23 -0
- package/lib/outbox/binding-resolver.d.ts.map +1 -0
- package/lib/outbox/binding-resolver.js +38 -0
- package/lib/outbox/binding-resolver.js.map +1 -0
- package/lib/outbox/capabilities.d.ts +48 -0
- package/lib/outbox/capabilities.d.ts.map +1 -0
- package/lib/outbox/capabilities.js +35 -0
- package/lib/outbox/capabilities.js.map +1 -0
- package/lib/outbox/index.d.ts +14 -0
- package/lib/outbox/index.d.ts.map +1 -0
- package/lib/outbox/index.js +14 -0
- package/lib/outbox/index.js.map +1 -0
- package/lib/outbox/service.d.ts +48 -0
- package/lib/outbox/service.d.ts.map +1 -0
- package/lib/outbox/service.js +74 -0
- package/lib/outbox/service.js.map +1 -0
- package/lib/outbox/target.d.ts +17 -0
- package/lib/outbox/target.d.ts.map +1 -0
- package/lib/outbox/target.js +15 -0
- package/lib/outbox/target.js.map +1 -0
- package/lib/outbox/tool-send.d.ts +19 -0
- package/lib/outbox/tool-send.d.ts.map +1 -0
- package/lib/outbox/tool-send.js +77 -0
- package/lib/outbox/tool-send.js.map +1 -0
- package/lib/outbox/types.d.ts +42 -0
- package/lib/outbox/types.d.ts.map +1 -0
- package/lib/outbox/types.js +30 -0
- package/lib/outbox/types.js.map +1 -0
- package/lib/plugin.d.ts +27 -0
- package/lib/plugin.d.ts.map +1 -0
- package/lib/plugin.js +47 -0
- package/lib/plugin.js.map +1 -0
- package/lib/reply-context-store.d.ts +69 -0
- package/lib/reply-context-store.d.ts.map +1 -0
- package/lib/reply-context-store.js +75 -0
- package/lib/reply-context-store.js.map +1 -0
- package/lib/reply-router.d.ts +138 -0
- package/lib/reply-router.d.ts.map +1 -0
- package/lib/reply-router.js +674 -0
- package/lib/reply-router.js.map +1 -0
- package/lib/session-router.d.ts +49 -0
- package/lib/session-router.d.ts.map +1 -0
- package/lib/session-router.js +45 -0
- package/lib/session-router.js.map +1 -0
- package/lib/workspace-resolver.d.ts +67 -0
- package/lib/workspace-resolver.d.ts.map +1 -0
- package/lib/workspace-resolver.js +102 -0
- package/lib/workspace-resolver.js.map +1 -0
- package/package.json +103 -0
package/README.md
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# @krischoichoi/channel-harness
|
|
2
|
+
|
|
3
|
+
Thin Harness-native bridge between channel adapters and DeepSeek Harness
|
|
4
|
+
agents/sessions.
|
|
5
|
+
|
|
6
|
+
This package is the **only** package allowed to import Harness public APIs. It
|
|
7
|
+
turns `ChannelEvent`s emitted by `@krischoichoi/channel-core` adapters into Harness
|
|
8
|
+
sessions, routes them to the right agent, and streams assistant output back to
|
|
9
|
+
the channel reply pipeline.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pnpm add @krischoichoi/channel-harness
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
As a Cordis plugin:
|
|
18
|
+
|
|
19
|
+
```yaml
|
|
20
|
+
- id: channels-harness
|
|
21
|
+
name: '@krischoichoi/channel-harness'
|
|
22
|
+
inject:
|
|
23
|
+
- channels
|
|
24
|
+
- agents
|
|
25
|
+
- agentDefaultModel
|
|
26
|
+
- agentPresets
|
|
27
|
+
- llm
|
|
28
|
+
- commands
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Channel images follow the official Harness image pipeline: the bridge hands
|
|
32
|
+
raw inbound image bytes to the Harness Attachment Store
|
|
33
|
+
(`attachments.saveImage` -> `ImageAttachmentRef` -> real `ImageBlock`) and
|
|
34
|
+
never rewrites messages for model compatibility. Whether the current model
|
|
35
|
+
can see images (vision variant vs. deterministic text placeholder) is decided
|
|
36
|
+
by Harness request projection, and the append-only session history keeps the
|
|
37
|
+
original attachment reference.
|
|
38
|
+
|
|
39
|
+
## What it does
|
|
40
|
+
|
|
41
|
+
| Area | Responsibility |
|
|
42
|
+
| --- | --- |
|
|
43
|
+
| `SessionBinding` / `BindingStore` | Binds platform conversations to Harness session ids (file-backed by default, survives restarts) |
|
|
44
|
+
| `AgentManager` / `AgentRouter` | Resolves/creates the agent for a conversation via `agent.default` plus per-channel/account/conversation overrides |
|
|
45
|
+
| `MessageConverter` | Maps structured `ChannelEvent` messages to Harness message types |
|
|
46
|
+
| `ReplyRouter` / `ReplyContextStore` | Streams `session/event` output back to the adapter (`ReplyHandle`) |
|
|
47
|
+
| `ChannelQuestionPresenter` (interactions/) | Presents `ask_user_question` requests through generic interactive actions. One root-level, **prepended** answerer on the official `user-questions/request` Cordis waterfall serves every profile (the official Remote/Web answerer registers first at boot, and a waterfall is first-claim-wins); a declined presentation delegates to the next answerer (the Web UI) via `next()` |
|
|
48
|
+
| `WorkspaceResolver` | Maps conversations to Harness workspaces (`channel-account` by default) |
|
|
49
|
+
| commands | Registers Agent-scoped slash commands (`/new`, `/stop`, `/help`, `/status`, `/model`, `/version`, `/mirror`, `/bind`) through Harness `CommandRuntime` |
|
|
50
|
+
| outbox | Proactive `send_channel_message` tool support (`OutboxService`) |
|
|
51
|
+
|
|
52
|
+
Model routing remains Harness-owned. A channel Session uses the model resolved
|
|
53
|
+
at create/resume time from its request header, route options, or the shared
|
|
54
|
+
`agentDefaultModel`. `/model` delegates the current-Session switch to the
|
|
55
|
+
official Host RPC (or the official headless hook) and persists the same choice
|
|
56
|
+
as the default for future Sessions. The bridge does not add a second owner or
|
|
57
|
+
run a first-turn model preparation RPC.
|
|
58
|
+
|
|
59
|
+
## Configuration
|
|
60
|
+
|
|
61
|
+
```yaml
|
|
62
|
+
- id: channels-harness
|
|
63
|
+
name: '@krischoichoi/channel-harness'
|
|
64
|
+
inject: [channels, agents, agentDefaultModel, agentPresets, llm, commands]
|
|
65
|
+
config:
|
|
66
|
+
agent:
|
|
67
|
+
default:
|
|
68
|
+
preset: main
|
|
69
|
+
provider: deepseek
|
|
70
|
+
model: deepseek-chat
|
|
71
|
+
maxTokens: 4096
|
|
72
|
+
routing:
|
|
73
|
+
mode: global # global | channel | account | conversation
|
|
74
|
+
bindingStore:
|
|
75
|
+
type: file # memory | file
|
|
76
|
+
workspace:
|
|
77
|
+
mode: channel-account # channel-account | host-cwd | disabled
|
|
78
|
+
autoCreate: true
|
|
79
|
+
userQuestions:
|
|
80
|
+
enabled: true
|
|
81
|
+
timeoutMs: 300000
|
|
82
|
+
reply:
|
|
83
|
+
updateIntervalMs: 200
|
|
84
|
+
splitParagraphs: true
|
|
85
|
+
splitCodeBlocks: true
|
|
86
|
+
finalFlush: true
|
|
87
|
+
maxConcurrency: 4
|
|
88
|
+
drainTimeoutMs: 5000
|
|
89
|
+
includeMetadataPrefix: false
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Routing priority when `routing.mode` is not `global`:
|
|
93
|
+
`conversation` > `account` > `channel` > `agent.default`.
|
|
94
|
+
|
|
95
|
+
## Development
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
pnpm --filter @krischoichoi/channel-harness build
|
|
99
|
+
pnpm --filter @krischoichoi/channel-harness typecheck
|
|
100
|
+
pnpm --filter @krischoichoi/channel-harness test
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Related
|
|
104
|
+
|
|
105
|
+
- [Repository root](../../README.md)
|
|
106
|
+
- [Architecture design](../../docs/architecture.md)
|
|
107
|
+
|
|
108
|
+
## License
|
|
109
|
+
|
|
110
|
+
[MIT](../../LICENSE)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure Inbound Access Controller.
|
|
3
|
+
*
|
|
4
|
+
* NO I/O, NO imports of storage/adapters/model. This is a deterministic pure
|
|
5
|
+
* function of a resolved policy + inbound identity facts. The bridge resolves
|
|
6
|
+
* the policy, then feeds both into `authorize` and acts ONLY on the returned
|
|
7
|
+
* decision (drop when !authorized or !activated).
|
|
8
|
+
*
|
|
9
|
+
* Algorithm (§19.1 DM / §19.2 Group):
|
|
10
|
+
* - DM: disabled -> DENY; open -> ALLOW; allowlist -> allowFrom includes
|
|
11
|
+
* senderId ? ALLOW : DENY (empty allowFrom == DENY ALL, never open).
|
|
12
|
+
* - Group: groupPolicy disabled -> DENY; allowlist -> groups[conversationId]
|
|
13
|
+
* must exist; open -> defaultGroupRule must exist. The selected rule
|
|
14
|
+
* must be enabled; then the
|
|
15
|
+
* sender gate (allowlist/open); finally the ACTIVATION gate: requireMention
|
|
16
|
+
* without mentionedBot === true is NOT activated (`undefined !== true`, no
|
|
17
|
+
* fail-open).
|
|
18
|
+
*
|
|
19
|
+
* The plan separates the Security Gate (authorized) from the Activation Gate
|
|
20
|
+
* (activated). `reason` is always the single most-specific cause.
|
|
21
|
+
*/
|
|
22
|
+
import type { ChannelAccessPolicy } from '@krischoichoi/channel-core';
|
|
23
|
+
import type { InboundAccessDecision } from './decision.js';
|
|
24
|
+
export interface InboundAccessInput {
|
|
25
|
+
conversationType: 'dm' | 'group';
|
|
26
|
+
senderId: string;
|
|
27
|
+
conversationId: string;
|
|
28
|
+
/** Adapter-supplied activation fact; `undefined`/`false` is never "mentioned". */
|
|
29
|
+
mentionedBot?: boolean;
|
|
30
|
+
policy: ChannelAccessPolicy;
|
|
31
|
+
}
|
|
32
|
+
export declare class InboundAccessController {
|
|
33
|
+
authorize(input: InboundAccessInput): InboundAccessDecision;
|
|
34
|
+
private authorizeDm;
|
|
35
|
+
private authorizeGroup;
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=controller.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"controller.d.ts","sourceRoot":"","sources":["../../src/access/controller.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AACtE,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC;AAE3D,MAAM,WAAW,kBAAkB;IACjC,gBAAgB,EAAE,IAAI,GAAG,OAAO,CAAC;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,EAAE,MAAM,CAAC;IACvB,kFAAkF;IAClF,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,MAAM,EAAE,mBAAmB,CAAC;CAC7B;AAED,qBAAa,uBAAuB;IAClC,SAAS,CAAC,KAAK,EAAE,kBAAkB,GAAG,qBAAqB;IAO3D,OAAO,CAAC,WAAW;IAgBnB,OAAO,CAAC,cAAc;CAiCvB"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export class InboundAccessController {
|
|
2
|
+
authorize(input) {
|
|
3
|
+
if (input.conversationType === 'dm') {
|
|
4
|
+
return this.authorizeDm(input);
|
|
5
|
+
}
|
|
6
|
+
return this.authorizeGroup(input);
|
|
7
|
+
}
|
|
8
|
+
authorizeDm(input) {
|
|
9
|
+
const { policy, senderId } = input;
|
|
10
|
+
if (policy.dmPolicy === 'disabled') {
|
|
11
|
+
return { authorized: false, activated: false, reason: 'dm_disabled' };
|
|
12
|
+
}
|
|
13
|
+
if (policy.dmPolicy === 'open') {
|
|
14
|
+
return { authorized: true, activated: true, reason: 'allowed' };
|
|
15
|
+
}
|
|
16
|
+
// policy.dmPolicy === 'allowlist' (incl. allowFrom = []).
|
|
17
|
+
if (policy.allowFrom.includes(senderId)) {
|
|
18
|
+
return { authorized: true, activated: true, reason: 'allowed' };
|
|
19
|
+
}
|
|
20
|
+
return { authorized: false, activated: false, reason: 'user_not_allowed' };
|
|
21
|
+
}
|
|
22
|
+
authorizeGroup(input) {
|
|
23
|
+
const { policy, senderId, conversationId, mentionedBot } = input;
|
|
24
|
+
if (policy.groupPolicy === 'disabled') {
|
|
25
|
+
return { authorized: false, activated: false, reason: 'group_disabled' };
|
|
26
|
+
}
|
|
27
|
+
const rule = policy.groupPolicy === 'open'
|
|
28
|
+
? policy.defaultGroupRule
|
|
29
|
+
: policy.groups[conversationId];
|
|
30
|
+
if (!rule) {
|
|
31
|
+
return { authorized: false, activated: false, reason: 'group_not_allowed' };
|
|
32
|
+
}
|
|
33
|
+
if (rule.enabled !== true) {
|
|
34
|
+
return { authorized: false, activated: false, reason: 'group_disabled' };
|
|
35
|
+
}
|
|
36
|
+
// Sender gate.
|
|
37
|
+
if (rule.senderPolicy === 'allowlist') {
|
|
38
|
+
if (!rule.allowFrom.includes(senderId)) {
|
|
39
|
+
return { authorized: false, activated: false, reason: 'group_user_not_allowed' };
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
// senderPolicy === 'open' -> any sender in this NAMED group is authorized.
|
|
43
|
+
// Activation gate (§14): requireMention without a reliable mention is NOT
|
|
44
|
+
// activated (undefined !== true, never fail-open).
|
|
45
|
+
if (rule.requireMention && mentionedBot !== true) {
|
|
46
|
+
return { authorized: true, activated: false, reason: 'mention_required' };
|
|
47
|
+
}
|
|
48
|
+
return { authorized: true, activated: true, reason: 'allowed' };
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=controller.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"controller.js","sourceRoot":"","sources":["../../src/access/controller.ts"],"names":[],"mappings":"AAiCA,MAAM,OAAO,uBAAuB;IAClC,SAAS,CAAC,KAAyB;QACjC,IAAI,KAAK,CAAC,gBAAgB,KAAK,IAAI,EAAE,CAAC;YACpC,OAAO,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;QACjC,CAAC;QACD,OAAO,IAAI,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC;IACpC,CAAC;IAEO,WAAW,CAAC,KAAyB;QAC3C,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,GAAG,KAAK,CAAC;QAEnC,IAAI,MAAM,CAAC,QAAQ,KAAK,UAAU,EAAE,CAAC;YACnC,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,aAAa,EAAE,CAAC;QACxE,CAAC;QACD,IAAI,MAAM,CAAC,QAAQ,KAAK,MAAM,EAAE,CAAC;YAC/B,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC;QAClE,CAAC;QACD,0DAA0D;QAC1D,IAAI,MAAM,CAAC,SAAS,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;YACxC,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC;QAClE,CAAC;QACD,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,kBAAkB,EAAE,CAAC;IAC7E,CAAC;IAEO,cAAc,CAAC,KAAyB;QAC9C,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,cAAc,EAAE,YAAY,EAAE,GAAG,KAAK,CAAC;QAEjE,IAAI,MAAM,CAAC,WAAW,KAAK,UAAU,EAAE,CAAC;YACtC,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,gBAAgB,EAAE,CAAC;QAC3E,CAAC;QAED,MAAM,IAAI,GAAG,MAAM,CAAC,WAAW,KAAK,MAAM;YACxC,CAAC,CAAC,MAAM,CAAC,gBAAgB;YACzB,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC;QAClC,IAAI,CAAC,IAAI,EAAE,CAAC;YACV,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,mBAAmB,EAAE,CAAC;QAC9E,CAAC;QACD,IAAI,IAAI,CAAC,OAAO,KAAK,IAAI,EAAE,CAAC;YAC1B,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,gBAAgB,EAAE,CAAC;QAC3E,CAAC;QAED,eAAe;QACf,IAAI,IAAI,CAAC,YAAY,KAAK,WAAW,EAAE,CAAC;YACtC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;gBACvC,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,wBAAwB,EAAE,CAAC;YACnF,CAAC;QACH,CAAC;QACD,2EAA2E;QAE3E,0EAA0E;QAC1E,mDAAmD;QACnD,IAAI,IAAI,CAAC,cAAc,IAAI,YAAY,KAAK,IAAI,EAAE,CAAC;YACjD,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,kBAAkB,EAAE,CAAC;QAC5E,CAAC;QAED,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC;IAClE,CAAC;CACF"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Access decision types.
|
|
3
|
+
*
|
|
4
|
+
* `AccessDecisionReason` enumerates every fail-closed outcome the Harness
|
|
5
|
+
* Access Gate can produce. `InboundAccessDecision` is the pure controller's
|
|
6
|
+
* output: `authorized` = the security gate passed; `activated` = the activation
|
|
7
|
+
* gate passed; `reason` is the singular, most specific cause (see §19).
|
|
8
|
+
*
|
|
9
|
+
* These types are intentionally free of I/O concerns — the controller that
|
|
10
|
+
* produces them never touches storage, adapters or the model.
|
|
11
|
+
*/
|
|
12
|
+
export type AccessDecisionReason = 'allowed' | 'missing_policy' | 'invalid_policy' | 'unidentified_sender' | 'invalid_conversation' | 'dm_disabled' | 'user_not_allowed' | 'group_disabled' | 'group_not_allowed' | 'group_user_not_allowed' | 'mention_required';
|
|
13
|
+
export interface InboundAccessDecision {
|
|
14
|
+
authorized: boolean;
|
|
15
|
+
activated: boolean;
|
|
16
|
+
reason: AccessDecisionReason;
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=decision.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"decision.d.ts","sourceRoot":"","sources":["../../src/access/decision.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,MAAM,MAAM,oBAAoB,GAC5B,SAAS,GACT,gBAAgB,GAChB,gBAAgB,GAChB,qBAAqB,GACrB,sBAAsB,GACtB,aAAa,GACb,kBAAkB,GAClB,gBAAgB,GAChB,mBAAmB,GACnB,wBAAwB,GACxB,kBAAkB,CAAC;AAEvB,MAAM,WAAW,qBAAqB;IACpC,UAAU,EAAE,OAAO,CAAC;IACpB,SAAS,EAAE,OAAO,CAAC;IACnB,MAAM,EAAE,oBAAoB,CAAC;CAC9B"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"decision.js","sourceRoot":"","sources":["../../src/access/decision.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Harness-side Access Policy Resolver.
|
|
3
|
+
*
|
|
4
|
+
* Failure-closed resolution over the shared ChannelStorage. The Harness DOES
|
|
5
|
+
* NOT depend on channel-control: it reads the shared versioned policy KV via
|
|
6
|
+
* `accessPolicyStorageKey` and validates it with
|
|
7
|
+
* the shared `channelAccessPolicySchema` (both live in @krischoichoi/channel-core).
|
|
8
|
+
*
|
|
9
|
+
* Reading rules (§15):
|
|
10
|
+
* - missing JSON -> `missing`
|
|
11
|
+
* - malformed JSON -> `invalid`
|
|
12
|
+
* - unknown version / schema failure -> `invalid`
|
|
13
|
+
*
|
|
14
|
+
* There is no policy caching: every inbound reads once, so Web
|
|
15
|
+
* saves and permission revocations take effect immediately with no invalidation
|
|
16
|
+
* machinery and no stale-ACL window.
|
|
17
|
+
*/
|
|
18
|
+
import type { ChannelAccessPolicy, ChannelStorage } from '@krischoichoi/channel-core';
|
|
19
|
+
export type ResolvedAccessPolicy = {
|
|
20
|
+
state: 'present';
|
|
21
|
+
policy: ChannelAccessPolicy;
|
|
22
|
+
} | {
|
|
23
|
+
state: 'missing';
|
|
24
|
+
} | {
|
|
25
|
+
state: 'invalid';
|
|
26
|
+
error: string;
|
|
27
|
+
};
|
|
28
|
+
export interface ChannelAccessPolicyResolver {
|
|
29
|
+
resolve(channelId: string, accountId: string): Promise<ResolvedAccessPolicy>;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Default resolver backed by the shared ChannelStorage (production wiring is
|
|
33
|
+
* `ctx.channels.resources.storage`). Lazily resolves storage via a getter so it
|
|
34
|
+
* stays decoupled from the Cordis context / ChannelService lifecycle.
|
|
35
|
+
*/
|
|
36
|
+
export declare class StoredChannelAccessPolicyResolver implements ChannelAccessPolicyResolver {
|
|
37
|
+
private readonly getStorage;
|
|
38
|
+
constructor(getStorage: () => ChannelStorage);
|
|
39
|
+
resolve(channelId: string, accountId: string): Promise<ResolvedAccessPolicy>;
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=resolver.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolver.d.ts","sourceRoot":"","sources":["../../src/access/resolver.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,KAAK,EAAE,mBAAmB,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAGtF,MAAM,MAAM,oBAAoB,GAC5B;IAAE,KAAK,EAAE,SAAS,CAAC;IAAC,MAAM,EAAE,mBAAmB,CAAA;CAAE,GACjD;IAAE,KAAK,EAAE,SAAS,CAAA;CAAE,GACpB;IAAE,KAAK,EAAE,SAAS,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAExC,MAAM,WAAW,2BAA2B;IAC1C,OAAO,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAAC;CAC9E;AAED;;;;GAIG;AACH,qBAAa,iCAAkC,YAAW,2BAA2B;IACvE,OAAO,CAAC,QAAQ,CAAC,UAAU;gBAAV,UAAU,EAAE,MAAM,cAAc;IAEvD,OAAO,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,oBAAoB,CAAC;CAkBnF"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { accessPolicyStorageKey, channelAccessPolicySchema } from '@krischoichoi/channel-core';
|
|
2
|
+
/**
|
|
3
|
+
* Default resolver backed by the shared ChannelStorage (production wiring is
|
|
4
|
+
* `ctx.channels.resources.storage`). Lazily resolves storage via a getter so it
|
|
5
|
+
* stays decoupled from the Cordis context / ChannelService lifecycle.
|
|
6
|
+
*/
|
|
7
|
+
export class StoredChannelAccessPolicyResolver {
|
|
8
|
+
getStorage;
|
|
9
|
+
constructor(getStorage) {
|
|
10
|
+
this.getStorage = getStorage;
|
|
11
|
+
}
|
|
12
|
+
async resolve(channelId, accountId) {
|
|
13
|
+
const storage = this.getStorage();
|
|
14
|
+
const raw = await storage.get(accessPolicyStorageKey(channelId, accountId));
|
|
15
|
+
if (raw === undefined)
|
|
16
|
+
return { state: 'missing' };
|
|
17
|
+
let parsed;
|
|
18
|
+
try {
|
|
19
|
+
parsed = JSON.parse(raw);
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return { state: 'invalid', error: 'malformed policy JSON' };
|
|
23
|
+
}
|
|
24
|
+
const result = channelAccessPolicySchema.safeParse(parsed);
|
|
25
|
+
if (!result.success) {
|
|
26
|
+
return { state: 'invalid', error: `policy schema validation failed: ${result.error.message}` };
|
|
27
|
+
}
|
|
28
|
+
return { state: 'present', policy: result.data };
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=resolver.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolver.js","sourceRoot":"","sources":["../../src/access/resolver.ts"],"names":[],"mappings":"AAkBA,OAAO,EAAE,sBAAsB,EAAE,yBAAyB,EAAE,MAAM,4BAA4B,CAAC;AAW/F;;;;GAIG;AACH,MAAM,OAAO,iCAAiC;IACf;IAA7B,YAA6B,UAAgC;QAAhC,eAAU,GAAV,UAAU,CAAsB;IAAG,CAAC;IAEjE,KAAK,CAAC,OAAO,CAAC,SAAiB,EAAE,SAAiB;QAChD,MAAM,OAAO,GAAG,IAAI,CAAC,UAAU,EAAE,CAAC;QAClC,MAAM,GAAG,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC,sBAAsB,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC,CAAC;QAC5E,IAAI,GAAG,KAAK,SAAS;YAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC;QAEnD,IAAI,MAAe,CAAC;QACpB,IAAI,CAAC;YACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QAC3B,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,uBAAuB,EAAE,CAAC;QAC9D,CAAC;QAED,MAAM,MAAM,GAAG,yBAAyB,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;QAC3D,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;YACpB,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,oCAAoC,MAAM,CAAC,KAAK,CAAC,OAAO,EAAE,EAAE,CAAC;QACjG,CAAC;QACD,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,CAAC,IAAI,EAAE,CAAC;IACnD,CAAC;CACF"}
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentManager — the single place that touches `ctx.agents`.
|
|
3
|
+
*
|
|
4
|
+
* Ownership model (doc H0.4/H0.7):
|
|
5
|
+
* - `ctx.agents.get()` returns a live agent the bridge does NOT own — it is
|
|
6
|
+
* never disposed here.
|
|
7
|
+
* - `ctx.agents.create()` / `ctx.agents.resume()` return an `AgentHandle`;
|
|
8
|
+
* the bridge MUST hold every handle it created/resumed and dispose it on
|
|
9
|
+
* plugin unload. Owned handles live in `owned` and are disposed exactly
|
|
10
|
+
* once by `disposeAll()`.
|
|
11
|
+
*
|
|
12
|
+
* Routing is expressed as an `AgentRouteSpec`, never an `agentId` (the
|
|
13
|
+
* Harness Agent identity is the `SessionId`). `create` and `resume` receive
|
|
14
|
+
* the SAME route so provider/model/maxTokens stay identical on both paths
|
|
15
|
+
* (doc H0.5 route parity).
|
|
16
|
+
*
|
|
17
|
+
* Create-vs-resume is decided by the CALLER (the bridge), not by error-regex
|
|
18
|
+
* fallback. Persistence is an OPTIONAL capability resolved LIVE at the use
|
|
19
|
+
* site: `canResume()` tells the caller whether a sessionPersistence service
|
|
20
|
+
* is currently mounted, `exists()` probes membership via the live service,
|
|
21
|
+
* and `probePersisted()` answers the atomic unavailable/present/missing
|
|
22
|
+
* question in one resolver call (no canResume/exists TOCTOU across a
|
|
23
|
+
* persistence HMR) — corruption / unsupported-format / backend failures
|
|
24
|
+
* propagate loudly rather than being misread as "no persistence".
|
|
25
|
+
*
|
|
26
|
+
* Global concurrency gate: at most `maxConcurrency` `create()`/`resume()` calls
|
|
27
|
+
* are in flight at once across all sessions; `get()` (a live lookup) is never
|
|
28
|
+
* limited.
|
|
29
|
+
*/
|
|
30
|
+
import { SessionId, type SessionEvent, type SessionHeader } from '@deepseek-ai/dsh-session';
|
|
31
|
+
import type { SessionPersistence } from '@deepseek-ai/dsh-session-persistence';
|
|
32
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
33
|
+
import type { Agent, AgentOptions, AgentSetup, ModelSelection } from '@deepseek-ai/dsh-agent';
|
|
34
|
+
import type { ChannelLogger } from '@krischoichoi/channel-core';
|
|
35
|
+
import type { AgentRouteSpec } from './agent-router.js';
|
|
36
|
+
import type { SessionBinding } from './session-router.js';
|
|
37
|
+
/** A live agent as seen through the gateway (id + raw Agent + drive surface). */
|
|
38
|
+
export interface GatewayAgent {
|
|
39
|
+
id: string;
|
|
40
|
+
/** The raw Harness Agent — the dsh-scope key used to install Agent-scoped commands. */
|
|
41
|
+
agent: Agent;
|
|
42
|
+
followup(message: unknown): void;
|
|
43
|
+
whenIdle(): Promise<void>;
|
|
44
|
+
}
|
|
45
|
+
/** A gateway-created/resumed agent that the caller must dispose. */
|
|
46
|
+
export interface GatewayAgentHandle extends GatewayAgent {
|
|
47
|
+
dispose(): Promise<void>;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Whether a persisted session exists, probed through the optional persistence
|
|
51
|
+
* service. Implemented via membership/inspection, NEVER by matching an error
|
|
52
|
+
* message against a /persistence|persist/i regex.
|
|
53
|
+
*/
|
|
54
|
+
export interface PersistenceProbe {
|
|
55
|
+
exists(sessionId: string): Promise<boolean>;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Atomic result of probing ONE session through the LIVE persistence
|
|
59
|
+
* capability: `unavailable` (no sessionPersistence service mounted right
|
|
60
|
+
* now), `present`, or `missing`. Resolving the capability once per probe
|
|
61
|
+
* closes the canResume/exists TOCTOU window (a persistence HMR between the
|
|
62
|
+
* two lookups would otherwise pair different backends).
|
|
63
|
+
*/
|
|
64
|
+
export type PersistedProbeResult = 'unavailable' | 'present' | 'missing';
|
|
65
|
+
/** Persistence was configured for the binding, but is unavailable right now. */
|
|
66
|
+
export declare class PersistenceUnavailableError extends Error {
|
|
67
|
+
readonly sessionId: string;
|
|
68
|
+
readonly bindingKey?: string | undefined;
|
|
69
|
+
constructor(sessionId: string, bindingKey?: string | undefined);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* A durable binding references a persisted session that no longer exists
|
|
73
|
+
* (session-not-found, aligned with the official Host's
|
|
74
|
+
* "persisted identity missing -> session-not-found"). With a
|
|
75
|
+
* sessionPersistence service mounted, a missing persisted session behind an
|
|
76
|
+
* existing binding is a binding/session durability inconsistency — NEVER a
|
|
77
|
+
* first create — so resolution fails loudly instead of silently
|
|
78
|
+
* blank-recreating the same session id. Deployments WITHOUT persistence
|
|
79
|
+
* explicitly ephemeral bindings never throw this: they recreate on the recorded id.
|
|
80
|
+
*/
|
|
81
|
+
export declare class SessionNotFoundError extends Error {
|
|
82
|
+
readonly sessionId: string;
|
|
83
|
+
readonly bindingKey?: string | undefined;
|
|
84
|
+
constructor(sessionId: string, bindingKey?: string | undefined);
|
|
85
|
+
}
|
|
86
|
+
/** Caller-supplied metadata for a fresh agent create. */
|
|
87
|
+
export interface AgentCreateMeta {
|
|
88
|
+
/** Explicit working directory for the new session's header.cwd. */
|
|
89
|
+
cwd?: string;
|
|
90
|
+
}
|
|
91
|
+
/** Minimal port the bridge needs from the Harness agent runtime. */
|
|
92
|
+
export interface AgentGateway {
|
|
93
|
+
get(sessionId: string): GatewayAgent | undefined;
|
|
94
|
+
create(sessionId: string, route: AgentRouteSpec, setup?: AgentSetup, meta?: AgentCreateMeta): Promise<GatewayAgentHandle>;
|
|
95
|
+
resume(sessionId: string, route: AgentRouteSpec, setup?: AgentSetup): Promise<GatewayAgentHandle>;
|
|
96
|
+
/** Whether a sessionPersistence service is available (enables resume). */
|
|
97
|
+
canResume(): boolean;
|
|
98
|
+
/** Probe whether a persisted session exists. */
|
|
99
|
+
exists(sessionId: string): Promise<boolean>;
|
|
100
|
+
/**
|
|
101
|
+
* Atomic persistence probe (live capability resolved once). Optional on
|
|
102
|
+
* the interface so minimal test doubles can rely on the AgentManager's
|
|
103
|
+
* canResume+exists fallback; the real gateway implements it.
|
|
104
|
+
*/
|
|
105
|
+
probePersisted?(sessionId: string): Promise<PersistedProbeResult>;
|
|
106
|
+
}
|
|
107
|
+
/** A resolved agent reference handed to the caller. */
|
|
108
|
+
export interface AgentRef {
|
|
109
|
+
sessionId: string;
|
|
110
|
+
route: AgentRouteSpec;
|
|
111
|
+
/** The raw Harness Agent — the dsh-scope key used to install Agent-scoped commands. */
|
|
112
|
+
agent: Agent;
|
|
113
|
+
followup(message: unknown): void;
|
|
114
|
+
whenIdle(): Promise<void>;
|
|
115
|
+
/**
|
|
116
|
+
* Marks the caller done with the ref. Simple v1 semantics: the ref never
|
|
117
|
+
* disposes the underlying handle itself — the manager owns lifecycle and
|
|
118
|
+
* disposes each handle exactly once during `disposeAll()`.
|
|
119
|
+
*/
|
|
120
|
+
release(): void;
|
|
121
|
+
}
|
|
122
|
+
/** Filter out undefined optional agentOptions fields (route parity helper). */
|
|
123
|
+
export declare function optionsFor(route: AgentRouteSpec): AgentOptions | undefined;
|
|
124
|
+
/**
|
|
125
|
+
* Resolve a route's provider/model, falling back to Harness's default model
|
|
126
|
+
* selection when the route leaves the model unset.
|
|
127
|
+
*
|
|
128
|
+
* `{{model}}` (the persona variable that fails with "prompt variable has no
|
|
129
|
+
* value") depends only on `model`, so a route that already pins a model —
|
|
130
|
+
* with or without a provider — is left untouched. Only a missing model is
|
|
131
|
+
* filled from the default (and the default's provider rides along when the
|
|
132
|
+
* route also left provider unset).
|
|
133
|
+
*
|
|
134
|
+
* The default model is read live (per agent creation), so new channel sessions
|
|
135
|
+
* follow the user's Harness-wide default while sessions pinned to an explicit
|
|
136
|
+
* route keep their own provider/model.
|
|
137
|
+
*/
|
|
138
|
+
export declare function resolveRoute(route: AgentRouteSpec, defaultSelection: ModelSelection | undefined): AgentRouteSpec;
|
|
139
|
+
/**
|
|
140
|
+
* Probe a persisted session's existence through the persistence service's
|
|
141
|
+
* `list()` membership. A session that is absent returns `false`; any backend
|
|
142
|
+
* failure (corruption, unsupported format, connectivity) propagates loudly.
|
|
143
|
+
*
|
|
144
|
+
* The persistence service is resolved LIVE on every call (a resolver, not a
|
|
145
|
+
* startup snapshot): a sessionPersistence mounted/unmounted/replaced after
|
|
146
|
+
* the bridge started is observed on the next probe — reversible Cordis
|
|
147
|
+
* lifecycle parity, since `ctx.get` only returns currently active providers.
|
|
148
|
+
*/
|
|
149
|
+
export declare class PersistenceMembershipProbe implements PersistenceProbe {
|
|
150
|
+
private readonly resolvePersistence;
|
|
151
|
+
constructor(resolvePersistence: () => SessionPersistence | undefined);
|
|
152
|
+
exists(sessionId: string): Promise<boolean>;
|
|
153
|
+
/** Atomic probe: the live capability is resolved exactly once per call. */
|
|
154
|
+
probe(sessionId: string): Promise<PersistedProbeResult>;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Read one durable V3 session through the public read handle. A channel bridge
|
|
158
|
+
* never depends on an implementation-specific persistence reader.
|
|
159
|
+
*/
|
|
160
|
+
export declare function resolvePersistedInspection(persistence: SessionPersistence, sessionId: SessionId): Promise<PersistedInspection | undefined>;
|
|
161
|
+
/** Structural subset of the official `SessionInspection` the bridge consumes. */
|
|
162
|
+
export interface PersistedInspection {
|
|
163
|
+
readonly meta: SessionHeader;
|
|
164
|
+
readonly events: readonly SessionEvent[];
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Real gateway over `ctx.agents`. This is the only Harness import surface in
|
|
168
|
+
* the bridge (besides the `session/event` feed consumed by ReplyRouter).
|
|
169
|
+
*
|
|
170
|
+
* Persistence is an OPTIONAL capability resolved LIVE at the use site: the
|
|
171
|
+
* caller passes a resolver (`() => ctx.get('sessionPersistence')`), so
|
|
172
|
+
* `canResume()` and the existence probe reflect the service's CURRENT
|
|
173
|
+
* presence — mounting, unmounting, or replacing the service after the bridge
|
|
174
|
+
* started is observed on the next probe (never a startup snapshot).
|
|
175
|
+
*/
|
|
176
|
+
export declare class HarnessAgentGateway implements AgentGateway {
|
|
177
|
+
private readonly ctx;
|
|
178
|
+
private readonly probe;
|
|
179
|
+
private readonly resolvePersistence;
|
|
180
|
+
/**
|
|
181
|
+
* All persisted session ids (shape-tolerant, issue #8) — the `/bind`
|
|
182
|
+
* resolution universe. Empty when no persistence is mounted.
|
|
183
|
+
*/
|
|
184
|
+
listPersistedSessionIds(): Promise<string[]>;
|
|
185
|
+
/**
|
|
186
|
+
* The persisted Agent preset of one session (issue #6 route parity): the
|
|
187
|
+
* official `agentPresetProjectionDefinition` fold over the durable
|
|
188
|
+
* inspection. Undefined when no persistence / no recorded preset.
|
|
189
|
+
*/
|
|
190
|
+
persistedPresetOf(sessionId: string): Promise<string | undefined>;
|
|
191
|
+
constructor(ctx: Context, resolvePersistence?: () => SessionPersistence | undefined);
|
|
192
|
+
get(sessionId: string): GatewayAgent | undefined;
|
|
193
|
+
canResume(): boolean;
|
|
194
|
+
exists(sessionId: string): Promise<boolean>;
|
|
195
|
+
probePersisted(sessionId: string): Promise<PersistedProbeResult>;
|
|
196
|
+
/** Read Harness's live default-model selection (undefined when absent). */
|
|
197
|
+
private defaultSelection;
|
|
198
|
+
/**
|
|
199
|
+
* Resolve and mount the official Agent preset before publication, matching
|
|
200
|
+
* the Harness Host's create/resume composition boundary. Without this join,
|
|
201
|
+
* preset-scoped tools such as `ask_user_question` are invisible to the Agent.
|
|
202
|
+
*/
|
|
203
|
+
private composePreset;
|
|
204
|
+
create(sessionId: string, route: AgentRouteSpec, setup?: AgentSetup, meta?: AgentCreateMeta): Promise<GatewayAgentHandle>;
|
|
205
|
+
resume(sessionId: string, route: AgentRouteSpec, setup?: AgentSetup): Promise<GatewayAgentHandle>;
|
|
206
|
+
private wrap;
|
|
207
|
+
}
|
|
208
|
+
export declare class AgentManager {
|
|
209
|
+
private readonly gateway;
|
|
210
|
+
private readonly logger;
|
|
211
|
+
private readonly inFlight;
|
|
212
|
+
/** Handles this manager created/resumed — the ones it must dispose. */
|
|
213
|
+
private readonly owned;
|
|
214
|
+
/** Resolved refs, kept for drain (`whenIdle`) lookups. */
|
|
215
|
+
private readonly refs;
|
|
216
|
+
/** Agents that already received the one-time channel-command setup. */
|
|
217
|
+
private readonly configuredAgents;
|
|
218
|
+
/** sessionId -> binding, the reverse lookup used by the ReplyRouter. */
|
|
219
|
+
private readonly bindings;
|
|
220
|
+
private readonly maxConcurrency;
|
|
221
|
+
private active;
|
|
222
|
+
/** Waiters queued for a free concurrency slot. */
|
|
223
|
+
private readonly waiters;
|
|
224
|
+
private closed;
|
|
225
|
+
constructor(gateway: AgentGateway, logger: ChannelLogger, maxConcurrency?: number);
|
|
226
|
+
/** Whether a sessionPersistence service enables `resume`. */
|
|
227
|
+
canResume(): boolean;
|
|
228
|
+
/** Probe whether a persisted session exists. Backend failures propagate. */
|
|
229
|
+
exists(sessionId: string): Promise<boolean>;
|
|
230
|
+
/**
|
|
231
|
+
* Atomic persistence probe: resolves the LIVE capability once and answers
|
|
232
|
+
* `unavailable` / `present` / `missing` — the bridge's single decision
|
|
233
|
+
* point for recreate-vs-resume-vs-fail, immune to a persistence HMR between
|
|
234
|
+
* `canResume()` and `exists()`.
|
|
235
|
+
*/
|
|
236
|
+
probePersisted(sessionId: string): Promise<PersistedProbeResult>;
|
|
237
|
+
/**
|
|
238
|
+
* Create a NEW agent for a session (no prior binding). Calls
|
|
239
|
+
* `gateway.create` directly (under the concurrency slot), owns the handle,
|
|
240
|
+
* and returns the ref. Never resumes — the caller already decided this is a
|
|
241
|
+
* fresh conversation. Single-flight per session id.
|
|
242
|
+
*/
|
|
243
|
+
create(sessionId: string, route: AgentRouteSpec, setup?: AgentSetup, meta?: AgentCreateMeta): Promise<AgentRef>;
|
|
244
|
+
/**
|
|
245
|
+
* Resolve an agent for an EXISTING, persisted session: live `get` first; on
|
|
246
|
+
* a miss, `gateway.resume(route)`. A resume failure propagates loudly
|
|
247
|
+
* (throws) — never falls back to create and never sniffs error messages.
|
|
248
|
+
* Single-flight per session id.
|
|
249
|
+
*/
|
|
250
|
+
resolve(sessionId: string, route: AgentRouteSpec, setup?: AgentSetup): Promise<AgentRef>;
|
|
251
|
+
/**
|
|
252
|
+
* Borrow the LIVE agent for a session, when one is loaded in this process.
|
|
253
|
+
* Returns `undefined` when there is no live agent — the caller (the Session
|
|
254
|
+
* factory, for channel-session lifecycle decisions) decides what to do next
|
|
255
|
+
* (resume, or a cwd/workspace-aware recreate). Never creates or resumes and
|
|
256
|
+
* never takes ownership (the borrowed agent is never disposed here). Runs
|
|
257
|
+
* the one-time setup against a borrowed agent, exactly once.
|
|
258
|
+
*/
|
|
259
|
+
borrowIfLive(sessionId: string, route: AgentRouteSpec, setup?: AgentSetup): Promise<AgentRef | undefined>;
|
|
260
|
+
/** Register the binding associated with a session (reverse reply routing). */
|
|
261
|
+
registerBinding(binding: SessionBinding): void;
|
|
262
|
+
/** Reverse lookup for the ReplyRouter. */
|
|
263
|
+
bindingFor(sessionId: string): SessionBinding | undefined;
|
|
264
|
+
/**
|
|
265
|
+
* Live agent lookup for the /stop fast path (spec §8). Returns the raw
|
|
266
|
+
* Agent when it is currently live in this process — NEVER creates or
|
|
267
|
+
* resumes a session just to answer this probe.
|
|
268
|
+
*/
|
|
269
|
+
getLiveAgent(sessionId: string): Agent | undefined;
|
|
270
|
+
/** Resolved ref for drain purposes, if any. */
|
|
271
|
+
refFor(sessionId: string): AgentRef | undefined;
|
|
272
|
+
/** Session ids that currently have an active turn (for drain). */
|
|
273
|
+
activeSessions(): string[];
|
|
274
|
+
/**
|
|
275
|
+
* Dispose every owned handle exactly once and clear all tracking. Agents
|
|
276
|
+
* obtained via `gateway.get()` (not owned) are never disposed.
|
|
277
|
+
*/
|
|
278
|
+
disposeAll(): Promise<void>;
|
|
279
|
+
/**
|
|
280
|
+
* Dispose a single owned handle (used to roll back a create whose binding
|
|
281
|
+
* write failed afterward). No-op for handles the manager does not own.
|
|
282
|
+
*/
|
|
283
|
+
disposeSession(sessionId: string): Promise<void>;
|
|
284
|
+
/**
|
|
285
|
+
* Retire a session's reference and reverse binding and dispose it ONLY if the
|
|
286
|
+
* manager owns the handle. Borrowed / unknown agents are released
|
|
287
|
+
* from local tracking but NEVER disposed. Retiring never touches persisted
|
|
288
|
+
* history — the old session keeps its durable log.
|
|
289
|
+
*/
|
|
290
|
+
retireSession(sessionId: string): Promise<void>;
|
|
291
|
+
/**
|
|
292
|
+
* One-time channel-command setup for a BORROWED live agent. A
|
|
293
|
+
* borrowed agent never went through create/resume, so its setup could not
|
|
294
|
+
* have run at publication; run it here exactly once against the agent's
|
|
295
|
+
* scoped context. Setup failure propagates to the caller. The borrowed
|
|
296
|
+
* agent is never disposed here.
|
|
297
|
+
*/
|
|
298
|
+
private ensureBorrowedSetup;
|
|
299
|
+
private doCreate;
|
|
300
|
+
private doResolve;
|
|
301
|
+
private doBorrowIfLive;
|
|
302
|
+
private withSlot;
|
|
303
|
+
private acquireSlot;
|
|
304
|
+
private releaseSlot;
|
|
305
|
+
private makeRef;
|
|
306
|
+
}
|
|
307
|
+
/** A durable session cannot be silently recomposed under a different preset. */
|
|
308
|
+
export declare class AgentPresetConflictError extends Error {
|
|
309
|
+
readonly sessionId: string;
|
|
310
|
+
readonly requested: string;
|
|
311
|
+
readonly persisted: string;
|
|
312
|
+
constructor(sessionId: string, requested: string, persisted: string);
|
|
313
|
+
}
|
|
314
|
+
//# sourceMappingURL=agent-manager.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agent-manager.d.ts","sourceRoot":"","sources":["../src/agent-manager.ts"],"names":[],"mappings":"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"}
|