@margay/ccl-core 1.5.0-alpha.0 → 1.5.1-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/README.md +1 -1
- package/dist/cli.js +95197 -74935
- package/dist/sdk/background.d.ts +48 -0
- package/dist/sdk/background.js +87 -0
- package/dist/sdk/backgroundRunTypes.d.ts +101 -0
- package/dist/sdk/backgroundRunTypes.js +1 -0
- package/dist/sdk/commandAdapter.d.ts +77 -0
- package/dist/sdk/commandAdapter.js +180 -0
- package/dist/sdk/commandProcess.d.ts +27 -0
- package/dist/sdk/commandProcess.js +130 -0
- package/dist/sdk/executionGuidance.d.ts +7 -0
- package/dist/sdk/executionGuidance.js +37 -0
- package/dist/sdk/index.d.ts +16 -1
- package/dist/sdk/index.js +10 -0
- package/dist/sdk/inputProvenance.d.ts +13 -0
- package/dist/sdk/inputProvenance.js +26 -0
- package/dist/sdk/mcpServer.d.ts +9 -5
- package/dist/sdk/mcpServer.js +51 -9
- package/dist/sdk/operations/command.d.ts +24 -0
- package/dist/sdk/operations/command.js +48 -0
- package/dist/sdk/operations/host.d.ts +3 -0
- package/dist/sdk/operations/host.js +376 -0
- package/dist/sdk/operations/json.d.ts +6 -0
- package/dist/sdk/operations/json.js +24 -0
- package/dist/sdk/operations/reference.d.ts +15 -0
- package/dist/sdk/operations/reference.js +19 -0
- package/dist/sdk/operations/store.d.ts +26 -0
- package/dist/sdk/operations/store.js +300 -0
- package/dist/sdk/operations/types.d.ts +171 -0
- package/dist/sdk/operations/types.js +8 -0
- package/dist/sdk/pendingApprovalIdentity.d.ts +35 -0
- package/dist/sdk/pendingApprovalIdentity.js +27 -0
- package/dist/sdk/permissionDelegation.d.ts +19 -0
- package/dist/sdk/permissionDelegation.js +11 -0
- package/dist/sdk/query.d.ts +4 -2
- package/dist/sdk/query.js +236 -56
- package/dist/sdk/queryInput.js +7 -0
- package/dist/sdk/queryLifecycle.d.ts +19 -0
- package/dist/sdk/queryLifecycle.js +93 -0
- package/dist/sdk/queryLifecycleTypes.d.ts +160 -0
- package/dist/sdk/queryLifecycleTypes.js +47 -0
- package/dist/sdk/runtimeResources.d.ts +197 -0
- package/dist/sdk/runtimeResources.js +36 -0
- package/dist/sdk/scheduledOccurrenceTypes.d.ts +103 -0
- package/dist/sdk/scheduledOccurrenceTypes.js +12 -0
- package/dist/sdk/session.d.ts +4 -3
- package/dist/sdk/session.js +19 -14
- package/dist/sdk/types.d.ts +21 -2
- package/docs/README.md +5 -0
- package/docs/background-runs.md +229 -0
- package/docs/ccl-duo-agent.md +1 -4
- package/docs/ccl-ide-usage.md +7 -70
- package/docs/cli-installation.md +37 -0
- package/docs/command-tool-recovery.md +62 -0
- package/docs/dev-quickstart.md +29 -7
- package/docs/error-recovery-usage.md +30 -2
- package/docs/features/business-facts.md +2 -4
- package/docs/hook-input-provenance.md +120 -0
- package/docs/index.md +30 -96
- package/docs/local-analytics-quickstart.md +11 -28
- package/docs/mcp-tool-recovery.md +82 -0
- package/docs/operations-client-integration.md +1 -7
- package/docs/peer-development-review.md +0 -1
- package/docs/releases/1.5.0-alpha.3.md +17 -0
- package/docs/releases/1.5.0-alpha.4.md +16 -0
- package/docs/releases/1.5.0-alpha.5.md +26 -0
- package/docs/releases/1.5.0-alpha.6.md +15 -0
- package/docs/releases/1.5.1-alpha.1.md +11 -0
- package/docs/releases/1.5.1-alpha.2.md +17 -0
- package/docs/releases/1.5.1-alpha.md +22 -0
- package/docs/runtime-resources.md +137 -0
- package/docs/self-hosted-product-events.md +6 -35
- package/docs/shared-skill-resources.md +50 -0
- package/examples/background-runs/agents/background-reviewer.md +6 -0
- package/examples/background-runs/agents/background-worker.md +6 -0
- package/examples/background-runs/worker-reviewer.mjs +42 -0
- package/examples/command-recovery/durable-server.mjs +44 -0
- package/examples/command-recovery/receipt-command.mjs +40 -0
- package/examples/runtime-resources/catalog.ts +68 -0
- package/package.json +25 -14
- package/sdk/API.md +236 -1
- package/sdk/README.md +6 -0
- package/docs/agent-owned-error-recovery-ux-2026-10-04.md +0 -146
- package/docs/analytics-capabilities-2026-10-08.md +0 -223
- package/docs/releases/1.4.5-rc.1.md +0 -39
- package/docs/releases/1.4.5-rc.10.md +0 -50
- package/docs/releases/1.4.5-rc.2.md +0 -26
- package/docs/releases/1.4.5-rc.3.md +0 -9
- package/docs/releases/1.4.5-rc.6.md +0 -11
- package/docs/releases/1.4.5-rc.7.md +0 -14
- package/docs/releases/1.4.5-rc.8.md +0 -18
- package/docs/releases/1.4.5-rc.9.md +0 -22
- package/docs/releases/1.4.6-beta.0.md +0 -54
- package/docs/releases/1.5.0-alpha.0.md +0 -44
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# 1.5.0-alpha.5
|
|
2
|
+
|
|
3
|
+
本版本修复记忆访问、技能上下文恢复和工具发现的通用宿主问题,并提供 CLI/SDK 共用的工具执行指引。通过 `alpha` 通道交付,`latest` 保持不变。
|
|
4
|
+
|
|
5
|
+
- **记忆按需读取**:保留 MEMORY.md 索引,取消启动及逐轮无条件扫描、相关性筛选和正文预取。用户或当前指令要求时,通过工具定向查找。记忆提取先判断是否有值得保存的信息,再读索引及相关主题核对;记忆内容仍需按实际来源判断准确性。
|
|
6
|
+
- **技能发现与恢复**:普通子 agent 首次请求可获得有权限且受长度限制的技能目录;模型或上下文预算变化会刷新目录。按资源身份区分同名技能,保留实际来源和调用标识,修复压缩及会话恢复时的技能契约丢失。
|
|
7
|
+
- **共享执行指引**:公开 SDK 导出 `getExecutionGuidance(toolNames)`,默认主助手、Explore 和 Plan 共用执行与调查的边界;保留岗位身份及用户定制。普通文件操作优先合适的专用工具,脚本等使用 Bash,允许获授权的缺工具回退。GitHub URL 不再强制使用 gh,PR 操作按依赖顺序执行。
|
|
8
|
+
- **MCP 工具元数据**:SDK 将 `searchHint` 转发到工具发现元数据,并提供可选 `alwaysLoad` 以请求首轮展示工具定义;这些选项不授予操作权限。
|
|
9
|
+
- **本地追踪记录**:修复关闭 OpenTelemetry exporter 时 Perfetto 请求、工具与权限记录因共享 span 身份而互相覆盖的问题。
|
|
10
|
+
|
|
11
|
+
共享指引的组合方式见 [SDK API](../../sdk/API.md)。默认系统提示已经包含该指引;自管 `systemPrompt` 的应用可以显式追加。CLI 与 SDK 应固定为同一包版本,不另复制一套 preset,也不覆盖用户 persona。
|
|
12
|
+
|
|
13
|
+
## 升级与兼容
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
npm install -g @margay/ccl-core@1.5.0-alpha.5
|
|
17
|
+
ccl --version
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
SDK 应用更新 `@margay/ccl-core` 依赖后,重启实际运行进程。新增技能身份元数据只对包含该元数据的记录生效;旧记录缺少身份时不会猜测补齐,也不会重写旧会话。
|
|
21
|
+
|
|
22
|
+
## 已知限制
|
|
23
|
+
|
|
24
|
+
- 本版本不宣称彻底消除模型的多余探查,也没有证明稳定提速;#55 的行为问题继续跟踪。
|
|
25
|
+
- `Agent(run_in_background: true)` 返回启动信息,不等于同一 SDK Session 已释放下一条输入。需要前台继续响应时,应用应维护独立 worker Session 并消费其结果。持久队列、重启恢复、取消和业务交付回执需要应用按实际入口接通并验收。
|
|
26
|
+
- 不改动现有权限边界;专用工具被拒绝不能通过 Bash 或另一个接口绕过。指引存在不保证模型在所有任务中都遵循。
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# 1.5.0-alpha.6
|
|
2
|
+
|
|
3
|
+
本版本包含 [alpha.5 的通用宿主修复](1.5.0-alpha.5.md),并修复一条后台任务终态通知遗漏路径。
|
|
4
|
+
|
|
5
|
+
后台 agent 可能先进入终态,再等待清理并发送完成通知。此前,`TaskOutput`
|
|
6
|
+
在此期间读取结果,会抑制后续模型通知,却没有通知 SDK,导致消费者仍显示任务运行中。
|
|
7
|
+
现在这条读取路径会补发一次结构化 SDK 终态,保留原工具调用 ID 和输出路径;
|
|
8
|
+
失败仍为 `failed`,取消映射为 `stopped`,已通知或重复读取不会再次发送。
|
|
9
|
+
|
|
10
|
+
SDK 应启用 `includeRaw: true` 消费 `task_started/task_progress/task_notification`。
|
|
11
|
+
用法和失败边界见 [SDK API](../../sdk/API.md)。本次未新增持久任务查询、崩溃后重放
|
|
12
|
+
或自动重派能力;任务状态也不代替业务核验和交付回执。
|
|
13
|
+
|
|
14
|
+
CLI 与 SDK 应安装相同固定版本,重启需要使用新版本的进程。通过 `alpha` 标签交付,
|
|
15
|
+
不更改 `latest`。本版本不宣称解决 #55 的全部工具绕路或证明稳定提速。
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# 1.5.1-alpha.1
|
|
2
|
+
|
|
3
|
+
本预发布版包含 [1.5.1-alpha 的能力与限制](1.5.1-alpha.md),并包含以下宿主改进。npm 安装使用明确版本或 `alpha` 标签;发布状态以 registry 为准,`latest` 不随本次预发布更新。
|
|
4
|
+
|
|
5
|
+
- 后台权限通知在入队前与实际消费时核对原任务、工具调用和当前状态。已经批准、拒绝、取消或终态的旧请求不再要求重复决定,后续结果可以继续送达。TUI、SDK/headless 和 query 中途消费路径共用该规则。仍未批准的请求不自动放行;读取失败不视为已解决,事件历史与原身份保留。见[后台任务说明](../background-runs.md)。
|
|
6
|
+
- 原生 Write 与工作流文件工具支持显式 create-only 写入:目标已存在时原子拒绝,不覆盖原文件。具体参数与平台限制见 [SDK API](../../sdk/API.md)。
|
|
7
|
+
- 查询诊断关联输入、停止检查点和终态结果,便于区分主会话停止与后台任务状态;诊断不证明远端执行者当前仍存活。
|
|
8
|
+
|
|
9
|
+
本次权限通知修复通过相关单元/集成测试,以及真实 SDK 和有头 TUI 模型场景:已决权限不再送入前台要求重复批准,原任务只发布一次并送达真实回执;未批准近邻保持等待。它不替代 Pepper 业务接入验收,不承诺所有模型措辞或历史 incomplete_stream 问题均已解决。
|
|
10
|
+
|
|
11
|
+
升级时保持 CLI 与 SDK 使用同一版本。旧运行进程不会热更新;先核对原任务状态,正常结束并重启后采用新代码。不要因状态未知而换键重派写操作。Windows 和断电恢复等未验证边界沿用上一版说明。
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# 1.5.1-alpha.2
|
|
2
|
+
|
|
3
|
+
本预发布版在 [1.5.1-alpha.1](1.5.1-alpha.1.md) 的基础上修复 #60 的周期后台任务权限交接及通知恢复。安装使用明确版本或 `alpha` 标签;npm 实际发布状态以 registry 为准,`latest` 保持不变。
|
|
4
|
+
|
|
5
|
+
- 新建后台 schedule 按任务保存创建会话的有效权限模式与可委派规则。共享 daemon 不因某项任务全局提权;执行时重新加载持久配置。真正需要审批的工具暂停并通知原会话,避免已授权动作因 SDK 回调路由而无声挂起。
|
|
6
|
+
- 挂起先冻结执行、保存 transcript 和原调用凭据,再确认退出;断连不冒充用户拒绝。批准绑定原 occurrence、pending revision、requestId、toolUseId 与稳定 actionId。恢复上下文保留完整原参数,执行前继续严格校验,防止同名工具或参数变化误用批准。
|
|
7
|
+
- TUI、持续运行的 SDK/headless 主循环接收原会话的待批及结果通知;离线后可恢复原会话补收。已有决定的旧审批在队列消费前再次核对,不反复询问。同一 pending 跨周期合并不会重复申请批准。
|
|
8
|
+
- SDK 提供 occurrence 查询、事件分页、精确审批和取消;模型沿用 `BackgroundRun occurrence_id`。`CronCreate operation: reauthorize` 显式更新未来调度的授权及接收会话,既有待批 occurrence 不被新授权升级,也不吞掉新 revision 的触发。
|
|
9
|
+
- 审批决定及恢复意图先持久化再启动执行;owner 崩溃后保留 interrupted 和原收据,不盲目重放结果未知的调用。
|
|
10
|
+
|
|
11
|
+
入口、参数和模式差异见[后台任务说明](../background-runs.md)。旧 `daemon approve <id> allow` 缺少精确身份,改为返回迁移说明;消费者应使用 `approve-occurrence` 或 SDK `approveOccurrence`。actionId 由调用方生成并保存,同一决定重试复用,不需要用户填写。
|
|
12
|
+
|
|
13
|
+
相关单元/集成测试、实际 CLI/SDK/daemon 调用、执行器强杀恢复及真实模型有头 TUI 流程已验证。真实模型曾在恢复时删掉原 Write 参数的尾部换行,严格守卫拒绝;补全原参数上下文后复测成功,未放松审批比较。测试证明这些观察到的行为,不保证任意模型始终遵从提示。
|
|
14
|
+
|
|
15
|
+
升级保持 CLI/SDK 同版本。已有进程不会热更新;先查询原任务状态,正常结束后重启采用新代码。旧无委派任务保持原行为,可显式重交接;不要因状态未知更换任务键重派写操作。读取旧记录不等于允许新代码自动恢复它,原身份与当前权限仍须匹配。
|
|
16
|
+
|
|
17
|
+
一次性 headless 退出后不承诺向关闭的输出流推送;外部消息送达、业务阶段恢复与最终验收由 assistant/产品负责。取消不能撤回已发生的外部效果,不承诺跨任意系统 exactly-once。TUI 强杀的全部交叉场景、Windows 与断电恢复未单独验证。#58 的宿主取消来源契约与 #59 的视觉预算恢复仍待处理,不包含在本次修复声明中。
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# 1.5.1-alpha
|
|
2
|
+
|
|
3
|
+
以下描述 `1.5.1-alpha` 工件的能力和已知限制;安装时固定该版本,不用旧 alpha 标签包代替。发布是否完成以 npm registry 的版本与工件完整性记录为准。
|
|
4
|
+
|
|
5
|
+
包含此前 alpha.6 的后台任务终态通知修复,并增加两项通用工具行为改进:
|
|
6
|
+
|
|
7
|
+
- 显式启用 Read 去重时,只有原工具结果仍完整保留在当前上下文中,才省略重复正文。结果已清除、截断或恢复缓存缺少来源时,重新返回文件内容。默认去重开关不变。
|
|
8
|
+
- 主助手、Explore/Plan 和 SDK 公开共享指引统一说明:参数已知、无结果依赖或副作用冲突的必要调用可同轮提出;依赖操作等待前一结果。保留权限限制和必要核验,不为组成批次增加无关操作。完整替换 system 的 SDK 调用方仍需显式追加共享指引。
|
|
9
|
+
|
|
10
|
+
相关测试与有限真实模型案例已验证调用顺序、权限拒绝和产物;不保证所有模型无回归,也不宣称已经消除全部重复探查或获得稳定提速。
|
|
11
|
+
|
|
12
|
+
#55 的通用宿主实现已增加 [持久后台任务](../background-runs.md):独立执行者、按稳定键查询、事件分页补收、权限与取消记录,以及执行器退出后关联原工具操作的恢复入口。SDK、CLI 和模型工具共用同一核心;大量事件不再要求一次读完整历史,SDK 流式解码保留跨 stdout 字节块的 UTF-8 内容。原操作如何核对与重放见 [命令恢复](../command-tool-recovery.md) 和 [MCP 恢复](../mcp-tool-recovery.md)。
|
|
13
|
+
|
|
14
|
+
已有 macOS 进程故障、ACK 丢失与原任务查回验证;本轮脚本化模型通过 SDK 和有头 TUI 验证后台运行时连续回答两条前台消息,前台退出后后台完成且未重复执行。它证明宿主机制,不代表真实模型提速或 Pepper 已接入新接口。Pepper 新入口业务验收、Windows 和断电恢复仍未完成;不会自动重跑整个 agent 或未知写操作。后台完成不等于业务成果通过核验。
|
|
15
|
+
|
|
16
|
+
文件工具错误在普通终端模式保留具体原因,避免只显示“Error writing file”而隐藏“必须先读取”等原错误。升级须采用同一固定 CLI/SDK 版本。正在运行的旧进程不会热更新;先核对原任务状态,再正常退出并重启。旧 owner 不支持新提交接口时明确拒绝,不自动接管或重跑任务。
|
|
17
|
+
|
|
18
|
+
记忆提取未完成提示保留 API/传输、轮次上限、工具结果、取消等实际失败类别;同类失败不刷屏,新类别仍可见。保留原保存确认和重试条件,不提高5轮上限、不增加全目录扫描。具体解释见 [错误恢复说明](../error-recovery-usage.md)。
|
|
19
|
+
|
|
20
|
+
CCL 自建提示词不再默认引导到上游产品文档或内部反馈频道,请求正文 system 中的 `x-anthropic-billing-header` attribution 文本也已移除(不是 HTTP header)。真实协议字段、旧配置名与用户上下文保留。真实 `claude-*` 命名网关目标和直连 OAuth 的认证兼容性未验证,不能假设网关会剥离该文本或所有下游均不依赖它。
|
|
21
|
+
|
|
22
|
+
已知限制:Operation 查询返回最后保存的状态,不证明远端任务仍存活;真实模型曾把这一快照误述为当前活性。未知效果须核对原操作,不靠换键重派解决。真实案例验证保留过迟执行、reviewer 工具错误和终端宏未正常收尾等失败,不以本版本发布声明这些业务行为已全部修复。
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# Runtime resource discovery (v1)
|
|
2
|
+
|
|
3
|
+
Introduced in 1.5.0-alpha.3. Verify the version and runtime response of the actual CLI used by the host.
|
|
4
|
+
|
|
5
|
+
CCL exposes `runtime_resources` in the `system/init` event and the `initialize`
|
|
6
|
+
control response. A repeated `initialize` resolves a fresh snapshot without
|
|
7
|
+
resetting the conversation or reapplying initial hooks/system prompts. The JS SDK
|
|
8
|
+
exposes the field through `initializationResult()` and `reinitialize()`:
|
|
9
|
+
|
|
10
|
+
```ts
|
|
11
|
+
import { RuntimeResourcesSchema } from '@margay/ccl-core/sdk'
|
|
12
|
+
|
|
13
|
+
// `query` is an existing, still-live Query handle.
|
|
14
|
+
const response = await query.reinitialize()
|
|
15
|
+
const parsed = RuntimeResourcesSchema.safeParse(response.runtime_resources)
|
|
16
|
+
if (!parsed.success || parsed.data.status !== 'available') {
|
|
17
|
+
// Discard old resource information; keep the normal permission UI.
|
|
18
|
+
} else {
|
|
19
|
+
// Bind to this trusted connection, current session and host account generation.
|
|
20
|
+
// Describe the resources in UI; do not add their paths to an allowlist.
|
|
21
|
+
const resources = parsed.data.resources
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
`initializationResult()` returns a cached response; it is not a freshness check.
|
|
26
|
+
The existing Query lifetime applies: after a single query's result, use the next
|
|
27
|
+
live handle rather than invoking control methods on an ended query. Direct
|
|
28
|
+
stream-json hosts may send another `initialize` while the connection remains open.
|
|
29
|
+
Hosts already consuming raw `system/init` can read the same field there; REPL
|
|
30
|
+
bridge initialization uses the same builder. Ordinary terminal use is unchanged.
|
|
31
|
+
|
|
32
|
+
Use `@margay/ccl-core@1.5.0-alpha.4` or a later release that includes this
|
|
33
|
+
contract. The interface was introduced in alpha.3, which was withdrawn from npm;
|
|
34
|
+
alpha.4 retains it with corrected package contents. Published alpha.2 does not
|
|
35
|
+
include it. Upgrade the CLI actually launched by the host and start a new backend
|
|
36
|
+
process, then check the response field and schema version. Updating a separate
|
|
37
|
+
global CLI does not update a Desktop application's bundled dependency.
|
|
38
|
+
|
|
39
|
+
## Meaning of the fields
|
|
40
|
+
|
|
41
|
+
| Field | Meaning |
|
|
42
|
+
| --- | --- |
|
|
43
|
+
| `schema_version` | `1`; missing/unknown versions require the existing permission flow. |
|
|
44
|
+
| `authorization` | Always `none`. The snapshot grants no access. |
|
|
45
|
+
| `runtime_id` | Random per loaded backend process; changes on restart. Not a PID or authenticated user ID. |
|
|
46
|
+
| `snapshot_id` | New for every emitted snapshot, including failures. |
|
|
47
|
+
| `status` | `available` means the current context was resolved. `unavailable` has `reason: context_unavailable`, no identity/context/path fields and an empty resource list. |
|
|
48
|
+
| `context_id` | Process-local opaque consistency marker over the emitted identity/resources, effective config home, gateway/account revision and explicit gateway environment tuple. Not monotonic, not an authorization token and not comparable across runtimes. |
|
|
49
|
+
| `session_id` | Current session after restore; an init event's outer session ID must agree. |
|
|
50
|
+
| `project_root` | Current stable project context; not proof that resources belong exclusively to it. |
|
|
51
|
+
| `original_cwd` | Runtime original working directory, used by the scratch resolver. |
|
|
52
|
+
| `cwd` | Current working directory at observation time. |
|
|
53
|
+
| `resources` | Only this context's `memory` and `scratch` descriptors, not sibling sessions or account inventories. |
|
|
54
|
+
|
|
55
|
+
Each descriptor has `kind`, `scope`, `lifetime`, `supported_operations` and `status`.
|
|
56
|
+
Only an `available` descriptor has a `path`. A disabled feature has
|
|
57
|
+
`status: disabled, reason: feature_disabled`; resolver failures have
|
|
58
|
+
`status: unavailable, reason: path_unavailable`. One failed resource does not hide
|
|
59
|
+
the other. Supported operations (`read`, `write`) describe file capability, not
|
|
60
|
+
permission to perform that operation in this task. Raw exception messages are
|
|
61
|
+
not part of this interface.
|
|
62
|
+
|
|
63
|
+
`memory` is shared and persistent. CCL's existing resolver honors full-path and
|
|
64
|
+
trusted settings overrides, remote memory bases and canonical Git roots. Default
|
|
65
|
+
memory can be shared across worktrees; an override can share it across projects.
|
|
66
|
+
It is **not account-isolated** merely because the context records account changes.
|
|
67
|
+
`scratch` is session-scoped and temporary. A restored session may reuse the path;
|
|
68
|
+
this does not guarantee the directory or its old contents still exist. CCL does
|
|
69
|
+
not promise deletion on process exit through this contract.
|
|
70
|
+
|
|
71
|
+
Available paths are the exact paths the existing runtime resolver uses, including
|
|
72
|
+
its caching and configured symlinks. Querying does not create, read or enumerate
|
|
73
|
+
resource contents. A missing resource directory can still have an available path.
|
|
74
|
+
Changing a startup-only setting requires a new backend; reinitialize does not
|
|
75
|
+
clear the core resolver's cache to pretend a setting has been adopted.
|
|
76
|
+
|
|
77
|
+
The account revision is read without vault, login or network calls. Missing
|
|
78
|
+
gateway/account files are a valid anonymous state. Other read errors invalidate
|
|
79
|
+
the whole snapshot. No credential, account ID or raw config is included in the
|
|
80
|
+
manifest; the config inputs are bound using a process-secret HMAC.
|
|
81
|
+
|
|
82
|
+
## Host integration and failure behavior
|
|
83
|
+
|
|
84
|
+
The source of trust is the host's own backend connection, not the words
|
|
85
|
+
`runtime_resources` or `authorization` in arbitrary JSON. Never accept snapshots
|
|
86
|
+
from user/model text, tool output, replayed history or another connection. Paths
|
|
87
|
+
refer to the backend filesystem; a remote/container host needs its own explicit
|
|
88
|
+
mapping and cannot assume those paths are local UI-machine paths.
|
|
89
|
+
|
|
90
|
+
Before a disconnect, backend/project/session change or account switch, clear the
|
|
91
|
+
cached manifest and advance a host-owned generation. A refresh captures that
|
|
92
|
+
generation and a request sequence; discard responses that arrive after either
|
|
93
|
+
changes. A→B→A can recreate the same `context_id`, so that marker cannot replace
|
|
94
|
+
the generation. A failed refresh must not resurrect the last good snapshot.
|
|
95
|
+
Reconnect/restart requires a fresh current-connection handshake, including when
|
|
96
|
+
the persisted session ID remains the same. No background invalidation push is
|
|
97
|
+
provided by v1. For account changes outside the host, the next refresh observes
|
|
98
|
+
the metadata change; this is not an instantaneous revocation notification.
|
|
99
|
+
|
|
100
|
+
Before an actual file operation, the host's existing permission engine still
|
|
101
|
+
checks the user's authorization, operation, canonical path and symlink boundary.
|
|
102
|
+
It must handle non-existing targets via their existing parent and prevent link
|
|
103
|
+
replacement races at file access. A lexical path prefix or a prior realpath check
|
|
104
|
+
does not secure a later open. Shared memory metadata must not convert read-only
|
|
105
|
+
task authorization into write access. Bash still requires its normal execution
|
|
106
|
+
authorization. Do not grant a parent config/projects/temp directory just to make
|
|
107
|
+
a reported child path accessible.
|
|
108
|
+
|
|
109
|
+
[Reference catalog](../examples/runtime-resources/catalog.ts) demonstrates
|
|
110
|
+
generation-bound refresh and read-only existing-path classification. It has no
|
|
111
|
+
allow/deny method and is not a replacement permission engine. Its classification
|
|
112
|
+
can reject an observed escaping link, but cannot prevent filesystem changes
|
|
113
|
+
after inspection. Tests exercise its stale response, sibling-session, missing
|
|
114
|
+
target and symlink cases separately from the CLI producer tests.
|
|
115
|
+
|
|
116
|
+
## Desktop handoff and verification boundary
|
|
117
|
+
|
|
118
|
+
The inspected Desktop reference is commit
|
|
119
|
+
`3c0f24b2ccde0f018bd6ea8855b8d51125f99012`, particularly
|
|
120
|
+
`src/agent/margayCli/MargayCliAgent.ts`. Its `MargayCliAgent.handleSDKMessage`
|
|
121
|
+
already receives init, and `MargayCliConnection.sendControlRequest` is the refresh
|
|
122
|
+
entry. The permission request handler currently invokes workspace/foreign-path
|
|
123
|
+
and symlink checks. That version does not consume this new manifest.
|
|
124
|
+
|
|
125
|
+
Desktop integration must capture the versioned manifest from its trusted init or
|
|
126
|
+
initialize response, bind it to the manager's connection/session/account
|
|
127
|
+
generation, invalidate on transitions, and supply descriptive context to its
|
|
128
|
+
permission UI without extending authorization. Keep its existing path and
|
|
129
|
+
symlink enforcement; show the existing confirmation when resources are unknown.
|
|
130
|
+
|
|
131
|
+
CCL's real CLI tests and reference catalog tests do not prove Desktop UI/manager
|
|
132
|
+
integration. The required Desktop acceptance remains pending until its consumer
|
|
133
|
+
is implemented and run with the candidate bundle: default/custom directories,
|
|
134
|
+
restore/reconnect/account switch, authorized resource reads, shared-memory write
|
|
135
|
+
limits, sibling sessions, escaping links and late/unknown responses. Record the
|
|
136
|
+
actual Desktop commit, candidate bundle hash, tool/permission frames and UI
|
|
137
|
+
result. Do not close #53 as end-to-end complete using only the producer tests.
|
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
# Self-hosted product events
|
|
1
|
+
# Self-hosted product events
|
|
2
2
|
|
|
3
|
-
The CLI persists and uploads `session_started` and the real `analytics.query` exposure/invocation/completion lifecycle to its authenticated Margay gateway. `analytics query <definition.json>` queries the isolated self-hosted platform through that gateway. `analytics gate off|auto|status` combines a local restriction with the fresh remote decision. See the [local quickstart](local-analytics-quickstart.md).
|
|
4
|
-
|
|
5
|
-
For the locally upgraded analytics build, use `ccl` as below. From this source checkout, build with `npm run build` and replace `ccl` with `node dist/cli.js`. The retained `margay` launcher was not upgraded to this analytics build:
|
|
3
|
+
The CLI persists and uploads `session_started` and the real `analytics.query` exposure/invocation/completion lifecycle to its authenticated Margay gateway. `analytics query <definition.json>` queries the isolated self-hosted platform through that gateway. `analytics gate off|auto|status` combines a local restriction with the fresh remote decision. See the [local quickstart](local-analytics-quickstart.md).
|
|
6
4
|
|
|
7
5
|
```sh
|
|
8
6
|
ccl analytics enable
|
|
@@ -21,22 +19,7 @@ Only an exact authenticated durable-acceptance ACK deletes sent records. Lost AC
|
|
|
21
19
|
|
|
22
20
|
Legacy GrowthBook/Datadog/1P and other legacy background traffic remain suppressed by `CCL_DISABLE_LEGACY_NONESSENTIAL_TRAFFIC`. The CLI no longer overwrites the user's explicit NONESSENTIAL setting. `isClientTelemetryDisabled` checks the original user privacy signals; the old privacy helpers continue suppressing old routes. Optional Anthropic preconnect and official-registry fetch now honor that boundary as well.
|
|
23
21
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
## Evidence and reproduction
|
|
27
|
-
|
|
28
|
-
`python3 deploy/analytics/cli_upload_admission.py` uses the retained owned gateway and platform. It runs production-bundle management commands, and compiles the real CLI entrypoint with an explicitly test-only credential-store adapter (`tests/fixtures/analytics-cli-entry.ts`), avoiding changes to the OS keychain. Account JWT validation, accountGatewayFetch, actual HTTP/outbox/capture and independent platform reads remain real. Its network guard rejects non-local traffic and records origins/paths without credentials. No model provider is called: the synthetic account's conversational command exits with insufficient quota; this is not a successful-model or business-outcome test.
|
|
29
|
-
|
|
30
|
-
Latest full result: `.local/analytics/evidence/cli-upload/20261007T210524Z/result.json`. It proves a real session event, a real gateway 202 discarded before the uploader receives it, durable pending data across CLI process exit, a fresh `analytics flush` with the identical event ID, one resulting platform UUID on three forced reader queries, opt-out with no new event, fast paths without events, and actual logout removing the spool. The original OS credential-store adapter itself is outside this test's boundary.
|
|
31
|
-
|
|
32
|
-
`logout-regression.log` has 115 passing targeted account/upload tests, including three real writer processes preserving 45 distinct queued events. The earlier `unit-final.log` additionally covers contract/privacy/capacity and existing byte-budget tests; `selected-bun-final.log` contains the graph-selected ten registry cases. `build-complete.log` is the full successful CLI+SDK build and existing gate-presence check. Full TypeScript checking still reports 11 pre-existing errors; a clean HEAD archive with the same dependency tree produces exactly the same diagnostics (`typecheck-baseline.log` vs `typecheck-final.log`).
|
|
33
|
-
|
|
34
|
-
Failures are retained: the first upload test used a wrongly shaped JWT fixture (corrected to its actual helper signature); the first two live runs exposed an optional Anthropic preconnect that ignored the privacy boundary. The preconnect now checks it, with focused tests and a successful real CLI run showing only local gateway requests. No external request was allowed by the fixture guard.
|
|
35
|
-
|
|
36
|
-
The R10 evidence above is historical. R13 added actual query lifecycle validation, R14 verified the basic SDK employee, R15 verified local/remote controls and ordinary CLI fault isolation, and R16 confirmed the current bounded local scope. Full analytics coverage, OS-vault E2E for this analytics fixture, production account-policy migration and complete automation remain deferred gaps; they are not newly imposed completion gates.
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
## Querying analytics (R13)
|
|
22
|
+
## Querying analytics
|
|
40
23
|
|
|
41
24
|
Use an active website account for the configured local analysis gateway. Collection is optional: querying works with collection disabled and with either explicit privacy environment setting. No native platform key is stored by this client.
|
|
42
25
|
|
|
@@ -44,14 +27,14 @@ Use an active website account for the configured local analysis gateway. Collect
|
|
|
44
27
|
ccl analytics query definition.json
|
|
45
28
|
```
|
|
46
29
|
|
|
47
|
-
Example
|
|
30
|
+
Example only: replace event names and dates with your gateway's registered scope.
|
|
48
31
|
|
|
49
32
|
```json
|
|
50
33
|
{
|
|
51
34
|
"protocolVersion": 1,
|
|
52
35
|
"kind": "funnel",
|
|
53
36
|
"dateRange": {"from": "2026-09-27T00:00:00Z", "to": "2026-09-30T12:00:00Z"},
|
|
54
|
-
"steps": [{"event": "
|
|
37
|
+
"steps": [{"event": "example_started"}, {"event": "example_completed"}],
|
|
55
38
|
"windowMinutes": 5
|
|
56
39
|
}
|
|
57
40
|
```
|
|
@@ -71,16 +54,4 @@ Every command evaluates a fresh boolean decision; there is no persisted permissi
|
|
|
71
54
|
|
|
72
55
|
The result envelope strips unexpected native fields and verifies that the returned definition matches the request after defaults. Feature responses must match issuer/account/environment and TTL, and be internally consistent. Feature HTTP is limited to 5 seconds/16 KiB; analysis HTTP to 45 seconds/1 MiB; aborts cancel body reading. These client checks supplement the server's fresh authorization; they do not grant authority themselves.
|
|
73
56
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
```sh
|
|
77
|
-
python3 deploy/analytics/cli_analysis_admission.py \
|
|
78
|
-
--gateway-runtime "$PWD/.local/analytics/ccl-gateway-runtime-ysf2jl8i" \
|
|
79
|
-
--baseline ../margay-gateway-feature-analytics-agent/.dual-agent/analytics/runtime-20261007T215031Z/result.json
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
The owned synthetic JWT must still be valid. This driver never prints or refreshes it automatically. It uses the same explicit private vault adapter as R10, isolates HOME/TMP/config/process groups and allows only the local gateway. It temporarily expires only the owned synthetic membership and restores its exact prior expiry in `finally`. The driver itself uses a separate native reader to verify counts.
|
|
83
|
-
|
|
84
|
-
`evidence/cli-analysis/20261007T221035Z-live/result.json` under `.local/analytics` is PASS: seven real compiled-CLI queries match R12 (funnel 10→4/6, retention 10/4/0 vs 10/0/0, paths and endpoint weights, A/B 504/109 vs 496/294 samples/conversions). Each of three lifecycle names has count=distinct UUIDs=7 on three forced native reads. One real 202 is lost, three persisted events survive process exit, and a fresh CLI flush preserves their IDs without duplicate rows. Privacy overrides/collection disabled still return results without new events; primary membership expiry rejects before invocation; invalid JSON schema/FIFO fail; logout removes the spool.
|
|
85
|
-
|
|
86
|
-
Two live driver failures remain recorded: stdout/stderr mixed a Node punycode warning with the JSON answer, then the same warning accompanied the error JSON on stderr. The driver now captures both streams separately and requires exactly one structured JSON record. An earlier output-directory collision failed before any owned subprocess or temporary root was started and is explicitly recorded as a reconstructed note. These were harness parsing failures, not accepted product runs.
|
|
57
|
+
Server deployment and query admission must be validated against your own gateway environment. The client contract does not imply that the server has enabled every query kind.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Shared skill resources: identity, updates and session binding
|
|
2
|
+
|
|
3
|
+
Introduced in 1.5.0-alpha.3. Check the actual CLI build/version and returned command metadata; earlier local candidates sharing an old version string are not evidence that an installed package supports this contract.
|
|
4
|
+
|
|
5
|
+
Use the existing `pluginDirs` or `skillsDirs` entry points. `MARGAY_SKILLS_DIR` alone is not the supported SDK contract. Explicit plugins use a stable `.claude-plugin/plugin.json` name and relative `skills` paths, or the standard `skills/` directory. The SDK adapts `skillsDirs` through that same plugin loader.
|
|
6
|
+
|
|
7
|
+
## Selecting a resource
|
|
8
|
+
|
|
9
|
+
Discover through `initialize`, `reload_plugins`, or the skill catalog. Preserve `invocation_id`, `internal_name`, `display_name`, `source`, `origin`, `loaded_from`, `version`, and availability. `origin` identifies the actual skill directory when available. These fields are metadata, not authorization. A directory can be visible but its tools still require permission.
|
|
10
|
+
|
|
11
|
+
Prefer `invocation_id` when the host explicitly selects a method. Resolution checks the exact ID, then a unique internal name, then a unique display name/alias. Conflicts are errors; an old ID does not fall back to an unrelated display name. Friendly descriptions remain available for agent selection.
|
|
12
|
+
|
|
13
|
+
An invocation ID identifies a source/location, not immutable bytes or business permission. `version: null` means unknown. A plugin package version is not automatically a skill content version. Renaming display text does not change location identity; moving to a different physical version directory does. The host must rediscover and explicitly update a saved binding when moving a resource.
|
|
14
|
+
|
|
15
|
+
The candidate SDK gives each canonical skills root a deterministic plugin name while keeping each wrapper directory private to one transport. Repeated sessions on the same actual root have stable names/IDs. Rediscover plugin skill IDs once when upgrading: adding canonical skill origins changes previous IDs, and old random SDK wrapper names are not aliases for the new identity. Custom explicit plugin names continue to work. Direct callers of `prepareSkillsWrappers` own and must remove their returned wrappers; managed SDK transports remove only wrappers they created, after exit/close or failed construction. They never remove caller plugin roots or skill contents.
|
|
16
|
+
|
|
17
|
+
## Publishing and consuming versions
|
|
18
|
+
|
|
19
|
+
For concurrently shared resources, publish complete immutable version directories. Keep the active directory and every version referenced by a running or resumable session intact. Prepare changes outside the active version, validate the entire package, then activate a completed version through the publisher's established protocol. Do not rewrite files underneath running readers.
|
|
20
|
+
|
|
21
|
+
```mermaid
|
|
22
|
+
flowchart LR
|
|
23
|
+
W[Resource publisher] -->|prepare and validate| V2[Immutable version V2]
|
|
24
|
+
W -->|activate completed version| A[Active entry]
|
|
25
|
+
A -->|discovery resolves actual directory| N[New runtime binding]
|
|
26
|
+
O[Existing runtime binding] --> V1[Retained immutable version V1]
|
|
27
|
+
N --> V2
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
The runtime resolves a skill's actual directory before loading its body. The resulting body, base-directory guidance and `CLAUDE_SKILL_DIR` remain bound to that directory. A subsequent active-link switch must not redirect an already loaded object's scripts or references. This is path binding under the immutable-version precondition, not a filesystem snapshot or protection against a publisher rewriting the bound directory.
|
|
31
|
+
|
|
32
|
+
Explicit plugin roots are resolved before reading their manifest, so subsequent component discovery uses that same plugin version. Independently activated child roots are separate resources; discovery is not a transaction across multiple roots. Skills supplied through SDK `skillsDirs` should reference their resources through `CLAUDE_SKILL_DIR`. Their generated `CLAUDE_PLUGIN_ROOT` is a temporary adapter and is not a persistent resume path; use an explicit retained plugin directory when a package needs plugin-root-relative assets.
|
|
33
|
+
|
|
34
|
+
Explicit `reload_plugins` refreshes discovery and related caches. Await its response and use its returned metadata before dispatching a newly selected method. Old messages remain history; their old body is not evidence that a fresh invocation loaded the old version. A resumed process must be given the intended retained resource roots and rediscover them. Native transcript resume alone is not a resource-version registry.
|
|
35
|
+
|
|
36
|
+
If the host requires a session to stay on V1, pass the concrete V1 root, retain it, and avoid replacing that session's bindings with V2 on reload. If it intends an upgrade, perform it at an explicit task boundary, rediscover, compare the resolved source/version, and update the binding. Do not silently reinterpret a missing old ID as a same-named newer skill.
|
|
37
|
+
|
|
38
|
+
## Mutation, recovery and readiness ownership
|
|
39
|
+
|
|
40
|
+
CCL currently does **not** expose a shared Desktop/CLI publication transaction, lock, or `ready/recovering` state protocol. `runtime_resources` is a memory/scratch location contract and does not supply that capability. The runtime does not inspect private Desktop lock files or recover Desktop transactions.
|
|
41
|
+
|
|
42
|
+
One publisher must own mutations and recovery of a shared root, or all writers must agree on the same protocol. A recovery worker must not roll back an active writer; elapsed time alone is insufficient to take ownership. Invalid/incomplete publication records must remain unavailable to new consumers until the publisher repairs them. Keep an already published version usable while a replacement is staged. These are publisher obligations and are not claimed as implemented by the read-only loader.
|
|
43
|
+
|
|
44
|
+
The publisher/host should resolve readiness before handing the runtime a version root. Missing directories are errors at the SDK boundary. Loader diagnostics and missing catalog entries are observable failure facts, not proof that a transaction was recovered or a resource is globally deleted. The host should show the actual failure and allow retry after recovery. It must not present mere directory existence or a registered command as installation success.
|
|
45
|
+
|
|
46
|
+
A publisher that must update in place needs a shared read/write coordination protocol covering discovery, body/script/reference reads and resume. A writer-only mutex is insufficient. That protocol and Desktop adaptation require joint implementation and testing; this candidate does not claim them.
|
|
47
|
+
|
|
48
|
+
## Integration acceptance
|
|
49
|
+
|
|
50
|
+
Use one synthetic resource package through both hosts. Verify exact command selection despite display-name collisions; same-root restart identity; changed-root rediscovery; V1 body and reference consumption while V2 is staged/activated; explicit reload to V2; retained-root resume; missing or corrupt publication refusal; live-writer/recovery concurrency and interrupted publication. Record which checks exercise CCL and which exercise the publisher/Desktop implementation. CLI fixture success is not Desktop UI or natural-model routing acceptance.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: background-reviewer
|
|
3
|
+
description: Independently inspect the actual task and produced files.
|
|
4
|
+
tools: Read, Glob, Grep
|
|
5
|
+
---
|
|
6
|
+
Read the user's task and the actual source and output files. Check the facts and requested behavior independently; the worker's conclusion is not evidence. State concrete discrepancies and remaining uncertainties. Do not edit files, approve permissions, rerun the worker or send external messages.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: background-worker
|
|
3
|
+
description: Execute the explicitly delegated file task and preserve actual artifacts.
|
|
4
|
+
tools: Read, Write, Glob, Grep
|
|
5
|
+
---
|
|
6
|
+
Read the original task and relevant source files. Work only within its authorized scope. Save the requested artifact and report its path. Retain tool failures and permission denials in your account of the outcome. Do not substitute a new task for an uncertain original execution. Do not send external messages.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { createBackgroundClient } from '../../dist/sdk/index.js'
|
|
2
|
+
import { resolve } from 'node:path'
|
|
3
|
+
import { setTimeout as delay } from 'node:timers/promises'
|
|
4
|
+
|
|
5
|
+
// Copy agents/*.md to the task project's .ccl/agents/ before running this example.
|
|
6
|
+
// Usage: node worker-reviewer.mjs PROJECT CLI_PATH STABLE_TASK_KEY ARTIFACT_PATH
|
|
7
|
+
// The project contains task.md (the user's bounded work request).
|
|
8
|
+
const [project, cliPath, taskKey, artifact] = process.argv.slice(2)
|
|
9
|
+
if (!project || !cliPath || !taskKey || !artifact) throw new Error('Expected PROJECT CLI_PATH STABLE_TASK_KEY ARTIFACT_PATH')
|
|
10
|
+
const client = createBackgroundClient({ cwd: resolve(project), cliPath: resolve(cliPath),
|
|
11
|
+
launch: { extraArgs: ['--allowed-tools', 'Read,Write,Glob,Grep'] } })
|
|
12
|
+
async function wait(runId) {
|
|
13
|
+
const deadline = Date.now() + 10 * 60_000
|
|
14
|
+
let cursor
|
|
15
|
+
while (Date.now() < deadline) {
|
|
16
|
+
let page
|
|
17
|
+
do {
|
|
18
|
+
if (Date.now() >= deadline) throw new Error(`Observation deadline elapsed; query original run ${runId}.`)
|
|
19
|
+
page = await client.readRunEvents({ runId, cursor })
|
|
20
|
+
// This demo only observes. A real assistant handles this page's events
|
|
21
|
+
// and persists its cursor after its own processing succeeds.
|
|
22
|
+
cursor = page.nextCursor
|
|
23
|
+
} while (page.hasMore)
|
|
24
|
+
const view = await client.getRun({ runId })
|
|
25
|
+
if (view.status !== 'found') throw new Error(`Original run unavailable: ${runId}`)
|
|
26
|
+
if (view.snapshot.state === 'waiting_permission') throw new Error(`Explicit permission needed; inspect ${runId}. No approval was invented.`)
|
|
27
|
+
if (['succeeded','completed_with_issues','failed','interrupted','cancelled'].includes(view.snapshot.state)) return view
|
|
28
|
+
if (view.snapshot.state !== 'queued' && view.observation.executorLock === 'free') throw new Error(`Executor unavailable: ${runId}; query its recorded operations before continuing`)
|
|
29
|
+
await delay(1000)
|
|
30
|
+
}
|
|
31
|
+
throw new Error(`Observation deadline elapsed; the run may still be executing. Query original run ${runId}.`)
|
|
32
|
+
}
|
|
33
|
+
const worker = await client.submitRun({ requestKey: `${taskKey}:worker`, spec: { agent: 'background-worker', maxTurns: 12,
|
|
34
|
+
prompt: `Working directory: ${resolve(project)}. Read ${resolve(project, 'task.md')} and perform its authorized work. Save the requested artifact at ${resolve(project, artifact)}. Report actual results and tool failures honestly.` } })
|
|
35
|
+
console.log(JSON.stringify({ accepted: worker.runId, sessionId: worker.sessionId }))
|
|
36
|
+
const work = await wait(worker.runId)
|
|
37
|
+
const reviewer = await client.submitRun({ requestKey: `${taskKey}:reviewer`, spec: { agent: 'background-reviewer', maxTurns: 8,
|
|
38
|
+
prompt: `Independently review the task in working directory ${resolve(project)}. Read ${resolve(project, 'task.md')} and the actual artifact ${resolve(project, artifact)}; compare the requested facts with the files. Do not accept the worker's prose as evidence. Worker execution facts: ${JSON.stringify({ runId: worker.runId, state: work.snapshot.state, toolErrors: work.snapshot.toolErrors, deniedToolUseIds: work.snapshot.deniedToolUseIds })}. Start with these supplied paths and the source files named by the task. Keep investigation within this task directory unless concrete task evidence requires another location; do not search parent directories for hypothetical evidence. Report what you verified, actual discrepancies, and limits of the available evidence. A missing before-run baseline limits a historical claim; it is not itself evidence that the source was modified and does not require a filesystem-wide investigation. Do not change files or send external messages.` } })
|
|
39
|
+
if (reviewer.sessionId === worker.sessionId) throw new Error('Worker and reviewer unexpectedly share a session')
|
|
40
|
+
const review = await wait(reviewer.runId)
|
|
41
|
+
// This is the reviewer's result, not a claim that an external recipient received it.
|
|
42
|
+
console.log(JSON.stringify({ worker: work, reviewer: review }, null, 2))
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { mkdirSync, realpathSync, statSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
|
+
import { z } from 'zod/v4'
|
|
5
|
+
import { createDurableCommandAdapter, createOperationHost, createSdkMcpServer, tool } from '../../dist/sdk/index.js'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Persistent local-file example. The embedding assistant supplies its CURRENT
|
|
9
|
+
* permission callback; no historical allow decision is reused after restart.
|
|
10
|
+
* Run after building the SDK, or use the copy shipped in the npm package.
|
|
11
|
+
*/
|
|
12
|
+
export function durableCommandRecoveryServer(root, authorize) {
|
|
13
|
+
if (typeof authorize !== 'function') throw new Error('Supply the current host authorization callback')
|
|
14
|
+
mkdirSync(root, { recursive: true, mode: 0o700 })
|
|
15
|
+
const directory = realpathSync(root)
|
|
16
|
+
const identity = () => {
|
|
17
|
+
const stat = statSync(directory)
|
|
18
|
+
if (!stat.isDirectory()) throw new Error('Example target is no longer a directory')
|
|
19
|
+
return `${directory}:${stat.dev}:${stat.ino}`
|
|
20
|
+
}
|
|
21
|
+
const script = fileURLToPath(new URL('./receipt-command.mjs', import.meta.url))
|
|
22
|
+
const host = createOperationHost({ storeDir: join(directory, 'operations'), scope: directory, authorize,
|
|
23
|
+
resolveAdapter: async ref => ref === 'local-example' ? createDurableCommandAdapter({
|
|
24
|
+
adapterId: ref, adapterVersion: '1', targetIdentity: identity(),
|
|
25
|
+
execute: { cmd: process.execPath, args: [script, join(directory, 'effects'), 'execute'], cwd: directory, timeoutMs: 5000 },
|
|
26
|
+
query: { cmd: process.execPath, args: [script, join(directory, 'effects'), 'query'], cwd: directory, timeoutMs: 5000 },
|
|
27
|
+
verifyIdentity: async () => ({ targetIdentity: identity(), adapterVersion: '1' }),
|
|
28
|
+
// Query after an unknown effect. Do not advertise an external service's
|
|
29
|
+
// replay guarantee merely because this local demonstration deduplicates.
|
|
30
|
+
}) : undefined,
|
|
31
|
+
})
|
|
32
|
+
return createSdkMcpServer({ name: 'durable-commands', tools: [
|
|
33
|
+
tool('execute', 'Accept and execute a local example; preserve operationId even if its reply is unknown',
|
|
34
|
+
{ input: z.json(), requestKey: z.string().min(1) }, async (args, extra) => {
|
|
35
|
+
const accepted = await host.submitOperation({ requestKey: args.requestKey, adapterRef: 'local-example', input: args.input })
|
|
36
|
+
return host.executeOperation({ operationId: accepted.operationId, expectedRevision: accepted.revision, signal: extra.signal })
|
|
37
|
+
}),
|
|
38
|
+
tool('get', 'Read saved execution and result, including after the original host exits',
|
|
39
|
+
{ operationId: z.string() }, args => host.getOperation(args), { annotations: { readOnlyHint: true } }),
|
|
40
|
+
tool('query', 'Query the original effect and save its verified receipt; never repeat the write',
|
|
41
|
+
{ operationId: z.string(), expectedRevision: z.number().int(), actionId: z.string().min(1) },
|
|
42
|
+
(args, extra) => host.reconcileOperation({ ...args, signal: extra.signal })),
|
|
43
|
+
] })
|
|
44
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Deterministic local example, not a production business service.
|
|
2
|
+
import { createHash } from 'node:crypto'
|
|
3
|
+
import { mkdir, readFile, writeFile, rename } from 'node:fs/promises'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
const [root, action] = process.argv.slice(2)
|
|
6
|
+
let raw = ''
|
|
7
|
+
for await (const chunk of process.stdin) raw += chunk
|
|
8
|
+
const request = JSON.parse(raw)
|
|
9
|
+
if (action === 'identity') {
|
|
10
|
+
process.stdout.write(JSON.stringify({ targetIdentity: root, adapterVersion: '1' }))
|
|
11
|
+
process.exit(0)
|
|
12
|
+
}
|
|
13
|
+
const key = createHash('sha256').update(`${request.targetIdentity}:${request.requestKey}`).digest('hex')
|
|
14
|
+
const dir = join(root, key)
|
|
15
|
+
const path = join(dir, 'receipt.json')
|
|
16
|
+
const reply = body => process.stdout.write(JSON.stringify({ ...request, input: undefined, ...body }))
|
|
17
|
+
const existing = async () => {
|
|
18
|
+
try {
|
|
19
|
+
const record = JSON.parse(await readFile(path, 'utf8'))
|
|
20
|
+
if (record.inputDigest !== request.inputDigest) return reply({ status: 'unknown', reason: 'Key/input mismatch' })
|
|
21
|
+
reply({ status: 'applied', receipt: record.receipt, result: record.result })
|
|
22
|
+
} catch { reply({ status: 'unknown', reason: 'No complete receipt; an old action may still be running' }) }
|
|
23
|
+
}
|
|
24
|
+
await mkdir(root, { recursive: true })
|
|
25
|
+
if (action === 'query') await existing()
|
|
26
|
+
else if (action === 'execute') {
|
|
27
|
+
try { await mkdir(dir) } catch (error) {
|
|
28
|
+
if (error.code !== 'EEXIST') throw error
|
|
29
|
+
await existing()
|
|
30
|
+
process.exit(0)
|
|
31
|
+
}
|
|
32
|
+
// Exclusive mkdir is the example service's deduplication claim. An interrupted claim
|
|
33
|
+
// without a receipt stays unknown; it never authorizes another write.
|
|
34
|
+
const record = { inputDigest: request.inputDigest, receipt: { key, writes: 1 }, result: request.input }
|
|
35
|
+
await writeFile(join(dir, 'effect.json'), JSON.stringify(request.input))
|
|
36
|
+
await writeFile(join(dir, 'receipt.tmp'), JSON.stringify(record))
|
|
37
|
+
await rename(join(dir, 'receipt.tmp'), path)
|
|
38
|
+
if (request.input?.loseReply) process.exit(9)
|
|
39
|
+
await existing()
|
|
40
|
+
} else throw new Error('Expected execute or query')
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// Reference consumer only. Production hosts must bind this object to their own
|
|
2
|
+
// authenticated connection/account lifecycle and retain their permission engine.
|
|
3
|
+
// This source-tree fixture is imported by the tests. When copying it into a host,
|
|
4
|
+
// import RuntimeResourcesSchema/RuntimeResources from '@margay/ccl-core/sdk'.
|
|
5
|
+
import { RuntimeResourcesSchema, type RuntimeResources } from '../../sdk/runtimeResources.js'
|
|
6
|
+
import { realpathSync } from 'node:fs'
|
|
7
|
+
import { isAbsolute, relative, resolve, sep } from 'node:path'
|
|
8
|
+
|
|
9
|
+
type Available = Extract<RuntimeResources, { status: 'available' }>
|
|
10
|
+
|
|
11
|
+
export class RuntimeResourceCatalog {
|
|
12
|
+
private generation = 0
|
|
13
|
+
private request = 0
|
|
14
|
+
private snapshot: Available | undefined
|
|
15
|
+
private runtimeId: string | undefined
|
|
16
|
+
|
|
17
|
+
/** Call BEFORE switching accounts/projects, restoring a session or disconnecting. */
|
|
18
|
+
invalidate(): void {
|
|
19
|
+
this.generation++
|
|
20
|
+
this.snapshot = undefined
|
|
21
|
+
this.runtimeId = undefined
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** `read` must query the current trusted backend, never model/tool text. */
|
|
25
|
+
async refresh(read: () => Promise<unknown>, expectedSession: string): Promise<boolean> {
|
|
26
|
+
const generation = this.generation, request = ++this.request
|
|
27
|
+
this.snapshot = undefined
|
|
28
|
+
try {
|
|
29
|
+
const parsed = RuntimeResourcesSchema.safeParse(await read())
|
|
30
|
+
if (generation !== this.generation || request !== this.request) return false
|
|
31
|
+
if (!parsed.success || parsed.data.status !== 'available' || parsed.data.session_id !== expectedSession) return false
|
|
32
|
+
if (this.runtimeId && parsed.data.runtime_id !== this.runtimeId) return false
|
|
33
|
+
this.runtimeId = parsed.data.runtime_id
|
|
34
|
+
this.snapshot = parsed.data
|
|
35
|
+
return true
|
|
36
|
+
} catch {
|
|
37
|
+
// A failed/old query never restores the previous snapshot.
|
|
38
|
+
return false
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** A description for UI/policy context. No allow/deny result is produced. */
|
|
43
|
+
resource(kind: 'memory' | 'scratch') {
|
|
44
|
+
const resource = this.snapshot?.resources.find(r => r.kind === kind)
|
|
45
|
+
return resource?.status === 'available' ? { ...resource } : undefined
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Read-only classification for an existing target. Missing paths stay unknown.
|
|
50
|
+
* This is NOT a secure file-open operation: a host must still authorize the
|
|
51
|
+
* requested action and prevent link races at the actual access boundary.
|
|
52
|
+
*/
|
|
53
|
+
describeExistingPath(target: string) {
|
|
54
|
+
if (!isAbsolute(target)) return undefined
|
|
55
|
+
const within = (root: string, path: string) => {
|
|
56
|
+
const suffix = relative(root, path)
|
|
57
|
+
return suffix === '' || (suffix !== '..' && !suffix.startsWith('..' + sep) && !isAbsolute(suffix))
|
|
58
|
+
}
|
|
59
|
+
for (const kind of ['memory', 'scratch'] as const) {
|
|
60
|
+
const resource = this.resource(kind)
|
|
61
|
+
if (!resource || !within(resolve(resource.path), resolve(target))) continue
|
|
62
|
+
try {
|
|
63
|
+
if (within(realpathSync(resource.path), realpathSync(target))) return resource
|
|
64
|
+
} catch { /* An unresolved target is not an identified resource. */ }
|
|
65
|
+
}
|
|
66
|
+
return undefined
|
|
67
|
+
}
|
|
68
|
+
}
|