mirror of
https://gh-proxy.org/https://github.com/klinsmaya/workdsh.git
synced 2026-11-10 02:17:22 +08:00
feat: add governed session host ingress
This commit is contained in:
@@ -28,7 +28,7 @@
|
||||
| --- | --- | --- | --- |
|
||||
| 领域公开契约 | **0.1** | `[email protected]` | P0-04 实施中;包含 Host identity/access/audit、Session owner/runtime binding 与排空契约 |
|
||||
| 本地身份提供方 | **0.1** | `[email protected]` | P0-05 实施中;官方 Storage Domain 持久化可信单用户 Profile,不用于远程认证 |
|
||||
| 资源授权 | **0.1** | `[email protected].2` | P1-09 实施中;持久 Session owner、RuntimeBinding 与官方工具前置授权/结果审计 |
|
||||
| 资源授权 | **0.1** | `[email protected].3` | P1-09 实施中;持久 Session owner、受控 Session Host 入口、RuntimeBinding 与官方工具授权/审计 |
|
||||
| 审计 | **0.1** | `[email protected]` | P1-09 实施中;官方 Storage Domain 持久审计、排空和敏感引用拒绝 |
|
||||
| 技能管理 | **0.1** | `[email protected]` | 默认/本地管理闭环完成;公共市场不在当前范围,企业服务端与管理 Web 列入后期 ToDo |
|
||||
| 工作台 | 0.1 | `[email protected]` | 官方 Sidebar/Conversation 组合切片 |
|
||||
|
||||
@@ -93,7 +93,7 @@ DOC-06 官方文档审查已完成 127/127;C01 的目录、正文、真实 Age
|
||||
|
||||
当前已实施 `workdsh-contracts` 0.1,固化 ActorContext、Organization、Membership、ResourceOwner、AccessGrant、RuntimeBinding、AuditEvent 及 identity/access/audit 服务边界,见 [ADR-0016](adr/0016-governance-contracts-first.md)。该包无 UI、数据库和传输,不复制 Harness User、Permission Preset、Approval、Sandbox 或 Session scope。
|
||||
|
||||
可信本地 IdentityProvider、Access 和 Audit 的 Host 基础服务已经建立,并以两主体、两组织、管理员访问他人私有资源、显式 grant/revoke、冲突和冷重启负例验证。官方工具流水线已通过 Session owner/runtime binding 接入同一 ActorContext/access/audit guard。下一步接 Session Controller 创建/恢复、文件、Remote 与成员撤权后的在途取消。团队远程入口继续关闭;企业 SSO、服务端与管理 Web 保留到后期阶段。全路径证据完成后才关闭 D01、进入 D02 基础插件和工作台最小版本。
|
||||
可信本地 IdentityProvider、Access 和 Audit 的 Host 基础服务已经建立,并以两主体、两组织、管理员访问他人私有资源、显式 grant/revoke、冲突和冷重启负例验证。官方工具流水线已通过 Session owner/runtime binding 接入同一 ActorContext/access/audit guard;受控 Session Host 入口会先绑定 owner 再委托官方 Controller 创建,并在恢复 Agent 前重新授权。下一步为该 Host 入口生成 WorkDSH Remote 并确保企业组合不暴露裸 Session Remote,再接文件、其他 Remote 与成员撤权后的在途取消。团队远程入口继续关闭;企业 SSO、服务端与管理 Web 保留到后期阶段。全路径证据完成后才关闭 D01、进入 D02 基础插件和工作台最小版本。
|
||||
|
||||
仍保留的运行差异探针按 [审查收尾清单](research/harness-review-closure.md) 独立执行:Plan 提交时机、Shell/PTY/Code runtime 停稳、feedback/inspect/lookup 主体限制,以及 Subagent/Goal 恢复。它们不能因 Skill 0.1 完成而删除,也不要求为验证创建假业务 UI。
|
||||
|
||||
|
||||
+2
-2
@@ -12,9 +12,9 @@ D01 当前推进 P0-04/P0-05。`[email protected]` 定义服务端
|
||||
|
||||
P0-05 同步推进到 `[email protected]`。本地 provider 已注册为 Cordis Host Service,通过官方 `ctx.storageDomain` 原子保存主体、个人组织与 owner 成员关系;冷启动保持同一 revision,配置与持久身份冲突时拒绝启动。principal/organization 仍只来自 Host 配置,每次请求生成独立 requestId,输入夹带的伪造身份字段会被忽略。Harness 官方匿名安装 ID 明确只用于遥测关联,未被误用为用户身份。
|
||||
|
||||
`[email protected]` 和 `[email protected].2` 已建立真实 Cordis Host Service。两者使用官方 Storage Domain 分域持久化;Access 只经 IdentityService 查询成员关系,实行组织隔离、资源 owner、显式 grant/revoke、revision 冲突检测,并在返回授权结果前写入审计。新增独立 Session owner Domain 与官方工具流水线桥接:`tools/pre-execute` 做 Host 身份解析和授权,`tools/result` 记录最终成功/失败,`session/flush` 排空审计。个人 Profile 可首次调用自动绑定;关闭自动绑定后未登记 Session 失败关闭,owner 替换被拒绝,冷重启保持绑定。当前尚未接入 Session Controller 创建/恢复、文件、Remote 与成员撤权后的在途取消,因此 D01/P1-09 保持进行中,团队远程入口继续关闭。证据见 [Access 与 Audit 验证](evidence/d01-access-audit.md)。
|
||||
`[email protected]` 和 `[email protected].3` 已建立真实 Cordis Host Service。两者使用官方 Storage Domain 分域持久化;Access 只经 IdentityService 查询成员关系,实行组织隔离、资源 owner、显式 grant/revoke、revision 冲突检测,并在返回授权结果前写入审计。独立 Session owner Domain 与官方工具流水线桥接已生效;新增 `workdshSessionAccess` Host 入口,在调用官方 Session Controller 创建前先保留不可替换的 owner,并在恢复 Agent 前按最新成员关系和 grant 重新授权。个人 Profile 可首次工具调用自动绑定;企业式组合应关闭该回退并只从受控入口创建/恢复。该入口当前是 Host Service,尚未生成并替换外部 Session Remote;文件、其他 Remote 与成员撤权后的在途取消也未接入,因此 D01/P1-09 保持进行中,团队远程入口继续关闭。证据见 [Access 与 Audit 验证](evidence/d01-access-audit.md)。
|
||||
|
||||
当前根构建、类型检查、31/31 集成测试、2/2 规划测试、计划清单、版本锁定和补丁格式检查均通过。
|
||||
当前根构建、类型检查、32/32 集成测试、2/2 规划测试、计划清单、版本锁定和补丁格式检查均通过。
|
||||
|
||||
skills alpha.23 / bundle alpha.35 收口认证 Fetch 的超时与取消。Client 对列表和修改请求设置有界超时,上传打包、流读取、预检和确认安装共用 `AbortSignal`;界面在上传和安装阶段均提供真实取消入口。Host 主动取消阻塞中的流读取并清理私有暂存目录,在文件遍历、摘要、复制及最终原子重命名前检查中止;原子发布完成后按成功结算,避免技能已安装但界面报告取消。中途上传取消与提交前取消/重试已进入 19/19 集成回归。该能力继续使用 Harness Connection 的认证 exact Fetch 扩展面,不增加第二套传输。
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
- Access 与 Audit 属于不同 Storage Domain,当前后端不提供跨 Domain 事务。grant/revoke 先记录 outcome=unknown 的操作意图,写入授权事实后再记录 succeeded,避免把部分成功伪装成原子成功。
|
||||
- Session owner 使用独立 `workdsh_runtime_binding` Domain 持久化,已绑定 owner 不能被另一主体替换;当前 RuntimeBinding 在每次执行时根据最新 membership 与 grant 重新生成。
|
||||
- 工具执行按官方扩展点接入:`tools/pre-execute` 完成异步身份解析和授权,`tools/result` 观察不可变最终结果,`session/flush` 与 Cordis disposer 等待排队审计落盘。
|
||||
- `workdshSessionAccess` 在调用官方 `sessionController.create()` 前生成稳定 Session id 并持久绑定 owner;显式 id 首次绑定前通过官方 `inspect()` 拒绝已有但无 owner 的 Session,创建失败则保留同一 owner 的可重试预留。恢复 Agent 前先按当前 membership/grant 调用 `resolveRuntime()`,拒绝时不触达官方 Controller。
|
||||
|
||||
## 验证
|
||||
|
||||
@@ -29,7 +30,8 @@
|
||||
- 个人 Profile 的首次官方 Agent 工具调用可以自动绑定 Session;未授权成员的工具正文不会运行,显式取得 Session use grant 后可以运行。
|
||||
- 工具成功、工具抛错和策略拒绝分别形成 succeeded、failed 和 denied 审计;最终结果审计携带本次 authorizationRevision。
|
||||
- 关闭个人自动绑定时,未在受信入口登记 owner 的 Session 失败关闭;显式绑定经冷重启保持,替换 owner 被拒绝。
|
||||
- 已验证未登记 owner 的历史 Session 采用失败、owner 记录先于官方 Session 创建、创建失败后的同 owner 重试、异主体接管拒绝、未授权恢复不激活 Agent,以及 grant 后恢复成功。
|
||||
|
||||
## 尚未完成
|
||||
|
||||
该证据覆盖 Host 服务直接调用及官方工具流水线,但尚未让 Session Controller 的创建/恢复入口显式登记 owner,也未接入文件/资产服务、Typert Remote/Connection、订阅和成员撤权后的在途取消。企业身份、服务器端管理 Web、组织目录和审计查询 UI 属于后期范围。D01、P0-04、P0-05 与 P1-09 因此保持进行中。
|
||||
该证据覆盖 Host 服务、受控 Session 创建/恢复入口及官方工具流水线。受控 Session 入口仍是内部 Host Service,尚未生成 WorkDSH Remote 并从企业组合中撤下裸 `session` Remote;文件/资产服务、其他 Typert Remote/Connection、订阅和成员撤权后的在途取消也未覆盖。企业身份、服务器端管理 Web、组织目录和审计查询 UI 属于后期范围。D01、P0-04、P0-05 与 P1-09 因此保持进行中。
|
||||
|
||||
@@ -15,4 +15,4 @@
|
||||
|
||||
## 验收与下一步
|
||||
|
||||
本地 identity、access 和 audit 已消费本包接口,并完成官方工具流水线的首个治理接入;下一步接 Session Controller、文件和 Remote。完成对应 PLAN 任务及 [验收矩阵](../../docs/ACCEPTANCE.md) 场景后才完成 P0-04/P0-05。企业 Skill 架构见 [ADR 0015](../../docs/adr/0015-skill-control-plane-and-runtime-projection.md)。
|
||||
本地 identity、access 和 audit 已消费本包接口,并完成受控 Session Host 入口和官方工具流水线的首个治理接入;下一步生成受控 Session Remote,再接文件和其他 Remote。完成对应 PLAN 任务及 [验收矩阵](../../docs/ACCEPTANCE.md) 场景后才完成 P0-04/P0-05。企业 Skill 架构见 [ADR 0015](../../docs/adr/0015-skill-control-plane-and-runtime-projection.md)。
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
# 0.1.0-alpha.3
|
||||
|
||||
- 增加 `workdshSessionAccess` 受控 Host 入口,在调用官方 Session Controller 创建前持久绑定稳定 owner。
|
||||
- Session 恢复在激活官方 Agent 前重新检查当前成员关系和 grant,拒绝请求不会触达 Controller。
|
||||
- 创建失败保留不可替换的 owner 预留,允许同一主体安全重试并阻止其他主体接管 Session id。
|
||||
- 显式 Session id 在首次绑定前使用官方冷读 `inspect()` 排除已存在但无 owner 的会话,阻止历史会话被补绑接管。
|
||||
|
||||
# 0.1.0-alpha.2
|
||||
|
||||
- 增加独立 Storage Domain 保存不可替换的 Session owner binding,冷重启后仍可解析当前 RuntimeBinding。
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 资源授权
|
||||
|
||||
状态:**0.1 实现中**。`0.1.0-alpha.2` 提供 Host 侧资源授权、持久 grant、Session owner/runtime binding、预期修订更新、撤权和官方工具流水线审计;尚未接入 Session Controller 创建/恢复、文件与 Remote 全入口。
|
||||
状态:**0.1 实现中**。`0.1.0-alpha.3` 提供 Host 侧资源授权、持久 grant、Session owner/runtime binding、受控 Session Host 入口、预期修订更新、撤权和官方工具流水线审计;尚未生成企业外部 Session Remote,也未接入文件与其他 Remote 入口。
|
||||
|
||||
- 实现阶段:P0/P1
|
||||
- 主任务:P1-09,详见 [开发计划](../../../docs/PLAN.md)
|
||||
@@ -19,4 +19,6 @@
|
||||
|
||||
`ToolAccessBridge` 使用官方 `tools/pre-execute` 做异步授权,并用最终 `tools/result` 形成结果审计;`session/flush` 和插件卸载会等待审计排空。个人 Profile 可在首次工具调用时绑定 Session,企业组合必须关闭该选项并在受信创建入口显式调用 `bindSession()`。
|
||||
|
||||
下一步将相同服务装配到 Session Controller 创建/恢复、文件、Remote 与订阅入口,并验证成员撤权后的在途取消。完成对应 PLAN 任务及 [验收矩阵](../../../docs/ACCEPTANCE.md) 场景后才结束 P1-09。
|
||||
`SessionAccessBridge` 是 WorkDSH 内部 Host 入口:创建时先生成 Session id 并持久绑定 owner,再委托官方 `sessionController.create()`;显式 id 首次绑定前用官方 `inspect()` 拒绝采用已有但无 owner 的 Session。恢复时先按当前身份和 grant 授权,再委托 `resolveAgent()`。它不会修改或包装官方服务,也不会在本版本声明为生成式 Remote。企业组合后续必须只暴露 WorkDSH 受控 Remote,并撤下外部裸 Session Remote。
|
||||
|
||||
下一步生成受控 Session Remote,再将相同服务装配到文件、其他 Remote 与订阅入口,并验证成员撤权后的在途取消。完成对应 PLAN 任务及 [验收矩阵](../../../docs/ACCEPTANCE.md) 场景后才结束 P1-09。
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "workdsh-plugin-access",
|
||||
"version": "0.1.0-alpha.2",
|
||||
"version": "0.1.0-alpha.3",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"exports": {
|
||||
@@ -24,12 +24,14 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/cordis": "^4.0.2",
|
||||
"@deepseek-ai/dsh-api-session-controller": "^0.1.5-rc.1",
|
||||
"@deepseek-ai/dsh-storage-domain": "^0.1.5-rc.1",
|
||||
"@deepseek-ai/dsh-session": "^0.1.5-rc.1",
|
||||
"@deepseek-ai/dsh-tools": "^0.1.5-rc.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@deepseek-ai/cordis": "4.0.2",
|
||||
"@deepseek-ai/dsh-api-session-controller": "0.1.5-rc.1",
|
||||
"@deepseek-ai/dsh-storage-domain": "0.1.5-rc.1",
|
||||
"@types/node": "22.19.0",
|
||||
"@deepseek-ai/dsh-session": "0.1.5-rc.1",
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { createHash, randomUUID } from 'node:crypto';
|
||||
import { Context, Service } from '@deepseek-ai/cordis';
|
||||
import type {
|
||||
SessionCreateRequest,
|
||||
SessionCreateValue,
|
||||
} from '@deepseek-ai/dsh-api-session-controller';
|
||||
import { defineDomain, domainTable, type KvTable } from '@deepseek-ai/dsh-storage-domain';
|
||||
import type {} from '@deepseek-ai/dsh-session';
|
||||
import type { SessionId } from '@deepseek-ai/dsh-session';
|
||||
import type { PreToolDecision, ToolExecution, ToolExecutionResult, ToolExecutionToken } from '@deepseek-ai/dsh-tools';
|
||||
import { z } from 'zod';
|
||||
import {
|
||||
@@ -25,6 +29,8 @@ import {
|
||||
type SessionOwnerBinding,
|
||||
} from 'workdsh-contracts';
|
||||
|
||||
type SessionAgentResult = Awaited<ReturnType<Context['sessionController']['resolveAgent']>>;
|
||||
|
||||
const bounded = z.string().min(1).max(256).refine((value) => !/[\u0000-\u001f]/.test(value));
|
||||
const resourceRefSchema: z.ZodType<ResourceRef> = z.object({ domain: bounded, id: bounded, revision: bounded.optional() });
|
||||
const accessGrantSchema: z.ZodType<AccessGrant> = z.object({
|
||||
@@ -63,6 +69,7 @@ declare module '@deepseek-ai/cordis' {
|
||||
workdshIdentity: IdentityService;
|
||||
workdshAudit: AuditService;
|
||||
workdshAccess: AccessManager;
|
||||
workdshSessionAccess: SessionAccessBridge;
|
||||
workdshToolAccess: ToolAccessBridge;
|
||||
}
|
||||
}
|
||||
@@ -375,6 +382,114 @@ export interface ToolAccessBridgeConfig {
|
||||
readonly autoBindPersonalSessions?: boolean;
|
||||
}
|
||||
|
||||
export interface SessionAccessBridgeConfig {
|
||||
readonly runtimeId?: string;
|
||||
readonly isolation?: RuntimeBindingRequest['isolation'];
|
||||
}
|
||||
|
||||
/** Trusted Host ingress for WorkDSH Session creation and resume. */
|
||||
export class SessionAccessBridge extends Service {
|
||||
static inject = ['sessionController', 'workdshIdentity', 'workdshAccess', 'workdshAudit'];
|
||||
private readonly runtimeId: string;
|
||||
private readonly isolation: RuntimeBindingRequest['isolation'];
|
||||
|
||||
constructor(ctx: Context, config: SessionAccessBridgeConfig = {}) {
|
||||
super(ctx, 'workdshSessionAccess');
|
||||
this.runtimeId = config.runtimeId ?? `workdsh-session-runtime-${randomUUID()}`;
|
||||
this.isolation = config.isolation ?? 'local-trusted';
|
||||
}
|
||||
|
||||
async create(request: SessionCreateRequest, signal?: AbortSignal): Promise<SessionCreateValue> {
|
||||
signal?.throwIfAborted();
|
||||
const sessionId = request.sessionId ?? (`session-${randomUUID()}` as SessionId);
|
||||
const actor = await this.ctx.workdshIdentity.resolve({ sessionId: String(sessionId) }, signal);
|
||||
if (request.sessionId !== undefined && !this.ctx.workdshAccess.sessionOwner(String(sessionId))) {
|
||||
let exists = false;
|
||||
try {
|
||||
await this.ctx.sessionController.inspect(sessionId, signal);
|
||||
exists = true;
|
||||
} catch (error) {
|
||||
if (!hasErrorCode(error, 'session/not-found')) throw error;
|
||||
}
|
||||
if (exists) {
|
||||
await this.audit(actor, 'session.create', 'denied', 'access/unbound-session-adoption');
|
||||
throw new GovernanceContractError(
|
||||
'access/unbound-session-adoption',
|
||||
'An existing Session without a trusted owner binding cannot be adopted.',
|
||||
);
|
||||
}
|
||||
}
|
||||
await this.ctx.workdshAccess.bindSession(actor, {
|
||||
sessionId: String(sessionId),
|
||||
...(request.workspaceId === undefined ? {} : { workspaceId: String(request.workspaceId) }),
|
||||
}, signal);
|
||||
signal?.throwIfAborted();
|
||||
try {
|
||||
const result = await this.ctx.sessionController.create({ ...request, sessionId });
|
||||
if (String(result.sessionId) !== String(sessionId)) {
|
||||
throw new GovernanceContractError('access/session-id-mismatch', 'Session Controller returned a different Session identity.');
|
||||
}
|
||||
await this.audit(actor, 'session.create', 'succeeded', 'session/create-succeeded');
|
||||
return result;
|
||||
} catch (error) {
|
||||
await this.audit(actor, 'session.create', 'failed', errorCode(error, 'session/create-failed'));
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async resolveAgent(sessionId: SessionId, signal?: AbortSignal): Promise<SessionAgentResult> {
|
||||
signal?.throwIfAborted();
|
||||
const id = String(sessionId);
|
||||
const actor = await this.ctx.workdshIdentity.resolve({ sessionId: id }, signal);
|
||||
const owner = this.ctx.workdshAccess.sessionOwner(id);
|
||||
await this.ctx.workdshAccess.resolveRuntime(actor, {
|
||||
sessionId: id,
|
||||
runtimeId: this.runtimeId,
|
||||
isolation: this.isolation,
|
||||
...(owner?.workspaceId === undefined ? {} : { workspaceId: owner.workspaceId }),
|
||||
}, signal);
|
||||
signal?.throwIfAborted();
|
||||
try {
|
||||
const result = await this.ctx.sessionController.resolveAgent(sessionId);
|
||||
if ('error' in result) {
|
||||
await this.audit(actor, 'session.resume', 'failed', errorCode(result.error, 'session/resume-failed'));
|
||||
} else {
|
||||
await this.audit(actor, 'session.resume', 'succeeded', 'session/resume-succeeded');
|
||||
}
|
||||
return result;
|
||||
} catch (error) {
|
||||
await this.audit(actor, 'session.resume', 'failed', errorCode(error, 'session/resume-failed'));
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private async audit(actor: ActorContext, action: string, outcome: AuditEvent['outcome'], code: string): Promise<void> {
|
||||
await this.ctx.workdshAudit.append({
|
||||
id: randomUUID(),
|
||||
occurredAt: new Date().toISOString(),
|
||||
requestId: actor.requestId,
|
||||
principalId: actor.principalId,
|
||||
organizationId: actor.organizationId,
|
||||
action,
|
||||
target: sessionResource(actor.sessionId ?? 'unknown'),
|
||||
outcome,
|
||||
code,
|
||||
...(actor.sessionId ? { sessionId: actor.sessionId } : {}),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function errorCode(error: unknown, fallback: string): string {
|
||||
if (error && typeof error === 'object' && 'code' in error && typeof error.code === 'string') {
|
||||
return safeAuditReference(error.code);
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function hasErrorCode(error: unknown, code: string): boolean {
|
||||
return Boolean(error && typeof error === 'object' && 'code' in error && error.code === code);
|
||||
}
|
||||
|
||||
/** Binds the official tool pipeline to Host identity, Session ownership, Access and Audit. */
|
||||
export class ToolAccessBridge extends Service {
|
||||
static inject = ['tools', 'workdshIdentity', 'workdshAccess', 'workdshAudit'];
|
||||
|
||||
Generated
+4
-1
@@ -392,6 +392,9 @@ importers:
|
||||
'@deepseek-ai/cordis':
|
||||
specifier: 4.0.2
|
||||
version: 4.0.2(@deepseek-ai/[email protected])(@deepseek-ai/[email protected])
|
||||
'@deepseek-ai/dsh-api-session-controller':
|
||||
specifier: 0.1.5-rc.1
|
||||
version: 0.1.5-rc.1(2c50dad3f37865829d6010a3cf949f80)
|
||||
'@deepseek-ai/dsh-session':
|
||||
specifier: 0.1.5-rc.1
|
||||
version: 0.1.5-rc.1(@deepseek-ai/[email protected])(@deepseek-ai/[email protected](@deepseek-ai/[email protected])(@deepseek-ai/[email protected](@deepseek-ai/[email protected])))
|
||||
@@ -4593,7 +4596,7 @@ snapshots:
|
||||
'@deepseek-ai/dsh-workspace': 0.1.5-rc.1(0d21b8701233c0d1ccd262c73d8d4b61)
|
||||
'@deepseek-ai/schemastery': 3.18.2
|
||||
mime-types: 3.0.2
|
||||
zod: 4.6.1
|
||||
zod: 4.4.3
|
||||
optionalDependencies:
|
||||
'@deepseek-ai/dsh-jobs': 0.1.5-rc.1(@deepseek-ai/[email protected])(@deepseek-ai/[email protected](011efc65c149ca53f6af28580be53483))(@deepseek-ai/[email protected](@deepseek-ai/[email protected]))(@deepseek-ai/[email protected](@deepseek-ai/[email protected]))(@deepseek-ai/[email protected](@deepseek-ai/[email protected])(@deepseek-ai/[email protected](@deepseek-ai/[email protected])(@deepseek-ai/[email protected](@deepseek-ai/[email protected]))))
|
||||
'@deepseek-ai/dsh-session-persistence': 0.1.5-rc.1(@deepseek-ai/[email protected])(@deepseek-ai/[email protected](@deepseek-ai/[email protected]))(@deepseek-ai/[email protected](@deepseek-ai/[email protected])(@deepseek-ai/[email protected](@deepseek-ai/[email protected])(@deepseek-ai/[email protected](@deepseek-ai/[email protected]))))(@deepseek-ai/[email protected](@deepseek-ai/[email protected]))
|
||||
|
||||
@@ -8,7 +8,7 @@ import Storage from '@deepseek-ai/dsh-storage';
|
||||
import * as JsonStorage from '@deepseek-ai/dsh-storage-json';
|
||||
import * as StorageDomain from '@deepseek-ai/dsh-storage-domain';
|
||||
import Tools from '@deepseek-ai/dsh-tools';
|
||||
import { AccessManager, ToolAccessBridge } from '../../packages/plugins/access/dist/index.js';
|
||||
import { AccessManager, SessionAccessBridge, ToolAccessBridge } from '../../packages/plugins/access/dist/index.js';
|
||||
import { AuditJournal } from '../../packages/plugins/audit/dist/index.js';
|
||||
|
||||
const membershipDirectory = new Map([
|
||||
@@ -187,3 +187,81 @@ test('explicit Session ownership survives restart and cannot be replaced', async
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('trusted Session ingress binds before create, preserves retry ownership and authorizes resume', async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), 'workdsh-session-ingress-'));
|
||||
const selected = new Map();
|
||||
const createCalls = [];
|
||||
const resumeCalls = [];
|
||||
const failCreate = new Set(['session-retry']);
|
||||
const existingSessions = new Set(['session-orphan']);
|
||||
let ctx;
|
||||
try {
|
||||
ctx = await boot(root, selected, { autoBindPersonalSessions: false });
|
||||
ctx.provide('sessionController', {
|
||||
async inspect(sessionId) {
|
||||
if (existingSessions.has(String(sessionId))) return { header: { id: sessionId }, events: [] };
|
||||
throw Object.assign(new Error('fixture Session not found'), { code: 'session/not-found' });
|
||||
},
|
||||
async create(request) {
|
||||
const sessionId = String(request.sessionId);
|
||||
assert.equal(ctx.workdshAccess.sessionOwner(sessionId)?.ownerPrincipalId, 'owner-a', 'owner is durable before official create');
|
||||
createCalls.push(sessionId);
|
||||
if (failCreate.delete(sessionId)) throw Object.assign(new Error('fixture create failed'), { code: 'session/create-fixture' });
|
||||
existingSessions.add(sessionId);
|
||||
return { sessionId: request.sessionId };
|
||||
},
|
||||
async resolveAgent(sessionId) {
|
||||
resumeCalls.push(String(sessionId));
|
||||
return { agent: { id: sessionId } };
|
||||
},
|
||||
});
|
||||
await ctx.plugin(SessionAccessBridge, { runtimeId: 'session-ingress-test' });
|
||||
|
||||
await assert.rejects(
|
||||
ctx.workdshSessionAccess.create({ sessionId: 'session-orphan', cwd: '/tmp' }),
|
||||
{ code: 'access/unbound-session-adoption' },
|
||||
);
|
||||
assert.equal(ctx.workdshAccess.sessionOwner('session-orphan'), undefined);
|
||||
|
||||
await assert.rejects(ctx.workdshSessionAccess.create({ sessionId: 'session-retry', cwd: '/tmp' }), /fixture create failed/);
|
||||
assert.equal(ctx.workdshAccess.sessionOwner('session-retry')?.ownerPrincipalId, 'owner-a');
|
||||
|
||||
selected.set('session-retry', ['member-a', 'personal-owner']);
|
||||
await assert.rejects(
|
||||
ctx.workdshSessionAccess.create({ sessionId: 'session-retry', cwd: '/tmp' }),
|
||||
{ code: 'access/session-owner-conflict' },
|
||||
);
|
||||
assert.deepEqual(createCalls, ['session-retry'], 'an ownership conflict never reaches the official controller');
|
||||
|
||||
selected.set('session-retry', ['owner-a', 'personal-owner']);
|
||||
assert.equal((await ctx.workdshSessionAccess.create({ sessionId: 'session-retry', cwd: '/tmp' })).sessionId, 'session-retry');
|
||||
const generated = await ctx.workdshSessionAccess.create({ cwd: '/tmp' });
|
||||
assert.match(String(generated.sessionId), /^session-/);
|
||||
assert.equal(ctx.workdshAccess.sessionOwner(String(generated.sessionId))?.ownerPrincipalId, 'owner-a');
|
||||
|
||||
selected.set('session-retry', ['member-a', 'personal-owner']);
|
||||
await assert.rejects(ctx.workdshSessionAccess.resolveAgent('session-retry'), { code: 'access/not-granted' });
|
||||
assert.deepEqual(resumeCalls, [], 'unauthorized resume never reaches the official controller');
|
||||
await ctx.workdshAccess.putGrant(
|
||||
{ principalId: 'owner-a', organizationId: 'personal-owner', requestId: 'session-grant', resolvedBy: 'test-session-identity' },
|
||||
{ organizationId: 'personal-owner', ownerPrincipalId: 'owner-a', scope: 'personal' },
|
||||
{
|
||||
id: 'session-retry-use-member-a', organizationId: 'personal-owner', subjectPrincipalId: 'member-a',
|
||||
resource: { domain: 'session', id: 'session-retry' }, actions: ['use'], revision: 'session-retry-grant-1',
|
||||
},
|
||||
);
|
||||
assert.ok('agent' in await ctx.workdshSessionAccess.resolveAgent('session-retry'));
|
||||
assert.deepEqual(resumeCalls, ['session-retry']);
|
||||
|
||||
await ctx.workdshToolAccess.flush();
|
||||
const events = ctx.workdshAudit.snapshot();
|
||||
assert.ok(events.some((event) => event.action === 'session.create' && event.outcome === 'failed' && event.code === 'session/create-fixture'));
|
||||
assert.ok(events.some((event) => event.action === 'session.create' && event.outcome === 'denied' && event.code === 'access/unbound-session-adoption'));
|
||||
assert.ok(events.some((event) => event.action === 'session.create' && event.outcome === 'succeeded'));
|
||||
assert.ok(events.some((event) => event.action === 'session.resume' && event.outcome === 'succeeded'));
|
||||
} finally {
|
||||
if (ctx) await ctx.fiber.dispose();
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user